diff --git a/qingjian_cs/.env.example b/qingjian_cs/.env.example new file mode 100644 index 0000000..3f90c14 --- /dev/null +++ b/qingjian_cs/.env.example @@ -0,0 +1,43 @@ +QINGJIAN_WS_URI=ws://127.0.0.1:9528 +OPENAI_API_KEY= +OPENAI_BASE_URL=https://ark.cn-beijing.volces.com/api/v3 +OPENAI_MODEL_NAME=doubao-seed-2-0-pro-260215 + +# 低延迟参数(首答快) +MESSAGE_DEBOUNCE_SECONDS=0.2 +IMAGE_MESSAGE_DEBOUNCE_SECONDS=0.4 +AUTO_QUOTE_WAIT_SECONDS=18 +AGENT_MAX_ITERS=1 +DECISION_TIMEOUT_SECONDS=8 + +STORE_BACKEND=mysql +STORE_SQLITE_PATH= +MYSQL_HOST=127.0.0.1 +MYSQL_PORT=3306 +MYSQL_USER=root +MYSQL_PASSWORD= +MYSQL_DATABASE=ai_cs +MYSQL_TABLE_PREFIX=qjcs_ + +HTTP_HOST=127.0.0.1 +HTTP_PORT=6060 +TIANWANG_CALLBACK_URL=http://139.199.3.75:18789/api/callback + +# 企业微信通知(留空即关闭) +WECHAT_WEBHOOK= + +# 邮件通知(留空即关闭) +EMAIL_NOTIFY_ENABLED=0 +SMTP_HOST=smtp.qq.com +SMTP_PORT=587 +SMTP_USER= +SMTP_PASSWORD= +SMTP_TO= + +# 自动作图 +AUTO_DRAW_ENABLED=1 +AUTO_DRAW_ENDPOINT= +AUTO_DRAW_TIMEOUT_SECONDS=25 + +# 并发 +MAX_CONCURRENT_TURNS=4 diff --git a/qingjian_cs/README.md b/qingjian_cs/README.md index 7b7d0b0..5d876d1 100644 --- a/qingjian_cs/README.md +++ b/qingjian_cs/README.md @@ -6,12 +6,11 @@ - `OpenAIChatFormatter` - `InMemoryMemory` - `structured_model` 输出 -- `Orchestrator` 编排(Router + Specialists) +- `Orchestrator` 编排(单阶段直达 Specialists) ## 架构 - `app/agents.py` - - `RouterAgent` - `PreSalesAgent` - `QuoteAgent` - `AfterSalesAgent` @@ -32,11 +31,20 @@ - `OPENAI_BASE_URL` 默认 `https://ark.cn-beijing.volces.com/api/v3` - `OPENAI_MODEL_NAME` 默认 `doubao-seed-2-0-pro-260215` - `AUTO_QUOTE_WAIT_SECONDS` 默认 `18` -- `MESSAGE_DEBOUNCE_SECONDS` 默认 `6` +- `MESSAGE_DEBOUNCE_SECONDS` 默认 `0.2` +- `IMAGE_MESSAGE_DEBOUNCE_SECONDS` 默认 `0.4` +- `AGENT_MAX_ITERS` 默认 `1` +- `DECISION_TIMEOUT_SECONDS` 默认 `8` - `STORE_BACKEND` 默认 `sqlite`,可设 `mysql` - `STORE_SQLITE_PATH` 可选 - `MYSQL_HOST/MYSQL_PORT/MYSQL_USER/MYSQL_PASSWORD/MYSQL_DATABASE` +可直接复制模板: + +```powershell +copy .env.example .env +``` + ## 启动(uv run) 当前环境建议使用(避免上层工程依赖干扰): @@ -45,6 +53,15 @@ uv run --with websockets --with pydantic --with openai --with pymysql python run.py ``` +### 生产建议(天网) + +- WebSocket:单实例(防重复回复) +- HTTP API:多 worker(提升并发) + +```powershell +uv run powershell -File .\scripts\start_tianwang_prod.ps1 -Host 127.0.0.1 -Port 6060 -ApiWorkers 2 +``` + ## Golden 回放 ```powershell diff --git a/qingjian_cs/app/config.py b/qingjian_cs/app/config.py index 670ec1b..7ff93b0 100644 --- a/qingjian_cs/app/config.py +++ b/qingjian_cs/app/config.py @@ -12,8 +12,8 @@ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "").strip() OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3").strip() OPENAI_MODEL_NAME = os.getenv("OPENAI_MODEL_NAME", "doubao-seed-2-0-pro-260215").strip() -MESSAGE_DEBOUNCE_SECONDS = float(os.getenv("MESSAGE_DEBOUNCE_SECONDS", "0.8")) -IMAGE_MESSAGE_DEBOUNCE_SECONDS = float(os.getenv("IMAGE_MESSAGE_DEBOUNCE_SECONDS", "1.2")) +MESSAGE_DEBOUNCE_SECONDS = float(os.getenv("MESSAGE_DEBOUNCE_SECONDS", "0.2")) +IMAGE_MESSAGE_DEBOUNCE_SECONDS = float(os.getenv("IMAGE_MESSAGE_DEBOUNCE_SECONDS", "0.4")) AUTO_QUOTE_WAIT_SECONDS = int(os.getenv("AUTO_QUOTE_WAIT_SECONDS", "18")) AGENT_MAX_ITERS = int(os.getenv("AGENT_MAX_ITERS", "1")) FAST_ROUTE_ENABLED = os.getenv("FAST_ROUTE_ENABLED", "1").strip() in {"1", "true", "True", "yes", "on"} @@ -47,4 +47,4 @@ AUTO_DRAW_TIMEOUT_SECONDS = int(os.getenv("AUTO_DRAW_TIMEOUT_SECONDS", "25")) # 并发与超时 MAX_CONCURRENT_TURNS = int(os.getenv("MAX_CONCURRENT_TURNS", "4")) -DECISION_TIMEOUT_SECONDS = int(os.getenv("DECISION_TIMEOUT_SECONDS", "60")) +DECISION_TIMEOUT_SECONDS = int(os.getenv("DECISION_TIMEOUT_SECONDS", "8")) diff --git a/qingjian_cs/app/orchestrator.py b/qingjian_cs/app/orchestrator.py index d9caae4..3ccfeba 100644 --- a/qingjian_cs/app/orchestrator.py +++ b/qingjian_cs/app/orchestrator.py @@ -2,8 +2,7 @@ from __future__ import annotations from typing import Any -from .agents import AfterSalesAgent, PreSalesAgent, QuoteAgent, RiskAgent, RouterAgent -from .config import FAST_ROUTE_ENABLED +from .agents import AfterSalesAgent, PreSalesAgent, QuoteAgent, RiskAgent from .image_quote_analyzer import analyze_image_for_quote from .models import Decision from .state_machine import evolve_after_sales_state, migrate_state_schema @@ -12,7 +11,6 @@ from .store import ConversationStore class Orchestrator: def __init__(self) -> None: - self.router = RouterAgent() self.pre_sales = PreSalesAgent() self.quote = QuoteAgent() self.after_sales = AfterSalesAgent() @@ -20,9 +18,7 @@ class Orchestrator: self.store = ConversationStore() @staticmethod - def _fast_route(context: dict[str, Any]) -> tuple[str, str] | None: - if not FAST_ROUTE_ENABLED: - return None + def _single_stage_route(context: dict[str, Any]) -> tuple[str, str]: msg = str(context.get("msg", "") or "") lower = msg.lower() if bool(context.get("auto_quote_trigger")): @@ -34,7 +30,7 @@ class Orchestrator: return "quote", "fast:pending_image_and_pricing" if any(k in lower for k in ("退款", "退钱", "不满意", "重做", "售后", "返工", "重发")): return "after_sales", "fast:after_sales_keyword" - return None + return "pre_sales", "single_stage:default_pre_sales" async def decide(self, context: dict[str, Any]) -> tuple[str, Decision, dict[str, Any]]: customer_key = context["customer_key"] @@ -54,11 +50,7 @@ class Orchestrator: "order_status": order_status, } - fast = self._fast_route(merged_ctx) - if fast is not None: - route, route_reason = fast - else: - route, route_reason = await self.router.route(merged_ctx) + route, route_reason = self._single_stage_route(merged_ctx) if route == "quote": latest_image_url = str(context.get("latest_image_url", "") or "").strip()