isage-middleware 0.2.4.3__cp311-none-any.whl
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.
- isage_middleware-0.2.4.3.dist-info/METADATA +266 -0
- isage_middleware-0.2.4.3.dist-info/RECORD +94 -0
- isage_middleware-0.2.4.3.dist-info/WHEEL +5 -0
- isage_middleware-0.2.4.3.dist-info/top_level.txt +1 -0
- sage/middleware/__init__.py +59 -0
- sage/middleware/_version.py +6 -0
- sage/middleware/components/__init__.py +30 -0
- sage/middleware/components/extensions_compat.py +141 -0
- sage/middleware/components/sage_db/__init__.py +116 -0
- sage/middleware/components/sage_db/backend.py +136 -0
- sage/middleware/components/sage_db/service.py +15 -0
- sage/middleware/components/sage_flow/__init__.py +76 -0
- sage/middleware/components/sage_flow/python/__init__.py +14 -0
- sage/middleware/components/sage_flow/python/micro_service/__init__.py +4 -0
- sage/middleware/components/sage_flow/python/micro_service/sage_flow_service.py +88 -0
- sage/middleware/components/sage_flow/python/sage_flow.py +30 -0
- sage/middleware/components/sage_flow/service.py +14 -0
- sage/middleware/components/sage_mem/__init__.py +83 -0
- sage/middleware/components/sage_sias/__init__.py +59 -0
- sage/middleware/components/sage_sias/continual_learner.py +184 -0
- sage/middleware/components/sage_sias/coreset_selector.py +302 -0
- sage/middleware/components/sage_sias/types.py +94 -0
- sage/middleware/components/sage_tsdb/__init__.py +81 -0
- sage/middleware/components/sage_tsdb/python/__init__.py +21 -0
- sage/middleware/components/sage_tsdb/python/_sage_tsdb.pyi +17 -0
- sage/middleware/components/sage_tsdb/python/algorithms/__init__.py +17 -0
- sage/middleware/components/sage_tsdb/python/algorithms/base.py +51 -0
- sage/middleware/components/sage_tsdb/python/algorithms/out_of_order_join.py +248 -0
- sage/middleware/components/sage_tsdb/python/algorithms/window_aggregator.py +296 -0
- sage/middleware/components/sage_tsdb/python/micro_service/__init__.py +7 -0
- sage/middleware/components/sage_tsdb/python/micro_service/sage_tsdb_service.py +365 -0
- sage/middleware/components/sage_tsdb/python/sage_tsdb.py +523 -0
- sage/middleware/components/sage_tsdb/service.py +17 -0
- sage/middleware/components/vector_stores/__init__.py +25 -0
- sage/middleware/components/vector_stores/chroma.py +483 -0
- sage/middleware/components/vector_stores/chroma_adapter.py +185 -0
- sage/middleware/components/vector_stores/milvus.py +677 -0
- sage/middleware/operators/__init__.py +56 -0
- sage/middleware/operators/agent/__init__.py +24 -0
- sage/middleware/operators/agent/planning/__init__.py +5 -0
- sage/middleware/operators/agent/planning/llm_adapter.py +41 -0
- sage/middleware/operators/agent/planning/planner_adapter.py +98 -0
- sage/middleware/operators/agent/planning/router.py +107 -0
- sage/middleware/operators/agent/runtime.py +296 -0
- sage/middleware/operators/agentic/__init__.py +41 -0
- sage/middleware/operators/agentic/config.py +254 -0
- sage/middleware/operators/agentic/planning_operator.py +125 -0
- sage/middleware/operators/agentic/refined_searcher.py +132 -0
- sage/middleware/operators/agentic/runtime.py +241 -0
- sage/middleware/operators/agentic/timing_operator.py +125 -0
- sage/middleware/operators/agentic/tool_selection_operator.py +127 -0
- sage/middleware/operators/context/__init__.py +17 -0
- sage/middleware/operators/context/critic_evaluation.py +16 -0
- sage/middleware/operators/context/model_context.py +565 -0
- sage/middleware/operators/context/quality_label.py +12 -0
- sage/middleware/operators/context/search_query_results.py +61 -0
- sage/middleware/operators/context/search_result.py +42 -0
- sage/middleware/operators/context/search_session.py +79 -0
- sage/middleware/operators/filters/__init__.py +26 -0
- sage/middleware/operators/filters/context_sink.py +387 -0
- sage/middleware/operators/filters/context_source.py +376 -0
- sage/middleware/operators/filters/evaluate_filter.py +83 -0
- sage/middleware/operators/filters/tool_filter.py +74 -0
- sage/middleware/operators/llm/__init__.py +18 -0
- sage/middleware/operators/llm/sagellm_generator.py +432 -0
- sage/middleware/operators/rag/__init__.py +147 -0
- sage/middleware/operators/rag/arxiv.py +331 -0
- sage/middleware/operators/rag/chunk.py +13 -0
- sage/middleware/operators/rag/document_loaders.py +23 -0
- sage/middleware/operators/rag/evaluate.py +658 -0
- sage/middleware/operators/rag/generator.py +340 -0
- sage/middleware/operators/rag/index_builder/__init__.py +48 -0
- sage/middleware/operators/rag/index_builder/builder.py +363 -0
- sage/middleware/operators/rag/index_builder/manifest.py +101 -0
- sage/middleware/operators/rag/index_builder/storage.py +131 -0
- sage/middleware/operators/rag/pipeline.py +46 -0
- sage/middleware/operators/rag/profiler.py +59 -0
- sage/middleware/operators/rag/promptor.py +400 -0
- sage/middleware/operators/rag/refiner.py +231 -0
- sage/middleware/operators/rag/reranker.py +364 -0
- sage/middleware/operators/rag/retriever.py +1308 -0
- sage/middleware/operators/rag/searcher.py +37 -0
- sage/middleware/operators/rag/types.py +28 -0
- sage/middleware/operators/rag/writer.py +80 -0
- sage/middleware/operators/tools/__init__.py +71 -0
- sage/middleware/operators/tools/arxiv_paper_searcher.py +175 -0
- sage/middleware/operators/tools/arxiv_searcher.py +102 -0
- sage/middleware/operators/tools/duckduckgo_searcher.py +105 -0
- sage/middleware/operators/tools/image_captioner.py +104 -0
- sage/middleware/operators/tools/nature_news_fetcher.py +224 -0
- sage/middleware/operators/tools/searcher_tool.py +514 -0
- sage/middleware/operators/tools/text_detector.py +185 -0
- sage/middleware/operators/tools/url_text_extractor.py +104 -0
- sage/middleware/py.typed +2 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SAGE Middleware Operators - 领域算子
|
|
3
|
+
|
|
4
|
+
这个模块提供面向特定业务领域的算子实现:
|
|
5
|
+
- LLM算子: 大语言模型推理 (SageLLMGenerator)
|
|
6
|
+
- RAG算子: 检索增强生成算子 (Retriever, Refiner, Reranker, Generator等)
|
|
7
|
+
- Tool算子: 工具调用 + 领域特定工具 (arxiv, image_captioner等)
|
|
8
|
+
- Filters: 业务过滤器 (tool_filter, evaluate_filter, context source/sink)
|
|
9
|
+
- Agentic: Agent runtime operators (requires isage-agentic, optional)
|
|
10
|
+
|
|
11
|
+
向量数据库集成位于: sage.middleware.components.vector_stores
|
|
12
|
+
|
|
13
|
+
这些算子继承 sage.kernel.operators 的基础算子,实现具体业务逻辑。
|
|
14
|
+
|
|
15
|
+
使用方式:
|
|
16
|
+
from sage.middleware.operators import rag, llm, tools, filters
|
|
17
|
+
|
|
18
|
+
# 或直接导入
|
|
19
|
+
from sage.middleware.operators.rag import ChromaRetriever
|
|
20
|
+
from sage.middleware.operators.llm import SageLLMGenerator
|
|
21
|
+
from sage.middleware.components.vector_stores import MilvusBackend, ChromaBackend
|
|
22
|
+
|
|
23
|
+
# Agentic operators (requires isage-agentic, install with: pip install isage-middleware[libs])
|
|
24
|
+
from sage.middleware.operators.agentic import PlanningOperator
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import warnings
|
|
28
|
+
|
|
29
|
+
from sage.middleware.operators.llm.sagellm_generator import SageLLMGenerator
|
|
30
|
+
|
|
31
|
+
# 导出核心子模块 (always available)
|
|
32
|
+
from . import filters, llm, rag, tools
|
|
33
|
+
|
|
34
|
+
# Agentic operators are optional (requires isage-agentic)
|
|
35
|
+
try:
|
|
36
|
+
from . import agentic
|
|
37
|
+
|
|
38
|
+
_HAS_AGENTIC = True
|
|
39
|
+
except ImportError as e:
|
|
40
|
+
_HAS_AGENTIC = False
|
|
41
|
+
agentic = None # type: ignore
|
|
42
|
+
warnings.warn(
|
|
43
|
+
f"Agentic operators not available: {e}\n"
|
|
44
|
+
"Install with: pip install isage-middleware[libs] or pip install isage-agentic",
|
|
45
|
+
UserWarning,
|
|
46
|
+
stacklevel=2,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"rag",
|
|
51
|
+
"llm",
|
|
52
|
+
"tools",
|
|
53
|
+
"filters",
|
|
54
|
+
"agentic",
|
|
55
|
+
"SageLLMGenerator",
|
|
56
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Agent components for SAGE middleware.
|
|
2
|
+
|
|
3
|
+
Provides agent runtime and planning capabilities.
|
|
4
|
+
|
|
5
|
+
Note: This module requires isage-agentic. Install with:
|
|
6
|
+
pip install isage-middleware[libs] or pip install isage-agentic
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from sage.middleware.operators.agent import runtime
|
|
13
|
+
|
|
14
|
+
_HAS_AGENTIC = True
|
|
15
|
+
__all__ = ["runtime"]
|
|
16
|
+
except ImportError as e:
|
|
17
|
+
_HAS_AGENTIC = False
|
|
18
|
+
runtime = None # type: ignore
|
|
19
|
+
__all__ = []
|
|
20
|
+
warnings.warn(
|
|
21
|
+
f"Agent runtime not available: {e}\nInstall with: pip install isage-agentic",
|
|
22
|
+
UserWarning,
|
|
23
|
+
stacklevel=2,
|
|
24
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GeneratorToClientAdapter:
|
|
7
|
+
"""
|
|
8
|
+
Adapts OpenAIGenerator/HFGenerator (L4) to UnifiedInferenceClient interface (L2/L3).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, generator):
|
|
12
|
+
self.generator = generator
|
|
13
|
+
|
|
14
|
+
def chat(
|
|
15
|
+
self, messages: list[dict[str, str]], temperature: float = 0.7, max_tokens: int = 512
|
|
16
|
+
) -> str:
|
|
17
|
+
"""
|
|
18
|
+
Execute chat completion.
|
|
19
|
+
"""
|
|
20
|
+
# OpenAIGenerator.execute takes [user_query, messages] or just messages depending on impl.
|
|
21
|
+
# Let's check OpenAIGenerator.execute signature.
|
|
22
|
+
# Based on usage in LLMPlanner: self.generator.execute([user_query, messages])
|
|
23
|
+
# But here we might not have user_query easily available if it's just a chat call.
|
|
24
|
+
# We can pass the last user message as user_query.
|
|
25
|
+
|
|
26
|
+
user_query = "Chat request"
|
|
27
|
+
for msg in reversed(messages):
|
|
28
|
+
if msg["role"] == "user":
|
|
29
|
+
user_query = msg["content"]
|
|
30
|
+
break
|
|
31
|
+
|
|
32
|
+
# The generator returns (token_usage, text_output)
|
|
33
|
+
_, output = self.generator.execute([user_query, messages])
|
|
34
|
+
return output
|
|
35
|
+
|
|
36
|
+
def generate(self, prompt: str, **kwargs) -> list[dict[str, Any]]:
|
|
37
|
+
"""
|
|
38
|
+
Execute text generation.
|
|
39
|
+
"""
|
|
40
|
+
_, output = self.generator.execute([prompt, [{"role": "user", "content": prompt}]])
|
|
41
|
+
return [{"generations": [{"text": output}]}]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from sage_libs.sage_agentic.agents.planning.schemas import PlannerConfig, PlanRequest, ToolMetadata
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SageLibsPlannerAdapter:
|
|
12
|
+
"""
|
|
13
|
+
Adapts sage-libs planners (ReAct, ToT, Hierarchical) to the AgentRuntime interface.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, planner_cls, config: PlannerConfig, llm_client):
|
|
17
|
+
self.planner = planner_cls(config=config, llm_client=llm_client)
|
|
18
|
+
|
|
19
|
+
def plan(
|
|
20
|
+
self,
|
|
21
|
+
profile_system_prompt: str,
|
|
22
|
+
user_query: str,
|
|
23
|
+
tools: dict[str, dict[str, Any]],
|
|
24
|
+
) -> list[dict[str, Any]]:
|
|
25
|
+
"""
|
|
26
|
+
Convert inputs to PlanRequest, call planner, and convert PlanResult to list[dict].
|
|
27
|
+
"""
|
|
28
|
+
# 1. Convert tools dict to List[ToolMetadata]
|
|
29
|
+
tool_metadata_list = []
|
|
30
|
+
for name, meta in tools.items():
|
|
31
|
+
tool_metadata_list.append(
|
|
32
|
+
ToolMetadata(
|
|
33
|
+
tool_id=name,
|
|
34
|
+
name=name,
|
|
35
|
+
description=meta.get("description", ""),
|
|
36
|
+
category=meta.get("category", "general"),
|
|
37
|
+
input_schema=meta.get("input_schema", {}),
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# 2. Create PlanRequest
|
|
42
|
+
request = PlanRequest(
|
|
43
|
+
goal=user_query,
|
|
44
|
+
context={"system_prompt": profile_system_prompt},
|
|
45
|
+
tools=tool_metadata_list,
|
|
46
|
+
max_steps=10, # Default
|
|
47
|
+
min_steps=1,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# 3. Call planner
|
|
51
|
+
try:
|
|
52
|
+
result = self.planner.plan(request)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.error(f"Planner {self.planner.name} failed: {e}")
|
|
55
|
+
return [{"type": "reply", "text": f"Planning failed: {str(e)}"}]
|
|
56
|
+
|
|
57
|
+
# 4. Convert PlanResult to list[dict]
|
|
58
|
+
# AgentRuntime expects: [{"type": "tool", "name": "...", "arguments": {...}}, ...]
|
|
59
|
+
runtime_steps = []
|
|
60
|
+
|
|
61
|
+
if not result.steps:
|
|
62
|
+
return [{"type": "reply", "text": "No plan generated."}]
|
|
63
|
+
|
|
64
|
+
for step in result.steps:
|
|
65
|
+
if step.action == "finish":
|
|
66
|
+
# Some planners might use a 'finish' action
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
# Check if it's a tool call
|
|
70
|
+
# In sage-libs, 'action' is usually the tool name
|
|
71
|
+
# 'inputs' are arguments
|
|
72
|
+
|
|
73
|
+
# Heuristic: if action matches a tool name, it's a tool call
|
|
74
|
+
if step.action in tools:
|
|
75
|
+
runtime_steps.append(
|
|
76
|
+
{"type": "tool", "name": step.action, "arguments": step.inputs}
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
# Treat as thought or unknown action?
|
|
80
|
+
# AgentRuntime doesn't support "thought" steps explicitly in the loop yet,
|
|
81
|
+
# but we can log them or ignore them.
|
|
82
|
+
# If it's a reply-like action?
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
# If no tool steps, maybe it's a direct reply?
|
|
86
|
+
if not runtime_steps:
|
|
87
|
+
# Try to find a final thought or result
|
|
88
|
+
final_thought = getattr(result, "final_thought", None) or "Plan completed."
|
|
89
|
+
runtime_steps.append({"type": "reply", "text": final_thought})
|
|
90
|
+
else:
|
|
91
|
+
# Append a final reply step if not present?
|
|
92
|
+
# AgentRuntime loop executes steps. If the last step is a tool, it will execute it.
|
|
93
|
+
# Then what? The loop continues?
|
|
94
|
+
# AgentRuntime loop: for step in plan: execute.
|
|
95
|
+
# If plan is static, it executes all steps.
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
return runtime_steps
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from sage_libs.sage_agentic.agents.planning.hierarchical_planner import HierarchicalPlanner
|
|
8
|
+
from sage_libs.sage_agentic.agents.planning.react_planner import ReActConfig, ReActPlanner
|
|
9
|
+
from sage_libs.sage_agentic.agents.planning.schemas import PlannerConfig
|
|
10
|
+
from sage_libs.sage_agentic.agents.planning.simple_llm_planner import SimpleLLMPlanner
|
|
11
|
+
from sage_libs.sage_agentic.agents.planning.tot_planner import ToTConfig
|
|
12
|
+
from sage_libs.sage_agentic.agents.planning.tot_planner import TreeOfThoughtsPlanner as ToTPlanner
|
|
13
|
+
|
|
14
|
+
from .llm_adapter import GeneratorToClientAdapter
|
|
15
|
+
from .planner_adapter import SageLibsPlannerAdapter
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PlannerRouter:
|
|
21
|
+
"""
|
|
22
|
+
Routes user queries to the appropriate planner based on intent classification.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, generator, default_planner="llm"):
|
|
26
|
+
self.generator = generator
|
|
27
|
+
self.llm_client = GeneratorToClientAdapter(generator)
|
|
28
|
+
self.default_planner_type = default_planner
|
|
29
|
+
|
|
30
|
+
# Initialize planners
|
|
31
|
+
# 1. Simple LLM Planner (Baseline)
|
|
32
|
+
self.simple_planner = SimpleLLMPlanner(generator=generator)
|
|
33
|
+
|
|
34
|
+
# 2. ReAct Planner (Reasoning)
|
|
35
|
+
self.react_planner = SageLibsPlannerAdapter(
|
|
36
|
+
ReActPlanner, ReActConfig(max_iterations=5), self.llm_client
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# 3. ToT Planner (Complex/Exploratory)
|
|
40
|
+
self.tot_planner = SageLibsPlannerAdapter(
|
|
41
|
+
ToTPlanner, ToTConfig(max_depth=3, branch_factor=3), self.llm_client
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# 4. Hierarchical Planner (Long-horizon)
|
|
45
|
+
self.hierarchical_planner = SageLibsPlannerAdapter(
|
|
46
|
+
HierarchicalPlanner, PlannerConfig(), self.llm_client
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def _classify_intent(self, user_query: str) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Classify the user query into one of the planner types.
|
|
52
|
+
"""
|
|
53
|
+
prompt = """
|
|
54
|
+
You are an expert intent classifier for an AI agent.
|
|
55
|
+
Analyze the user's query and select the most suitable planning strategy.
|
|
56
|
+
|
|
57
|
+
Strategies:
|
|
58
|
+
1. "simple": For direct questions, simple tasks, or when no tools are needed. (e.g., "Hello", "What is 2+2?")
|
|
59
|
+
2. "react": For tasks requiring multi-step reasoning and tool usage. (e.g., "Search for X and summarize it")
|
|
60
|
+
3. "tot": For complex problems requiring exploration of multiple possibilities or creative writing. (e.g., "Write a novel outline", "Solve a complex riddle")
|
|
61
|
+
4. "hierarchical": For very long, complex tasks with many sub-tasks. (e.g., "Plan a 3-day trip including flights, hotels, and restaurants")
|
|
62
|
+
|
|
63
|
+
User Query: "{query}"
|
|
64
|
+
|
|
65
|
+
Return ONLY the strategy name (simple, react, tot, hierarchical) in JSON format: {{"strategy": "..."}}
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
response = self.llm_client.chat(
|
|
69
|
+
[
|
|
70
|
+
{"role": "system", "content": "You are an intent classifier."},
|
|
71
|
+
{"role": "user", "content": prompt.format(query=user_query)},
|
|
72
|
+
],
|
|
73
|
+
temperature=0.1,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Parse JSON
|
|
77
|
+
import re
|
|
78
|
+
|
|
79
|
+
match = re.search(r"\{.*\}", response, re.DOTALL)
|
|
80
|
+
if match:
|
|
81
|
+
data = json.loads(match.group(0))
|
|
82
|
+
return data.get("strategy", "simple").lower()
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.warning(f"Intent classification failed: {e}. Using default.")
|
|
85
|
+
|
|
86
|
+
return "simple"
|
|
87
|
+
|
|
88
|
+
def plan(
|
|
89
|
+
self,
|
|
90
|
+
profile_system_prompt: str,
|
|
91
|
+
user_query: str,
|
|
92
|
+
tools: dict[str, dict[str, Any]],
|
|
93
|
+
) -> list[dict[str, Any]]:
|
|
94
|
+
"""
|
|
95
|
+
Route to the appropriate planner.
|
|
96
|
+
"""
|
|
97
|
+
strategy = self._classify_intent(user_query)
|
|
98
|
+
logger.info(f"Selected planning strategy: {strategy}")
|
|
99
|
+
|
|
100
|
+
if strategy == "react":
|
|
101
|
+
return self.react_planner.plan(profile_system_prompt, user_query, tools)
|
|
102
|
+
elif strategy == "tot":
|
|
103
|
+
return self.tot_planner.plan(profile_system_prompt, user_query, tools)
|
|
104
|
+
elif strategy == "hierarchical":
|
|
105
|
+
return self.hierarchical_planner.plan(profile_system_prompt, user_query, tools)
|
|
106
|
+
else:
|
|
107
|
+
return self.simple_planner.plan(profile_system_prompt, user_query, tools)
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Runtime (Middleware Layer)
|
|
3
|
+
|
|
4
|
+
This component acts as a Dynamic Pipeline Orchestrator.
|
|
5
|
+
It takes a user query, generates a dynamic execution plan (DAG), and executes it using available tools.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
# Import from L3 (Libs) - Allowed dependency direction (L4 -> L3)
|
|
15
|
+
from sage_libs.sage_agentic.agents.action.mcp_registry import MCPRegistry
|
|
16
|
+
from sage_libs.sage_agentic.agents.planning import PlanStep, SimpleLLMPlanner
|
|
17
|
+
from sage_libs.sage_agentic.agents.profile.profile import BaseProfile
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _missing_required(arguments: dict[str, Any], input_schema: dict[str, Any]) -> list[str]:
|
|
23
|
+
"""基于 MCP JSON Schema 做最小必填参数校验。"""
|
|
24
|
+
req = (input_schema or {}).get("required") or []
|
|
25
|
+
return [k for k in req if k not in arguments]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AgentRuntime:
|
|
29
|
+
"""
|
|
30
|
+
Production-Ready Runtime (Middleware Layer):
|
|
31
|
+
- Input: user_query
|
|
32
|
+
- Process: Planner generates JSON plan -> Step-by-step execution -> Optional LLM summary -> Return
|
|
33
|
+
- Features: Safety checks, Error handling, Structured logging/output
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
profile: BaseProfile,
|
|
39
|
+
planner: SimpleLLMPlanner,
|
|
40
|
+
tools: MCPRegistry,
|
|
41
|
+
summarizer=None,
|
|
42
|
+
max_steps: int = 6,
|
|
43
|
+
):
|
|
44
|
+
self.profile = profile
|
|
45
|
+
self.planner = planner
|
|
46
|
+
self.tools = tools
|
|
47
|
+
self.summarizer = summarizer
|
|
48
|
+
self.max_steps = max_steps
|
|
49
|
+
|
|
50
|
+
def step_stream(self, user_query: str):
|
|
51
|
+
"""
|
|
52
|
+
Execute a single turn of conversation with streaming feedback.
|
|
53
|
+
|
|
54
|
+
Yields:
|
|
55
|
+
Dict containing event type and data
|
|
56
|
+
"""
|
|
57
|
+
logger.info(f"AgentRuntime (Middleware) step_stream started for query: {user_query}")
|
|
58
|
+
|
|
59
|
+
observations: list[dict[str, Any]] = []
|
|
60
|
+
plan: list[PlanStep] = []
|
|
61
|
+
|
|
62
|
+
# 1) 生成计划(流式)
|
|
63
|
+
try:
|
|
64
|
+
# 检查 planner 是否支持流式
|
|
65
|
+
if hasattr(self.planner, "plan_stream"):
|
|
66
|
+
for event in self.planner.plan_stream(
|
|
67
|
+
profile_system_prompt=self.profile.render_system_prompt(),
|
|
68
|
+
user_query=user_query,
|
|
69
|
+
tools=self.tools.describe(),
|
|
70
|
+
):
|
|
71
|
+
if event["type"] == "thought":
|
|
72
|
+
yield {"type": "planning_thought", "content": event["content"]}
|
|
73
|
+
elif event["type"] == "plan":
|
|
74
|
+
plan = event["steps"]
|
|
75
|
+
yield {"type": "plan_generated", "plan": plan}
|
|
76
|
+
else:
|
|
77
|
+
# 降级到非流式
|
|
78
|
+
yield {"type": "planning_thought", "content": "正在生成计划..."}
|
|
79
|
+
plan = self.planner.plan(
|
|
80
|
+
profile_system_prompt=self.profile.render_system_prompt(),
|
|
81
|
+
user_query=user_query,
|
|
82
|
+
tools=self.tools.describe(),
|
|
83
|
+
)
|
|
84
|
+
yield {"type": "plan_generated", "plan": plan}
|
|
85
|
+
|
|
86
|
+
logger.info(f"Plan generated with {len(plan)} steps")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.error(f"Planning failed: {e}")
|
|
89
|
+
yield {"type": "error", "content": f"Planning failed: {str(e)}"}
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
reply_text: str | None = None
|
|
93
|
+
|
|
94
|
+
# 2) 逐步执行
|
|
95
|
+
for i, step in enumerate(plan[: self.max_steps]):
|
|
96
|
+
logger.debug(f"Executing step {i}: {step}")
|
|
97
|
+
|
|
98
|
+
if step.get("type") == "reply":
|
|
99
|
+
reply_text = step.get("text", "").strip()
|
|
100
|
+
logger.info("Plan reached reply step")
|
|
101
|
+
yield {"type": "reply", "content": reply_text}
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
if step.get("type") == "tool":
|
|
105
|
+
name = step.get("name")
|
|
106
|
+
arguments = step.get("arguments", {}) or {}
|
|
107
|
+
|
|
108
|
+
yield {"type": "tool_start", "tool": name, "arguments": arguments}
|
|
109
|
+
|
|
110
|
+
# Safety Check: Validate arguments against schema
|
|
111
|
+
tools_meta = self.tools.describe()
|
|
112
|
+
tool_desc = tools_meta.get(name) if isinstance(name, str) else None
|
|
113
|
+
|
|
114
|
+
if not tool_desc:
|
|
115
|
+
error_msg = f"Tool '{name}' not found in registry"
|
|
116
|
+
logger.warning(error_msg)
|
|
117
|
+
obs = {
|
|
118
|
+
"step": i,
|
|
119
|
+
"tool": name,
|
|
120
|
+
"ok": False,
|
|
121
|
+
"error": error_msg,
|
|
122
|
+
"arguments": arguments,
|
|
123
|
+
}
|
|
124
|
+
observations.append(obs)
|
|
125
|
+
yield {"type": "tool_error", "tool": name, "error": error_msg}
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
schema = tool_desc.get("input_schema", {}) if tool_desc else {}
|
|
129
|
+
miss = _missing_required(arguments, schema)
|
|
130
|
+
|
|
131
|
+
if miss:
|
|
132
|
+
error_msg = f"Missing required fields: {miss}"
|
|
133
|
+
logger.warning(f"Tool '{name}' validation failed: {error_msg}")
|
|
134
|
+
obs = {
|
|
135
|
+
"step": i,
|
|
136
|
+
"tool": name,
|
|
137
|
+
"ok": False,
|
|
138
|
+
"error": error_msg,
|
|
139
|
+
"arguments": arguments,
|
|
140
|
+
}
|
|
141
|
+
observations.append(obs)
|
|
142
|
+
yield {"type": "tool_error", "tool": name, "error": error_msg}
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
t0 = time.time()
|
|
146
|
+
try:
|
|
147
|
+
logger.info(f"Calling tool '{name}' with args: {arguments}")
|
|
148
|
+
out = self.tools.call(name, arguments) # type: ignore[arg-type]
|
|
149
|
+
latency = int((time.time() - t0) * 1000)
|
|
150
|
+
|
|
151
|
+
obs = {
|
|
152
|
+
"step": i,
|
|
153
|
+
"tool": name,
|
|
154
|
+
"ok": True,
|
|
155
|
+
"latency_ms": latency,
|
|
156
|
+
"result": out,
|
|
157
|
+
}
|
|
158
|
+
observations.append(obs)
|
|
159
|
+
logger.info(f"Tool '{name}' success ({latency}ms)")
|
|
160
|
+
yield {"type": "tool_result", "tool": name, "result": out}
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
latency = int((time.time() - t0) * 1000)
|
|
164
|
+
logger.error(f"Tool '{name}' failed: {e}")
|
|
165
|
+
obs = {
|
|
166
|
+
"step": i,
|
|
167
|
+
"tool": name,
|
|
168
|
+
"ok": False,
|
|
169
|
+
"latency_ms": latency,
|
|
170
|
+
"error": str(e),
|
|
171
|
+
"arguments": arguments,
|
|
172
|
+
}
|
|
173
|
+
observations.append(obs)
|
|
174
|
+
yield {"type": "tool_error", "tool": name, "error": str(e)}
|
|
175
|
+
|
|
176
|
+
# 3) 汇总输出
|
|
177
|
+
final_reply = ""
|
|
178
|
+
|
|
179
|
+
if reply_text:
|
|
180
|
+
final_reply = reply_text
|
|
181
|
+
elif not observations:
|
|
182
|
+
final_reply = "(没有可执行的步骤或工具返回空结果)"
|
|
183
|
+
elif self.summarizer:
|
|
184
|
+
yield {"type": "planning_thought", "content": "正在汇总执行结果..."}
|
|
185
|
+
# 用你的生成器来生成自然语言总结
|
|
186
|
+
profile_hint = self.profile.render_system_prompt()
|
|
187
|
+
prompt = f"""请将以下工具步骤结果用中文简洁汇总给用户,保留关键信息和结论。
|
|
188
|
+
|
|
189
|
+
[Profile]
|
|
190
|
+
{profile_hint}
|
|
191
|
+
|
|
192
|
+
[Observations]
|
|
193
|
+
{observations}
|
|
194
|
+
|
|
195
|
+
只输出给用户的总结文本。"""
|
|
196
|
+
messages = [
|
|
197
|
+
{
|
|
198
|
+
"role": "system",
|
|
199
|
+
"content": "你是一个严谨的助理。只输出中文总结,不要额外解释。",
|
|
200
|
+
},
|
|
201
|
+
{"role": "user", "content": prompt},
|
|
202
|
+
]
|
|
203
|
+
try:
|
|
204
|
+
_, summary = self.summarizer.execute([None, messages])
|
|
205
|
+
final_reply = summary.strip()
|
|
206
|
+
yield {"type": "reply", "content": final_reply}
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error(f"Summarization failed: {e}")
|
|
209
|
+
final_reply = "Summarization failed."
|
|
210
|
+
yield {"type": "error", "content": "Summarization failed."}
|
|
211
|
+
else:
|
|
212
|
+
# 简单模板
|
|
213
|
+
lines = []
|
|
214
|
+
for obs in observations:
|
|
215
|
+
if obs.get("ok"):
|
|
216
|
+
lines.append(f"#{obs['step'] + 1} 工具 {obs['tool']} 成功:{obs.get('result')}")
|
|
217
|
+
else:
|
|
218
|
+
lines.append(f"#{obs['step'] + 1} 工具 {obs['tool']} 失败:{obs.get('error')}")
|
|
219
|
+
final_reply = "\n".join(lines)
|
|
220
|
+
yield {"type": "reply", "content": final_reply}
|
|
221
|
+
|
|
222
|
+
yield {
|
|
223
|
+
"type": "completed",
|
|
224
|
+
"observations": observations,
|
|
225
|
+
"plan": plan,
|
|
226
|
+
"reply": final_reply,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
def step(self, user_query: str) -> dict[str, Any]:
|
|
230
|
+
"""
|
|
231
|
+
Execute a single turn of conversation.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Dict containing:
|
|
235
|
+
- reply: The final text response
|
|
236
|
+
- observations: List of execution steps and results
|
|
237
|
+
- plan: The original plan
|
|
238
|
+
"""
|
|
239
|
+
# 兼容旧接口,收集流式结果
|
|
240
|
+
result = {"reply": "", "observations": [], "plan": []}
|
|
241
|
+
|
|
242
|
+
for event in self.step_stream(user_query):
|
|
243
|
+
if event["type"] == "completed":
|
|
244
|
+
result["reply"] = event.get("reply", "")
|
|
245
|
+
result["observations"] = event.get("observations", [])
|
|
246
|
+
result["plan"] = event.get("plan", [])
|
|
247
|
+
|
|
248
|
+
return result
|
|
249
|
+
|
|
250
|
+
def execute(self, data: Any) -> dict[str, Any]:
|
|
251
|
+
"""
|
|
252
|
+
Unified Entry Point.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
data: str (query) or dict (config + query)
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Dict containing 'reply', 'observations', 'plan'
|
|
259
|
+
"""
|
|
260
|
+
# 形态 1:直接字符串
|
|
261
|
+
if isinstance(data, str):
|
|
262
|
+
return self.step(data)
|
|
263
|
+
|
|
264
|
+
# 形态 2:字典
|
|
265
|
+
if isinstance(data, dict):
|
|
266
|
+
user_query = data.get("user_query") or data.get("query")
|
|
267
|
+
if not isinstance(user_query, str) or not user_query.strip():
|
|
268
|
+
raise ValueError(
|
|
269
|
+
"AgentRuntime.execute(dict) 需要提供 'user_query' 或 'query'(非空字符串)。"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# 临时覆写 max_steps
|
|
273
|
+
original_max = self.max_steps
|
|
274
|
+
if "max_steps" in data:
|
|
275
|
+
ms = data["max_steps"]
|
|
276
|
+
if not isinstance(ms, int) or ms <= 0:
|
|
277
|
+
raise ValueError("'max_steps' 必须是正整数。")
|
|
278
|
+
self.max_steps = ms
|
|
279
|
+
|
|
280
|
+
# 临时覆写 profile(一次性,不污染实例)
|
|
281
|
+
original_profile = self.profile
|
|
282
|
+
if "profile_overrides" in data and isinstance(data["profile_overrides"], dict):
|
|
283
|
+
try:
|
|
284
|
+
self.profile = self.profile.merged(**data["profile_overrides"])
|
|
285
|
+
except Exception:
|
|
286
|
+
# 失败则回退,不中断主流程
|
|
287
|
+
self.profile = original_profile
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
return self.step(user_query)
|
|
291
|
+
finally:
|
|
292
|
+
# 还原
|
|
293
|
+
self.max_steps = original_max
|
|
294
|
+
self.profile = original_profile
|
|
295
|
+
|
|
296
|
+
raise TypeError("AgentRuntime.execute 仅接受 str 或 dict 两种输入。")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""L4 Agentic Operators.
|
|
2
|
+
|
|
3
|
+
This package exposes ready-to-use operator wrappers (MapOperators) built on
|
|
4
|
+
sage.libs.agentic components so Studio and pipeline builders can drag-and-drop
|
|
5
|
+
agent runtimes without wiring boilerplate.
|
|
6
|
+
|
|
7
|
+
Supports engine_type switching for LLM generators:
|
|
8
|
+
- sagellm (default): SageLLMGenerator with configurable backend
|
|
9
|
+
- backend_type="auto": Automatically select best available backend
|
|
10
|
+
- backend_type="mock": Mock backend for testing without GPU
|
|
11
|
+
- backend_type="cuda": NVIDIA CUDA backend
|
|
12
|
+
- backend_type="ascend": Huawei Ascend NPU backend
|
|
13
|
+
- openai: OpenAIGenerator for OpenAI-compatible APIs
|
|
14
|
+
- hf: HFGenerator for HuggingFace models
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .config import (
|
|
18
|
+
AgentRuntimeConfig,
|
|
19
|
+
GeneratorConfig,
|
|
20
|
+
ProfileConfig,
|
|
21
|
+
RuntimeSettings,
|
|
22
|
+
)
|
|
23
|
+
from .planning_operator import PlanningOperator
|
|
24
|
+
from .refined_searcher import RefinedSearcherOperator
|
|
25
|
+
from .runtime import AgentRuntimeOperator
|
|
26
|
+
from .timing_operator import TimingOperator
|
|
27
|
+
from .tool_selection_operator import ToolSelectionOperator
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
# Operators
|
|
31
|
+
"AgentRuntimeOperator",
|
|
32
|
+
"ToolSelectionOperator",
|
|
33
|
+
"PlanningOperator",
|
|
34
|
+
"TimingOperator",
|
|
35
|
+
"RefinedSearcherOperator",
|
|
36
|
+
# Config classes
|
|
37
|
+
"AgentRuntimeConfig",
|
|
38
|
+
"GeneratorConfig",
|
|
39
|
+
"ProfileConfig",
|
|
40
|
+
"RuntimeSettings",
|
|
41
|
+
]
|