Files
scud_context_api/llm/agent.py
T

264 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.
import json
import urllib.request
import urllib.error
import logging
from typing import List, Dict, Any, Tuple, Optional
from datetime import datetime, timedelta
from .db_tools import (
db_get_active_system_prompt,
db_add_system_prompt,
db_get_tasks,
db_update_task_status,
db_delete_task,
db_add_task,
db_get_rules,
db_set_session_state,
db_get_session_state,
db_get_snapshots,
db_delete_snapshots,
db_clear_session_state,
db_get_current_server_time,
db_save_chat_message,
db_get_chat_history,
db_get_stats,
db_get_anomalies,
db_get_session_states,
db_get_reference,
get_db_connection
)
from .schemas import TOOLS_SCHEMA
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("SCUD_AGENT")
OLLAMA_URL = "http://192.168.11.3:11434/api/chat"
MODEL_NAME = "qwen2.5:14b"
DAYS_RU = [
"понедельник", "вторник", "среда", "четверг",
"пятница", "суббота", "воскресенье"
]
def clean_output(text: str) -> str:
if not text:
return text
artifacts = ["почемучто", "почто", "почему что"]
lower_text = text.lower()
for art in artifacts:
if lower_text.startswith(art):
text = text[len(art):].lstrip(",.!?:; -")
return text.strip()
def get_dynamic_calendar_context() -> str:
now = datetime.now()
current_wd = now.weekday()
lines = [
f"СЕГОДНЯ: {DAYS_RU[current_wd].upper()}, {now.strftime('%d.%m.%Y')} (время сервера: {now.strftime('%H:%M:%S')}).",
"\nСПРАВОЧНИК ДАТ ДЛЯ ОТВЕТОВ (БЕРИ ДАТЫ СТРОГО ОТСЮДА):",
f"• Сегодня: {now.strftime('%d.%m.%Y')} ({DAYS_RU[current_wd]})",
f"• Вчера: {(now - timedelta(days=1)).strftime('%d.%m.%Y')} ({DAYS_RU[(current_wd - 1) % 7]})",
f"• Позавчера: {(now - timedelta(days=2)).strftime('%d.%m.%Y')} ({DAYS_RU[(current_wd - 2) % 7]})",
"\nПрошедшие дни недели:"
]
for days_back in range(1, 8):
dt = now - timedelta(days=days_back)
day_name = DAYS_RU[dt.weekday()]
if days_back == 7:
label = f"Прошлый {day_name}" if dt.weekday() in [0, 1, 3, 6] else f"Прошлая {day_name}"
lines.append(f"• {label} (ровно неделю назад): {dt.strftime('%d.%m.%Y')}")
else:
label = f"Ближайший прошедший {day_name}" if dt.weekday() in [0, 1, 3, 6] else f"Ближайшая прошедшая {day_name}"
lines.append(f"• {label} / {day_name}: {dt.strftime('%d.%m.%Y')}")
return "\n".join(lines)
def process_chat_message(
user_id: int,
user_message: str,
file_context: str = "",
chat_history: List[Dict[str, Any]] = None,
session_id: str = "web_session_main"
) -> Tuple[str, List[Dict[str, Any]]]:
logger.info(f"Получено сообщение от user_id={user_id}, session_id={session_id}: {user_message}")
# Формируем итоговое содержимое запроса
full_user_content = user_message
if file_context:
full_user_content = f"{user_message}\n\n[СОДЕРЖИМОЕ ПРИКРЕПЛЕННОГО ФАЙЛА]:\n{file_context}"
db_history = db_get_chat_history(session_id, limit=20)
db_save_chat_message(session_id, "user", full_user_content)
dynamic_prompt_text = db_get_active_system_prompt()
calendar_context = get_dynamic_calendar_context()
session_state = db_get_session_state(session_id)
preview_status_note = ""
if session_state and session_state.get("state_type") == "PROMPT_PREVIEW":
preview_status_note = "\n\n[АКТИВНО ПРЕВЬЮ ПРОМПТА: Ожидается подтверждение или отмена изменений пользователем]."
system_prompt_content = (
f"[СИСТЕМНЫЙ КАЛЕНДАРЬ СЕРВЕРА]\n"
f"{calendar_context}\n\n"
f"ПРАВИЛО РАБОТЫ С ДАТАМИ:\n"
f"При любых вопросах про дни недели ('прошлая среда', 'вторник', 'дата в прошлый понедельник') бери ГОТОВУЮ точную дату из справочника выше. Тебе ЗАПРЕЩЕНО вычислять даты самостоятельно!\n\n"
f"ТЕКУЩИЙ АКТИВНЫЙ СИСТЕМНЫЙ ПРОМПТ:\n{dynamic_prompt_text}{preview_status_note}"
)
system_prompt = {
"role": "system",
"content": system_prompt_content
}
messages = [system_prompt] + db_history + [{"role": "user", "content": full_user_content}]
payload = {
"model": MODEL_NAME,
"messages": messages,
"tools": TOOLS_SCHEMA,
"stream": False,
"options": {"num_predict": 8192, "num_ctx": 8192, "temperature": 0.1}
}
try:
req = urllib.request.Request(
OLLAMA_URL,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"}
)
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", [])
logger.info(f"Ответ от Ollama получен. Tool calls: {bool(tool_calls)}")
if tool_calls:
messages.append(msg)
for tool in tool_calls:
fn_name = tool["function"]["name"]
fn_args = tool["function"].get("arguments", {})
logger.info(f"Вызов функции (Tool): {fn_name} с аргументами: {fn_args}")
tool_result_content = ""
if fn_name == "db_get_snapshots":
date_arg = fn_args.get("date_str")
snapshots_res = db_get_snapshots(session_id=session_id, date_str=date_arg, original_user_message=user_message)
tool_result_content = json.dumps(snapshots_res, ensure_ascii=False)
elif fn_name == "db_get_current_server_time":
time_res = db_get_current_server_time()
tool_result_content = json.dumps(time_res, ensure_ascii=False)
elif fn_name == "db_get_tasks":
tasks = db_get_tasks(user_id)
tool_result_content = json.dumps(tasks, ensure_ascii=False)
elif fn_name in ["db_get_system_prompt", "db_get_system_prompts"]:
prompt_content = db_get_active_system_prompt()
tool_result_content = json.dumps({"system_prompt": prompt_content}, ensure_ascii=False)
elif fn_name == "db_get_stats":
tool_result_content = json.dumps(db_get_stats(), ensure_ascii=False)
elif fn_name == "db_get_anomalies":
limit_arg = fn_args.get("limit", 100)
date_arg = fn_args.get("date_str")
tool_result_content = json.dumps(db_get_anomalies(limit=limit_arg, date_str=date_arg), ensure_ascii=False)
elif fn_name == "db_get_session_states":
tool_result_content = json.dumps(db_get_session_states(), ensure_ascii=False)
elif fn_name == "db_delete_snapshots":
snap_id = fn_args.get("snapshot_id")
day_arg = fn_args.get("day_str")
tool_result_content = json.dumps(db_delete_snapshots(snapshot_id=snap_id, day_str=day_arg), ensure_ascii=False)
elif fn_name == "db_get_reference":
cat_arg = fn_args.get("category")
tool_result_content = json.dumps(db_get_reference(category=cat_arg), ensure_ascii=False)
elif fn_name == "db_preview_prompt_merge":
proposed_text = fn_args.get("proposed_prompt", "")
if proposed_text:
db_set_session_state(session_id, "PROMPT_PREVIEW", proposed_text)
preview_reply = f"Ваше изменение успешно предпросмотрено. Полный обновленный системный промпт теперь выглядит так:\n\n{proposed_text}\n\nДля применения изменений подтвердите действие («подтверждаю») или отмените («отмена»)."
db_save_chat_message(session_id, "assistant", preview_reply)
return preview_reply, db_get_chat_history(session_id)
else:
tool_result_content = json.dumps({"status": "error", "message": "Текст превью пуст."}, ensure_ascii=False)
elif fn_name == "db_confirm_prompt_preview":
if session_state and session_state.get("state_type") == "PROMPT_PREVIEW":
pending_text = session_state.get("pending_data", "")
res = db_add_system_prompt("main_agent", pending_text)
db_clear_session_state(session_id)
tool_result_content = json.dumps(res, ensure_ascii=False)
else:
tool_result_content = json.dumps({"status": "error", "message": "Нет активного превью для подтверждения."}, ensure_ascii=False)
elif fn_name == "db_cancel_prompt_preview":
db_clear_session_state(session_id)
tool_result_content = json.dumps({"status": "success", "message": "Превью отменено."}, ensure_ascii=False)
elif fn_name == "db_add_system_prompt":
try:
prompt_text = fn_args.get("prompt_text") if isinstance(fn_args, dict) else str(fn_args)
name = fn_args.get("name", "main_agent") if isinstance(fn_args, dict) else "main_agent"
res = db_add_system_prompt(name=name, prompt_text=prompt_text)
db_clear_session_state(session_id)
tool_result_content = json.dumps(res, ensure_ascii=False)
except Exception as e:
tool_result_content = json.dumps({"status": "error", "error": str(e)}, ensure_ascii=False)
elif fn_name == "db_get_rules":
tool_result_content = json.dumps(db_get_rules(), ensure_ascii=False)
elif fn_name == "db_add_task":
res = db_add_task(user_id=user_id, module=fn_args.get("module", "general"), title=fn_args.get("title"), priority=fn_args.get("priority", "MEDIUM"), due_date=fn_args.get("due_date"))
tool_result_content = json.dumps(res, ensure_ascii=False)
elif fn_name == "db_update_task_status":
res = db_update_task_status(user_id=user_id, task_id=str(fn_args.get("task_id")), status=fn_args.get("status", "COMPLETED"), due_date=fn_args.get("due_date"))
tool_result_content = json.dumps(res, ensure_ascii=False)
elif fn_name == "db_delete_task":
res = db_delete_task(user_id=user_id, task_id=str(fn_args.get("task_id", "")).upper())
tool_result_content = json.dumps(res, ensure_ascii=False)
messages.append({
"role": "tool",
"content": tool_result_content
})
second_payload = {
"model": MODEL_NAME,
"messages": messages,
"stream": False,
"options": {"num_predict": 8192, "num_ctx": 8192, "temperature": 0.1}
}
sec_req = urllib.request.Request(
OLLAMA_URL,
data=json.dumps(second_payload).encode("utf-8"),
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(sec_req) as sec_response:
sec_res_data = json.loads(sec_response.read().decode("utf-8"))
final_content = clean_output(sec_res_data.get("message", {}).get("content", "").strip().replace("**", ""))
db_save_chat_message(session_id, "assistant", final_content)
return final_content, db_get_chat_history(session_id)
content_str = clean_output(msg.get("content", "").strip().replace("**", ""))
final_reply = content_str or "Запрос обработан."
db_save_chat_message(session_id, "assistant", final_reply)
return final_reply, db_get_chat_history(session_id)
except Exception as ex:
logger.exception(f"Непредвиденная ошибка: {ex}")
error_reply = f"Внутренняя ошибка сервера: {ex}"
return error_reply, db_get_chat_history(session_id)