feihong-code 8.0.0 → 8.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +193 -193
- package/CHANGELOG.md +140 -117
- package/README.md +656 -636
- package/agent-team/.env.example +13 -0
- package/agent-team/README.md +42 -0
- package/agent-team/agents/__init__.py +0 -0
- package/agent-team/agents/l1_acquisition.py +207 -0
- package/agent-team/agents/l2_content.py +237 -0
- package/agent-team/agents/l3_conversion.py +148 -0
- package/agent-team/agents/l4_analysis.py +118 -0
- package/agent-team/agents/l5_management.py +52 -0
- package/agent-team/agents/registry.py +60 -0
- package/agent-team/config.py +39 -0
- package/agent-team/core/__init__.py +0 -0
- package/agent-team/core/agent.py +73 -0
- package/agent-team/core/models.py +347 -0
- package/agent-team/core/scheduler.py +150 -0
- package/agent-team/core/task.py +121 -0
- package/agent-team/feihongzhi.db +0 -0
- package/agent-team/llm/__init__.py +12 -0
- package/agent-team/llm/base.py +22 -0
- package/agent-team/llm/doubao_llm.py +52 -0
- package/agent-team/llm/mock_llm.py +42 -0
- package/agent-team/main.py +235 -0
- package/agent-team/requirements.txt +2 -0
- package/agent-team/skill_packager.py +248 -0
- package/agent-team/skills/01_AI/350/216/267/345/256/242/347/272/277/347/264/242/351/233/267/350/276/276/SKILL.md +40 -0
- package/agent-team/skills/01_AI/350/216/267/345/256/242/347/272/277/347/264/242/351/233/267/350/276/276/run.py +111 -0
- package/agent-team/skills/02_AI/351/224/200/345/224/256/350/267/237/350/277/233/351/224/200/345/206/240/SKILL.md +38 -0
- package/agent-team/skills/02_AI/351/224/200/345/224/256/350/267/237/350/277/233/351/224/200/345/206/240/run.py +108 -0
- package/agent-team/skills/03_/350/257/204/350/256/272/345/214/272/345/225/206/346/234/272/350/257/206/345/210/253/345/256/230/SKILL.md +38 -0
- package/agent-team/skills/03_/350/257/204/350/256/272/345/214/272/345/225/206/346/234/272/350/257/206/345/210/253/345/256/230/run.py +106 -0
- package/agent-team/skills/04_/345/256/242/346/210/267/350/201/212/345/244/251/346/210/220/344/272/244/345/210/206/346/236/220/345/256/230/SKILL.md +37 -0
- package/agent-team/skills/04_/345/256/242/346/210/267/350/201/212/345/244/251/346/210/220/344/272/244/345/210/206/346/236/220/345/256/230/run.py +107 -0
- package/agent-team/skills/05_/345/220/214/350/241/214/347/210/206/346/254/276/351/233/267/350/276/276/SKILL.md +38 -0
- package/agent-team/skills/05_/345/220/214/350/241/214/347/210/206/346/254/276/351/233/267/350/276/276/run.py +99 -0
- package/agent-team/skills/06_/346/257/217/346/227/245/347/262/276/345/207/206/350/216/267/345/256/242/351/200/211/351/242/230/345/256/230/SKILL.md +39 -0
- package/agent-team/skills/06_/346/257/217/346/227/245/347/262/276/345/207/206/350/216/267/345/256/242/351/200/211/351/242/230/345/256/230/run.py +103 -0
- package/agent-team/skills/07_AI/345/244/226/350/264/270/345/274/200/345/217/221/345/256/242/346/210/267/345/221/230/345/267/245/SKILL.md +39 -0
- package/agent-team/skills/07_AI/345/244/226/350/264/270/345/274/200/345/217/221/345/256/242/346/210/267/345/221/230/345/267/245/run.py +103 -0
- package/agent-team/skills/08_/345/260/217/347/272/242/344/271/246/347/262/276/345/207/206/350/216/267/345/256/242/345/221/230/345/267/245/SKILL.md +40 -0
- package/agent-team/skills/08_/345/260/217/347/272/242/344/271/246/347/262/276/345/207/206/350/216/267/345/256/242/345/221/230/345/267/245/run.py +97 -0
- package/agent-team/skills/09_/347/247/201/345/237/237/345/256/242/346/210/267/345/210/206/345/261/202/350/267/237/350/277/233/345/256/230/SKILL.md +38 -0
- package/agent-team/skills/09_/347/247/201/345/237/237/345/256/242/346/210/267/345/210/206/345/261/202/350/267/237/350/277/233/345/256/230/run.py +106 -0
- package/agent-team/skills/10_GEO/345/225/206/346/234/272/350/257/212/346/226/255/345/256/230/SKILL.md +38 -0
- package/agent-team/skills/10_GEO/345/225/206/346/234/272/350/257/212/346/226/255/345/256/230/run.py +108 -0
- package/agent-team/skills/11_/344/270/200/344/272/272/345/205/254/345/217/270AI/345/221/230/345/267/245/350/260/203/345/272/246/345/256/230/SKILL.md +37 -0
- package/agent-team/skills/11_/344/270/200/344/272/272/345/205/254/345/217/270AI/345/221/230/345/267/245/350/260/203/345/272/246/345/256/230/run.py +111 -0
- package/agent-team/skills/12_/350/200/201/346/235/277/347/237/255/350/247/206/351/242/221/350/216/267/345/256/242/350/204/232/346/234/254/345/256/230/SKILL.md +39 -0
- package/agent-team/skills/12_/350/200/201/346/235/277/347/237/255/350/247/206/351/242/221/350/216/267/345/256/242/350/204/232/346/234/254/345/256/230/run.py +104 -0
- package/agent-team/skills/13_/345/215/232/344/270/273/350/264/246/345/217/267/346/267/261/345/272/246/346/213/206/350/247/243/345/256/230/SKILL.md +37 -0
- package/agent-team/skills/13_/345/215/232/344/270/273/350/264/246/345/217/267/346/267/261/345/272/246/346/213/206/350/247/243/345/256/230/run.py +105 -0
- package/agent-team/skills/14_AI/347/237/255/350/247/206/351/242/221/350/207/252/345/212/250/345/211/252/350/276/221/345/270/210/SKILL.md +39 -0
- package/agent-team/skills/14_AI/347/237/255/350/247/206/351/242/221/350/207/252/345/212/250/345/211/252/350/276/221/345/270/210/run.py +108 -0
- package/agent-team/skills/15_/345/205/254/344/274/227/345/217/267/350/207/252/345/212/250/350/277/220/350/220/245/345/217/221/345/270/203/345/256/230/SKILL.md +38 -0
- package/agent-team/skills/15_/345/205/254/344/274/227/345/217/267/350/207/252/345/212/250/350/277/220/350/220/245/345/217/221/345/270/203/345/256/230/run.py +105 -0
- package/agent-team/skills/16_/350/241/214/344/270/232/346/267/261/345/272/246/345/210/206/346/236/220/345/270/210/SKILL.md +38 -0
- package/agent-team/skills/16_/350/241/214/344/270/232/346/267/261/345/272/246/345/210/206/346/236/220/345/270/210/run.py +108 -0
- package/agent-team/skills/17_/346/212/225/350/265/204/347/240/224/347/251/266/345/222/250/350/257/242/345/256/230/SKILL.md +38 -0
- package/agent-team/skills/17_/346/212/225/350/265/204/347/240/224/347/251/266/345/222/250/350/257/242/345/256/230/run.py +106 -0
- package/agent-team/tests/__init__.py +0 -0
- package/agent-team/tests/test_core.py +156 -0
- package/dist/agent/agent-session.js +103 -0
- package/dist/agent/agent-session.js.map +1 -0
- package/dist/agent/orchestrator.js +8 -0
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/steer.js +52 -0
- package/dist/agent/steer.js.map +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/node-guard.js +17 -0
- package/dist/cli/node-guard.js.map +1 -0
- package/dist/cli/run.js +2 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/enterprise/policy.js +40 -17
- package/dist/enterprise/policy.js.map +1 -1
- package/dist/tools/agents/agents-bridge.js +144 -0
- package/dist/tools/agents/agents-bridge.js.map +1 -0
- package/dist/tools/desktop/desktop-guard.js +81 -0
- package/dist/tools/desktop/desktop-guard.js.map +1 -0
- package/dist/tools/desktop/desktop.tool.js +201 -0
- package/dist/tools/desktop/desktop.tool.js.map +1 -0
- package/dist/tools/desktop/feihong-win.js +163 -0
- package/dist/tools/desktop/feihong-win.js.map +1 -0
- package/dist/tools/desktop/index.js +37 -0
- package/dist/tools/desktop/index.js.map +1 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/mcp/index.js +10 -2
- package/dist/tools/mcp/index.js.map +1 -1
- package/dist/tools/mcp/mcp-client.js +4 -0
- package/dist/tools/mcp/mcp-client.js.map +1 -1
- package/dist/tools/sandbox.js +28 -0
- package/dist/tools/sandbox.js.map +1 -1
- package/dist/web/public/public/css/style.css +1756 -0
- package/dist/web/public/public/favicon.svg +11 -0
- package/dist/web/public/public/index.html +1246 -0
- package/dist/web/public/public/js/api.js +558 -0
- package/dist/web/public/public/js/app.js +2233 -0
- package/dist/web/public/public/js/mock-api.js +259 -0
- package/dist/web/public/public/js/monaco-editor.js +363 -0
- package/dist/web/public/public/js/ui.js +2217 -0
- package/dist/web/public/public/js/utils.js +214 -0
- package/dist/web/public/public/manifest.json +59 -0
- package/dist/web/public/public/robots.txt +14 -0
- package/dist/web/public/public/service-worker.js +149 -0
- package/docs/AI/345/221/230/345/267/245/345/206/233/345/233/242-/346/225/264/345/220/210/350/257/264/346/230/216.md +68 -0
- package/docs/API.md +1 -1
- package/docs/App/346/212/200/346/234/257/350/257/264/346/230/216/344/271/246.md +3 -3
- package/docs/P1.1-P3-P5/345/267/245/347/250/213/345/256/236/346/226/275/346/200/273/347/273/223.md +328 -0
- package/docs/P5-Pi/345/257/271/347/205/247/350/257/204/346/265/213/346/212/245/345/221/212-mock/346/250/241/345/274/217.json +125 -0
- package/docs/P5-Pi/345/257/271/347/205/247/350/257/204/346/265/213/346/212/245/345/221/212-/347/234/237/345/256/236/346/250/241/345/236/213.json +138 -0
- package/docs/P5-pi-compare-real-report.json +138 -0
- package/docs/P5-pi-compare-report.json +125 -0
- package/docs/Pi-Agent/346/216/245/345/205/245/351/243/236/350/231/271Code-MVP/351/233/206/346/210/220/344/272/244/344/273/230/347/211/251.md +355 -0
- package/docs/Pi-Agent/346/216/245/345/205/245/351/243/236/350/231/271Code/344/273/267/345/200/274/345/210/206/346/236/220-/345/274/200/345/217/221/346/226/207/346/241/243.md +291 -0
- package/docs/Pi-Agent/346/216/245/345/205/245/351/243/236/350/231/271Code/344/273/267/345/200/274/351/207/215/344/274/260-/345/217/257/350/247/206/345/214/226.html +84 -0
- package/docs/SWE-bench-300-agnes-2.5-flash-/346/234/200/347/273/210/346/212/245/345/221/212.json +2012 -0
- package/docs/SWE-bench-300-agnes-2.5-flash-/350/267/221/345/210/206/346/212/245/345/221/212.md +74 -0
- package/docs/SWE-bench-Lite-DeepSeek-V4-Flash-/350/267/221/345/210/206/346/212/245/345/221/212.md +102 -0
- package/docs/TECHNICAL-SPECIFICATION.md +66 -0
- package/docs/TECHNICAL-SPECIFICATION.pdf +0 -0
- package/docs/Windows/346/211/213/350/204/232-POC/346/274/224/347/244/272/350/204/232/346/234/254.md +131 -0
- package/docs/Windows/346/211/213/350/204/232/346/216/245/345/205/245/346/214/207/345/215/227.md +181 -0
- package/docs//344/275/277/347/224/250/350/257/264/346/230/216/344/271/246-v8.0.1.md +146 -0
- package/docs//344/275/277/347/224/250/350/257/264/346/230/216/344/271/246.md +1 -1
- package/package.json +6 -3
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""数据模型与 SQLite 数据访问层。
|
|
2
|
+
|
|
3
|
+
对齐开发文档《统一协议与数据模型》:
|
|
4
|
+
- leads 线索(含来源渠道、状态、分级、实时录入时间)
|
|
5
|
+
- customers 客户(五列格式:公司名称 / 客户姓名 / 联系电话 / 联系时间 / 备注)
|
|
6
|
+
- content_assets 内容资产(选题 / 脚本 / 成片 / 文章)
|
|
7
|
+
- tasks 任务(任务协议:状态、优先级、重试、人工确认、trace)
|
|
8
|
+
|
|
9
|
+
所有写操作使用参数化 SQL,时间字段一律记录实时时间戳。
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import sqlite3
|
|
13
|
+
import threading
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
import config
|
|
19
|
+
|
|
20
|
+
_lock = threading.Lock()
|
|
21
|
+
|
|
22
|
+
SCHEMA = """
|
|
23
|
+
CREATE TABLE IF NOT EXISTS leads (
|
|
24
|
+
lead_id TEXT PRIMARY KEY,
|
|
25
|
+
source_channel TEXT NOT NULL,
|
|
26
|
+
company_name TEXT DEFAULT '',
|
|
27
|
+
contact_name TEXT DEFAULT '',
|
|
28
|
+
contact_phone TEXT DEFAULT '',
|
|
29
|
+
status TEXT DEFAULT 'new',
|
|
30
|
+
grade TEXT DEFAULT 'C',
|
|
31
|
+
remark TEXT DEFAULT '',
|
|
32
|
+
created_at TEXT NOT NULL
|
|
33
|
+
);
|
|
34
|
+
CREATE TABLE IF NOT EXISTS customers (
|
|
35
|
+
customer_id TEXT PRIMARY KEY,
|
|
36
|
+
company_name TEXT DEFAULT '',
|
|
37
|
+
contact_name TEXT DEFAULT '',
|
|
38
|
+
contact_phone TEXT DEFAULT '',
|
|
39
|
+
contact_time TEXT DEFAULT '',
|
|
40
|
+
remark TEXT DEFAULT '',
|
|
41
|
+
tier TEXT DEFAULT 'untiered'
|
|
42
|
+
);
|
|
43
|
+
CREATE TABLE IF NOT EXISTS content_assets (
|
|
44
|
+
content_id TEXT PRIMARY KEY,
|
|
45
|
+
content_type TEXT DEFAULT '',
|
|
46
|
+
topic TEXT DEFAULT '',
|
|
47
|
+
script TEXT DEFAULT '',
|
|
48
|
+
asset_url TEXT DEFAULT '',
|
|
49
|
+
publish_status TEXT DEFAULT 'draft',
|
|
50
|
+
metrics TEXT DEFAULT '{}'
|
|
51
|
+
);
|
|
52
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
53
|
+
task_id TEXT PRIMARY KEY,
|
|
54
|
+
task_type TEXT NOT NULL,
|
|
55
|
+
module_id TEXT NOT NULL,
|
|
56
|
+
status TEXT NOT NULL,
|
|
57
|
+
priority TEXT DEFAULT 'P1',
|
|
58
|
+
input_json TEXT DEFAULT '{}',
|
|
59
|
+
output_json TEXT DEFAULT '{}',
|
|
60
|
+
retry_count INTEGER DEFAULT 0,
|
|
61
|
+
need_human INTEGER DEFAULT 0,
|
|
62
|
+
trace TEXT DEFAULT '[]',
|
|
63
|
+
created_at TEXT NOT NULL,
|
|
64
|
+
finished_at TEXT DEFAULT ''
|
|
65
|
+
);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status, priority, created_at);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_leads_status ON leads(status);
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def utcnow() -> str:
|
|
72
|
+
"""实时时间戳(本地时间,YYYY-MM-DD HH:MM:SS)。"""
|
|
73
|
+
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _connect(db_path: Optional[str] = None) -> sqlite3.Connection:
|
|
77
|
+
path = db_path or config.DB_PATH
|
|
78
|
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
conn = sqlite3.connect(path, check_same_thread=False)
|
|
80
|
+
conn.row_factory = sqlite3.Row
|
|
81
|
+
conn.execute("PRAGMA journal_mode=WAL;")
|
|
82
|
+
return conn
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def init_db(db_path: Optional[str] = None) -> None:
|
|
86
|
+
"""初始化全部表结构(幂等)。"""
|
|
87
|
+
conn = _connect(db_path)
|
|
88
|
+
try:
|
|
89
|
+
with _lock:
|
|
90
|
+
conn.executescript(SCHEMA)
|
|
91
|
+
conn.commit()
|
|
92
|
+
finally:
|
|
93
|
+
conn.close()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _j(d: Any) -> str:
|
|
97
|
+
return json.dumps(d, ensure_ascii=False)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _uj(s: str) -> Any:
|
|
101
|
+
try:
|
|
102
|
+
return json.loads(s) if s else {}
|
|
103
|
+
except (TypeError, ValueError):
|
|
104
|
+
return {}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------- leads
|
|
108
|
+
def insert_lead(lead: Dict[str, str]) -> Dict[str, str]:
|
|
109
|
+
conn = _connect()
|
|
110
|
+
try:
|
|
111
|
+
with _lock:
|
|
112
|
+
conn.execute(
|
|
113
|
+
"INSERT INTO leads (lead_id, source_channel, company_name, contact_name,"
|
|
114
|
+
" contact_phone, status, grade, remark, created_at)"
|
|
115
|
+
" VALUES (?,?,?,?,?,?,?,?,?)",
|
|
116
|
+
(lead["lead_id"], lead.get("source_channel", ""), lead.get("company_name", ""),
|
|
117
|
+
lead.get("contact_name", ""), lead.get("contact_phone", ""),
|
|
118
|
+
lead.get("status", "new"), lead.get("grade", "C"),
|
|
119
|
+
lead.get("remark", ""), utcnow()),
|
|
120
|
+
)
|
|
121
|
+
conn.commit()
|
|
122
|
+
return lead
|
|
123
|
+
finally:
|
|
124
|
+
conn.close()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def list_leads(status: Optional[str] = None) -> List[Dict[str, str]]:
|
|
128
|
+
conn = _connect()
|
|
129
|
+
try:
|
|
130
|
+
if status:
|
|
131
|
+
rows = conn.execute("SELECT * FROM leads WHERE status=? ORDER BY created_at", (status,)).fetchall()
|
|
132
|
+
else:
|
|
133
|
+
rows = conn.execute("SELECT * FROM leads ORDER BY created_at").fetchall()
|
|
134
|
+
return [dict(r) for r in rows]
|
|
135
|
+
finally:
|
|
136
|
+
conn.close()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def update_lead(lead_id: str, **fields: str) -> None:
|
|
140
|
+
if not fields:
|
|
141
|
+
return
|
|
142
|
+
conn = _connect()
|
|
143
|
+
try:
|
|
144
|
+
with _lock:
|
|
145
|
+
cols = ", ".join(f"{k}=?" for k in fields)
|
|
146
|
+
conn.execute(f"UPDATE leads SET {cols} WHERE lead_id=?", (*fields.values(), lead_id))
|
|
147
|
+
conn.commit()
|
|
148
|
+
finally:
|
|
149
|
+
conn.close()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ------------------------------------------------------------- customers
|
|
153
|
+
def insert_customer(cust: Dict[str, str]) -> Dict[str, str]:
|
|
154
|
+
"""客户五列格式入库:公司名称 / 客户姓名 / 联系电话 / 联系时间 / 备注。"""
|
|
155
|
+
conn = _connect()
|
|
156
|
+
try:
|
|
157
|
+
with _lock:
|
|
158
|
+
conn.execute(
|
|
159
|
+
"INSERT INTO customers (customer_id, company_name, contact_name,"
|
|
160
|
+
" contact_phone, contact_time, remark, tier) VALUES (?,?,?,?,?,?,?)",
|
|
161
|
+
(cust["customer_id"], cust.get("company_name", ""), cust.get("contact_name", ""),
|
|
162
|
+
cust.get("contact_phone", ""), cust.get("contact_time", "") or utcnow(),
|
|
163
|
+
cust.get("remark", ""), cust.get("tier", "untiered")),
|
|
164
|
+
)
|
|
165
|
+
conn.commit()
|
|
166
|
+
return cust
|
|
167
|
+
finally:
|
|
168
|
+
conn.close()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def list_customers() -> List[Dict[str, str]]:
|
|
172
|
+
conn = _connect()
|
|
173
|
+
try:
|
|
174
|
+
return [dict(r) for r in conn.execute("SELECT * FROM customers ORDER BY contact_time").fetchall()]
|
|
175
|
+
finally:
|
|
176
|
+
conn.close()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def upsert_customer(company_name: str = "", contact_name: str = "",
|
|
180
|
+
contact_phone: str = "", remark: str = "",
|
|
181
|
+
tier: str = "untiered", customer_id: Optional[str] = None,
|
|
182
|
+
contact_time: Optional[str] = None) -> str:
|
|
183
|
+
"""按联系电话查重:存在则更新联系时间/备注/分层,不存在则新增。返回 customer_id。"""
|
|
184
|
+
conn = _connect()
|
|
185
|
+
try:
|
|
186
|
+
with _lock:
|
|
187
|
+
if contact_phone:
|
|
188
|
+
row = conn.execute("SELECT customer_id FROM customers WHERE contact_phone=?",
|
|
189
|
+
(contact_phone,)).fetchone()
|
|
190
|
+
else:
|
|
191
|
+
row = None
|
|
192
|
+
if row:
|
|
193
|
+
cid = row["customer_id"]
|
|
194
|
+
now = contact_time or utcnow()
|
|
195
|
+
conn.execute(
|
|
196
|
+
"UPDATE customers SET company_name=?, contact_name=?, remark=?, tier=?,"
|
|
197
|
+
" contact_time=? WHERE customer_id=?",
|
|
198
|
+
(company_name, contact_name, remark, tier, now, cid))
|
|
199
|
+
else:
|
|
200
|
+
cid = customer_id or f"C{int(datetime.now().timestamp())}"
|
|
201
|
+
now = contact_time or utcnow()
|
|
202
|
+
conn.execute(
|
|
203
|
+
"INSERT INTO customers (customer_id, company_name, contact_name,"
|
|
204
|
+
" contact_phone, contact_time, remark, tier) VALUES (?,?,?,?,?,?,?)",
|
|
205
|
+
(cid, company_name, contact_name, contact_phone, now, remark, tier))
|
|
206
|
+
conn.commit()
|
|
207
|
+
return cid
|
|
208
|
+
finally:
|
|
209
|
+
conn.close()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def update_customer(customer_id: str, **fields: str) -> None:
|
|
213
|
+
if not fields:
|
|
214
|
+
return
|
|
215
|
+
conn = _connect()
|
|
216
|
+
try:
|
|
217
|
+
with _lock:
|
|
218
|
+
cols = ", ".join(f"{k}=?" for k in fields)
|
|
219
|
+
conn.execute(f"UPDATE customers SET {cols} WHERE customer_id=?", (*fields.values(), customer_id))
|
|
220
|
+
conn.commit()
|
|
221
|
+
finally:
|
|
222
|
+
conn.close()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# -------------------------------------------------------- content_assets
|
|
226
|
+
def insert_asset(asset: Dict[str, Any]) -> Dict[str, Any]:
|
|
227
|
+
conn = _connect()
|
|
228
|
+
try:
|
|
229
|
+
with _lock:
|
|
230
|
+
conn.execute(
|
|
231
|
+
"INSERT INTO content_assets (content_id, content_type, topic, script,"
|
|
232
|
+
" asset_url, publish_status, metrics) VALUES (?,?,?,?,?,?,?)",
|
|
233
|
+
(asset["content_id"], asset.get("content_type", ""), asset.get("topic", ""),
|
|
234
|
+
asset.get("script", ""), asset.get("asset_url", ""),
|
|
235
|
+
asset.get("publish_status", "draft"), _j(asset.get("metrics", {}))),
|
|
236
|
+
)
|
|
237
|
+
conn.commit()
|
|
238
|
+
return asset
|
|
239
|
+
finally:
|
|
240
|
+
conn.close()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def list_assets(content_type: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
244
|
+
conn = _connect()
|
|
245
|
+
try:
|
|
246
|
+
if content_type:
|
|
247
|
+
rows = conn.execute("SELECT * FROM content_assets WHERE content_type=?", (content_type,)).fetchall()
|
|
248
|
+
else:
|
|
249
|
+
rows = conn.execute("SELECT * FROM content_assets").fetchall()
|
|
250
|
+
out = []
|
|
251
|
+
for r in rows:
|
|
252
|
+
d = dict(r)
|
|
253
|
+
d["metrics"] = _uj(d.get("metrics", "{}"))
|
|
254
|
+
out.append(d)
|
|
255
|
+
return out
|
|
256
|
+
finally:
|
|
257
|
+
conn.close()
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# ----------------------------------------------------------------- tasks
|
|
261
|
+
def insert_task(task: Dict[str, Any]) -> Dict[str, Any]:
|
|
262
|
+
conn = _connect()
|
|
263
|
+
try:
|
|
264
|
+
with _lock:
|
|
265
|
+
conn.execute(
|
|
266
|
+
"INSERT INTO tasks (task_id, task_type, module_id, status, priority,"
|
|
267
|
+
" input_json, output_json, retry_count, need_human, trace, created_at, finished_at)"
|
|
268
|
+
" VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
269
|
+
(task["task_id"], task["task_type"], task["module_id"], task["status"],
|
|
270
|
+
task.get("priority", "P1"), _j(task.get("input_json", {})),
|
|
271
|
+
_j(task.get("output_json", {})), task.get("retry_count", 0),
|
|
272
|
+
1 if task.get("need_human") else 0, _j(task.get("trace", [])),
|
|
273
|
+
task.get("created_at", utcnow()), task.get("finished_at", "")),
|
|
274
|
+
)
|
|
275
|
+
conn.commit()
|
|
276
|
+
return task
|
|
277
|
+
finally:
|
|
278
|
+
conn.close()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def get_task(task_id: str) -> Optional[Dict[str, Any]]:
|
|
282
|
+
conn = _connect()
|
|
283
|
+
try:
|
|
284
|
+
row = conn.execute("SELECT * FROM tasks WHERE task_id=?", (task_id,)).fetchone()
|
|
285
|
+
if not row:
|
|
286
|
+
return None
|
|
287
|
+
d = dict(row)
|
|
288
|
+
d["input_json"] = _uj(d.get("input_json", "{}"))
|
|
289
|
+
d["output_json"] = _uj(d.get("output_json", "{}"))
|
|
290
|
+
d["trace"] = _uj(d.get("trace", "[]"))
|
|
291
|
+
d["need_human"] = bool(d.get("need_human"))
|
|
292
|
+
return d
|
|
293
|
+
finally:
|
|
294
|
+
conn.close()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def update_task(task_id: str, **fields: Any) -> None:
|
|
298
|
+
if not fields:
|
|
299
|
+
return
|
|
300
|
+
conn = _connect()
|
|
301
|
+
try:
|
|
302
|
+
with _lock:
|
|
303
|
+
cols = ", ".join(f"{k}=?" for k in fields)
|
|
304
|
+
vals = []
|
|
305
|
+
for v in fields.values():
|
|
306
|
+
if isinstance(v, (dict, list)):
|
|
307
|
+
vals.append(_j(v))
|
|
308
|
+
else:
|
|
309
|
+
vals.append(v)
|
|
310
|
+
conn.execute(f"UPDATE tasks SET {cols} WHERE task_id=?", (*vals, task_id))
|
|
311
|
+
conn.commit()
|
|
312
|
+
finally:
|
|
313
|
+
conn.close()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def list_tasks(status: Optional[str] = None, module_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
317
|
+
conn = _connect()
|
|
318
|
+
try:
|
|
319
|
+
sql, args = "SELECT * FROM tasks WHERE 1=1", []
|
|
320
|
+
if status:
|
|
321
|
+
sql += " AND status=?"
|
|
322
|
+
args.append(status)
|
|
323
|
+
if module_id:
|
|
324
|
+
sql += " AND module_id=?"
|
|
325
|
+
args.append(module_id)
|
|
326
|
+
sql += " ORDER BY created_at"
|
|
327
|
+
rows = conn.execute(sql, args).fetchall()
|
|
328
|
+
out = []
|
|
329
|
+
for r in rows:
|
|
330
|
+
d = dict(r)
|
|
331
|
+
d["input_json"] = _uj(d.get("input_json", "{}"))
|
|
332
|
+
d["output_json"] = _uj(d.get("output_json", "{}"))
|
|
333
|
+
d["trace"] = _uj(d.get("trace", "[]"))
|
|
334
|
+
d["need_human"] = bool(d.get("need_human"))
|
|
335
|
+
out.append(d)
|
|
336
|
+
return out
|
|
337
|
+
finally:
|
|
338
|
+
conn.close()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def count_tasks() -> Dict[str, int]:
|
|
342
|
+
conn = _connect()
|
|
343
|
+
try:
|
|
344
|
+
rows = conn.execute("SELECT status, COUNT(*) AS c FROM tasks GROUP BY status").fetchall()
|
|
345
|
+
return {r["status"]: r["c"] for r in rows}
|
|
346
|
+
finally:
|
|
347
|
+
conn.close()
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""11 调度官:任务创建、分派、状态机、重试、人工确认与告警。
|
|
2
|
+
|
|
3
|
+
对齐开发文档《调度编排流程》:
|
|
4
|
+
1. 接收触发:定时 / 事件 / 人工;
|
|
5
|
+
2. 校验依赖(前置任务 success、数据就绪),不满足则 blocked 提示;
|
|
6
|
+
3. 按优先级分派执行,全程记录 trace;
|
|
7
|
+
4. 失败自动重试(上限 MAX_RETRY),超限转 blocked 并告警;
|
|
8
|
+
涉及对外动作(need_human)的任务执行后进入 awaiting_human,人工确认后 success。
|
|
9
|
+
"""
|
|
10
|
+
import logging
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
import config
|
|
15
|
+
from core import models
|
|
16
|
+
from core.agent import BaseAgent
|
|
17
|
+
from core.task import Task, TaskStatus
|
|
18
|
+
|
|
19
|
+
log = logging.getLogger("scheduler")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Scheduler:
|
|
23
|
+
def __init__(self, agents: Dict[str, BaseAgent]):
|
|
24
|
+
self.agents = agents
|
|
25
|
+
self.max_retry = config.MAX_RETRY
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------- 提交
|
|
28
|
+
def submit(self, module_id: str, task_type: str, input_data: Dict[str, Any],
|
|
29
|
+
priority: str = "P1", need_human: Optional[bool] = None,
|
|
30
|
+
task_id: Optional[str] = None) -> Task:
|
|
31
|
+
if module_id not in self.agents:
|
|
32
|
+
raise ValueError(f"未知模块编号: {module_id}(可用模块见 list-agents)")
|
|
33
|
+
agent = self.agents[module_id]
|
|
34
|
+
# 契约校验前置:尽早暴露输入错误
|
|
35
|
+
agent.validate_input(input_data)
|
|
36
|
+
human = agent.need_human if need_human is None else need_human
|
|
37
|
+
task = Task(task_type=task_type, module_id=module_id,
|
|
38
|
+
input_json=input_data, priority=priority, need_human=human)
|
|
39
|
+
if task_id:
|
|
40
|
+
task.task_id = task_id
|
|
41
|
+
models.insert_task(task.to_dict())
|
|
42
|
+
log.info("已提交任务 %s -> %s(%s) pri=%s", task.task_id, module_id, task_type, priority)
|
|
43
|
+
return task
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------- 执行一轮
|
|
46
|
+
def run_once(self, limit: int = 10) -> List[Dict[str, Any]]:
|
|
47
|
+
"""拉取 pending 任务(按优先级 + 创建时间),逐个分派执行。返回执行结果列表。"""
|
|
48
|
+
results: List[Dict[str, Any]] = []
|
|
49
|
+
rows = models.list_tasks(status=TaskStatus.PENDING.value)
|
|
50
|
+
rows.sort(key=lambda r: ({"P0": 0, "P1": 1, "P2": 2, "P3": 3}.get(r["priority"], 1), r["created_at"]))
|
|
51
|
+
for row in rows[:limit]:
|
|
52
|
+
results.append(self._dispatch(Task.from_dict(row)))
|
|
53
|
+
return results
|
|
54
|
+
|
|
55
|
+
def _dispatch(self, task: Task) -> Dict[str, Any]:
|
|
56
|
+
agent = self.agents.get(task.module_id)
|
|
57
|
+
if agent is None:
|
|
58
|
+
self._fail(task, f"模块 {task.module_id} 未注册")
|
|
59
|
+
return {"task_id": task.task_id, "status": task.status.value, "error": "模块未注册"}
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
task.transition(TaskStatus.RUNNING)
|
|
63
|
+
task.append_trace("dispatch", f"module={task.module_id}")
|
|
64
|
+
models.update_task(task.task_id, status=task.status.value, trace=task.trace)
|
|
65
|
+
|
|
66
|
+
output = agent.execute(task)
|
|
67
|
+
|
|
68
|
+
task.output_json = output
|
|
69
|
+
task.append_trace("executed", "ok")
|
|
70
|
+
# 结果落库(失败视为任务失败,走重试)
|
|
71
|
+
agent.persist(output, task)
|
|
72
|
+
task.append_trace("persisted", "ok")
|
|
73
|
+
if task.need_human:
|
|
74
|
+
task.transition(TaskStatus.AWAITING_HUMAN)
|
|
75
|
+
task.append_trace("human_gate", "等待人工确认")
|
|
76
|
+
else:
|
|
77
|
+
task.transition(TaskStatus.SUCCESS)
|
|
78
|
+
models.update_task(task.task_id, status=task.status.value, output_json=task.output_json,
|
|
79
|
+
trace=task.trace, finished_at=task.finished_at)
|
|
80
|
+
log.info("任务 %s 完成 -> %s", task.task_id, task.status.value)
|
|
81
|
+
return {"task_id": task.task_id, "status": task.status.value}
|
|
82
|
+
except Exception as e: # noqa: BLE001 业务异常统一转失败
|
|
83
|
+
log.warning("任务 %s 执行异常: %s", task.task_id, e)
|
|
84
|
+
self._fail(task, str(e))
|
|
85
|
+
return {"task_id": task.task_id, "status": task.status.value, "error": str(e)}
|
|
86
|
+
|
|
87
|
+
def _fail(self, task: Task, reason: str) -> None:
|
|
88
|
+
task.retry_count += 1
|
|
89
|
+
task.append_trace("failed", reason)
|
|
90
|
+
if task.retry_count <= self.max_retry:
|
|
91
|
+
task.status = TaskStatus.PENDING # 自动重试
|
|
92
|
+
log.info("任务 %s 重试 %d/%d", task.task_id, task.retry_count, self.max_retry)
|
|
93
|
+
else:
|
|
94
|
+
task.transition(TaskStatus.BLOCKED)
|
|
95
|
+
self._alert(f"任务 {task.task_id} 重试超限已阻塞: {reason}")
|
|
96
|
+
models.update_task(task.task_id, status=task.status.value,
|
|
97
|
+
retry_count=task.retry_count, trace=task.trace)
|
|
98
|
+
|
|
99
|
+
def _alert(self, msg: str) -> None:
|
|
100
|
+
# M0 阶段告警落日志;后续可扩展飞书 / 短信通知
|
|
101
|
+
log.warning("【告警】%s", msg)
|
|
102
|
+
if config.ALERT_LEVEL == "stdout":
|
|
103
|
+
print(f"ALERT: {msg}")
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------- 人工确认
|
|
106
|
+
def approve(self, task_id: str, approved: bool = True) -> Dict[str, Any]:
|
|
107
|
+
row = models.get_task(task_id)
|
|
108
|
+
if row is None:
|
|
109
|
+
raise ValueError(f"任务不存在: {task_id}")
|
|
110
|
+
task = Task.from_dict(row)
|
|
111
|
+
if task.status != TaskStatus.AWAITING_HUMAN:
|
|
112
|
+
raise ValueError(f"任务 {task_id} 不在人工确认状态(当前 {task.status.value})")
|
|
113
|
+
if approved:
|
|
114
|
+
task.transition(TaskStatus.SUCCESS)
|
|
115
|
+
task.append_trace("human_approved", "ok")
|
|
116
|
+
else:
|
|
117
|
+
task.transition(TaskStatus.FAILED)
|
|
118
|
+
task.append_trace("human_rejected", "人工驳回")
|
|
119
|
+
self._fail(task, "人工驳回") # 走重试逻辑(若无重试价值可置 blocked)
|
|
120
|
+
models.update_task(task.task_id, status=task.status.value,
|
|
121
|
+
retry_count=task.retry_count, trace=task.trace)
|
|
122
|
+
return {"task_id": task_id, "status": task.status.value, "approved": False}
|
|
123
|
+
models.update_task(task.task_id, status=task.status.value, trace=task.trace,
|
|
124
|
+
finished_at=task.finished_at)
|
|
125
|
+
return {"task_id": task_id, "status": task.status.value, "approved": True}
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------- 统计
|
|
128
|
+
def stats(self) -> Dict[str, Any]:
|
|
129
|
+
counts = models.count_tasks()
|
|
130
|
+
by_module: Dict[str, int] = {}
|
|
131
|
+
for row in models.list_tasks():
|
|
132
|
+
by_module[row["module_id"]] = by_module.get(row["module_id"], 0) + 1
|
|
133
|
+
return {
|
|
134
|
+
"task_counts": counts,
|
|
135
|
+
"by_module": by_module,
|
|
136
|
+
"agents_online": len(self.agents),
|
|
137
|
+
"max_retry": self.max_retry,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------- 链路演示
|
|
141
|
+
def create_pipeline(self, steps: List[Dict[str, Any]]) -> List[str]:
|
|
142
|
+
"""按顺序提交一组任务(链路演示用),随后由 run_once 依次执行。"""
|
|
143
|
+
ids: List[str] = []
|
|
144
|
+
for i, step in enumerate(steps):
|
|
145
|
+
t = self.submit(module_id=step["module_id"],
|
|
146
|
+
task_type=step.get("task_type", f"pipeline_{i + 1}"),
|
|
147
|
+
input_data=step.get("input", {}),
|
|
148
|
+
priority=step.get("priority", "P1"))
|
|
149
|
+
ids.append(t.task_id)
|
|
150
|
+
return ids
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""任务协议与状态机。
|
|
2
|
+
|
|
3
|
+
对齐开发文档《统一协议与数据模型》任务部分:
|
|
4
|
+
状态流转约束:pending → running → success / failed / awaiting_human / blocked;
|
|
5
|
+
failed 可重试回 pending(上限由调度官控制);awaiting_human 经人工确认后 success / failed。
|
|
6
|
+
"""
|
|
7
|
+
import uuid
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from core.models import utcnow
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TaskStatus(str, Enum):
|
|
16
|
+
PENDING = "pending"
|
|
17
|
+
RUNNING = "running"
|
|
18
|
+
SUCCESS = "success"
|
|
19
|
+
FAILED = "failed"
|
|
20
|
+
BLOCKED = "blocked"
|
|
21
|
+
AWAITING_HUMAN = "awaiting_human"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TaskError(Exception):
|
|
25
|
+
"""任务执行异常:携带可诊断信息。"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TaskStatusError(TaskError):
|
|
29
|
+
"""非法状态迁移。"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# 允许的状态迁移表
|
|
33
|
+
_TRANSITIONS: Dict[TaskStatus, set] = {
|
|
34
|
+
TaskStatus.PENDING: {TaskStatus.RUNNING, TaskStatus.BLOCKED},
|
|
35
|
+
TaskStatus.RUNNING: {TaskStatus.SUCCESS, TaskStatus.FAILED,
|
|
36
|
+
TaskStatus.AWAITING_HUMAN, TaskStatus.BLOCKED},
|
|
37
|
+
TaskStatus.FAILED: {TaskStatus.PENDING, TaskStatus.BLOCKED},
|
|
38
|
+
TaskStatus.AWAITING_HUMAN: {TaskStatus.SUCCESS, TaskStatus.FAILED},
|
|
39
|
+
TaskStatus.SUCCESS: set(),
|
|
40
|
+
TaskStatus.BLOCKED: {TaskStatus.PENDING},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def assert_transition(old: TaskStatus, new: TaskStatus) -> None:
|
|
45
|
+
if old == new:
|
|
46
|
+
return
|
|
47
|
+
if new not in _TRANSITIONS.get(old, set()):
|
|
48
|
+
raise TaskStatusError(f"非法状态迁移: {old.value} -> {new.value}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def new_task_id() -> str:
|
|
52
|
+
return "T" + uuid.uuid4().hex[:12]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Task:
|
|
57
|
+
"""领域任务对象:创建时校验必填字段,序列化/反序列化与 DB 行对齐。"""
|
|
58
|
+
|
|
59
|
+
task_type: str
|
|
60
|
+
module_id: str
|
|
61
|
+
input_json: Dict[str, Any] = field(default_factory=dict)
|
|
62
|
+
priority: str = "P1"
|
|
63
|
+
need_human: bool = False
|
|
64
|
+
status: TaskStatus = TaskStatus.PENDING
|
|
65
|
+
task_id: str = field(default_factory=new_task_id)
|
|
66
|
+
output_json: Dict[str, Any] = field(default_factory=dict)
|
|
67
|
+
retry_count: int = 0
|
|
68
|
+
trace: List[Dict[str, Any]] = field(default_factory=list)
|
|
69
|
+
created_at: str = field(default_factory=utcnow)
|
|
70
|
+
finished_at: str = ""
|
|
71
|
+
|
|
72
|
+
def __post_init__(self) -> None:
|
|
73
|
+
if not self.task_type or not self.module_id:
|
|
74
|
+
raise TaskError("task_type 与 module_id 为必填字段")
|
|
75
|
+
if self.priority not in ("P0", "P1", "P2", "P3"):
|
|
76
|
+
raise TaskError(f"非法优先级: {self.priority}")
|
|
77
|
+
|
|
78
|
+
# ---- 状态机 ----
|
|
79
|
+
def transition(self, new_status: TaskStatus) -> None:
|
|
80
|
+
assert_transition(self.status, new_status)
|
|
81
|
+
self.status = new_status
|
|
82
|
+
if new_status in (TaskStatus.SUCCESS, TaskStatus.FAILED, TaskStatus.BLOCKED):
|
|
83
|
+
self.finished_at = utcnow()
|
|
84
|
+
|
|
85
|
+
def append_trace(self, step: str, detail: Any = None) -> None:
|
|
86
|
+
self.trace.append({"step": step, "detail": detail, "at": utcnow()})
|
|
87
|
+
|
|
88
|
+
# ---- 序列化 ----
|
|
89
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
90
|
+
return {
|
|
91
|
+
"task_id": self.task_id,
|
|
92
|
+
"task_type": self.task_type,
|
|
93
|
+
"module_id": self.module_id,
|
|
94
|
+
"status": self.status.value,
|
|
95
|
+
"priority": self.priority,
|
|
96
|
+
"input_json": self.input_json,
|
|
97
|
+
"output_json": self.output_json,
|
|
98
|
+
"retry_count": self.retry_count,
|
|
99
|
+
"need_human": self.need_human,
|
|
100
|
+
"trace": self.trace,
|
|
101
|
+
"created_at": self.created_at,
|
|
102
|
+
"finished_at": self.finished_at,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_dict(cls, d: Dict[str, Any]) -> "Task":
|
|
107
|
+
t = cls(
|
|
108
|
+
task_id=d["task_id"],
|
|
109
|
+
task_type=d["task_type"],
|
|
110
|
+
module_id=d["module_id"],
|
|
111
|
+
status=TaskStatus(d["status"]),
|
|
112
|
+
priority=d.get("priority", "P1"),
|
|
113
|
+
input_json=d.get("input_json", {}),
|
|
114
|
+
output_json=d.get("output_json", {}),
|
|
115
|
+
retry_count=d.get("retry_count", 0),
|
|
116
|
+
need_human=bool(d.get("need_human")),
|
|
117
|
+
trace=d.get("trace", []),
|
|
118
|
+
created_at=d.get("created_at", ""),
|
|
119
|
+
finished_at=d.get("finished_at", ""),
|
|
120
|
+
)
|
|
121
|
+
return t
|
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""LLM 工厂:按配置返回 mock 或 doubao 客户端。"""
|
|
2
|
+
from config import (DOUBAO_API_KEY, DOUBAO_BASE_URL, DOUBAO_MODEL, LLM_PROVIDER)
|
|
3
|
+
|
|
4
|
+
from llm.base import LLMClient
|
|
5
|
+
from llm.mock_llm import MockLLM
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_llm() -> LLMClient:
|
|
9
|
+
if LLM_PROVIDER == "doubao":
|
|
10
|
+
from llm.doubao_llm import DoubaoLLM
|
|
11
|
+
return DoubaoLLM(api_key=DOUBAO_API_KEY, model=DOUBAO_MODEL, base_url=DOUBAO_BASE_URL)
|
|
12
|
+
return MockLLM()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""LLM 客户端抽象:所有模块通过统一接口调用大模型。"""
|
|
2
|
+
from typing import Any, Dict, List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class LLMClient:
|
|
6
|
+
"""大模型客户端接口。
|
|
7
|
+
|
|
8
|
+
chat(messages, **kwargs) -> str
|
|
9
|
+
messages: [{"role": "system"|"user"|"assistant", "content": str}, ...]
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
provider: str = "base"
|
|
13
|
+
|
|
14
|
+
def chat(self, messages: List[Dict[str, str]], **kwargs: Any) -> str:
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
|
|
17
|
+
def complete(self, prompt: str, system: str = "", **kwargs: Any) -> str:
|
|
18
|
+
msgs = []
|
|
19
|
+
if system:
|
|
20
|
+
msgs.append({"role": "system", "content": system})
|
|
21
|
+
msgs.append({"role": "user", "content": prompt})
|
|
22
|
+
return self.chat(msgs, **kwargs)
|