langchain-agentx-python 0.8.0__py3-none-any.whl → 0.8.2__py3-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.
- langchain_agentx/__init__.py +64 -64
- langchain_agentx/loop/config/agent_config.py +18 -8
- langchain_agentx/loop/subagent/runner.py +27 -2
- langchain_agentx/observability/events/langchain_agentx_event_adapter.py +14 -10
- langchain_agentx/provider/semantics/__init__.py +74 -0
- langchain_agentx/provider/semantics/answer_assembler.py +105 -0
- langchain_agentx/provider/semantics/contracts.py +184 -0
- langchain_agentx/provider/semantics/families/__init__.py +16 -0
- langchain_agentx/provider/semantics/families/anthropic.py +186 -0
- langchain_agentx/provider/semantics/families/anthropic_codec.py +62 -0
- langchain_agentx/provider/semantics/families/anthropic_interpreter.py +69 -0
- langchain_agentx/provider/semantics/families/anthropic_normalizer.py +87 -0
- langchain_agentx/provider/semantics/families/compatible_openai.py +229 -0
- langchain_agentx/provider/semantics/families/compatible_openai_codec.py +67 -0
- langchain_agentx/provider/semantics/families/compatible_openai_interpreter.py +101 -0
- langchain_agentx/provider/semantics/families/compatible_openai_normalizer.py +97 -0
- langchain_agentx/provider/semantics/families/openai.py +212 -0
- langchain_agentx/provider/semantics/families/openai_codec.py +62 -0
- langchain_agentx/provider/semantics/families/openai_interpreter.py +61 -0
- langchain_agentx/provider/semantics/families/openai_normalizer.py +67 -0
- langchain_agentx/provider/semantics/message_normalizer.py +104 -0
- langchain_agentx/provider/semantics/profile.py +185 -0
- langchain_agentx/provider/semantics/registry.py +209 -0
- langchain_agentx/provider/semantics/request_codec.py +74 -0
- langchain_agentx/provider/semantics/runtime.py +359 -0
- langchain_agentx/provider/semantics/stream_normalizer.py +74 -0
- langchain_agentx/provider/semantics/tool_call_interpreter.py +192 -0
- langchain_agentx/provider/semantics/turn_accumulator.py +85 -0
- {langchain_agentx_python-0.8.0.dist-info → langchain_agentx_python-0.8.2.dist-info}/METADATA +519 -519
- {langchain_agentx_python-0.8.0.dist-info → langchain_agentx_python-0.8.2.dist-info}/RECORD +33 -9
- {langchain_agentx_python-0.8.0.dist-info → langchain_agentx_python-0.8.2.dist-info}/LICENSE +0 -0
- {langchain_agentx_python-0.8.0.dist-info → langchain_agentx_python-0.8.2.dist-info}/WHEEL +0 -0
- {langchain_agentx_python-0.8.0.dist-info → langchain_agentx_python-0.8.2.dist-info}/top_level.txt +0 -0
langchain_agentx/__init__.py
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
"""
|
|
2
|
-
LangChain-based agent utilities for CodeBaseX.
|
|
3
|
-
|
|
4
|
-
This package provides:
|
|
5
|
-
- LangChain-AgentX LangGraph agent factory (`loop`)
|
|
6
|
-
|
|
7
|
-
Typical usage:
|
|
8
|
-
|
|
9
|
-
```python
|
|
10
|
-
from langchain_agentx import create_loop_agent
|
|
11
|
-
```
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
__version__ = "0.8.
|
|
15
|
-
|
|
16
|
-
from .loop import ( # noqa: F401
|
|
17
|
-
create_loop_agent,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
from .loop.hook import ( # noqa: F401
|
|
21
|
-
ATTACHMENT_HOOK_BLOCKING_ERROR,
|
|
22
|
-
ATTACHMENT_HOOK_CANCELLED,
|
|
23
|
-
ATTACHMENT_HOOK_NON_BLOCKING_ERROR,
|
|
24
|
-
ATTACHMENT_HOOK_STOPPED_CONTINUATION,
|
|
25
|
-
ATTACHMENT_HOOK_SUCCESS,
|
|
26
|
-
)
|
|
27
|
-
from .observability import ( # noqa: F401
|
|
28
|
-
EventAdapterState,
|
|
29
|
-
LangchainAgentEvent,
|
|
30
|
-
LangchainAgentEventType,
|
|
31
|
-
LangGraphToLangchainAgentEventAdapter,
|
|
32
|
-
TraceCollector,
|
|
33
|
-
TraceSession,
|
|
34
|
-
TraceSpan,
|
|
35
|
-
)
|
|
36
|
-
from .provider import CompatibleChatOpenAI # noqa: F401
|
|
37
|
-
from .session import AgentSession, create_agent_session # noqa: F401
|
|
38
|
-
from .health import HealthCheck, HealthReport, PreflightResult, preflight, sdk_health # noqa: F401
|
|
39
|
-
from .workflow import BaseWorkflow # noqa: F401
|
|
40
|
-
|
|
41
|
-
__all__ = [
|
|
42
|
-
"ATTACHMENT_HOOK_BLOCKING_ERROR",
|
|
43
|
-
"ATTACHMENT_HOOK_CANCELLED",
|
|
44
|
-
"ATTACHMENT_HOOK_NON_BLOCKING_ERROR",
|
|
45
|
-
"ATTACHMENT_HOOK_STOPPED_CONTINUATION",
|
|
46
|
-
"ATTACHMENT_HOOK_SUCCESS",
|
|
47
|
-
"create_loop_agent",
|
|
48
|
-
"create_agent_session",
|
|
49
|
-
"AgentSession",
|
|
50
|
-
"BaseWorkflow",
|
|
51
|
-
"EventAdapterState",
|
|
52
|
-
"LangchainAgentEvent",
|
|
53
|
-
"LangchainAgentEventType",
|
|
54
|
-
"LangGraphToLangchainAgentEventAdapter",
|
|
55
|
-
"TraceCollector",
|
|
56
|
-
"TraceSession",
|
|
57
|
-
"TraceSpan",
|
|
58
|
-
"CompatibleChatOpenAI",
|
|
59
|
-
"HealthCheck",
|
|
60
|
-
"HealthReport",
|
|
61
|
-
"PreflightResult",
|
|
62
|
-
"preflight",
|
|
63
|
-
"sdk_health",
|
|
64
|
-
]
|
|
1
|
+
"""
|
|
2
|
+
LangChain-based agent utilities for CodeBaseX.
|
|
3
|
+
|
|
4
|
+
This package provides:
|
|
5
|
+
- LangChain-AgentX LangGraph agent factory (`loop`)
|
|
6
|
+
|
|
7
|
+
Typical usage:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from langchain_agentx import create_loop_agent
|
|
11
|
+
```
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__version__ = "0.8.2"
|
|
15
|
+
|
|
16
|
+
from .loop import ( # noqa: F401
|
|
17
|
+
create_loop_agent,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from .loop.hook import ( # noqa: F401
|
|
21
|
+
ATTACHMENT_HOOK_BLOCKING_ERROR,
|
|
22
|
+
ATTACHMENT_HOOK_CANCELLED,
|
|
23
|
+
ATTACHMENT_HOOK_NON_BLOCKING_ERROR,
|
|
24
|
+
ATTACHMENT_HOOK_STOPPED_CONTINUATION,
|
|
25
|
+
ATTACHMENT_HOOK_SUCCESS,
|
|
26
|
+
)
|
|
27
|
+
from .observability import ( # noqa: F401
|
|
28
|
+
EventAdapterState,
|
|
29
|
+
LangchainAgentEvent,
|
|
30
|
+
LangchainAgentEventType,
|
|
31
|
+
LangGraphToLangchainAgentEventAdapter,
|
|
32
|
+
TraceCollector,
|
|
33
|
+
TraceSession,
|
|
34
|
+
TraceSpan,
|
|
35
|
+
)
|
|
36
|
+
from .provider import CompatibleChatOpenAI # noqa: F401
|
|
37
|
+
from .session import AgentSession, create_agent_session # noqa: F401
|
|
38
|
+
from .health import HealthCheck, HealthReport, PreflightResult, preflight, sdk_health # noqa: F401
|
|
39
|
+
from .workflow import BaseWorkflow # noqa: F401
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"ATTACHMENT_HOOK_BLOCKING_ERROR",
|
|
43
|
+
"ATTACHMENT_HOOK_CANCELLED",
|
|
44
|
+
"ATTACHMENT_HOOK_NON_BLOCKING_ERROR",
|
|
45
|
+
"ATTACHMENT_HOOK_STOPPED_CONTINUATION",
|
|
46
|
+
"ATTACHMENT_HOOK_SUCCESS",
|
|
47
|
+
"create_loop_agent",
|
|
48
|
+
"create_agent_session",
|
|
49
|
+
"AgentSession",
|
|
50
|
+
"BaseWorkflow",
|
|
51
|
+
"EventAdapterState",
|
|
52
|
+
"LangchainAgentEvent",
|
|
53
|
+
"LangchainAgentEventType",
|
|
54
|
+
"LangGraphToLangchainAgentEventAdapter",
|
|
55
|
+
"TraceCollector",
|
|
56
|
+
"TraceSession",
|
|
57
|
+
"TraceSpan",
|
|
58
|
+
"CompatibleChatOpenAI",
|
|
59
|
+
"HealthCheck",
|
|
60
|
+
"HealthReport",
|
|
61
|
+
"PreflightResult",
|
|
62
|
+
"preflight",
|
|
63
|
+
"sdk_health",
|
|
64
|
+
]
|
|
@@ -14,21 +14,31 @@ from __future__ import annotations
|
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
DEFAULT_RECURSION_LIMIT = 9_999
|
|
18
|
+
|
|
19
|
+
|
|
17
20
|
def merge_run_config(
|
|
18
21
|
graph: Any,
|
|
19
22
|
caller: dict[str, Any] | None = None,
|
|
20
23
|
) -> dict[str, Any]:
|
|
21
|
-
"""合并 graph
|
|
24
|
+
"""合并 graph 绑定 config 与调用方 config。
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
``caller``
|
|
26
|
+
保留 graph 上通过 ``with_config`` 绑定的全部 LangGraph config(如
|
|
27
|
+
``configurable`` / ``metadata``),同时让 ``caller`` 的同名字段覆盖。
|
|
28
|
+
对于 ``metadata`` 与 ``configurable`` 这两个嵌套 dict,采用浅合并,
|
|
29
|
+
避免调用方仅传部分字段时整体替换 graph 绑定值。
|
|
25
30
|
"""
|
|
26
31
|
bound_raw = getattr(graph, "config", None)
|
|
27
|
-
bound: dict[str, Any] = bound_raw if isinstance(bound_raw, dict) else {}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
bound: dict[str, Any] = dict(bound_raw) if isinstance(bound_raw, dict) else {}
|
|
33
|
+
bound.setdefault("recursion_limit", DEFAULT_RECURSION_LIMIT)
|
|
34
|
+
if not caller:
|
|
35
|
+
return bound
|
|
36
|
+
merged = dict(bound)
|
|
37
|
+
merged.update(caller)
|
|
38
|
+
if isinstance(bound.get("metadata"), dict) and isinstance(caller.get("metadata"), dict):
|
|
39
|
+
merged["metadata"] = {**bound["metadata"], **caller["metadata"]}
|
|
40
|
+
if isinstance(bound.get("configurable"), dict) and isinstance(caller.get("configurable"), dict):
|
|
41
|
+
merged["configurable"] = {**bound["configurable"], **caller["configurable"]}
|
|
32
42
|
return merged
|
|
33
43
|
|
|
34
44
|
|
|
@@ -407,10 +407,13 @@ async def _collect_subagent_events(
|
|
|
407
407
|
tool_step = 0
|
|
408
408
|
|
|
409
409
|
# 构建子 graph config:合并父 runnable_config 使事件自动冒泡
|
|
410
|
-
config = {"tags": run_tags
|
|
410
|
+
config = {"tags": run_tags}
|
|
411
411
|
parent_runnable_config = getattr(ctx, "runnable_config", None)
|
|
412
412
|
if parent_runnable_config is not None:
|
|
413
413
|
if isinstance(parent_runnable_config, dict):
|
|
414
|
+
# 从父 config 继承 recursion_limit(如果有)
|
|
415
|
+
if "recursion_limit" in parent_runnable_config:
|
|
416
|
+
config["recursion_limit"] = parent_runnable_config["recursion_limit"]
|
|
414
417
|
# 合并父 config 的 configurable 字段,使子 graph 事件冒泡到父 graph
|
|
415
418
|
configurable = parent_runnable_config.get("configurable", {})
|
|
416
419
|
if configurable:
|
|
@@ -480,8 +483,29 @@ async def _build_subagent_result(
|
|
|
480
483
|
transcript: SubagentTranscriptManager,
|
|
481
484
|
allowed_tool_names: set[str] | None,
|
|
482
485
|
trace: SubagentExecutionTrace,
|
|
486
|
+
model: Any,
|
|
483
487
|
) -> SubagentResult:
|
|
484
|
-
|
|
488
|
+
"""构建子 Agent 执行结果。
|
|
489
|
+
|
|
490
|
+
使用 ProviderSemanticsRuntime 提取最终答案,确保与主链逻辑一致。
|
|
491
|
+
"""
|
|
492
|
+
from langchain_agentx.provider.semantics.runtime import (
|
|
493
|
+
create_runtime_from_messages,
|
|
494
|
+
create_runtime_from_model,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
# 字符串 model 无法可靠解析 provider family;按消息形状推断 runtime
|
|
498
|
+
if isinstance(model, str):
|
|
499
|
+
runtime = create_runtime_from_messages(collected.collected_assistant_messages)
|
|
500
|
+
else:
|
|
501
|
+
runtime = create_runtime_from_model(model)
|
|
502
|
+
|
|
503
|
+
# 使用 runtime 提取最终答案
|
|
504
|
+
final_answer = runtime.extract_final_answer_from_messages(
|
|
505
|
+
collected.collected_assistant_messages,
|
|
506
|
+
allowed_tool_names=allowed_tool_names,
|
|
507
|
+
)
|
|
508
|
+
content = final_answer.text
|
|
485
509
|
status = "error" if collected.terminal_reason == "error" else "completed"
|
|
486
510
|
transcript.flush([_to_message_dict(m) for m in collected.collected_assistant_messages])
|
|
487
511
|
if not content and status == "completed":
|
|
@@ -571,6 +595,7 @@ async def run_subagent(
|
|
|
571
595
|
transcript=transcript,
|
|
572
596
|
allowed_tool_names=allowed_tool_names,
|
|
573
597
|
trace=trace,
|
|
598
|
+
model=resolved_model,
|
|
574
599
|
)
|
|
575
600
|
|
|
576
601
|
finally:
|
|
@@ -564,19 +564,24 @@ class LangGraphToLangchainAgentEventAdapter:
|
|
|
564
564
|
)
|
|
565
565
|
|
|
566
566
|
def _extract_answer_from_chain_end_output(self, data: Dict[str, Any]) -> str:
|
|
567
|
-
"""从 on_chain_end 的 output
|
|
567
|
+
"""从 on_chain_end 的 output 中提取最终可见答案。
|
|
568
|
+
|
|
569
|
+
对齐 A19 visible-answer-only 规则,统一复用 ProviderSemanticsRuntime,
|
|
570
|
+
避免在 adapter 内继续直接解析 provider 原始消息形状。
|
|
571
|
+
"""
|
|
568
572
|
output = data.get("output") if isinstance(data, dict) else None
|
|
569
573
|
if not isinstance(output, dict) or "messages" not in output:
|
|
570
574
|
return ""
|
|
575
|
+
|
|
571
576
|
messages = output.get("messages")
|
|
572
577
|
if not isinstance(messages, (list, tuple)):
|
|
573
578
|
return ""
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
return
|
|
579
|
+
|
|
580
|
+
from langchain_agentx.provider.semantics.runtime import create_runtime_from_messages
|
|
581
|
+
|
|
582
|
+
runtime = create_runtime_from_messages(list(messages))
|
|
583
|
+
answer = runtime.extract_final_answer_from_messages(list(messages))
|
|
584
|
+
return answer.text
|
|
580
585
|
|
|
581
586
|
@staticmethod
|
|
582
587
|
def _extract_text_from_message_content(content: Any) -> str:
|
|
@@ -818,10 +823,9 @@ class LangGraphToLangchainAgentEventAdapter:
|
|
|
818
823
|
)
|
|
819
824
|
self.state.is_step_started = False
|
|
820
825
|
|
|
821
|
-
# answer
|
|
826
|
+
# answer:只输出可见文本(A19 visible-answer-only 规则)
|
|
827
|
+
# reasoning 不再作为 fallback,reasoning_content 仅用于 reasoning 事件流
|
|
822
828
|
answer = self.state.accumulated_answer.strip()
|
|
823
|
-
if not answer:
|
|
824
|
-
answer = self.state.accumulated_reasoning.strip()
|
|
825
829
|
if not answer:
|
|
826
830
|
answer = self._extract_answer_from_chain_end_output(data)
|
|
827
831
|
loop_payload = self._build_loop_observation_payload()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
provider/semantics — Provider Semantics Boundary
|
|
3
|
+
|
|
4
|
+
本模块提供 provider 语义归一化能力:
|
|
5
|
+
- contracts: 契约对象定义
|
|
6
|
+
- profile: Provider profile 解析
|
|
7
|
+
- registry: Family → suite 注册表
|
|
8
|
+
- runtime: 业务层门面对象
|
|
9
|
+
- turn_accumulator: 状态收集器
|
|
10
|
+
- answer_assembler: 最终答案装配器
|
|
11
|
+
- families: 各 family 特定实现
|
|
12
|
+
|
|
13
|
+
使用方式:
|
|
14
|
+
from langchain_agentx.provider.semantics import create_runtime_from_model
|
|
15
|
+
|
|
16
|
+
runtime = create_runtime_from_model(model)
|
|
17
|
+
accumulator = runtime.new_accumulator()
|
|
18
|
+
# ... 使用 runtime 和 accumulator
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from langchain_agentx.provider.semantics.answer_assembler import (
|
|
24
|
+
FinalAnswerAssembler,
|
|
25
|
+
get_default_assembler,
|
|
26
|
+
)
|
|
27
|
+
from langchain_agentx.provider.semantics.contracts import (
|
|
28
|
+
FinalAnswer,
|
|
29
|
+
NormalizedAssistantTurn,
|
|
30
|
+
NormalizedDelta,
|
|
31
|
+
NormalizedTurnLog,
|
|
32
|
+
ProviderCapabilities,
|
|
33
|
+
ProviderProfile,
|
|
34
|
+
)
|
|
35
|
+
from langchain_agentx.provider.semantics.profile import (
|
|
36
|
+
ProviderProfileResolver,
|
|
37
|
+
get_default_profile_resolver,
|
|
38
|
+
)
|
|
39
|
+
from langchain_agentx.provider.semantics.registry import (
|
|
40
|
+
ProviderSemanticsRegistry,
|
|
41
|
+
ProviderSemanticsSuite,
|
|
42
|
+
get_default_registry,
|
|
43
|
+
)
|
|
44
|
+
from langchain_agentx.provider.semantics.runtime import (
|
|
45
|
+
ProviderSemanticsRuntime,
|
|
46
|
+
create_runtime_from_model,
|
|
47
|
+
create_runtime_from_messages,
|
|
48
|
+
)
|
|
49
|
+
from langchain_agentx.provider.semantics.turn_accumulator import TurnAccumulator
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
# Contracts
|
|
53
|
+
"FinalAnswer",
|
|
54
|
+
"NormalizedAssistantTurn",
|
|
55
|
+
"NormalizedDelta",
|
|
56
|
+
"NormalizedTurnLog",
|
|
57
|
+
"ProviderCapabilities",
|
|
58
|
+
"ProviderProfile",
|
|
59
|
+
# Profile
|
|
60
|
+
"ProviderProfileResolver",
|
|
61
|
+
"get_default_profile_resolver",
|
|
62
|
+
# Registry
|
|
63
|
+
"ProviderSemanticsRegistry",
|
|
64
|
+
"ProviderSemanticsSuite",
|
|
65
|
+
"get_default_registry",
|
|
66
|
+
# Runtime
|
|
67
|
+
"ProviderSemanticsRuntime",
|
|
68
|
+
"create_runtime_from_model",
|
|
69
|
+
"create_runtime_from_messages",
|
|
70
|
+
# Components
|
|
71
|
+
"TurnAccumulator",
|
|
72
|
+
"FinalAnswerAssembler",
|
|
73
|
+
"get_default_assembler",
|
|
74
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
answer_assembler.py — Provider-Agnostic 最终答案装配器
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
从 NormalizedTurnLog 中装配最终可见答案
|
|
6
|
+
必须对 provider 无感知,只消费统一数据结构
|
|
7
|
+
|
|
8
|
+
链路位置:
|
|
9
|
+
在整体流程中的位置:TurnAccumulator → FinalAnswerAssembler → 业务层
|
|
10
|
+
主路径和子路径共享同一装配规则,确保一致性
|
|
11
|
+
|
|
12
|
+
当前裁剪范围:
|
|
13
|
+
- 不直接读取原始 AIMessage
|
|
14
|
+
- 不关心 provider family
|
|
15
|
+
- 默认不允许 reasoning 充当 answer fallback
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from langchain_agentx.provider.semantics.contracts import (
|
|
25
|
+
FinalAnswer,
|
|
26
|
+
NormalizedTurnLog,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FinalAnswerAssembler:
|
|
33
|
+
"""Provider-Agnostic 最终答案装配器。
|
|
34
|
+
|
|
35
|
+
从 NormalizedTurnLog 中按优先级选择最终可见答案:
|
|
36
|
+
1. model_end 有可见文本且不是纯工具调度消息
|
|
37
|
+
2. stream 累积有可见文本
|
|
38
|
+
3. chain_end 有可见文本且不是纯工具调度消息
|
|
39
|
+
4. 返回空字符串(不拿 reasoning 顶替)
|
|
40
|
+
|
|
41
|
+
业务层(loop、observability、subagent)共享此装配器,确保一致性。
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def assemble(self, log: "NormalizedTurnLog") -> "FinalAnswer":
|
|
45
|
+
"""从 turn log 中装配最终可见答案。
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
log: NormalizedTurnLog 实例
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
FinalAnswer 实例
|
|
52
|
+
"""
|
|
53
|
+
from langchain_agentx.provider.semantics.contracts import FinalAnswer
|
|
54
|
+
|
|
55
|
+
# 优先:最后一条 model_end 有可见文本且不是纯工具调度
|
|
56
|
+
model_turn = log.last_model_end_turn_with_visible_text()
|
|
57
|
+
if model_turn and not model_turn.tool_dispatch_only:
|
|
58
|
+
return FinalAnswer(
|
|
59
|
+
text=model_turn.visible_text,
|
|
60
|
+
source="model_end",
|
|
61
|
+
diagnostics=[],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# 次选:stream 累积的可见文本
|
|
65
|
+
stream_text = log.visible_stream_text()
|
|
66
|
+
if stream_text:
|
|
67
|
+
return FinalAnswer(
|
|
68
|
+
text=stream_text,
|
|
69
|
+
source="stream",
|
|
70
|
+
diagnostics=["model_end_missing_visible_text"],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# 再次选:最后一条 chain_end 有可见文本且不是纯工具调度
|
|
74
|
+
chain_turn = log.last_chain_end_turn_with_visible_text()
|
|
75
|
+
if chain_turn and not chain_turn.tool_dispatch_only:
|
|
76
|
+
return FinalAnswer(
|
|
77
|
+
text=chain_turn.visible_text,
|
|
78
|
+
source="chain_end",
|
|
79
|
+
diagnostics=["model_end_missing_visible_text"],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# 最后:返回空字符串(不拿 reasoning 顶替)
|
|
83
|
+
return FinalAnswer(
|
|
84
|
+
text="",
|
|
85
|
+
source="empty",
|
|
86
|
+
diagnostics=["no_visible_answer"],
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# 单例实例
|
|
91
|
+
_default_assembler: FinalAnswerAssembler | None = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_default_assembler() -> FinalAnswerAssembler:
|
|
95
|
+
"""获取默认的 FinalAnswerAssembler 单例。"""
|
|
96
|
+
global _default_assembler
|
|
97
|
+
if _default_assembler is None:
|
|
98
|
+
_default_assembler = FinalAnswerAssembler()
|
|
99
|
+
return _default_assembler
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
__all__ = [
|
|
103
|
+
"FinalAnswerAssembler",
|
|
104
|
+
"get_default_assembler",
|
|
105
|
+
]
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
contracts.py — Provider 语义契约对象
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
定义 provider 语义归一化的核心数据结构,包括:
|
|
6
|
+
- ProviderCapabilities: provider 能力画像
|
|
7
|
+
- ProviderProfile: provider 家族与能力组合
|
|
8
|
+
- NormalizedDelta: 归一化后的流式增量事件
|
|
9
|
+
- NormalizedAssistantTurn: 归一化后的 assistant 轮次表示
|
|
10
|
+
- FinalAnswer: 最终给业务层的输出
|
|
11
|
+
|
|
12
|
+
链路位置:
|
|
13
|
+
在整体流程中的位置:ProviderSemanticsBoundary → 业务层
|
|
14
|
+
业务层只消费这些统一契约,不感知 provider 原始形状
|
|
15
|
+
|
|
16
|
+
当前裁剪范围:
|
|
17
|
+
- 不包含 provider 特定的解析逻辑(由 family 实现)
|
|
18
|
+
- 不包含流式事件的实际解析(由 normalizer 实现)
|
|
19
|
+
- 不包含最终答案的选择算法(由 assembler 实现)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class ProviderCapabilities:
|
|
30
|
+
"""Provider 能力画像。
|
|
31
|
+
|
|
32
|
+
描述 provider 支持或常见的语义特征,用于兼容官方与网关之间的细微差异。
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# content_blocks: Anthropic 使用 blocks 列表格式
|
|
36
|
+
content_blocks: bool = False
|
|
37
|
+
|
|
38
|
+
# reasoning_channel: 是否支持独立的 reasoning 通道(如 OpenAI reasoning_content)
|
|
39
|
+
reasoning_channel: bool = False
|
|
40
|
+
|
|
41
|
+
# structured_tool_calls: 是否支持结构化 tool_calls 字段
|
|
42
|
+
structured_tool_calls: bool = True
|
|
43
|
+
|
|
44
|
+
# degraded_tool_tags: 是否可能产生退化格式的工具调用(XML tag / CALL_TOOL: 前缀)
|
|
45
|
+
degraded_tool_tags: bool = False
|
|
46
|
+
|
|
47
|
+
# final_text_may_only_appear_in_model_end: 是否只在 model_end 时才有最终文本
|
|
48
|
+
#(某些 provider 的 stream 只包含 delta,不包含完整内容)
|
|
49
|
+
final_text_may_only_appear_in_model_end: bool = False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class ProviderProfile:
|
|
54
|
+
"""Provider 家族与能力组合。
|
|
55
|
+
|
|
56
|
+
业务层只依赖 family 字段做粗粒度分支,细粒度差异由 capabilities 处理。
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
# family: provider 家族标识(anthropic / openai / compatible_openai)
|
|
60
|
+
family: str
|
|
61
|
+
|
|
62
|
+
# model_name: 模型名称(可选,用于调试和日志)
|
|
63
|
+
model_name: str | None = None
|
|
64
|
+
|
|
65
|
+
# capabilities: 该 provider 的能力画像
|
|
66
|
+
capabilities: ProviderCapabilities = field(default_factory=ProviderCapabilities)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class NormalizedDelta:
|
|
71
|
+
"""归一化后的流式增量事件。
|
|
72
|
+
|
|
73
|
+
将不同 provider 的 stream chunk 统一为相同格式。
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
# kind: 增量类型(text / reasoning / tool_call)
|
|
77
|
+
kind: str
|
|
78
|
+
|
|
79
|
+
# text: 文本内容(kind=text 或 kind=reasoning 时使用)
|
|
80
|
+
text: str = ""
|
|
81
|
+
|
|
82
|
+
# tool_calls: 工具调用列表(kind=tool_call 时使用)
|
|
83
|
+
tool_calls: list[dict] = field(default_factory=list)
|
|
84
|
+
|
|
85
|
+
# source: 数据来源标记(stream)
|
|
86
|
+
source: str = "stream"
|
|
87
|
+
|
|
88
|
+
# diagnostics: 诊断信息列表(用于调试)
|
|
89
|
+
diagnostics: list[str] = field(default_factory=list)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class NormalizedAssistantTurn:
|
|
94
|
+
"""归一化后的 assistant 轮次表示。
|
|
95
|
+
|
|
96
|
+
将 model_end 或 chain_end 的 AIMessage 统一为相同格式。
|
|
97
|
+
业务层只消费此结构,不直接读取 AIMessage.content 原始形状。
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
# visible_text: 可见文本(用户可见的答案内容)
|
|
101
|
+
visible_text: str = ""
|
|
102
|
+
|
|
103
|
+
# reasoning_text: 推理文本(过程内容,不作为默认答案)
|
|
104
|
+
reasoning_text: str = ""
|
|
105
|
+
|
|
106
|
+
# tool_calls: 工具调用列表
|
|
107
|
+
tool_calls: list[dict] = field(default_factory=list)
|
|
108
|
+
|
|
109
|
+
# tool_dispatch_only: 是否是纯工具调度消息(无可见文本)
|
|
110
|
+
# 用于 FinalAnswerAssembler 跳过此类消息
|
|
111
|
+
tool_dispatch_only: bool = False
|
|
112
|
+
|
|
113
|
+
# source: 数据来源(model_end / chain_end)
|
|
114
|
+
source: str = "model_end"
|
|
115
|
+
|
|
116
|
+
# diagnostics: 诊断信息列表(用于调试)
|
|
117
|
+
diagnostics: list[str] = field(default_factory=list)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class FinalAnswer:
|
|
122
|
+
"""最终给业务层的输出。
|
|
123
|
+
|
|
124
|
+
由 FinalAnswerAssembler 从归一化结果装配而来。
|
|
125
|
+
业务层(loop、observability、subagent)只消费此结构。
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
# text: 最终可见文本
|
|
129
|
+
text: str = ""
|
|
130
|
+
|
|
131
|
+
# source: 答案来源(model_end / stream / chain_end / empty)
|
|
132
|
+
source: str = "empty"
|
|
133
|
+
|
|
134
|
+
# diagnostics: 诊断信息列表(用于调试和错误追踪)
|
|
135
|
+
diagnostics: list[str] = field(default_factory=list)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass
|
|
139
|
+
class NormalizedTurnLog:
|
|
140
|
+
"""单轮模型调用的归一化事实记录。
|
|
141
|
+
|
|
142
|
+
由 TurnAccumulator 收集,供 FinalAnswerAssembler 使用。
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
# stream_deltas: 本轮累积的流式增量列表
|
|
146
|
+
stream_deltas: list[NormalizedDelta] = field(default_factory=list)
|
|
147
|
+
|
|
148
|
+
# model_end_turns: 本轮收集的 model_end turns(可能有多个,通常只有一个)
|
|
149
|
+
model_end_turns: list[NormalizedAssistantTurn] = field(default_factory=list)
|
|
150
|
+
|
|
151
|
+
# chain_end_turns: 本轮收集的 chain_end turns(用于 fallback)
|
|
152
|
+
chain_end_turns: list[NormalizedAssistantTurn] = field(default_factory=list)
|
|
153
|
+
|
|
154
|
+
def last_model_end_turn_with_visible_text(self) -> NormalizedAssistantTurn | None:
|
|
155
|
+
"""获取最后一条有可见文本且不是纯工具调度的 model_end turn。"""
|
|
156
|
+
for turn in reversed(self.model_end_turns):
|
|
157
|
+
if turn.visible_text and not turn.tool_dispatch_only:
|
|
158
|
+
return turn
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def last_chain_end_turn_with_visible_text(self) -> NormalizedAssistantTurn | None:
|
|
162
|
+
"""获取最后一条有可见文本且不是纯工具调度的 chain_end turn。"""
|
|
163
|
+
for turn in reversed(self.chain_end_turns):
|
|
164
|
+
if turn.visible_text and not turn.tool_dispatch_only:
|
|
165
|
+
return turn
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
def visible_stream_text(self) -> str:
|
|
169
|
+
"""获取本轮 stream 累积的可见文本。"""
|
|
170
|
+
return "".join(
|
|
171
|
+
delta.text
|
|
172
|
+
for delta in self.stream_deltas
|
|
173
|
+
if delta.kind == "text"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
__all__ = [
|
|
178
|
+
"ProviderCapabilities",
|
|
179
|
+
"ProviderProfile",
|
|
180
|
+
"NormalizedDelta",
|
|
181
|
+
"NormalizedAssistantTurn",
|
|
182
|
+
"FinalAnswer",
|
|
183
|
+
"NormalizedTurnLog",
|
|
184
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
families — Provider Family 特定实现
|
|
3
|
+
|
|
4
|
+
本模块包含各 provider family 的语义协作者:
|
|
5
|
+
- anthropic.py: Anthropic family
|
|
6
|
+
- openai.py: OpenAI family
|
|
7
|
+
- compatible_openai.py: Compatible OpenAI family
|
|
8
|
+
|
|
9
|
+
每个 family 包含三个协作者:
|
|
10
|
+
- MessageNormalizer: 归一化 model_end / chain_end 消息
|
|
11
|
+
- StreamNormalizer: 归一化流式增量
|
|
12
|
+
- ToolCallInterpreter: 识别工具调度
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
__all__ = []
|