57 KiB
minor Agent 代码深度分析报告
分析范围:
Agent/llm_server/、Agent/src/agent/cron/、Agent/src/agent/tts/streaming_client.py、Agent/src/agent/utils/、Agent/src/agent/memory/system_prompt.py、Agent/src/agent/agents/agent_runtime.py、Agent/src/agent/history/、Agent/src/web/ui_session.py。全部路径精确到文件与关键函数/类/变量。
一、C:\Users\86166\Desktop\Agent_Learning_minor\Agent\llm_server\(本地推理后端,6 个模型/5 个服务端口)
1. README.md —— 部署与总览
- 定位:为 minor Agent 提供本地化推理后端,全部跑在 GPU 服务器上,以 OpenAI 兼容 / 自定义 HTTP API 暴露;可整体部署或按需启用。
- 服务总览表(README 自称 6 个独立服务,实际为 5 个服务、6 个模型 —— RAG 一个服务内装 Embedding+Reranker 两个模型):
| 服务 | 端口 | 模型 | 启动脚本 | Python 服务端 | 关键端点 |
|---|---|---|---|---|---|
| LLM | 8900 | Qwen3.6-35B-A3B-FP8 | start_llm.sh |
vLLM 内置 | /v1/chat/completions |
| ASR | 8901 | Qwen3-ASR-1.7B | start_asr.sh |
vLLM 内置 | /v1/chat/completions |
| TTS | 8902 | VoxCPM 1.5 / 2 | start_streaming_tts.sh |
streaming_tts_server.py |
/stream_tts |
| RAG | 8903 | Qwen3-Embedding-0.6B + Qwen3-Reranker-4B | start_rag_server.sh |
rag_server.py |
/embed /rerank |
| Image Gen | 8904 | Z-Image-Turbo | start_image_gen.sh |
image_gen_server.py |
/generate |
- 所有脚本/服务端通过
os.path.dirname(__file__)与${SCRIPT_DIR}动态定位模型路径,目录整体迁移无需改代码。 - 部署流程要点:
scp -r Agent/llm_server/*上传 → 服务器上chmod +x *.sh。- 环境准备:
export HF_ENDPOINT=https://hf-mirror.com、HF_HUB_ENABLE_XET=0(国内必需);python3 -m venv myenv;一次性安装vllm modelscope transformers fastapi uvicorn torch numpy torchaudio soundfile bitsandbytes huggingface_hub voxcpm diffusers;apt安装 ffmpeg 全家桶。 - 模型统一存
~/llm_server/models/,用modelscope download --model ... --local_dir ./models/...下载(注意 modelscope 目录名中/转___、.转_,如Qwen3___6-35B-A3B-FP8、Qwen3-Embedding-0___6B、VoxCPM1__5)。 - 每个
start_*.sh支持环境变量覆盖:HOST_PORT(端口)、LOCAL_MODEL_DIR/MODEL_PATH/VOXCPM_MODEL_PATH(模型目录)、CUDA_VISIBLE_DEVICES/SPECIFIC_GPU(GPU 指定)。
- 本地调试:
ssh -fN -L 8900:0.0.0.0:8900 ...一次性映射 5 个端口;curl http://localhost:$p/health验证。 - 停止服务:
for p in 8900..8904; do kill $(lsof -t -i:$p); done。 - 接入 Agent(配置向导或
env_config.json):models[].base_url=http://localhost:8900/v1、ASR_BASE_URL=http://localhost:8901/v1、STREAMING_TTS_URL=http://localhost:8902、RAG_BASE_URL=http://localhost:8903、图像生成由image_gen工具读取http://localhost:8904。也支持任意 OpenAI 兼容云端 API。
2. start_llm.sh —— LLM 启动参数(vLLM 推理引擎)
固定 CUDA_VISIBLE_DEVICES=3,VLLM_USE_MODELSCOPE=true、MODELSCOPE_DOWNLOAD_MODE=local、HF_HUB_OFFLINE=1 等离线环境变量。vLLM 关键参数:
vllm serve <MODEL_DIR>:--tensor-parallel-size 1、--max-model-len 262144(26 万超长上下文)、--reasoning-parser qwen3、--enable-auto-tool-choice+--tool-call-parser qwen3_coder(自动工具调用)、--speculative-config '{"method":"qwen3_next_mtp","num_speculative_tokens":2}'(MTP 投机解码,2 个投机 token)、--enable-prefix-caching、--max_num_batched_tokens 4096、--max_num_seqs 8、--kv-cache-dtype fp8_e4m3(KV 缓存 FP8)、--chat-template chat_template.jinja(自定义模板)、--safetensors-load-strategy eager、--served-model-name Qwen/Qwen3.6-35B-A3B-FP8。
3. chat_template.jinja —— ChatML 风格自定义模板
enable_thinking默认false(|default(false))。- 有工具时:system 段 = 首条 system 消息(content 为 list 时把 image/audio/video part 替换为
<|vision_start|><|image_pad|><|vision_end|>、<|audio_start|><|audio_pad|><|audio_end|>、<|vision_start|><|video_pad|><|vision_end|>占位符)+ 固定的# Tools\n...说明 +<tools>...</tools>内逐条tool | tojson序列化工具签名;工具调用格式约束为<tool_call>\n{"name": ..., "arguments": ...}\n</tool_call>。 - 无工具时:首条 system 按 content 类型输出多模态占位符或文本。
- multi_step_tool 检测(第 47-54 行):从后往前找第一个"非
<tool_response>包裹"的 user 消息,记为last_query_index,用于判断后续 assistant 是否需要显式输出<think>块。 - assistant 消息:优先用
message.reasoning_content(字符串);否则若 content 含</think>,用split('<think>')/split('</think>')拆出 reasoning 与正文。loop.index0 > last_query_index且(末条或含 reasoning)时,输出<think>...</think>包裹的思考段。tool_calls逐个渲染为<tool_call>...</tool_call>。 - tool(函数返回)消息:连续的 tool 消息被合并进同一个
user角色块,每条包在<tool_response>\n...\n</tool_response>中,首条前加<|im_start|>user、末条后加<|im_end|>。 add_generation_prompt时输出<|im_start|>assistant\n;若显式传enable_thinking=false则先输出空<think>\n\n</think>\n\n(禁用思考)。
4. test_llm.py —— 测试方式
python test_llm.py [--url http://127.0.0.1:8900] [--model Qwen/Qwen3.6-35B-A3B-FP8] [--stream]。- 非流式
test_non_stream:POST/v1/chat/completions,打印完整响应 + 提取 model/finish_reason/content/tool_calls/usage(prompt/completion/total tokens)。 - 流式
test_stream:stream=True,逐行解析data:前缀的 SSE,[DONE]结束,累加delta.content输出。
5. start_asr.sh + test_asr.py —— ASR 服务
- 引擎 = vLLM,模型 Qwen3-ASR-1.7B(README 标注;test_asr.py 文档字符串仍引用旧模型 Qwen3-Omni-30B-A3B-Captioner,说明测试脚本是从旧版继承)。启动参数:
--quantization fp8、--dtype auto、--kv-cache-dtype fp8、--max-model-len 16384、--allowed-local-media-path /(允许读取本地媒体路径)、--tensor-parallel-size 1、--gpu-memory-utilization 0.15(只占 15% 显存)、--max-num-batched-tokens 4096;CUDA_VISIBLE_DEVICES=3。 - 调用方式:
test_asr.py --audio <file> [--url http://127.0.0.1:8901/v1/chat/completions] [--base64]。核心函数caption_audio(audio_path, server_url, use_base64):- 音频转 URL:本地直传用
file://+ 绝对路径(/分隔);远程场景--base64用data:<mime>;base64,...(file_to_url)。 - 不带任何文本提示,payload 仅一条 user 消息、content 为
[{"type": "audio_url", "audio_url": {"url": audio_url}}](音频描述式 ASR,模型输出整段音频内容的描述/转写)。 - 用
soundfile(回退 librosa)检查时长,>30s 打警告(建议裁剪到 ≤30s)。timeout=120,取result["choices"][0]["message"]["content"]。
- 音频转 URL:本地直传用
6. streaming_tts_server.py —— 流式 TTS 服务器(FastAPI + VoxCPM)
- 引擎 = OpenBMB VoxCPM(
from voxcpm import VoxCPM,README 注释"Whispera 同款")。DEFAULT_MODEL_PATH=models/VoxCPM1__5,DEFAULT_PORT=8902。 - 音色克隆:
DEFAULT_PROMPT_WAV=reference_audio.wav+DEFAULT_PROMPT_TEXT="日子缓缓前行,不必急于追赶…",每次合成都克隆该音色;音频质量参数DEFAULT_CFG_VALUE=1.5(音色贴合度)、DEFAULT_INFERENCE_STEPS=15(推理步数)。 - 单例与并发控制:
_get_model(model_path)线程安全惰性加载(_singleton_lock,enable_denoiser=False, optimize=True,采样率取model.tts_model.sample_rate,默认 24000Hz);_model_lock全局模型锁(VoxCPM 只允许单线程);cancel_current_and_set(new_request_id)在新请求到达时自动取消旧请求(设置 cancel Event,并强制释放被旧请求持有的模型锁);set_cancel/is_cancelled请求级取消标志。 - 端点:
POST /stream_tts:body{"text": ..., "request_id": ...}(缺省 request_id 自动生成tts-{ms})。返回StreamingResponse(media_type="text/event-stream"),头带Cache-Control: no-cache、X-Accel-Buffering: no。空文本直接返回audio.error。POST /cancel_tts:{"request_id": ...}取消指定合成。GET /health。
- 流式音频块格式(SSE,
_build_sse为data: {json}\n\n):audio.start:{type, request_id, sample_rate, audio_format: "pcm_f32le"}。audio.chunk:{type, request_id, index, num_samples, sample_rate, data: base64(PCM float32 LE 字节)}——np.asarray(chunk, float32).ravel()→tobytes()→ base64;生成循环中检查is_cancelled,取消则发audio.cancelled并 return。audio.done:{type, request_id, total_samples, interrupted: false}。audio.error:{type, request_id, message}(模型加载失败、等锁超时 3 次等)。- 模型调用:
model.generate_streaming(text=text, prompt_wav_path=..., prompt_text=..., cfg_value=..., inference_timesteps=..., retry_badcase=False)。
- 关于"分句":本文件没有显式分句逻辑——整段 text 一次性交给
generate_streaming,由 VoxCPM 内部流式产出音频块(docstring 说"按句分块合成"是沿用旧设计的注释;test_tts.py里读取的num_segments/total_segments字段服务器实际并不发送,客户端读到默认值)。真实分句若需要,应在客户端或未来版本做文本切分。 - 启动:
uvicorn.run(app, host="0.0.0.0", port=args.port);lifespan 启动时预加载模型。
7. start_streaming_tts.sh + test_tts.py
- 启动脚本:
VOXCPM_MODEL_PATH可覆盖(默认models/VoxCPM1__5);依次尝试../myenv、../../myenv激活虚拟环境;CUDA_VISIBLE_DEVICES默认 3;exec python streaming_tts_server.py --port --model-path。 - 测试脚本:健康检查 →
POST /stream_tts(stream=True, timeout=300)→ 逐行解析data:SSE → 按事件类型处理:audio.start取 sample_rate;audio.chunk累积 base64 解码的 PCM、进度打印(idx%5==0);audio.done汇总;audio.error退出;audio.cancelledbreak。结束后计算 RTF(耗时/音频时长,<1 即比实时快)。_save_wav:f32 →(arr*32767).clip(-32768,32767).astype(int16)写 16-bit 单声道 WAV;_play_pcm用 pyaudio 播放。实测产物test_output.wav已存在。
8. rag_server.py —— RAG 服务器(FastAPI + Transformers)
- 框架:FastAPI +
modelscope.AutoTokenizer/AutoModel/AutoModelForCausalLM(无 LangChain/RAG 框架,纯手写检索基座)。 - 模型路径:
EMBED_MODEL_PATH=models/Qwen/Qwen3-Embedding-0___6B、RERANK_MODEL_PATH=models/Qwen/Qwen3-Reranker-4B。 - 加载(
@app.on_event("startup")load_models):Embedding 用AutoModel.from_pretrained(..., device_map="auto", dtype=torch.float16)(fp16,未量化,注释里 8-bit 被禁用);Reranker 用AutoModelForCausalLM.from_pretrained(..., quantization_config=BitsAndBytesConfig(load_in_8bit=True), device_map="auto", dtype=torch.float16)。 - 工具函数:
last_token_pool(取每条样本最后有效 token 的 hidden state,兼容左/右 padding)、get_detailed_instruct(query 拼Instruct: {task}\nQuery:{query},document 不加指令)、format_rerank_instruction(<Instruct>: ...\n<Query>: ...\n<Document>: ...,默认 instruct 文本 "Given a web search query, retrieve relevant passages that answer the query")。 - Reranker 技巧:预编码固定 prefix/suffix(ChatML system+user 头、assistant 尾带空 think),
max_rerank_length=32768;取logits[:, -1, :]中"yes"/"no"两个 token id 的 logit,log_softmax([false, true])后取 true 的exp作为得分(生成式 rerank)。 - API 端点:
POST /embed:{task_description, queries: [...], documents: [...]}→{query_embeddings, doc_embeddings}(L2 归一化,queries 在前 documents 在后拼 batch)。POST /rerank:{task_description, query, documents}→{scores: [...]}。GET /health。
- 启动:
start_rag_server.sh用uvicorn rag_server:app --host 0.0.0.0 --port 8903 --workers 1(离线环境变量 +CUDA_VISIBLE_DEVICES=3)。测试:test_rag.py依次测 health/embed/rerank,打印向量维度与得分。
9. image_gen_server.py —— 图像生成服务器(Diffusers)
- 模型 = Tongyi-MAI/Z-Image-Turbo(README:基于 Diffusers,9 步推理出 1024×1024;代码默认
num_inference_steps=11)。LOCAL_MODEL_DIR环境变量优先本地路径,否则回退远程 model id;SPECIFIC_GPU直接写CUDA_VISIBLE_DEVICES。 - 加载:
DiffusionPipeline.from_pretrained(MODEL_PATH, torch_dtype=torch.bfloat16(if cuda), low_cpu_mem_usage=False, trust_remote_code=True);本地路径时local_files_only=True;惰性单例_get_pipe()+_pipe_lock。 POST /generate:入参prompt(必填)、negative_prompt、height/width(默认 1024,须为 64 的整数倍且 ∈[256,2048])、num_inference_steps(默认 11)、guidance_scale(默认 0.0)、seed(-1 随机,≥0 用torch.Generator(device).manual_seed)、return_format(base64默认 |url)。响应{success, image: base64-PNG 或原始 PNG 流, format:"png", width, height, elapsed_seconds}。GET /health。启动时 lifespan 预加载模型。
二、C:\Users\86166\Desktop\Agent_Learning_minor\Agent\src\agent\cron\(定时任务子系统)
models.py —— 数据模型(3 个 dataclass + 类型别名)
TriggerType = Literal["cron","interval","once"];TaskStatus = Literal["pending","running","completed","failed","expired","disabled"];TriggerSource = Literal["schedule","manual"]。Trigger(触发配置):type(默认 "cron")、cron(标准 5 段 cron 表达式如"0 9 * * *")、interval_seconds、run_at(once 的目标本地时间 ISO)。有to_dict/from_dict。ExecutionLog(单次执行记录):started_at、ended_at、status、error、trigger(schedule/manual)、turn_id(关联工具调用记录)。CronTask(任务完整定义):task_id(时间戳风格,与 session_id 同构)、name、prompt(触发时投递给 Agent 的提示词)、trigger、enabled、timeout_seconds(默认 300)、created_at、next_run_at、last_run_at、last_status、last_error、executions: list[ExecutionLog]。summary()返回不含 executions 全量的精简 dict(附executions_count)供前端列表。now_iso():datetime.now().strftime("%Y-%m-%dT%H:%M:%S")(无时区,与前端一致)。
scheduler.py —— 调度器(CronScheduler 单例)
- 常量:
_SCAN_INTERVAL = 5.0(扫描周期秒)、_CRON_TIME_PERIOD_MINUTES = 30.0(时段长度,同一时间段只允许一个任务运行,全局并发=1;由env_config.json的CRON_TIME_PERIOD_MINUTES配置)。 _load_time_period_minutes()/get_cron_time_period()/set_cron_time_period(minutes):读取/热更新时段长度并持久化到 env_config.json(save_env_config)。_parse_iso(dt_str):解析%Y-%m-%dT%H:%M:%S,失败 None。_compute_next_run(trigger, now):cron →croniter(trigger.cron, now).get_next(datetime)(异常返回 "");interval →now + timedelta(interval_seconds);once →run_at若未来,否则 ""(不再调度)。compute_next_fire包装为 datetime。detect_time_period_conflict(trigger, exclude_task_id=None):计算候选任务下次触发时刻,与所有 enabled 任务(排除自身)的下次触发时刻求绝对差 < 时段长度 → conflict=True,返回{"conflict", "conflict_with": [name...], "period_minutes", "next_fire"}。供前端 check-conflict API 与 cron_manager 工具复用。CronScheduler(get_instance()/模块级get_scheduler()单例):- 状态:
_thread、_stop_event、_running_procs: dict[task_id, Popen]、_procs_lock、_app_start_time、_started、_period_start_ts(当前占用时段起点,None 空闲)。 start():启动 daemon 线程cron-scheduler跑_run_loop;shutdown():set stop、terminate 所有子进程、join 3s。_run_loop:先_on_startup()(启动扫描),再每 5s_scan_once()。_on_startup/_handle_startup_task(错失/过期处理):once 目标已过且从未成功 → 追加expired执行记录并update_status(...,"expired", error="App was not running, missed execution time", next_run_at="")+enabled=False禁用;cron 若next_run_at < app 启动时间→ 记 expired 执行记录并推进_compute_next_run(trigger, now);interval 直接推进到 now+interval。_scan_once:遍历 enabled 任务,next_run_at <= now且不在_running_procs→_spawn(task, trigger_source="schedule", started_iso=now_iso_str)。_get_python_and_cwd:Python 解释器优先env_config.json的USER_PYTHON_PATH(文件存在才用),否则sys.executable;cwd =agent/cron/storage.py的parents[2](即 src 目录)。_spawn(task, trigger_source, started_iso, prompt=None):时段 gate —— 锁内检查_running_procs非空或now - _period_start_ts < 时段秒数则跳过;否则占用新时段_period_start_ts = now_ts。命令:[py, "-m", "agent.cron.runner", "--task-id", ..., "--prompt", use_prompt, "--trigger", trigger_source, "--timeout", str(task.timeout_seconds)];env 注入PYTHONPATH=cwd;Windows 用CREATE_NEW_PROCESS_GROUP。spawn 失败 →update_status(...,"failed", error="Failed to spawn subprocess: ...")并释放时段。成功后update_status(...,"running", last_run_at=started_iso),起 daemon 线程_monitor_proc。_monitor_proc:proc.wait()回收;子进程自身会 POST/api/cron/internal/finished回传终态,本线程仅兜底——若last_status已是 completed/failed/expired 则不覆盖;否则按 exit code 推断(0→completed,非 0→failedSubprocess exited abnormally (exit code N)),并补记 execution。spawn_now(task_id, prompt=None):手动立即运行/追问(trigger=manual,prompt 可覆盖原任务提示词);is_running(task_id);kill_task(task_id)(terminate);reload_task(task_id)(配置变更后重算 next_run_at,供 cron_manager 工具调用)。
- 状态:
runner.py —— 子进程执行器(python -m agent.cron.runner)
- 回传地址常量:
_MAIN_HOST="127.0.0.1"、_MAIN_PORT=8765、_LIVE_URL=http://127.0.0.1:8765/api/cron/internal/live、_FINISHED_URL=http://127.0.0.1:8765/api/cron/internal/finished。 _redirect_storage_roots(task_id)(必须在构建 runtime 前调用):把三个模块的全局根目录显式覆盖到 cron 目录,并把统一存储作用域切到 "cron":agent.utils.agent_utils.SESSIONS_ROOT = CRON_ROOT(turn 文件/session_dir/get_history)agent.history.tool_call_recorder.TOOL_CALLING_ROOT = Path(CRON_ROOT)(tool_*.json、token)web.ui_session.SESSIONS_ROOT = CRON_ROOT(turn relpath 计算)agent.core.storage.set_storage_scope("cron")(消息/工具调用记录直写 cron 作用域:CRON_ROOT 文件或 cron_* 表,无需执行后的文件→DB 桥接)- 任务目录
task_dir = CRON_ROOT/{task_id}并 mkdir。
_start_live_forwarder(task_id):后台线程cron-live-{task_id}订阅subscribe_live(task_id),把每个快照POST {"task_id": task_id, "snapshot": snapshot}到_LIVE_URL(timeout=5,失败不阻塞),返回 stop Event。_post_finished(task_id, status, error, turn_id):POST{"task_id","status","error","turn_id"}到_FINISHED_URL(timeout=10,失败仅打印)。_run_with_timeout(func, timeout):daemon 工作线程执行,done.wait(timeout)判超时;超时返回(None, None, True)(Python 无法杀线程,靠子进程随后sys.exit一起销毁);异常(None, exc, False);成功(value, None, False)。_classify_runner_error:复用agent.core.runtime._classify_error。main()流程:- 解析
--task-id/--prompt/--trigger/--timeout(timeout 下限 10s)→_redirect_storage_roots(task_id),失败则_post_finished(failed, "存储初始化失败...")返回 1。 agent.core.workspace_policy.mark_headless():无人值守标记,approval 模式退化为拦截(待审批项随进程退出丢失)。build_all_agent_runtimes()+get_main_agent_runtime(),无主 Agent → failed。_start_live_forwarder(task_id)。execute_runtime_agent(runtime=runtime, chat_history=[{"role":"user","content":prompt}], session_id=task_id, agent_mode="cron")(直接走 core.runtime,绕开 {"agent","plan"} 归一化),包_run_with_timeout;超时→failed"任务超时";异常→_classify_runner_error;正常完成时检查最后一条 assistant content 是否以错误前缀开头("OOM:"、"执行过程中出现错误"、"API Key"、"无法连接"、"指定的模型"、"模型服务端"、"对话内容超过"、"API 调用频率"、"Agent 执行轮数超过限制")→ 判定 failed。stop_forwarder.set()→get_last_turn_id(task_id)取 turn_id →_post_finished(task_id, status, error_msg, turn_id)→sleep(0.3)给转发线程发最后快照 → 返回 0/1。
- 解析
storage.py —— 转发层
纯转发到 agent.core.storage:导出 CRON_ROOT、TaskConfigRepository、ConversationRecordRepository、get_task_repository、new_task_id、set_storage_scope、get_storage_scope;并自定义 get_record_repository() = _get_core_record_repository(scope="cron")(固定返回 cron 作用域记录仓库:cron_messages/cron_tool_calls 表或 history/cron 下 JSON 文件,按 STORAGE_BACKEND 选择 json/mysql)。
live.py —— 主进程侧实时流中继
- 子进程无法直接与前端建 SSE,故经 HTTP 回传主进程后由本模块按 task_id 分发。
_cron_live_subs: dict[task_id, list[Queue]]+_subscribers_lock。subscribe(task_id)/unsubscribe(task_id, q):每任务订阅者队列(maxsize=64)。push_snapshot(task_id, snapshot):向所有订阅者put_nowait({"type":"update","data":snapshot})(队列满丢最旧)。push_finished(task_id, payload):推{"type":"finished","data":payload},前端据此重载消息并刷新状态。- 快照格式与
tool_call_recorder._build_live_snapshot一致:{tool_calls, reflections, started_at}(前端复用 toolcalls.js 渲染)。
__init__.py
包说明:Cron 模式与 Edit/Chat 模式并列,通过子进程隔离执行;子模块 roles:models/storage/scheduler/runner/live。
★ cron 与主进程的通信机制(总结)
- 调度:主进程 FastAPI lifespan 启动
CronScheduler(daemon 线程)→ 5s 扫描 →subprocess.Popen([py, "-m", "agent.cron.runner", ...])拉起子进程。 - 子进程 → 主进程(HTTP 回传,主进程固定监听 127.0.0.1:8765):
POST /api/cron/internal/live:{task_id, snapshot}→ 主进程cron.live.push_snapshot→ 分发给前端 SSE 订阅者。POST /api/cron/internal/finished:{task_id, status, error, turn_id}→ 主进程 finished handler 更新任务状态(scheduler_monitor_proc以此为兜底依据,不重复覆盖终态)。
- 主进程 → 前端:SSE(cron live 订阅队列),前端收
update/finished事件。 - 存储天然隔离:子进程把
SESSIONS_ROOT/TOOL_CALLING_ROOT/ui_session.SESSIONS_ROOT重定向到history/cron/{task_id}/,且统一存储 scope="cron",无需桥接导入。
三、C:\Users\86166\Desktop\Agent_Learning_minor\Agent\src\agent\tts\streaming_client.py(流式 TTS 客户端)
TTSEventdataclass:type(audio.start | audio.chunk | audio.done | audio.error | audio.cancelled)、request_id、index=0、num_samples=0、sample_rate=24000、data: bytes|None(PCM f32le 原始字节,已由 base64 解码)、message=""。StreamingTTSClient(base_url="http://localhost:8902"):stream_tts(text, request_id="")→ Generator[TTSEvent]:requests.post(f"{base_url}/stream_tts", json={"text","request_id"}, stream=True, timeout=300);resp.raise_for_status();resp.iter_lines(decode_unicode=True)逐行,跳过非data:行;json.loads(line[len("data: "):]),解码失败 continue;按 payload 字段构造TTSEvent,data=_decode_pcm_b64(payload.get("data"));yield evt。cancel_tts(request_id)→ bool:POST{base_url}/cancel_tts(timeout=5),200 即成功。
_decode_pcm_b64(b64_data):base64 解码失败返回 None。
四、C:\Users\86166\Desktop\Agent_Learning_minor\Agent\src\agent\utils\(工具函数集)
env_utils.py —— 环境变量加载与全局配置
_get_env(name, default, cast, required):读 os.environ,支持必填校验(缺失抛EnvironmentError)与类型转换(失败抛ValueError)。- 配置加载链:
_load_config()优先agent.core.config_manager.load_env_config()(单一数据源),把非models键写入os.environ,并从models[0]提取api_key/base_url/model/timeout写入LLM_API_KEY/LLM_BASE_URL/LLM_MODEL/LLM_TIMEOUT;config_manager 不可用时回退直接读src/agent/config/env_config.json;最后_refresh_globals()。模块导入时执行_load_config(); _refresh_globals()。 _refresh_globals()定义的模块级常量(均可用环境变量覆盖):- 路径:
WORKING_DIR(相对路径转绝对)、WORKSPACE_DIR、USER_PYTHON_PATH。 - LLM:
LLM_BASE_URL/LLM_API_KEY/LLM_MODEL(required=True)、LLM_TIMEOUT(默认 60.0)、LLM_CONTEXT_WINDOW(默认 262144)。 - 多模态服务:
STREAMING_TTS_URL、ASR_BASE_URL、RAG_BASE_URL(三者均为可选,对应 llm_server 的 8902/8901/v1、8903)。 - 压缩/图像:
COMPRESS_RATE(0.6)、IMG_SIZE(768)、RAG_CHUNK_SIZE(500)、RAG_CHUNK_OVERLAP(50)、GROUNDING_WIDTH/HEIGHT(1000)、SEND_FILE_SIZE_LIMIT(30MB)。
- 路径:
- THINKING_LEVELS 各档位(
("low","high","xhigh","max","ultra"),默认"low";非法值回退 low):low= agent 模式 + 不思考;high= plan 模式 + 不思考;xhigh= agent 模式 + 思考;max= plan 模式 + 思考;ultra为未来 react→审批图预留占位,暂按 "agent+思考" 处理。_THINKING_ENABLED_LEVELS = {"xhigh","max","ultra"}。
thinking_enabled(level=None):档位是否启用深度思考。get_thinking_extra_body(level=None):思考开启返回{}(恢复模型原生行为=启用思考);否则返回None(禁用思考,默认)。该 extra_body 注入Multimodel_LLM。reload_config():热重载并刷新全局(调用方须import agent.utils.env_utils as env_utils访问属性,from ... import会绑定旧值)。BROWSER_MAP:edge/chrome/firefox/safari/opera/brave → (可执行名, 显示名)。get_workspace_dir():WORKSPACE_DIR 绝对化(相对则相对 WORKING_DIR),缺省回退 WORKING_DIR,确保目录存在。get_venv_dir():由USER_PYTHON_PATH(.../Scripts/python.exe)取上级目录。
agent_utils.py —— 会话/历史/附件/文档核心工具
- 常量:
AUDIO_FILE_EXTENSIONS(wav/mp3/flac/ogg/m4a/aac/wma/opus/aiff/ape/amr/au)、DOC_EXT_MAP(txt/json/md/cpp/c/java/m/html/svg→plain_text;docx→docx;pptx→pptx;pdf→pdf;csv/xlsx/xls→tabular)、路径:HISTORY_ROOT = src/agent/history、SESSIONS_ROOT = history/sessions(启动即 mkdir)。 JsonChatMessageHistory(session_id)(替代 SQLite):基于 turn 文件读写 LangChain 消息,_load()用load_all_turn_messages,_dict_to_langchain把 user/assistant 条目转 HumanMessage/AIMessage;get_history(session_id)返回实例。- session_meta:
_load_session_meta/_save_session_meta(经session_storage.load_session_extra/save_session_extra的"agent_meta"键);get_session_meta(session_id, agent_name)→{cursor_index, compressed_content};update_session_meta写入。 - 动态尾部长期记忆:
update_session_dynamic_tail(session_id, agent_key, text, max_entries=100)—— 存dynamic_tail_history列表,连续相同内容去重,超出上限丢最旧;get_session_dynamic_tail_history读取(时间正序)。用于 TodoList 状态/循环提醒长期记忆,与 compressed_content 合并保存不覆盖(前缀缓存友好:变化部分在消息末尾)。 build_tool_history_messages(session_id, agent_key="main"):读 cursor_index,把压缩游标之后所有轮次工具调用构造成 synthetic HumanMessage(additional_kwargs={"synthetic":True,"tool_ctx":True},id=f"tool_ctx_{idx}"供压缩节点 RemoveMessage 精确移除),内容[历史工具调用] {name}({args[:300]}) → {result[:500]}。- Turn 存储:
TURN_JSON_PREFIX="turn_";turn_files_dir(session_id, turn_id)=sessions/{sid}/{turn_id}/files;save_turn_messages(→session_storage.save_turn,后端无关,附件二进制由调用方落盘);load_all_turn_messages;delete_turns_after(session_id, keep_until_turn_id)(按目录名排序删除 > keep 的 turn 目录 + 存储记录,keep 自身保留);get_last_turn_id;session_dir(session_id)=sessions/{sid}。 - pending turn_id:
set_pending_turn_id/take_pending_turn_id(跨请求共享,_pending_turn_idsdict + 锁,仅取一次)。 summarize_chat_text(llm, chat_text):ChatPromptTemplate(SUMMARY_SYSTEM_PROMPT + 压缩指令) | llm,config={"metadata":{"output_type":"text"}},供 in-loop 压缩节点复用。assistant_text(value):str/list/dict 多形态归一为纯文本(dict 依次尝试 text/content/input/output/query/answer/result 键,失败 json.dumps)。make_attachment_text(category, file_path, extra=""):生成附件描述文本——图片(含尺寸[用户提供了图片,文件路径: ...,尺寸: ...])、音频(路径 + extra)、有 extra 时直接返回 extra(如 ASR 结果/文档内容)、附件文件/通用格式。Documents_process(静态方法类):doc_classify(file_path)按扩展名+MIME 返回plain_text|docx|pdf|image|audio|tabular(不支持抛 ValueError);process_plain_text(UTF-8 读取);process_docx(python-docx,按文档顺序遍历段落/表格,run 中提取<image>N</image>占位与图片字节,表格转<table>,支持"表1/图1"短标题缓存合并);process_pdf(pypdf,逐页文字 +<image>N</image>占位 + 图片字节);process_pptx(python-pptx 提文字 +ppt_utils.convert_pptx_to_svg提布局,SVG 内 base64 data URL 替换为#[内嵌图片 N]占位,输出<slide>N</slide>[文字][布局]结构);process_tabular(csv →<table>Sheet1...;xlsx openpyxl 多 sheet;xls xlrd 日期转可读);process_image/process_audio(读字节,文本为空);process(file_path)统一分发入口(供core/llm._preprocess_messages调用)。audio_file_to_text(audio_path):ASR 调用链 —— 非 wav 先用 pydubAudioSegment.from_file转临时 wav(tempfile.mkstemp(prefix="asr_", suffix=".wav"));读字节 → base64 →data:audio/wav;base64,...;payloadmessages=[{role:user, content:[{"type":"audio_url","audio_url":{"url": data_url}}]}];POST{ASR_BASE_URL.rstrip('/')}/v1/chat/completions(timeout=120);返回choices[0].message.content.strip();finally 删除临时 wav。- 附件存储规则(归纳):
- 用户上传附件:
web/ui_session._rewrite_user_files_to_dir把外部文件shutil.copy2复制到SESSIONS_ROOT/{session_id}/(会话根目录),命名{清洗后文件名主干}_{session_id}_{递增序号}{ext}(_next_session_copy_name);已在该目录内的跳过。 - 单 turn 附件目录:
turn_files_dir=sessions/{sid}/{turn_id}/files。 - 工具产物(tool_call_recorder):直接写
TOOL_CALLING_ROOT/{session_id}/,命名_artifact_{工具名}_{序号}.png(图片)、_file_{工具名}{序号}{ext}(send_file 文件),音频不落盘只存路径元信息。
- 用户上传附件:
image_utils.py —— 图片多模态适配
IMAGE_FILE_EXTENSIONS = {.png,.jpg,.jpeg,.bmp,.gif,.webp,.ico}。resize_image_if_needed(image_path_or_bytes, max_size=IMG_SIZE=768):超长边等比缩放(LANCZOS);RGBA/LA 透明图贴白底后转 JPEG;JPEG quality=70 + optimize + progressive,PNG 走 optimize。image_to_base64:路径/字节 → base64(失败 None)。_mime_from_magic:按魔数判 jpeg(FFD8)/png(\x89PNG)/gif,默认 jpeg。image_bytes_to_openai_image_url_part(image_bytes, detail=None)/image_path_to_openai_image_url_part(file_path, detail=None):统一返回{"type":"image_url","image_url":{"url": "data:{mime};base64,...", "detail": 可选}}。供core/nodes.process_tool_artifact(工具截图注入对话)与core/llm(用户上传图片)使用。
tool_call_utils.py —— 工具执行钳点
_invoke_with_timeout(tool, tool_call_input, timeout):daemon 线程执行tool.invoke;用contextvars.copy_context()把调用方上下文(session/turn、agent 名称、sub_agent_context、tool_call_id 等 ContextVar)复制进线程(保证并行工具/子 Agent 隔离);t.join(timeout),超时返回 None;工具异常在主线程 re-raise(保留 ABORTED_BY_USER 冒泡)。invoke_tool_and_build_message(tool, tool_name, tool_args, tool_call_id="", timeout=None)→(content, artifact)。工具执行前的唯一钳点,顺序:- 工作空间越界拦截:
workspace_policy.decision(tool_name, tool_args) == "block"→ 直接返回build_block_message(tool_name, tool_args)(策略模块异常不阻断执行)。 - 人工确认钳点(human_interaction 除外):
classify_tool_execution(...)=="confirm"或 workspace decision=="approve"(附check_violation说明)→ask_human(session_id, meta),meta 含{type:"tool_call", title:"待确认执行工具:{name}", args, instruction, options:["approve","reject","skip"], policy_note, agent_name, is_sub_agent, resolved:false};decision reject/skip → 返回"用户拒绝/已跳过此工具调用({name}):{hint}";timeout →"等待用户确认超时,未执行工具";approve 继续。RuntimeError(ABORTED_BY_USER)向上冒泡。 - 超时取值:显式 timeout >
config_manager.get_tool_timeout(tool_name)(缺省DEFAULT_TOOL_TIMEOUT300)。 human_interaction为阻塞工具绕过超时包装直连 invoke(内部 ask_human 的 ABORTED_BY_USER 直接冒泡 runtime 按暂停处理);其余走_invoke_with_timeout,None→"工具执行超时({n}s)"。- 返回归一化:ToolMessage → (content, artifact);tuple → (str(t0), t1);否则 str(result)。
- 工作空间越界拦截:
json_safe(value):递归转 JSON 可序列化(UUID→str、datetime→isoformat、dict/list/tuple 递归、其余 str)。normalize_tool_call(tool_call):兼容 dict 或对象,args 为 JSON 字符串时解析(失败{"raw": args}),输出固定{"id","name","args"}。供core/routing判断工具执行策略/提取待确认项。split_inline_thinking(text):按最后一个</think>拆分(兼容"思考\n</think>\n\n回复"与<think>思考</think>回复),返回 (thinking, reply);无标记 → ("", text)。extract_reasoning_text(msg):思考开启(xhigh/max/ultra)优先additional_kwargs.reasoning_content,其次 content 内联<think>块;思考关闭(low/high)content 即一句话反思直接返回。供 nodes 与 tool_call_recorder 提取思维链。format_args_summary(args, max_len=80):json.dumps(default=str)截断 + "...",供死循环检测与记录展示。
ppt_utils.py —— PPT 读取/预览(仅 pptx→SVG,不生成 PPT)
- 模块头明确:原先 svg→pptx 制作路线已移除,仅保留 PPT 读取/解析与预览渲染(自研 PPTX 解析,不依赖外部 ppt-master 项目)。
- 常量:
EMU_PER_INCH=914400、EMU_PER_PX=9525、HUNDREDTHS_PT_PER_PX=75、ANGLE_UNIT=60000;XML 命名空间 NS_A/NS_P/NS_R/NS_REL/NS_SVG;_ns/_etree_fromstring。 - 单位换算:
emu_to_px、px_to_emu、fmt_num(去尾零)。 - 颜色:
_resolve_srgb(srgbClr→#RRGGBB)、_find_color_elem、_resolve_color(srgbClr/schemeClr/sysClr/prstClr + alpha)、_parse_color_simple。 OoxmlPackage(pptx_path):zipfile 打开,读_rels/.rels→ presentation.xml(sldSz取画布尺寸 px)→sldIdLst按顺序列出 slide → 每 slide 解析 layout/master 链(_resolve_related);resolve_theme(master)从 master→theme 的clrScheme取主题色映射;iter_slides/read_media/close(支持 with)。Xfrm(坐标变换):x/y/w/h/rot/flip_h/flip_v/ch_*(group),to_svg_transform生成 rotate/translate+scale 字符串;parse_xfrm。- 预设几何
_convert_prst_geom:rect/roundRect(rx=min*0.05)/ellipse/line。 _resolve_fill_svg:noFill→fill:none,solidFill→hex,默认#CCCCCC。_convert_picture:媒体按扩展名映射 MIME,内联data:...;base64的<image>元素(preserveAspectRatio="none")。_convert_txbody:bodyPr 内边距/锚点/换行;段落对齐(l/ctr/r→start/middle/end);run 级 sz(/100 得 px)、b/i、latin/ea typeface、颜色;行高 1.25、y = inner_y + font_size + idx1.25font_size;超出形状高度截断;wrap none 加white-space:pre。ShapeNode(kind: shape/picture/group/graphic)+walk_sp_tree(递归遍历 sp/cxnSp、pic、grpSp、graphicFrame,读 hidden)。- 主入口
convert_pptx_to_svg(pptx_path)→PptxToSvgResult{slides:[SlideSvgResult{index, svg, media_files}], canvas_px, theme_colors}:每页画背景 rect → 遍历形状树_convert_node→ 组装<svg viewBox>。_convert_shape_to_svg/_convert_pic_to_svg/_convert_group_to_svg递归实现。
五、C:\Users\86166\Desktop\Agent_Learning_minor\Agent\src\agent\memory\system_prompt.py(提示词体系)
_BASE_PROMPT(基础主提示词,未命名 MAIN_SYSTEM_PROMPT,实际入口为get_main_system_prompt()):多模态桌面操作助手。要点:理解意图高效完成任务;图片/音频/文件直接多模态分析,不为此调用截图等无关工具;只有信息确实无法获取才调工具;拿到工具结果必须观察实际返回继续推进、严禁编造;同一页面 GUI 连续操作合并为一次 gui_tool 调用(actions 列表),严禁逐个单步;复杂任务才用 todo_list 拆解并 done_step 标记进度,简单任务直接执行;完成时总结。_WS_TAIL:{workspace_dir}占位的工作空间配置(代码/脚本/图片默认存工作空间;terminal_execute 默认 cwd;doc_tool 用绝对路径;越界访问拦截或审批)。get_main_system_prompt(workspace_dir=None)=_BASE_PROMPT + _reflection_section() + _WS_TAIL;set_current_workspace(dir)热改_current_workspace_dir。get_cron_system_prompt(workspace_dir=None)=CRON_MODE_PROMPT.format(workspace_dir=...) + _reflection_section()(.format 失败时替换占位符兜底)。SUMMARY_SYSTEM_PROMPT:Summary Agent 摘要助手(客观准确、抓要点、简洁、保原意;默认压到 20%30% 或 35 句;对话记录总结主题/立场/共识/分歧/行动项;过短回复"内容过短,无法生成有效摘要";多语言同语言输出)。VISION_SYSTEM_PROMPT:专业图像理解助手(客观中文描述、命名品牌/时间/地点/人物、多图编号「图1」「图2」、看不清如实说明、结合图片回答问题)。DB_SYSTEM_PROMT(注意拼写为 PROMT):PostgreSQL 数据分析助手(只读 SELECT/WITH、先展示表/查结构/验证后执行、结果归纳成人话、安全底线、非 DB 问题直接回复)。_PLAN_CORE_RULES(Plan 模式核心规则):任务模糊先human_interaction确认;计划写入工作空间.minorAgent/任务名.md(目标/步骤/产出物);send_file发给用户确认,确认前不得执行;每步完成 doc_tool 更新[x]进度;完成总结并告知计划位置。PLAN_MODE_PROMPT = _PLAN_CORE_RULES + "\n\n" + _BASE_PROMPT;get_plan_system_prompt= PLAN_MODE_PROMPT + 反思段 + WS_TAIL。CRON_MODE_PROMPT:无人值守自主完成;慎用 human_interaction(用户大概率不在场);复杂任务 todo_list;时效性判断(滞后说明/已过时效跳过);执行汇报(做了什么/关键结果/是否有异常);【强制规则】PPT 制作、代码项目等复杂产出或系统操作类任务必须先调 skill_router 查询 Skill 并严格遵循;可自用 cron_manager 管理定时任务;无人值守越界访问直接拦截。REFLECTION_PROMPT(固定反思引导):不需要工具直接回复不输出反思;需要工具先一句话反思再调用;GUI 操作未命中按最新截图重估坐标重试;成功则继续。gui_tool 批量调用规则同上(一次 actions 传全部动作)。_reflection_section():思考关闭档位(low/high)注入 REFLECTION_PROMPT 到系统提示词前缀(前缀缓存友好),思考开启档位(xhigh/max/ultra)返回空(保持模型原生思考语义)。惰性导入 env_utils 防循环依赖。SUB_AGENT_OOM_PROMPT:{total_tokens}/{threshold}占位,子 Agent 上下文超限时注入——立即停止调用工具、整理进度(已完成/关键结果/问题原因)、直接文字回复主 Agent。- Skill Router 相关:
SKILL_ROUTER_PROMPT(从 Skill 列表选 1~3 个,每行一个 name,无匹配输出 NONE)、SKILL_ROUTER_NO_MATCH_MSG/SKILL_ROUTER_EMPTY_MSG/SKILL_ROUTER_NO_QUERY_MSG/SKILL_ROUTER_NO_SKILLS_MSG。 DEFAULT_SUB_AGENT_PROMPT(子 Agent 默认提示词):主 Agent 委派的专业执行助手;专注任务不越权;工作流程(接收→工具逐步完成→清晰摘要→复杂先 todo_list);关键准则(聚焦/高效/如实/按需用工具/结果导向);输出格式(简洁概括、分点编号、[完成]标记)。get_default_sub_agent_prompt()=DEFAULT_SUB_AGENT_PROMPT + _reflection_section()。
六、C:\Users\86166\Desktop\Agent_Learning_minor\Agent\src\agent\agents\agent_runtime.py(动态 Agent 工厂 + Web 入口)
AgentMode = Literal["agent","plan","cron"];缓存_all_agent_runtimes: dict[str, AgentRuntime]、_main_agent_name。_build_llm_for_agent(cfg):从load_models()(env_config 的 models 列表)按cfg.llm_model_id找模型配置,构造Multimodel_LLM(model, api_key, base_url, timeout, max_retries, extra_body=get_thinking_extra_body())——深度思考由全局 THINKING_LEVEL 档位控制,取代 per-model enable_thinking 开关;未匹配回退build_default_llm()(优先主 Agent 配置模型)。_build_agent_runtime(cfg):① 构建 LLM;②get_tools()全量按cfg.tools过滤;主 Agent(role=="main")强制加载 doc_tool 与 todo_list(不可移除);③agent_llm.bind_tools(agent_tools, tool_choice="auto");④build_agent_graph(tools, model_with_tools, trajectory_rounds=cfg.trajectory_rounds);⑤ 组装AgentRuntime(name, description, graph, llm, tools, tools_by_name, system_prompt, max_iterations, trajectory_rounds)。_refresh_subagent_tool_description():遍历非 main 的 runtime 收集sub_names/sub_descriptions,调用主 Agent 中CallSubAgentTool._refresh_description(sub_names, sub_descriptions),让主 Agent 知晓可委派的子 Agent。build_all_agent_runtimes():load_agent_configs()只构建 enabled;第一个role=="main"记为_main_agent_name;无 main 时自动把第一个设为主;先刷新 call_subagent 描述,再重建主 Agent(bind_tools 携带最新子 Agent 列表)。- 取值函数:
get_main_agent_runtime()/get_all_agent_runtimes()/get_agent_runtime(name)(缓存空则自动构建)。 reload_all_agent_runtimes():清空重建;同步模块级引用main_agent_runtime/agent_graph/tools/TOOLS_BY_NAME/llm;再刷新子 Agent 描述;返回{"agents":[{name,description,tools_count,max_iterations}], "main_agent"}。execute_agent(chat_history, session_id="test", agent_mode="agent"):Web/API 层入口,直接委托execute_runtime_agent(runtime=main_agent_runtime, ...)。- 模块导入时(底部)即执行
build_all_agent_runtimes()并缓存main_agent_runtime/agent_graph/tools/TOOLS_BY_NAME/llm。
七、C:\Users\86166\Desktop\Agent_Learning_minor\Agent\src\agent\history\(会话/工具调用记录)
session_storage.py —— 转发层
纯转发 agent.core.storage:ConversationRecordRepository/JsonRecordRepository/MysqlRecordRepository(Protocol+实现)、便捷函数 save_turn/load_messages/load_turn_messages/list_turn_ids/save_tool_calls/get_turn_record/get_latest_turn_record/list_turn_records/persist_live_records/load_session_extra/save_session_extra/list_session_ids/get_last_turn_id/delete_turns_after/delete_tool_records/delete_session,作用域 set_storage_scope/get_storage_scope/get_record_repository。消费方无需感知 json/mysql 后端。
tool_call_recorder.py —— 工具调用记录器(live 实时推送 + 持久化)
- 全局:
TOOL_CALLING_ROOT = Path(SESSIONS_ROOT)(默认history/sessions;cron 子进程改指 CRON_ROOT)。 - 上下文 ContextVar:
_current_turn_id/_current_session_id(带线程安全 fallback_current_turn_fallback/_current_session_fallback)、_sub_agent_context(子 Agent 执行标记)、_current_tool_call_id(并行工具隔离)。配套set_current_turn/get_current_turn/set_current_session/get_current_session/set_current_tool_call_id/get_current_tool_call_id/is_sub_agent_context。 start_live_turn(session_id):turn_id =datetime.now().strftime("%Y%m%d_%H%M%S");init_live_turn_with_id(session_id, turn_id):清空该 session 的_live_records、记_turn_start_times、加_active_sessions、清 pending thinking。record_tool_call_live(session_id, name, args, tool_call_id=None):条目{id, name, args(JSON安全), args_summary(format_args_summary 40 字符), status:"running", result_text:"", 可选 thinking};消费 pending thinking(上一条反思附加到本条);子 Agent 上下文路由到_sub_live_records[(session, tool_call_id)](隔离 key 取父级 call_subagent 的 tool_call_id,并行/同名子 Agent 互不串扰);结尾_notify_live(session_id)。record_tool_result_live(session_id, name, result_text, artifact=None):从后往前找同名 running 条目 → status="done"、result_text 截 500 字、result_images(_artifact_image_data_urls:artifact 内 bytes/image_bytes/images 列表/data_url/http URL → data URL 列表)、result_audio(_artifact_audio_info,仅路径元数据)、result_file_info(_artifact_file_infos:file/image/folder);json 后端额外_safe_persist_live_records增量落盘防崩溃丢失。record_reflection_live(session_id, content):写_think_store(去重,summary=content[:60] 换行转空格)并置 pending thinking;子 Agent 上下文只写_sub_pending_thinking(不独立显示,嵌套在各子工具条目的 thinking 中)。get_live_reflections/clear_live_reflections。- 订阅推送:
subscribe_live(session_id)→ Queue(maxsize=64);unsubscribe_live;_build_live_snapshot(session_id)={tool_calls: get_live_tool_calls(), reflections, started_at, human_requests(惰性导入 human_request)};_notify_live向所有订阅者put_nowait(snapshot)(满则丢最旧放最新)。cron 子进程正是复用该 subscribe_live 转发到主进程。 get_live_tool_calls(session_id):内存优先;空且 session 活跃 → 回退磁盘最近tool_*.json;session 已结束直接返回空(避免跨 turn 残留);并把_sub_live_records按 tool_call_id 精确注入 call_subagent/agent_call 条目的sub_tool_calls(无 id 回退"最后一条 running")。get_turn_started_at(session_id):轮次开始时间戳(内存 → 磁盘回退)。end_live_turn(session_id):清 turn/反思/pending、清理该 session 所有子 Agent 槽、_active_sessions.discard、返回并弹出_live_records。save_aborted_turn(session_id, turn_id):Abort 时把内存 live 记录转 save_turn 兼容格式落盘(meta 含aborted: True)。extract_tool_calls_from_messages(messages):从图执行结果消息提取完整工具链——AIMessage.tool_calls(首个附加 thinking,extract_reasoning_text)→ pending;ToolMessage 配对 → result_text + artifact 落盘(_save_artifact_files→_artifact_{name}_{idx}.png/_file_{name}{ext})+ result_images/audio/file_info;未配对 pending 标记 done。save_turn(session_id, turn_id, user_message, messages):工具调用持久化唯一入口。① extract;② 产物字节直接写session_tool_dir(session_id);③ 从存储读 live 记录(get_turn_record)合并(_merge_extracted_with_live:按索引优先、name+顺序回退,从 live 补 thinking/args_summary);④_inject_sub_traces_to_record注入子 Agent 轨迹;⑤session_storage.save_tool_calls(..., meta={started_at, ended_at, duration})(轮次级计时,单条工具计时已移除)。- 读取 API:
list_turns(session_id, limit=5)(倒序)、get_latest_turn、get_tool_calls_for_latest_turn、get_tool_calls_for_turn、get_turn_meta(started_at/ended_at/duration)、get_thinking_for_turn(从 tool_calls 各条 thinking 提取链)。 _tool_calls_from_turn_record(record, session_id):为每条工具调用补result_files_full(session_tool_dir(sid)/fname绝对路径)、result_images(_artifact_*.png→/api/tool-image?path=...&session={turn_id};仅当 live 无 data URL 时回退)、result_download_files(_file_*→/api/tool-file?path=...&session={turn_id})、result_audio.api_url、turn_id。- 子 Agent 轨迹:
_sub_trace_key = f"{session_id}/{turn_id}/{agent_name}[/{tool_call_id}]";record_sub_agent_trace/get_sub_agent_traces_for_turn(内存_sub_traces_store,并行按 tool_call_id 精确隔离)。 - Token 统计:
set_session_tokens(session_id, total_tokens, breakdown)(内存 + 经 session extra 持久化;breakdown={messages, tools, system} 估算占比由nodes._estimate_token_breakdown算)、get_session_tokens(内存→磁盘→0)、get_session_token_breakdown、clear_session_tokens。 - 目录结构(session_tool_dir):
history/sessions/{session_id}/下:turn_*.json(对话消息,由 ui_session 写)、tool_*.json(每 turn 工具调用记录 + started_at/ended_at/duration)、_artifact_*.png/_file_*(工具产物字节)、session extra(agent_meta键含 cursor/compressed_content/dynamic_tail_history,tokens/token_breakdown);turn 附件在{session_id}/{turn_id}/files/。cron 场景整体换到history/cron/{task_id}/。
八、C:\Users\86166\Desktop\Agent_Learning_minor\Agent\src\web\ui_session.py(会话 UI 状态管理)
_AUDIO_EXTENSIONS = AUDIO_FILE_EXTENSIONS(别名,向后兼容);_memory_session_ids: set[str](内存会话 id 缓存防冲突)。session_dir(session_id)= agent_utils 的sessions/{sid}。list_session_ids_ordered():session_storage.list_session_ids()(mysql 按 sessions 表 updated_at 倒序;json 扫描目录含 turn_*.json 的按 mtime 倒序)。new_timestamp_session_id():datetime.now().strftime("%Y%m%d_%H%M%S"),与磁盘+内存已有 id 冲突时追加_1、_2后缀;新 id 加入_memory_session_ids。prune_memory_session_ids():把已持久化的 id 从内存集合移除,防无限增长(每次会话操作后调用)。delete_session(session_id):删内存缓存 →shutil.rmtree(session_dir)+session_tool_dir→session_storage.delete_session(后端无关)。_build_user_chat_content(query):query={text, files, file_names}→ 多模态 content:图片文件 →{"type":"image_url","image_url":{"url": filepath}, "name": 原名};其余(含音频)→{"path": filepath, "name": 原名}(音频作为普通文件 part,不再生成 audio part;麦克风录音由/api/chat/start阻塞 ASR 转 text,不走此函数);文本 →{"type":"text","text": text};仅一段纯文本时直接返回 str。_sanitize_filename_stem(name):清洗<>:"/\|?*为_、去尾部空格点、空回退 "file"。_next_session_copy_name(dest_dir, session_id, original_name, ext):{stem}_{session_id}_{max_index+1}{ext}(扫描目录现有同名前缀取最大序号递增)。_rewrite_user_files_to_dir(user_msg, dest_dir, session_id):附件从临时目录复制到会话目录的核心——遍历 content list,text 直接保留;对 path/image_url.url/audio_path 源文件:若已在 dest_dir 内跳过;否则_next_session_copy_name生成唯一名 →shutil.copy2(src, dest)→ 原地更新piece["path"](image_url 同时更新image_url.url,audio 更新audio_path)。rewrite_user_message_files_to_session_dir(user_msg, session_id):向后兼容包装,dest = 会话根目录。_serialize_user_msg_for_turn(user_msg):序列化为 turn 文件 user 条目——str 拆 text part;list 中 text 保留、附件转{"type":"image_url"|"file", "relpath": 相对 SESSIONS_ROOT 的路径, "name"}。_turn_user_entry_to_content(user_entry):还原 UI/LLM content——relpath 拼绝对路径并校验在 SESSIONS_ROOT 内;image_url part 文件存在给{"type":"image_url","image_url":{"url": abs}, "path", "name"},缺失累计any_image_missing;历史 audio part 降级为普通文件;file part 缺失 →[附件已丢失: name];有图丢失补[图片已丢失];纯文本单段直接返回 str。_serialize_assistant_entry:{text, output_type, turn_id};voice 输出额外autoplay/audio_duration_seconds/audio_relpath(相对 SESSIONS_ROOT)。_serialize_message_for_turn(msg):按 role 分发 →{"role":"user","user":...}/{"role":"assistant","assistant":...}。sync_turn_from_chat_history(session_id, chat_history, latest_user_image_description):turn 级消息落盘——get_last_turn_id得 turn_id;从最后一条 user 消息起截取本轮消息区间;逐条序列化并把turn_id注入 meta;save_turn_messages(session_id, turn_id, messages_out)(内部 →session_storage.save_turn)。rollback_turns_from_history(session_id, chat_history):从 chat_history 找最后一个 turn_id,delete_turns_after(session_id, keep_turn_id)删除多余 turn 目录。_assistant_turn_entry_to_content/_assistant_turn_entry_to_message:还原 assistant 消息(text/voice →{"type":"text"}+{"type":"audio","audio_path","autoplay","duration_seconds"},meta 含 output_type/autoplay/pending_action/turn_id/audio{path,format:"wav",sample_rate:24000})。_load_ui_messages_from_turn(session_id)/load_ui_messages_for_session(session_id):从 turn 文件重建 UI 消息列表(无数据返回空列表)。rewrite_user_files_for_last_turn(chat_history, session_id):若末条为 user 消息,将其附件复制到会话目录(API 层添加用户消息后立即调用)。add_message(chat_history, query):_build_user_chat_content→ 无有效内容返回 None,否则 append{"role":"user","content": user_content}并返回新 chat_history(API 层构造用户消息核心)。clear_ui_and_turns(session_id):delete_turns_after(session_id, "00000000_000000")全删,返回([], {"text":"","files":[]})。
附:跨模块关键链路速查
- 语音输入:
/api/chat/start(阻塞 ASR)→agent_utils.audio_file_to_text→ POST{ASR_BASE_URL}/v1/chat/completions(audio_url data URL,无文本提示)→choices[0].message.content。 - 语音输出:
StreamingTTSClient.stream_tts(SSE 逐事件收 PCM f32le chunk)→ 前端 Web Audio 播放;服务端新请求自动打断旧请求。 - 图片入模:
image_utils.image_path_to_openai_image_url_part(缩放→base64→data URL)或Documents_process.process(docx/pdf/pptx 提取文本+图片字节)。 - 工具执行:
tool_call_utils.invoke_tool_and_build_message(workspace block → confirm ask_human → 超时 → 归一化 ToolMessage)→tool_call_recorder实时/持久化记录 → SSE 推送前端。 - Cron:
CronScheduler(5s 扫描 + 时段互斥)→ 子进程agent.cron.runner(重定向存储根 + headless + execute_agent(agent_mode="cron"))→ HTTP 回传:8765/api/cron/internal/live|finished→ 主进程cron.live分发 SSE。