Files
scud_context_api/llm_agent.py
T
2026-08-06 18:55:21 +03:00

292 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# === ANCHOR: IMPORTS_START ===
import sqlite3
import json
import urllib.request
import urllib.error
from typing import List, Dict, Any, Tuple
from datetime import datetime
DB_NAME = "context_memory.db"
OLLAMA_URL = "http://192.168.11.3:11434/api/chat"
MODEL_NAME = "qwen2.5:14b"
# === ANCHOR: IMPORTS_END ===
# === ANCHOR: DB_TOOLS_START ===
def db_get_tasks(status: str = None) -> str:
"""Извлекает список задач из реестра БД."""
conn = sqlite3.connect(DB_NAME)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
if status:
cursor.execute("SELECT * FROM tasks WHERE status = ? ORDER BY task_id", (status.upper(),))
else:
cursor.execute("SELECT * FROM tasks ORDER BY task_id")
rows = [dict(r) for r in cursor.fetchall()]
conn.close()
return json.dumps(rows, ensure_ascii=False)
def db_update_task_status(task_id: str, status: str = None, due_date: str = None) -> str:
"""Обновляет статус и/или срок выполнения задачи."""
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()
updates = []
params = []
if status:
status_upper = status.upper()
updates.append("status = ?")
params.append(status_upper)
if status_upper == "COMPLETED":
updates.append("completed_at = CURRENT_TIMESTAMP")
else:
updates.append("completed_at = NULL")
if due_date:
updates.append("due_date = ?")
params.append(due_date)
if not updates:
conn.close()
return json.dumps({"error": "Не указаны параметры для обновления"}, ensure_ascii=False)
params.append(task_id.upper())
query = f"UPDATE tasks SET {', '.join(updates)} WHERE task_id = ?"
cursor.execute(query, tuple(params))
if cursor.rowcount == 0:
conn.close()
return json.dumps({"error": f"Задача {task_id} не найдена"}, ensure_ascii=False)
conn.commit()
conn.close()
return json.dumps({"status": "success", "message": f"Задача {task_id} обновлена"}, ensure_ascii=False)
def db_add_task(task_id: str, module: str, title: str, priority: str = "HIGH", due_date: str = None) -> str:
"""Добавляет новую задачу в реестр."""
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()
try:
cursor.execute(
"INSERT INTO tasks (task_id, module, title, status, priority, due_date) VALUES (?, ?, ?, 'BACKLOG', ?, ?)",
(task_id.upper(), module, title, priority.upper(), due_date)
)
conn.commit()
res = {"status": "success", "message": f"Задача {task_id} создана"}
except sqlite3.IntegrityError:
res = {"error": f"Задача с ID {task_id} уже существует"}
finally:
conn.close()
return json.dumps(res, ensure_ascii=False)
def db_get_rules() -> str:
"""Возвращает действующие архитектурные правила."""
conn = sqlite3.connect(DB_NAME)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("SELECT category, rule_text FROM architecture_memory WHERE is_active = 1")
rows = [dict(r) for r in cursor.fetchall()]
conn.close()
return json.dumps(rows, ensure_ascii=False)
# === ANCHOR: DB_TOOLS_END ===
# === ANCHOR: TOOLS_SCHEMA_START ===
TOOLS_SCHEMA = [
{
"type": "function",
"function": {
"name": "db_get_tasks",
"description": "Получить список задач проекта с возможностью фильтрации по статусу (BACKLOG, IN_PROGRESS, COMPLETED).",
"parameters": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Фильтр статуса: BACKLOG, IN_PROGRESS или COMPLETED."
}
}
}
}
},
{
"type": "function",
"function": {
"name": "db_update_task_status",
"description": "Изменить статус и/или срок выполнения (due_date) задачи в реестре.",
"parameters": {
"type": "object",
"properties": {
"task_id": {"type": "string", "description": "Идентификатор задачи, например TASK-17"},
"status": {"type": "string", "description": "Новый статус: COMPLETED, IN_PROGRESS или BACKLOG"},
"due_date": {"type": "string", "description": "Срок выполнения задачи, например '2026-08-07 14:00'"}
},
"required": ["task_id"]
}
}
},
{
"type": "function",
"function": {
"name": "db_add_task",
"description": "Добавить новую задачу в бэклог проекта.",
"parameters": {
"type": "object",
"properties": {
"task_id": {"type": "string", "description": "Уникальный ID задачи, например TASK-18"},
"module": {"type": "string", "description": "Модуль проекта, например services/scud_export"},
"title": {"type": "string", "description": "Краткое описание задачи"},
"priority": {"type": "string", "description": "Приоритет: HIGH, MEDIUM, LOW"},
"due_date": {"type": "string", "description": "Срок выполнения задачи, например '2026-08-07 14:00'"}
},
"required": ["task_id", "module", "title"]
}
}
},
{
"type": "function",
"function": {
"name": "db_get_rules",
"description": "Получить список архитектурных правил и ограничений проекта.",
"parameters": {"type": "object", "properties": {}}
}
}
]
# === ANCHOR: TOOLS_SCHEMA_END ===
# === ANCHOR: CHAT_AGENT_START ===
def process_chat_message(user_message: str, chat_history: List[Dict[str, Any]] = None) -> Tuple[str, List[Dict[str, Any]]]:
if chat_history is None:
chat_history = []
current_now = datetime.now().strftime("%Y-%m-%d %H:%M")
system_prompt = {
"role": "system",
"content": (
f"Текущая дата и время сервера: {current_now}.\n"
f"Текущая дата и время сервера: {current_now}.\n"
"Ты — ИИ-ассистент проекта SCUD Orion AI. "
"У тебя есть доступ к функциям для просмотра, добавления и изменения статусов и сроков задач (due_date) в БД. "
"Когда пользователь просит показать задачи, добавить новую или изменить статус — обязательно вызывай нужную функцию. "
"После вызова функции давай пользователю понятное, лаконичное резюме результата на русском языке. "
"При запросе списка всех задач не расписывай каждую подробно, а давай краткую сводку по количествам, "
"так как детали отображаются в выезжающем реестре задач."
)
}
messages = [system_prompt] + chat_history + [{"role": "user", "content": user_message}]
payload = {
"model": MODEL_NAME,
"messages": messages,
"tools": TOOLS_SCHEMA,
"stream": False,
"options": {
"num_predict": 2048, # Достаточно для любых длинных списков и кода
"num_ctx": 8192, # Расширенная память диалога без потери скорости
"temperature": 0.2, # Низкая температура: ответы точные и без галлюцинаций
"top_p": 0.9, # Оптимальная выборка токенов
"num_gpu": 99 # Заставляет Ollama загрузить 100% слоев модели в VRAM
}
}
req = urllib.request.Request(
OLLAMA_URL,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(req) as response:
res_data = json.loads(response.read().decode("utf-8"))
msg = res_data.get("message", {})
tool_calls = msg.get("tool_calls", [])
# Резервная проверка, если Ollama прислала вызов функции текстом
content_str = msg.get("content", "")
if not tool_calls and "name" in content_str and "db_" in content_str:
try:
parsed = json.loads(content_str)
if "name" in parsed:
tool_calls = [{"function": parsed}]
except Exception:
pass
if tool_calls:
messages.append(msg)
for tool in tool_calls:
fn_name = tool["function"]["name"]
fn_args = tool["function"].get("arguments", {})
if fn_name == "db_get_tasks":
result = db_get_tasks(status=fn_args.get("status"))
elif fn_name == "db_update_task_status":
result = db_update_task_status(
task_id=fn_args.get("task_id"),
status=fn_args.get("status"),
due_date=fn_args.get("due_date")
)
elif fn_name == "db_add_task":
result = db_add_task(
task_id=fn_args.get("task_id"),
module=fn_args.get("module"),
title=fn_args.get("title"),
priority=fn_args.get("priority", "HIGH"),
due_date=fn_args.get("due_date")
)
elif fn_name == "db_get_rules":
result = db_get_rules()
else:
result = json.dumps({"error": "Неизвестная функция"})
messages.append({
"role": "tool",
"content": result
})
# Вторым запросом передаем результат обратно модели для текстового ответа
payload = {
"model": MODEL_NAME,
"messages": messages,
"tools": TOOLS_SCHEMA,
"stream": False,
"options": {
"num_predict": 2048, # Достаточно для любых длинных списков и кода
"num_ctx": 8192, # Расширенная память диалога без потери скорости
"temperature": 0.2, # Низкая температура: ответы точные и без галлюцинаций
"top_p": 0.9, # Оптимальная выборка токенов
"num_gpu": 99 # Заставляет Ollama загрузить 100% слоев модели в VRAM
}
}
req_second = urllib.request.Request(
OLLAMA_URL,
data=json.dumps(payload_second).encode("utf-8"),
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req_second) as resp_second:
final_res = json.loads(resp_second.read().decode("utf-8"))
final_msg = final_res.get("message", {}).get("content", "")
updated_history = chat_history + [
{"role": "user", "content": user_message},
{"role": "assistant", "content": final_msg}
]
return final_msg, updated_history
else:
final_msg = msg.get("content", "")
updated_history = chat_history + [
{"role": "user", "content": user_message},
{"role": "assistant", "content": final_msg}
]
return final_msg, updated_history
except urllib.error.URLError as e:
return f"Ошибка связи с Ollama ({OLLAMA_URL}): {e}", chat_history
# === ANCHOR: CHAT_AGENT_END ===