From 0423fa3c99006a7e0063a85274e589182c83041a Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sun, 2 Aug 2026 00:00:20 +0800 Subject: [PATCH] refactor: add intent_classifier.py, register in init.py, LLM bridge shared --- pipeline_service/init.py | 6 +++ pipeline_service/intent_classifier.py | 60 +++++++++++++++++++++++++++ pipeline_service/llm_bridge.py | 4 +- 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 pipeline_service/intent_classifier.py diff --git a/pipeline_service/init.py b/pipeline_service/init.py index 40738b0..b2989b6 100644 --- a/pipeline_service/init.py +++ b/pipeline_service/init.py @@ -367,5 +367,11 @@ def load_pipeline_service(): # Load built-in interactive step types load_builtin_types() + # Register intent classifier + LLM bridge (shared across all pipelines) + from .intent_classifier import intent_classify + from .llm_bridge import llm_call + env.intent_classify = intent_classify + env.pipeline_llm_call = llm_call + debug(f"[{MODULE_NAME}] v{MODULE_VERSION} loaded — pipeline engine with human-in-the-loop support") return True diff --git a/pipeline_service/intent_classifier.py b/pipeline_service/intent_classifier.py new file mode 100644 index 0000000..99b6c43 --- /dev/null +++ b/pipeline_service/intent_classifier.py @@ -0,0 +1,60 @@ +""" +pipeline_service/intent_classifier.py — Reusable intent classification. + +DSPY can call: await intent_classify(model_name, message, intents, context) +Registered on ServerEnv in pipeline_service/init.py +""" + +import json +import logging + +logger = logging.getLogger("pipeline.intent_classifier") + +INTENT_PROMPT = """你是一个意图分类器。分析用户输入,返回 JSON。 + +可用意图: +{intent_list} + +当前上下文:{context_str} + +返回纯 JSON(不要 markdown 包裹): +{{"intent":"...","confidence":0.0-1.0,"params":{{"key":"value"...}},"missing_info":"..."}}""" + + +async def intent_classify(intents: list, message: str, context: dict = None) -> dict: + """Classify user intent. Returns {intent, confidence, params, missing_info}. + + Args: + intents: [{"name":"new_project","description":"创建新项目"},...] + message: user input text + context: {"project_name":"...","iteration_name":"..."} + """ + if not intents: + return {"intent": "chat", "confidence": 0.5, "params": {}, "missing_info": ""} + + intent_list = "\n".join( + f"- {i['name']}: {i.get('description','')}" for i in intents + ) + ctx_parts = [] + if context: + for k, v in context.items(): + if v: + ctx_parts.append(f"{k}={v}") + context_str = ", ".join(ctx_parts) if ctx_parts else "无上下文" + + prompt = INTENT_PROMPT.format(intent_list=intent_list, context_str=context_str) + + try: + from pipeline_service.llm_bridge import llm_call + raw = await llm_call( + prompt=f"{prompt}\n\n用户输入:{message}", + model=None, + temperature=0.2, + ) + raw = raw.strip() + if raw.startswith("```"): + raw = raw.split("\n", 1)[1].rsplit("```", 1)[0] + return json.loads(raw) + except Exception as e: + logger.warning("intent_classify failed: %s", e) + return {"intent": "chat", "confidence": 0.3, "params": {}, "missing_info": ""} diff --git a/pipeline_service/llm_bridge.py b/pipeline_service/llm_bridge.py index 2825c41..1644d59 100644 --- a/pipeline_service/llm_bridge.py +++ b/pipeline_service/llm_bridge.py @@ -17,7 +17,7 @@ _model_cache: dict = {} def _decrypt_key(encrypted: str) -> str: - """Decrypt api_key stored with password_encode.""" + """Decrypt api_key stored with password_encode. Falls back to plaintext.""" if not encrypted: return "" try: @@ -27,7 +27,7 @@ def _decrypt_key(encrypted: str) -> str: key = config.password_key return unpassword(key, encrypted) except Exception: - return encrypted + return encrypted # already plaintext or decrypt failed async def _get_model_config(model_name: str = None) -> dict: