- 扩展 test_agent_execution(--homework、UTF-8 控制台) - 后端:uploads 预览、file_read、工作流与对话落盘等 - 前端:AgentChatPreview 与设计器相关调整 - 忽略 redis二进制、agent_workspaces、uploads、tessdata 等本机产物 Made-with: Cursor
303 lines
9.3 KiB
Python
303 lines
9.3 KiB
Python
"""
|
||
每个智能体在工作区根目录下对应一个 Markdown 对话文件(自动追加)。
|
||
与 builtin_tools._local_file_workspace_root 使用同一根目录,避免越权路径。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import logging
|
||
from datetime import datetime, timezone
|
||
from pathlib import Path
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.config import settings
|
||
from app.models.execution import Execution
|
||
from app.models.agent import Agent
|
||
from app.services.builtin_tools import _local_file_workspace_root
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
_MAX_BODY_CHARS = 100_000
|
||
|
||
|
||
def _workspace_subdir() -> str:
|
||
s = (getattr(settings, "AGENT_WORKSPACE_CHAT_SUBDIR", None) or "agent_workspaces").strip()
|
||
return s.strip("/\\") or "agent_workspaces"
|
||
|
||
|
||
def _extract_user_text(input_data: Optional[Dict[str, Any]]) -> str:
|
||
if not input_data:
|
||
return ""
|
||
keys = ("query", "USER_INPUT", "message", "content", "text", "prompt", "user_message")
|
||
for k in keys:
|
||
v = input_data.get(k)
|
||
if v is None:
|
||
continue
|
||
if isinstance(v, str) and v.strip():
|
||
return v.strip()
|
||
if v is not None and not isinstance(v, (dict, list)):
|
||
s = str(v).strip()
|
||
if s:
|
||
return s
|
||
filtered = {k: v for k, v in input_data.items() if not str(k).startswith("__")}
|
||
try:
|
||
raw = json.dumps(filtered, ensure_ascii=False, default=str)
|
||
except Exception:
|
||
raw = str(filtered)
|
||
if len(raw) > _MAX_BODY_CHARS:
|
||
raw = raw[:_MAX_BODY_CHARS] + "\n\n…(已截断)"
|
||
return raw or ""
|
||
|
||
|
||
def _extract_assistant_text(result: Optional[Dict[str, Any]]) -> str:
|
||
if not result:
|
||
return ""
|
||
r = result.get("result")
|
||
if r is None:
|
||
try:
|
||
raw = json.dumps(result, ensure_ascii=False, default=str)
|
||
except Exception:
|
||
raw = str(result)
|
||
elif isinstance(r, str):
|
||
return r if len(r) <= _MAX_BODY_CHARS else r[:_MAX_BODY_CHARS] + "\n\n…(已截断)"
|
||
else:
|
||
try:
|
||
raw = json.dumps(r, ensure_ascii=False, default=str)
|
||
except Exception:
|
||
raw = str(r)
|
||
if len(raw) > _MAX_BODY_CHARS:
|
||
raw = raw[:_MAX_BODY_CHARS] + "\n\n…(已截断)"
|
||
return raw
|
||
|
||
|
||
def _file_already_has_execution_id(md_path: Path, execution_id: str) -> bool:
|
||
"""同一执行只记一次,避免 Celery 与 API 补救重复写入。"""
|
||
if not md_path.exists():
|
||
return False
|
||
try:
|
||
text = md_path.read_text(encoding="utf-8", errors="replace")
|
||
except OSError:
|
||
return False
|
||
needle = f"**执行 ID**:`{execution_id}`"
|
||
return needle in text
|
||
|
||
|
||
def _user_id_from_input(input_data: Optional[Dict[str, Any]]) -> Optional[str]:
|
||
if not input_data:
|
||
return None
|
||
for k in ("user_id", "USER_ID", "userId"):
|
||
v = input_data.get(k)
|
||
if v is None:
|
||
continue
|
||
s = str(v).strip()
|
||
if s:
|
||
return s
|
||
return None
|
||
|
||
|
||
def fetch_agent_preview_chat_turns(
|
||
db: Session,
|
||
agent_id: str,
|
||
*,
|
||
preview_user_id: Optional[str] = None,
|
||
limit: int = 50,
|
||
max_scan: int = 500,
|
||
) -> List[Dict[str, Any]]:
|
||
"""
|
||
设计器预览侧恢复对话:从已完成执行解析用户/助手文本。
|
||
preview_user_id 与 input_data.user_id 一致时只返回该浏览器预览会话的记录。
|
||
"""
|
||
from app.models.execution import Execution
|
||
|
||
limit = max(1, min(int(limit), 200))
|
||
max_scan = max(limit, min(int(max_scan), 1000))
|
||
rows = (
|
||
db.query(Execution)
|
||
.filter(
|
||
Execution.agent_id == agent_id,
|
||
Execution.status == "completed",
|
||
)
|
||
.order_by(Execution.created_at.desc())
|
||
.limit(max_scan)
|
||
.all()
|
||
)
|
||
picked: List[Execution] = []
|
||
for ex in rows:
|
||
if preview_user_id:
|
||
inp = ex.input_data if isinstance(ex.input_data, dict) else None
|
||
uid = _user_id_from_input(inp)
|
||
if uid != preview_user_id:
|
||
continue
|
||
picked.append(ex)
|
||
if len(picked) >= limit:
|
||
break
|
||
picked.reverse()
|
||
out: List[Dict[str, Any]] = []
|
||
for ex in picked:
|
||
inp = ex.input_data if isinstance(ex.input_data, dict) else None
|
||
od = ex.output_data
|
||
wf: Dict[str, Any] = od if isinstance(od, dict) else {"result": od}
|
||
user_text = (_extract_user_text(inp) or "").strip()
|
||
agent_text = (_extract_assistant_text(wf) or "").strip()
|
||
if not user_text and not agent_text:
|
||
continue
|
||
out.append(
|
||
{
|
||
"execution_id": str(ex.id),
|
||
"created_at": ex.created_at,
|
||
"user_text": user_text or "(无文本)",
|
||
"agent_text": agent_text or "(无输出)",
|
||
}
|
||
)
|
||
return out
|
||
|
||
|
||
def append_agent_dialogue_md(
|
||
*,
|
||
db: Session,
|
||
execution: Execution,
|
||
input_data: Optional[Dict[str, Any]],
|
||
workflow_result: Dict[str, Any],
|
||
) -> Optional[Path]:
|
||
"""
|
||
在 agent_workspaces/<agent_id>/dialogue.md 末尾追加一轮对话。
|
||
仅当 execution.agent_id 存在且开关开启时写入。
|
||
"""
|
||
if not getattr(settings, "AGENT_WORKSPACE_CHAT_LOG_ENABLED", True):
|
||
return None
|
||
if not execution or not getattr(execution, "agent_id", None):
|
||
return None
|
||
|
||
agent_id = str(execution.agent_id)
|
||
ag = db.query(Agent).filter(Agent.id == execution.agent_id).first()
|
||
agent_name = (ag.name if ag and ag.name else agent_id).strip() or agent_id
|
||
|
||
root = _local_file_workspace_root()
|
||
sub = _workspace_subdir()
|
||
agent_dir = root / sub / agent_id
|
||
try:
|
||
agent_dir.mkdir(parents=True, exist_ok=True)
|
||
except OSError as e:
|
||
logger.warning("创建智能体对话目录失败: %s", e)
|
||
return None
|
||
|
||
md_path = agent_dir / "dialogue.md"
|
||
eid = str(execution.id)
|
||
if _file_already_has_execution_id(md_path, eid):
|
||
logger.debug("对话 MD 已包含执行 %s,跳过追加", eid)
|
||
return md_path
|
||
|
||
user_text = _extract_user_text(input_data)
|
||
assistant_text = _extract_assistant_text(workflow_result)
|
||
uid = _user_id_from_input(input_data)
|
||
ts = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S %z")
|
||
|
||
rel_note = f"{sub}/{agent_id}/dialogue.md"
|
||
|
||
block_lines = [
|
||
f"### {ts}",
|
||
"",
|
||
f"- **执行 ID**:`{eid}`",
|
||
]
|
||
if uid:
|
||
block_lines.append(f"- **用户 ID**:`{uid}`")
|
||
block_lines.extend(
|
||
[
|
||
"",
|
||
"**用户**",
|
||
"",
|
||
user_text or "(无文本输入,详见 input_data)",
|
||
"",
|
||
"**助手**",
|
||
"",
|
||
assistant_text or "(无输出)",
|
||
"",
|
||
"---",
|
||
"",
|
||
]
|
||
)
|
||
block = "\n".join(block_lines)
|
||
|
||
try:
|
||
if not md_path.exists():
|
||
header = "\n".join(
|
||
[
|
||
f"# 对话记录 · {agent_name}",
|
||
"",
|
||
f"- **Agent ID**:`{agent_id}`",
|
||
f"- **文件路径**(相对工作区根):`{rel_note}`",
|
||
"- **说明**:每次关联本智能体的执行成功完成后自动追加。",
|
||
"",
|
||
"---",
|
||
"",
|
||
]
|
||
)
|
||
md_path.write_text(header + block, encoding="utf-8")
|
||
else:
|
||
with md_path.open("a", encoding="utf-8", newline="\n") as f:
|
||
f.write(block)
|
||
except OSError as e:
|
||
logger.warning("写入智能体对话 MD 失败: %s", e)
|
||
return None
|
||
|
||
logger.info("智能体对话已写入: %s", md_path)
|
||
return md_path
|
||
|
||
|
||
def ensure_agent_dialogue_logged_from_db_execution(
|
||
db: Session, execution: Execution
|
||
) -> Optional[Path]:
|
||
"""
|
||
根据已落库的执行记录补写对话 MD(供 GET execution 等路径调用)。
|
||
仅 completed + 有 agent_id + 有 output_data 时写入;与 Celery 路径幂等。
|
||
"""
|
||
if not getattr(settings, "AGENT_WORKSPACE_CHAT_LOG_ENABLED", True):
|
||
return None
|
||
if not execution or execution.status != "completed":
|
||
return None
|
||
if not getattr(execution, "agent_id", None):
|
||
return None
|
||
od = execution.output_data
|
||
if od is None:
|
||
return None
|
||
wf: Dict[str, Any]
|
||
if isinstance(od, dict):
|
||
wf = od
|
||
else:
|
||
wf = {"result": od}
|
||
try:
|
||
return append_agent_dialogue_md(
|
||
db=db,
|
||
execution=execution,
|
||
input_data=execution.input_data if isinstance(execution.input_data, dict) else None,
|
||
workflow_result=wf,
|
||
)
|
||
except Exception as e:
|
||
logger.warning("从执行记录补写对话 MD 失败: %s", e)
|
||
return None
|
||
|
||
|
||
def try_append_agent_dialogue_after_success(
|
||
db: Session,
|
||
execution: Optional[Execution],
|
||
input_data: Optional[Dict[str, Any]],
|
||
workflow_result: Optional[Dict[str, Any]],
|
||
execution_logger: Any = None,
|
||
) -> None:
|
||
if not workflow_result:
|
||
return
|
||
try:
|
||
append_agent_dialogue_md(
|
||
db=db,
|
||
execution=execution,
|
||
input_data=input_data,
|
||
workflow_result=workflow_result,
|
||
)
|
||
except Exception as e:
|
||
msg = f"写入智能体对话 MD 异常: {e}"
|
||
logger.warning(msg)
|
||
if execution_logger:
|
||
execution_logger.warn(msg)
|