|
17 | 17 | from ldai_langchain.langgraph_callback_handler import LDMetricsCallbackHandler |
18 | 18 |
|
19 | 19 |
|
20 | | -def _tool_call_id_from_entry(tc: Any) -> Any: |
21 | | - """Return tool_call id from a dict or ToolCall-like object.""" |
22 | | - if isinstance(tc, dict): |
23 | | - return tc.get('id') |
24 | | - return getattr(tc, 'id', None) |
25 | | - |
26 | | - |
27 | 20 | def _make_handoff_tool(child_key: str, description: str) -> Any: |
28 | 21 | """ |
29 | 22 | Create a tool that transfers control to ``child_key``. |
@@ -58,114 +51,6 @@ def handoff( |
58 | 51 | return handoff |
59 | 52 |
|
60 | 53 |
|
61 | | -def _coalesce_tool_messages_for_openai(msgs: List[Any]) -> List[Any]: |
62 | | - """ |
63 | | - Rewind shared LangGraph message state into OpenAI's required shape. |
64 | | -
|
65 | | - Multi-agent graphs append multiple AIMessages before tool outputs are written, so |
66 | | - ToolMessages can appear *after* later assistant turns. OpenAI requires each |
67 | | - assistant ``tool_calls`` block to be followed immediately by matching ToolMessages. |
68 | | -
|
69 | | - This walks non-tool messages in order; after each AIMessage with ``tool_calls``, |
70 | | - appends the corresponding ToolMessages (looked up by ``tool_call_id``). Any |
71 | | - ToolMessage not referenced by some assistant ``tool_calls`` in the list is dropped. |
72 | | -
|
73 | | - An AIMessage whose tool_calls have *no* matching ToolMessages in the state is also |
74 | | - dropped. This handles the parallel fan-out case where a sibling branch's tool |
75 | | - execution hasn't run yet (or routes to END) by the time a downstream node reads the |
76 | | - accumulated state — sending such an AIMessage to OpenAI would cause a 400 error. |
77 | | - """ |
78 | | - from langchain_core.messages import AIMessage, ToolMessage |
79 | | - |
80 | | - pending: Dict[str, Any] = {} |
81 | | - for m in msgs: |
82 | | - if isinstance(m, ToolMessage): |
83 | | - tid = getattr(m, 'tool_call_id', None) |
84 | | - if tid: |
85 | | - if tid in pending: |
86 | | - log.warning( |
87 | | - 'LangGraphAgentGraphRunner: duplicate ToolMessage for tool_call_id=%r; keeping last', |
88 | | - tid, |
89 | | - ) |
90 | | - pending[tid] = m |
91 | | - |
92 | | - output: List[Any] = [] |
93 | | - for m in msgs: |
94 | | - if isinstance(m, ToolMessage): |
95 | | - continue |
96 | | - if isinstance(m, AIMessage) and getattr(m, 'tool_calls', None): |
97 | | - call_ids = [_tool_call_id_from_entry(tc) for tc in m.tool_calls] |
98 | | - resolved = [tid for tid in call_ids if tid and tid in pending] |
99 | | - if not resolved: |
100 | | - # None of this AIMessage's tool_calls have responses in the current |
101 | | - # state — it belongs to a sibling branch whose ToolMessages aren't |
102 | | - # available here. Drop it to avoid an OpenAI 400. |
103 | | - log.warning( |
104 | | - 'LangGraphAgentGraphRunner: dropping AIMessage with unresolvable ' |
105 | | - 'tool_calls %s (no matching ToolMessages in state — likely from a ' |
106 | | - 'sibling branch in a parallel fan-out)', |
107 | | - call_ids, |
108 | | - ) |
109 | | - continue |
110 | | - output.append(m) |
111 | | - for tid in resolved: |
112 | | - output.append(pending.pop(tid)) |
113 | | - else: |
114 | | - output.append(m) |
115 | | - |
116 | | - if pending: |
117 | | - log.warning( |
118 | | - 'LangGraphAgentGraphRunner: dropping %s orphan ToolMessage(s) (no assistant ' |
119 | | - 'tool_calls in history): %s', |
120 | | - len(pending), |
121 | | - list(pending.keys())[:32], |
122 | | - ) |
123 | | - |
124 | | - return output |
125 | | - |
126 | | - |
127 | | -def _message_content_len(msg: Any) -> int: |
128 | | - c = getattr(msg, 'content', None) |
129 | | - if c is None: |
130 | | - return 0 |
131 | | - if isinstance(c, str): |
132 | | - return len(c) |
133 | | - return len(str(c)) |
134 | | - |
135 | | - |
136 | | -def _format_chat_messages_for_log(msgs: List[Any]) -> str: |
137 | | - """ |
138 | | - One line per message index — matches OpenAI error indices (e.g. messages.[5]). |
139 | | - Logged at DEBUG before each model ainvoke. |
140 | | - """ |
141 | | - from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage |
142 | | - |
143 | | - lines: List[str] = [] |
144 | | - for idx, m in enumerate(msgs): |
145 | | - cls = type(m).__name__ |
146 | | - if isinstance(m, AIMessage): |
147 | | - tc = getattr(m, 'tool_calls', None) or [] |
148 | | - pairs = [] |
149 | | - for x in tc: |
150 | | - if isinstance(x, dict): |
151 | | - pairs.append((x.get('name'), x.get('id'))) |
152 | | - else: |
153 | | - pairs.append((getattr(x, 'name', None), getattr(x, 'id', None))) |
154 | | - lines.append( |
155 | | - f'[{idx}] {cls} tool_calls={pairs!r} content_len={_message_content_len(m)}' |
156 | | - ) |
157 | | - elif isinstance(m, ToolMessage): |
158 | | - lines.append( |
159 | | - f'[{idx}] {cls} tool_call_id={getattr(m, "tool_call_id", None)!r} ' |
160 | | - f'name={getattr(m, "name", None)!r} content_len={_message_content_len(m)}' |
161 | | - ) |
162 | | - elif isinstance(m, (HumanMessage, SystemMessage)): |
163 | | - lines.append(f'[{idx}] {cls} content_len={_message_content_len(m)}') |
164 | | - else: |
165 | | - lines.append(f'[{idx}] {cls}') |
166 | | - return '\n'.join(lines) |
167 | | - |
168 | | - |
169 | 54 | class LangGraphAgentGraphRunner(AgentGraphRunner): |
170 | 55 | """ |
171 | 56 | CAUTION: |
@@ -268,15 +153,9 @@ def make_node_fn(bound_model: Any, node_instructions: Any, nk: str): |
268 | 153 | async def invoke(state: WorkflowState) -> dict: |
269 | 154 | if not bound_model: |
270 | 155 | return {'messages': []} |
271 | | - msgs = _coalesce_tool_messages_for_openai(list(state['messages'])) |
| 156 | + msgs = list(state['messages']) |
272 | 157 | if node_instructions: |
273 | 158 | msgs = [SystemMessage(content=node_instructions)] + msgs |
274 | | - log.debug( |
275 | | - 'LangGraphAgentGraphRunner node=%s: CHAT_INPUT (%s messages)\n%s', |
276 | | - nk, |
277 | | - len(msgs), |
278 | | - _format_chat_messages_for_log(msgs), |
279 | | - ) |
280 | 159 | response = await bound_model.ainvoke(msgs) |
281 | 160 | return {'messages': [response]} |
282 | 161 |
|
|
0 commit comments