langchain-agentx-python 1.2.3__py3-none-any.whl → 1.2.6__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 +1 -1
- langchain_agentx/loop/config/loop_config.py +2 -2
- langchain_agentx/loop/config/thinking_config.py +2 -2
- langchain_agentx/loop/config/ui_display_context.py +4 -4
- langchain_agentx/loop/exit/__init__.py +2 -0
- langchain_agentx/loop/graph/factory.py +4 -3
- langchain_agentx/loop/message_contract.py +72 -0
- langchain_agentx/loop/messages/__init__.py +63 -0
- langchain_agentx/loop/messages/contract.py +194 -0
- langchain_agentx/loop/messages/engine_protocol.py +29 -0
- langchain_agentx/loop/messages/langgraph_adapter.py +116 -0
- langchain_agentx/loop/messages/normalizer.py +136 -0
- langchain_agentx/loop/messages/transcript_codec.py +73 -0
- langchain_agentx/loop/messages/types.py +109 -0
- langchain_agentx/loop/messages/validate.py +106 -0
- langchain_agentx/loop/model/model_node.py +19 -10
- langchain_agentx/loop/subagent/cache_safe_params.py +119 -0
- langchain_agentx/loop/subagent/context.py +30 -46
- langchain_agentx/loop/subagent/display_projector.py +64 -20
- langchain_agentx/loop/subagent/graph.py +1 -1
- langchain_agentx/loop/subagent/orchestrator.py +10 -26
- langchain_agentx/loop/subagent/progress.py +19 -25
- langchain_agentx/loop/subagent/runner.py +55 -39
- langchain_agentx/memory/memdir/prefetch.py +3 -2
- langchain_agentx/memory/session/session_memory.py +13 -9
- langchain_agentx/observability/events/langchain_agentx_event_adapter.py +1 -0
- langchain_agentx/observability/events/subagent_event_mapper.py +5 -0
- langchain_agentx/provider/compatible_chat_openai.py +1 -0
- langchain_agentx/provider/semantics/__init__.py +28 -0
- langchain_agentx/provider/semantics/contracts.py +7 -0
- langchain_agentx/provider/semantics/profile.py +33 -6
- langchain_agentx/provider/semantics/thinking_capability.py +244 -0
- langchain_agentx/session/agent_session.py +6 -9
- langchain_agentx/session/conversation_recovery.py +48 -39
- langchain_agentx/tool_runtime/adapter.py +27 -11
- langchain_agentx/tool_runtime/models.py +4 -2
- langchain_agentx/tools/agent/tool.py +11 -22
- langchain_agentx/tools/bash/backend.py +17 -1
- langchain_agentx/tools/bash/tool.py +7 -0
- langchain_agentx/workflow/node.py +21 -8
- {langchain_agentx_python-1.2.3.dist-info → langchain_agentx_python-1.2.6.dist-info}/METADATA +1 -1
- {langchain_agentx_python-1.2.3.dist-info → langchain_agentx_python-1.2.6.dist-info}/RECORD +45 -34
- {langchain_agentx_python-1.2.3.dist-info → langchain_agentx_python-1.2.6.dist-info}/LICENSE +0 -0
- {langchain_agentx_python-1.2.3.dist-info → langchain_agentx_python-1.2.6.dist-info}/WHEEL +0 -0
- {langchain_agentx_python-1.2.3.dist-info → langchain_agentx_python-1.2.6.dist-info}/top_level.txt +0 -0
langchain_agentx/__init__.py
CHANGED
|
@@ -43,13 +43,13 @@ class PromptConfig:
|
|
|
43
43
|
- QueryParams.systemPrompt → system
|
|
44
44
|
- options.customSystemPrompt → override_system
|
|
45
45
|
- options.appendSystemPrompt → append_system
|
|
46
|
-
- options.thinkingConfig →
|
|
46
|
+
- options.thinkingConfig → thinking_generation_enabled
|
|
47
47
|
"""
|
|
48
48
|
system: str | SystemMessage | None = None
|
|
49
49
|
override_system: str | None = None
|
|
50
50
|
append_system: str | None = None
|
|
51
51
|
dynamic_sections: tuple[SystemPromptSection, ...] = ()
|
|
52
|
-
|
|
52
|
+
thinking_generation_enabled: bool = True # 是否启用 request-side thinking 生成(对齐 CC thinkingConfig)
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
@dataclass(frozen=True)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
链路位置:
|
|
8
8
|
SubagentOrchestrator → ThinkingConfig.from_subagent_mode() →
|
|
9
|
-
SubagentContext.
|
|
9
|
+
SubagentContext.thinking_generation_enabled → AgentLoopConfig
|
|
10
10
|
|
|
11
11
|
当前裁剪范围:
|
|
12
12
|
v1 基础实现:mode 驱动启用/禁用;预留父配置覆盖路径。
|
|
@@ -63,7 +63,7 @@ class ThinkingConfig:
|
|
|
63
63
|
Args:
|
|
64
64
|
mode: Subagent 模式 ("plain" | "fork" | "coordinate")
|
|
65
65
|
parent_thinking_config: 父 ThinkingConfig 或 dict(优先于 mode 规则)
|
|
66
|
-
parent_thinking_enabled: 父 ToolExecutionContext.
|
|
66
|
+
parent_thinking_enabled: 父 ToolExecutionContext.thinking_generation_enabled(fork/coordinate 继承)
|
|
67
67
|
|
|
68
68
|
Returns:
|
|
69
69
|
ThinkingConfig 实例
|
|
@@ -23,7 +23,7 @@ class UiDisplayContext:
|
|
|
23
23
|
|
|
24
24
|
is_transcript_mode: bool = False
|
|
25
25
|
verbose: bool = False
|
|
26
|
-
|
|
26
|
+
show_thinking: bool | None = None
|
|
27
27
|
|
|
28
28
|
@classmethod
|
|
29
29
|
def from_mapping(cls, raw: Mapping[str, Any] | None) -> UiDisplayContext | None:
|
|
@@ -31,12 +31,12 @@ class UiDisplayContext:
|
|
|
31
31
|
return None
|
|
32
32
|
is_transcript = bool(raw.get("is_transcript_mode", False))
|
|
33
33
|
verbose = bool(raw.get("verbose", False))
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
show_thinking_raw = raw.get("show_thinking")
|
|
35
|
+
show_thinking = None if show_thinking_raw is None else bool(show_thinking_raw)
|
|
36
36
|
return cls(
|
|
37
37
|
is_transcript_mode=is_transcript,
|
|
38
38
|
verbose=verbose,
|
|
39
|
-
|
|
39
|
+
show_thinking=show_thinking,
|
|
40
40
|
)
|
|
41
41
|
|
|
42
42
|
@classmethod
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""LangChain AgentX 退出语义与状态字段。"""
|
|
2
2
|
|
|
3
|
+
from .query_source import QuerySource
|
|
3
4
|
from .resolution import (
|
|
4
5
|
ContinueResolution,
|
|
5
6
|
LoopResolution,
|
|
@@ -10,6 +11,7 @@ from .resolution import (
|
|
|
10
11
|
from .terminal import LoopTerminal, build_loop_terminal
|
|
11
12
|
|
|
12
13
|
__all__ = [
|
|
14
|
+
"QuerySource",
|
|
13
15
|
"ContinueResolution",
|
|
14
16
|
"LoopResolution",
|
|
15
17
|
"TerminalResolution",
|
|
@@ -64,6 +64,7 @@ from ..model.model_nodes import make_model_nodes
|
|
|
64
64
|
from ..context import default_loop_context_compaction
|
|
65
65
|
from ..context.settings import CompactionSettings
|
|
66
66
|
from ..injection.dedup import filter_attachment_dicts_by_task_command_id
|
|
67
|
+
from ..message_contract import to_record
|
|
67
68
|
from ..model.tool_call_degradation_corrector import (
|
|
68
69
|
ToolCallDegradationCorrector,
|
|
69
70
|
_build_allowed_tool_names_for_degradation_correction,
|
|
@@ -302,7 +303,7 @@ def _flatten_config_for_runtime(config: AgentLoopConfig) -> _FlatLoopConfig:
|
|
|
302
303
|
model_profile_registry=config.model_profile_registry,
|
|
303
304
|
compact_buffer_tokens=config.compact_buffer_tokens,
|
|
304
305
|
max_output_tokens_reserved=config.max_output_tokens_reserved,
|
|
305
|
-
|
|
306
|
+
thinking_generation_enabled=config.prompt.thinking_generation_enabled,
|
|
306
307
|
)
|
|
307
308
|
|
|
308
309
|
|
|
@@ -955,7 +956,7 @@ class LoopGraphBuilder:
|
|
|
955
956
|
if not memory_attachments:
|
|
956
957
|
return patch
|
|
957
958
|
messages = list(patch.get("messages") or state.get("messages") or [])
|
|
958
|
-
messages.extend(memory_attachments)
|
|
959
|
+
messages.extend(to_record(m) for m in memory_attachments)
|
|
959
960
|
patch = dict(patch)
|
|
960
961
|
patch["messages"] = messages
|
|
961
962
|
return patch
|
|
@@ -1545,7 +1546,7 @@ class LoopGraphBuilder:
|
|
|
1545
1546
|
# 冻结契约:task 通知附件仅经本节点注入;按 command_id 与已有 messages 去重
|
|
1546
1547
|
# (见 docs/design-docs/loop/agent-loop/post-tools-injection-contract.md)
|
|
1547
1548
|
to_attach = filter_attachment_dicts_by_task_command_id(
|
|
1548
|
-
messages,
|
|
1549
|
+
messages, [to_record(m) for m in batch.attachment_messages]
|
|
1549
1550
|
)
|
|
1550
1551
|
messages.extend(to_attach)
|
|
1551
1552
|
emit_task_event(
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Loop 级消息契约公开入口(re-export façade)。
|
|
2
|
+
|
|
3
|
+
职责:
|
|
4
|
+
对外稳定 import 路径;内部实现位于 ``loop.messages`` 包(contract / validate /
|
|
5
|
+
langgraph_adapter / transcript_codec)。
|
|
6
|
+
|
|
7
|
+
链路位置:
|
|
8
|
+
subagent · session · memory recoverable 边界 → 本模块 → loop.messages.*
|
|
9
|
+
|
|
10
|
+
当前裁剪范围:
|
|
11
|
+
Phase 4 已拆包;LangGraph 为当前唯一 engine adapter;第二引擎仅预留 Protocol。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from langchain_agentx.loop.messages.contract import (
|
|
15
|
+
LoopMessageContract,
|
|
16
|
+
build_engine_input,
|
|
17
|
+
from_transcript_row,
|
|
18
|
+
get_message_contract,
|
|
19
|
+
to_engine_message,
|
|
20
|
+
to_record,
|
|
21
|
+
to_transcript_row,
|
|
22
|
+
to_transcript_tool_row,
|
|
23
|
+
validate_record,
|
|
24
|
+
)
|
|
25
|
+
from langchain_agentx.loop.messages.engine_protocol import EngineMessageAdapter
|
|
26
|
+
from langchain_agentx.loop.messages.langgraph_adapter import LangGraphMessageAdapter
|
|
27
|
+
from langchain_agentx.loop.messages.normalizer import MessageRecordNormalizer
|
|
28
|
+
from langchain_agentx.loop.messages.transcript_codec import TranscriptCodec
|
|
29
|
+
from langchain_agentx.loop.messages.types import (
|
|
30
|
+
EngineName,
|
|
31
|
+
MessageSurface,
|
|
32
|
+
PROVIDER_EXTENSIONS_KEY,
|
|
33
|
+
REASONING_CONTENT_KEY,
|
|
34
|
+
RECORD_CLASS_KEY,
|
|
35
|
+
RecordClass,
|
|
36
|
+
SYNTHETIC_TRANSCRIPT_TOOL_ID_PREFIX,
|
|
37
|
+
ValidationResult,
|
|
38
|
+
is_observational_record,
|
|
39
|
+
is_synthetic_transcript_tool_id,
|
|
40
|
+
merge_provider_extensions,
|
|
41
|
+
normalize_role,
|
|
42
|
+
)
|
|
43
|
+
from langchain_agentx.loop.messages.validate import MessageRecordValidator
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"EngineMessageAdapter",
|
|
47
|
+
"EngineName",
|
|
48
|
+
"LangGraphMessageAdapter",
|
|
49
|
+
"LoopMessageContract",
|
|
50
|
+
"MessageRecordNormalizer",
|
|
51
|
+
"MessageRecordValidator",
|
|
52
|
+
"MessageSurface",
|
|
53
|
+
"PROVIDER_EXTENSIONS_KEY",
|
|
54
|
+
"REASONING_CONTENT_KEY",
|
|
55
|
+
"RECORD_CLASS_KEY",
|
|
56
|
+
"RecordClass",
|
|
57
|
+
"SYNTHETIC_TRANSCRIPT_TOOL_ID_PREFIX",
|
|
58
|
+
"TranscriptCodec",
|
|
59
|
+
"ValidationResult",
|
|
60
|
+
"build_engine_input",
|
|
61
|
+
"from_transcript_row",
|
|
62
|
+
"get_message_contract",
|
|
63
|
+
"is_observational_record",
|
|
64
|
+
"is_synthetic_transcript_tool_id",
|
|
65
|
+
"merge_provider_extensions",
|
|
66
|
+
"normalize_role",
|
|
67
|
+
"to_engine_message",
|
|
68
|
+
"to_record",
|
|
69
|
+
"to_transcript_row",
|
|
70
|
+
"to_transcript_tool_row",
|
|
71
|
+
"validate_record",
|
|
72
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""loop.messages — Message Contract 内部分层(contract / validate / adapter / codec)。
|
|
2
|
+
|
|
3
|
+
职责:
|
|
4
|
+
实现 loop 级 message contract 内部分层;对外稳定入口为 ``loop.message_contract``。
|
|
5
|
+
|
|
6
|
+
链路位置:
|
|
7
|
+
subagent · session · memory · workflow recoverable 边界经 façade 间接使用本包。
|
|
8
|
+
|
|
9
|
+
当前裁剪范围:
|
|
10
|
+
consumer 禁止直接 import adapter/codec;仅 contract 测试与 package 内互引用允许。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from langchain_agentx.loop.messages.contract import (
|
|
14
|
+
LoopMessageContract,
|
|
15
|
+
build_engine_input,
|
|
16
|
+
from_transcript_row,
|
|
17
|
+
get_message_contract,
|
|
18
|
+
to_engine_message,
|
|
19
|
+
to_record,
|
|
20
|
+
to_transcript_row,
|
|
21
|
+
to_transcript_tool_row,
|
|
22
|
+
validate_record,
|
|
23
|
+
)
|
|
24
|
+
from langchain_agentx.loop.messages.langgraph_adapter import LangGraphMessageAdapter
|
|
25
|
+
from langchain_agentx.loop.messages.normalizer import MessageRecordNormalizer
|
|
26
|
+
from langchain_agentx.loop.messages.transcript_codec import TranscriptCodec
|
|
27
|
+
from langchain_agentx.loop.messages.types import (
|
|
28
|
+
EngineName,
|
|
29
|
+
MessageSurface,
|
|
30
|
+
RECORD_CLASS_KEY,
|
|
31
|
+
RecordClass,
|
|
32
|
+
SYNTHETIC_TRANSCRIPT_TOOL_ID_PREFIX,
|
|
33
|
+
ValidationResult,
|
|
34
|
+
is_observational_record,
|
|
35
|
+
is_synthetic_transcript_tool_id,
|
|
36
|
+
normalize_role,
|
|
37
|
+
)
|
|
38
|
+
from langchain_agentx.loop.messages.validate import MessageRecordValidator
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"EngineName",
|
|
42
|
+
"LangGraphMessageAdapter",
|
|
43
|
+
"LoopMessageContract",
|
|
44
|
+
"MessageRecordNormalizer",
|
|
45
|
+
"MessageRecordValidator",
|
|
46
|
+
"MessageSurface",
|
|
47
|
+
"RECORD_CLASS_KEY",
|
|
48
|
+
"RecordClass",
|
|
49
|
+
"SYNTHETIC_TRANSCRIPT_TOOL_ID_PREFIX",
|
|
50
|
+
"TranscriptCodec",
|
|
51
|
+
"ValidationResult",
|
|
52
|
+
"build_engine_input",
|
|
53
|
+
"from_transcript_row",
|
|
54
|
+
"get_message_contract",
|
|
55
|
+
"is_observational_record",
|
|
56
|
+
"is_synthetic_transcript_tool_id",
|
|
57
|
+
"normalize_role",
|
|
58
|
+
"to_engine_message",
|
|
59
|
+
"to_record",
|
|
60
|
+
"to_transcript_row",
|
|
61
|
+
"to_transcript_tool_row",
|
|
62
|
+
"validate_record",
|
|
63
|
+
]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""LoopMessageContract façade 与模块级便捷 API。
|
|
2
|
+
|
|
3
|
+
职责:
|
|
4
|
+
组合 normalizer / validator / transcript_codec / engine adapter,对外提供 loop 级契约 API。
|
|
5
|
+
|
|
6
|
+
链路位置:
|
|
7
|
+
``loop.message_contract`` re-export 本模块;consumer 应经 façade 调用,不直接依赖 adapter。
|
|
8
|
+
|
|
9
|
+
当前裁剪范围:
|
|
10
|
+
仅 LangGraph engine;``EngineMessageAdapter`` 协议预留第二引擎,本阶段不实现。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from langchain_core.messages import BaseMessage
|
|
18
|
+
|
|
19
|
+
from langchain_agentx.loop.messages.langgraph_adapter import LangGraphMessageAdapter
|
|
20
|
+
from langchain_agentx.loop.messages.normalizer import MessageRecordNormalizer
|
|
21
|
+
from langchain_agentx.loop.messages.transcript_codec import TranscriptCodec
|
|
22
|
+
from langchain_agentx.loop.messages.types import EngineName, MessageSurface, ValidationResult
|
|
23
|
+
from langchain_agentx.loop.messages.validate import MessageRecordValidator
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LoopMessageContract:
|
|
27
|
+
"""Loop 级消息契约 owner(对外 façade)。"""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self._normalizer = MessageRecordNormalizer()
|
|
31
|
+
self._validator = MessageRecordValidator()
|
|
32
|
+
self._codec = TranscriptCodec(self._normalizer, self._validator)
|
|
33
|
+
self._adapters: dict[EngineName, LangGraphMessageAdapter] = {
|
|
34
|
+
"langgraph": LangGraphMessageAdapter(self._normalizer),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def to_record(
|
|
38
|
+
self,
|
|
39
|
+
msg: Any,
|
|
40
|
+
*,
|
|
41
|
+
surface: MessageSurface = MessageSurface.RECOVERABLE,
|
|
42
|
+
) -> dict[str, Any]:
|
|
43
|
+
"""归一化为 canonical record;surface 仅作调用方语义标记,不在此步抛错。"""
|
|
44
|
+
del surface
|
|
45
|
+
return self._normalizer.to_record(msg)
|
|
46
|
+
|
|
47
|
+
def validate(
|
|
48
|
+
self,
|
|
49
|
+
record: dict[str, Any],
|
|
50
|
+
*,
|
|
51
|
+
surface: MessageSurface,
|
|
52
|
+
) -> ValidationResult:
|
|
53
|
+
normalized = self._normalizer.normalize_dict(record)
|
|
54
|
+
return self._validator.validate(normalized, surface=surface)
|
|
55
|
+
|
|
56
|
+
def to_engine_message(
|
|
57
|
+
self,
|
|
58
|
+
record: dict[str, Any],
|
|
59
|
+
*,
|
|
60
|
+
engine: EngineName = "langgraph",
|
|
61
|
+
) -> BaseMessage:
|
|
62
|
+
adapter = self._adapters.get(engine)
|
|
63
|
+
if adapter is None:
|
|
64
|
+
raise ValueError(f"unsupported engine adapter: {engine!r}")
|
|
65
|
+
return adapter.to_engine_message(record)
|
|
66
|
+
|
|
67
|
+
def build_engine_input(
|
|
68
|
+
self,
|
|
69
|
+
records: list[Any],
|
|
70
|
+
*,
|
|
71
|
+
surface: MessageSurface = MessageSurface.RECOVERABLE,
|
|
72
|
+
engine: EngineName = "langgraph",
|
|
73
|
+
) -> list[BaseMessage]:
|
|
74
|
+
messages: list[BaseMessage] = []
|
|
75
|
+
for item in records:
|
|
76
|
+
if isinstance(item, BaseMessage):
|
|
77
|
+
messages.append(item)
|
|
78
|
+
continue
|
|
79
|
+
record = (
|
|
80
|
+
item
|
|
81
|
+
if isinstance(item, dict) and item.get("role")
|
|
82
|
+
else self._normalizer.to_record(item)
|
|
83
|
+
)
|
|
84
|
+
result = self._validator.validate(record, surface=surface)
|
|
85
|
+
if not result.ok or result.record is None:
|
|
86
|
+
raise ValueError(result.error or "invalid message record")
|
|
87
|
+
messages.append(self.to_engine_message(result.record, engine=engine))
|
|
88
|
+
return messages
|
|
89
|
+
|
|
90
|
+
def to_transcript_row(self, record: dict[str, Any]) -> dict[str, Any]:
|
|
91
|
+
return self._codec.to_transcript_row(record)
|
|
92
|
+
|
|
93
|
+
def to_transcript_tool_row(
|
|
94
|
+
self,
|
|
95
|
+
*,
|
|
96
|
+
content: str,
|
|
97
|
+
tool_call_id: str,
|
|
98
|
+
name: str,
|
|
99
|
+
synthetic: bool = False,
|
|
100
|
+
) -> dict[str, Any]:
|
|
101
|
+
return self._codec.to_transcript_tool_row(
|
|
102
|
+
content=content,
|
|
103
|
+
tool_call_id=tool_call_id,
|
|
104
|
+
name=name,
|
|
105
|
+
synthetic=synthetic,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def from_transcript_row(
|
|
109
|
+
self,
|
|
110
|
+
row: dict[str, Any],
|
|
111
|
+
*,
|
|
112
|
+
surface: MessageSurface = MessageSurface.RECOVERABLE,
|
|
113
|
+
) -> dict[str, Any]:
|
|
114
|
+
return self._codec.from_transcript_row(row, surface=surface)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
_default_contract = LoopMessageContract()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_message_contract() -> LoopMessageContract:
|
|
121
|
+
return _default_contract
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def to_record(
|
|
125
|
+
msg: Any,
|
|
126
|
+
*,
|
|
127
|
+
surface: MessageSurface = MessageSurface.RECOVERABLE,
|
|
128
|
+
) -> dict[str, Any]:
|
|
129
|
+
return _default_contract.to_record(msg, surface=surface)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def validate_record(
|
|
133
|
+
record: dict[str, Any],
|
|
134
|
+
*,
|
|
135
|
+
surface: MessageSurface,
|
|
136
|
+
) -> ValidationResult:
|
|
137
|
+
return _default_contract.validate(record, surface=surface)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def to_engine_message(
|
|
141
|
+
record: dict[str, Any],
|
|
142
|
+
*,
|
|
143
|
+
engine: EngineName = "langgraph",
|
|
144
|
+
) -> BaseMessage:
|
|
145
|
+
return _default_contract.to_engine_message(record, engine=engine)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def build_engine_input(
|
|
149
|
+
records: list[Any],
|
|
150
|
+
*,
|
|
151
|
+
surface: MessageSurface = MessageSurface.RECOVERABLE,
|
|
152
|
+
engine: EngineName = "langgraph",
|
|
153
|
+
) -> list[BaseMessage]:
|
|
154
|
+
return _default_contract.build_engine_input(records, surface=surface, engine=engine)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def from_transcript_row(
|
|
158
|
+
row: dict[str, Any],
|
|
159
|
+
*,
|
|
160
|
+
surface: MessageSurface = MessageSurface.RECOVERABLE,
|
|
161
|
+
) -> dict[str, Any]:
|
|
162
|
+
return _default_contract.from_transcript_row(row, surface=surface)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def to_transcript_tool_row(
|
|
166
|
+
*,
|
|
167
|
+
content: str,
|
|
168
|
+
tool_call_id: str,
|
|
169
|
+
name: str,
|
|
170
|
+
synthetic: bool = False,
|
|
171
|
+
) -> dict[str, Any]:
|
|
172
|
+
return _default_contract.to_transcript_tool_row(
|
|
173
|
+
content=content,
|
|
174
|
+
tool_call_id=tool_call_id,
|
|
175
|
+
name=name,
|
|
176
|
+
synthetic=synthetic,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def to_transcript_row(record: dict[str, Any]) -> dict[str, Any]:
|
|
181
|
+
return _default_contract.to_transcript_row(record)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
__all__ = [
|
|
185
|
+
"LoopMessageContract",
|
|
186
|
+
"build_engine_input",
|
|
187
|
+
"from_transcript_row",
|
|
188
|
+
"get_message_contract",
|
|
189
|
+
"to_engine_message",
|
|
190
|
+
"to_record",
|
|
191
|
+
"to_transcript_row",
|
|
192
|
+
"to_transcript_tool_row",
|
|
193
|
+
"validate_record",
|
|
194
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Engine adapter 协议:canonical record ↔ 执行引擎消息(Phase 4 预留第二引擎)。
|
|
2
|
+
|
|
3
|
+
职责:
|
|
4
|
+
``EngineMessageAdapter`` Protocol 定义 engine 边界;当前仅 LangGraph 实现。
|
|
5
|
+
|
|
6
|
+
链路位置:
|
|
7
|
+
contract 通过 ``_adapters`` 字典按 ``EngineName`` 选择 adapter。
|
|
8
|
+
|
|
9
|
+
当前裁剪范围:
|
|
10
|
+
协议仅声明 ``to_engine_message``;不绑定 LangChain 具体消息类型。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, Protocol
|
|
16
|
+
|
|
17
|
+
from langchain_agentx.loop.messages.types import EngineName
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EngineMessageAdapter(Protocol):
|
|
21
|
+
"""执行引擎边界 adapter;当前仅 LangGraph 实现。"""
|
|
22
|
+
|
|
23
|
+
engine: EngineName
|
|
24
|
+
|
|
25
|
+
def to_engine_message(self, record: dict[str, Any]) -> Any:
|
|
26
|
+
"""Canonical record → engine-native message object."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = ["EngineMessageAdapter"]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""LangGraph / LangChain BaseMessage adapter(当前唯一 engine 实现)。
|
|
2
|
+
|
|
3
|
+
职责:
|
|
4
|
+
canonical record → ``HumanMessage`` / ``AIMessage`` / ``ToolMessage`` 等 BaseMessage。
|
|
5
|
+
|
|
6
|
+
链路位置:
|
|
7
|
+
contract.to_engine_message / build_engine_input 的 LangGraph 边界 adapter。
|
|
8
|
+
|
|
9
|
+
当前裁剪范围:
|
|
10
|
+
user/assistant 多模态 content 透传;tool 使用 block-aware string 序列化(非裸 ``str(list)``)。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage, ToolMessage
|
|
19
|
+
|
|
20
|
+
from langchain_agentx.loop.messages.normalizer import MessageRecordNormalizer
|
|
21
|
+
from langchain_agentx.loop.messages.types import EngineName, PROVIDER_EXTENSIONS_KEY, REASONING_CONTENT_KEY
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _engine_content(content: Any) -> str | list[Any]:
|
|
25
|
+
"""LangChain BaseMessage 接受 str 或多模态 block list;其余标量转 str。"""
|
|
26
|
+
if isinstance(content, list):
|
|
27
|
+
return content
|
|
28
|
+
if isinstance(content, str):
|
|
29
|
+
return content
|
|
30
|
+
if content is None:
|
|
31
|
+
return ""
|
|
32
|
+
return str(content)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _tool_content_for_engine(content: Any) -> str:
|
|
36
|
+
"""ToolMessage 需要 string body;list/dict 做 block-aware 序列化,避免 silent str(list) 塌缩。"""
|
|
37
|
+
if isinstance(content, str):
|
|
38
|
+
return content
|
|
39
|
+
if content is None:
|
|
40
|
+
return ""
|
|
41
|
+
if isinstance(content, list):
|
|
42
|
+
parts: list[str] = []
|
|
43
|
+
for block in content:
|
|
44
|
+
if isinstance(block, dict):
|
|
45
|
+
block_type = block.get("type")
|
|
46
|
+
if block_type == "text":
|
|
47
|
+
text = block.get("text")
|
|
48
|
+
if isinstance(text, str) and text:
|
|
49
|
+
parts.append(text)
|
|
50
|
+
elif block_type == "thinking":
|
|
51
|
+
thinking = block.get("thinking")
|
|
52
|
+
if isinstance(thinking, str) and thinking:
|
|
53
|
+
parts.append(thinking)
|
|
54
|
+
else:
|
|
55
|
+
parts.append(json.dumps(block, ensure_ascii=False))
|
|
56
|
+
elif block is not None:
|
|
57
|
+
parts.append(str(block))
|
|
58
|
+
if parts:
|
|
59
|
+
return "\n".join(parts)
|
|
60
|
+
return json.dumps(content, ensure_ascii=False)
|
|
61
|
+
if isinstance(content, dict):
|
|
62
|
+
return json.dumps(content, ensure_ascii=False)
|
|
63
|
+
return str(content)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class LangGraphMessageAdapter:
|
|
67
|
+
"""Canonical record → LangChain BaseMessage。"""
|
|
68
|
+
|
|
69
|
+
engine: EngineName = "langgraph"
|
|
70
|
+
|
|
71
|
+
def __init__(self, normalizer: MessageRecordNormalizer) -> None:
|
|
72
|
+
self._normalizer = normalizer
|
|
73
|
+
|
|
74
|
+
def to_engine_message(self, record: dict[str, Any]) -> BaseMessage:
|
|
75
|
+
normalized = self._normalizer.normalize_dict(record)
|
|
76
|
+
role = normalized["role"]
|
|
77
|
+
content = _engine_content(normalized.get("content", ""))
|
|
78
|
+
|
|
79
|
+
if role == "user":
|
|
80
|
+
return HumanMessage(content=content)
|
|
81
|
+
if role == "assistant":
|
|
82
|
+
additional_kwargs: dict[str, Any] = {}
|
|
83
|
+
extensions = normalized.get(PROVIDER_EXTENSIONS_KEY)
|
|
84
|
+
if isinstance(extensions, dict):
|
|
85
|
+
rc = extensions.get(REASONING_CONTENT_KEY)
|
|
86
|
+
if isinstance(rc, str) and rc:
|
|
87
|
+
additional_kwargs[REASONING_CONTENT_KEY] = rc
|
|
88
|
+
return AIMessage(
|
|
89
|
+
content=content,
|
|
90
|
+
tool_calls=list(normalized.get("tool_calls") or []),
|
|
91
|
+
additional_kwargs=additional_kwargs or {},
|
|
92
|
+
)
|
|
93
|
+
if role == "system":
|
|
94
|
+
return SystemMessage(content=content)
|
|
95
|
+
if role == "tool":
|
|
96
|
+
tool_call_id = normalized.get("tool_call_id")
|
|
97
|
+
if not isinstance(tool_call_id, str) or not tool_call_id:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
"tool message record missing tool_call_id; "
|
|
100
|
+
"cannot convert to LangGraph ToolMessage / add_messages"
|
|
101
|
+
)
|
|
102
|
+
name = str(normalized.get("name") or "tool")
|
|
103
|
+
status = normalized.get("status")
|
|
104
|
+
kwargs: dict[str, Any] = {
|
|
105
|
+
"content": _tool_content_for_engine(normalized.get("content", "")),
|
|
106
|
+
"tool_call_id": tool_call_id,
|
|
107
|
+
"name": name,
|
|
108
|
+
}
|
|
109
|
+
if isinstance(status, str) and status:
|
|
110
|
+
kwargs["status"] = status
|
|
111
|
+
return ToolMessage(**kwargs)
|
|
112
|
+
|
|
113
|
+
return HumanMessage(content=content)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
__all__ = ["LangGraphMessageAdapter"]
|