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.
Files changed (33) hide show
  1. langchain_agentx/__init__.py +64 -64
  2. langchain_agentx/loop/config/agent_config.py +18 -8
  3. langchain_agentx/loop/subagent/runner.py +27 -2
  4. langchain_agentx/observability/events/langchain_agentx_event_adapter.py +14 -10
  5. langchain_agentx/provider/semantics/__init__.py +74 -0
  6. langchain_agentx/provider/semantics/answer_assembler.py +105 -0
  7. langchain_agentx/provider/semantics/contracts.py +184 -0
  8. langchain_agentx/provider/semantics/families/__init__.py +16 -0
  9. langchain_agentx/provider/semantics/families/anthropic.py +186 -0
  10. langchain_agentx/provider/semantics/families/anthropic_codec.py +62 -0
  11. langchain_agentx/provider/semantics/families/anthropic_interpreter.py +69 -0
  12. langchain_agentx/provider/semantics/families/anthropic_normalizer.py +87 -0
  13. langchain_agentx/provider/semantics/families/compatible_openai.py +229 -0
  14. langchain_agentx/provider/semantics/families/compatible_openai_codec.py +67 -0
  15. langchain_agentx/provider/semantics/families/compatible_openai_interpreter.py +101 -0
  16. langchain_agentx/provider/semantics/families/compatible_openai_normalizer.py +97 -0
  17. langchain_agentx/provider/semantics/families/openai.py +212 -0
  18. langchain_agentx/provider/semantics/families/openai_codec.py +62 -0
  19. langchain_agentx/provider/semantics/families/openai_interpreter.py +61 -0
  20. langchain_agentx/provider/semantics/families/openai_normalizer.py +67 -0
  21. langchain_agentx/provider/semantics/message_normalizer.py +104 -0
  22. langchain_agentx/provider/semantics/profile.py +185 -0
  23. langchain_agentx/provider/semantics/registry.py +209 -0
  24. langchain_agentx/provider/semantics/request_codec.py +74 -0
  25. langchain_agentx/provider/semantics/runtime.py +359 -0
  26. langchain_agentx/provider/semantics/stream_normalizer.py +74 -0
  27. langchain_agentx/provider/semantics/tool_call_interpreter.py +192 -0
  28. langchain_agentx/provider/semantics/turn_accumulator.py +85 -0
  29. {langchain_agentx_python-0.8.0.dist-info → langchain_agentx_python-0.8.2.dist-info}/METADATA +519 -519
  30. {langchain_agentx_python-0.8.0.dist-info → langchain_agentx_python-0.8.2.dist-info}/RECORD +33 -9
  31. {langchain_agentx_python-0.8.0.dist-info → langchain_agentx_python-0.8.2.dist-info}/LICENSE +0 -0
  32. {langchain_agentx_python-0.8.0.dist-info → langchain_agentx_python-0.8.2.dist-info}/WHEEL +0 -0
  33. {langchain_agentx_python-0.8.0.dist-info → langchain_agentx_python-0.8.2.dist-info}/top_level.txt +0 -0
@@ -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.0"
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 绑定的 recursion_limit 与调用方 config。
24
+ """合并 graph 绑定 config 与调用方 config。
22
25
 
23
- ``create_loop_agent(max_steps=...)`` ``compute_recursion_limit`` 写入 graph 的上限对齐;
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
- limit = bound.get("recursion_limit", 9_999)
29
- merged = build_agent_config(recursion_limit=limit)
30
- if caller:
31
- merged.update(caller)
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, "recursion_limit": 9999}
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
- content = _extract_final_content(collected.collected_assistant_messages, allowed_tool_names)
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 中取最后一条 AIMessage.content。"""
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
- for msg in reversed(messages):
575
- if isinstance(msg, AIMessage):
576
- content = getattr(msg, "content", None)
577
- if isinstance(content, str):
578
- return content
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:优先 text,其次 reason(CoT 无 text 时),最后 on_chain_end 兜底
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__ = []