copilotkit 0.1.81__tar.gz → 0.1.82__tar.gz

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 (26) hide show
  1. {copilotkit-0.1.81 → copilotkit-0.1.82}/PKG-INFO +1 -1
  2. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/copilotkit_lg_middleware.py +83 -10
  3. {copilotkit-0.1.81 → copilotkit-0.1.82}/pyproject.toml +1 -1
  4. {copilotkit-0.1.81 → copilotkit-0.1.82}/README.md +0 -0
  5. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/__init__.py +0 -0
  6. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/action.py +0 -0
  7. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/agent.py +0 -0
  8. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/crewai/__init__.py +0 -0
  9. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/crewai/copilotkit_integration.py +0 -0
  10. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/crewai/crewai_agent.py +0 -0
  11. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/crewai/crewai_sdk.py +0 -0
  12. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/exc.py +0 -0
  13. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/html.py +0 -0
  14. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/integrations/__init__.py +0 -0
  15. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/integrations/fastapi.py +0 -0
  16. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/langchain.py +0 -0
  17. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/langgraph.py +0 -0
  18. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/langgraph_agent.py +0 -0
  19. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/langgraph_agui_agent.py +0 -0
  20. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/logging.py +0 -0
  21. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/parameter.py +0 -0
  22. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/protocol.py +0 -0
  23. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/runloop.py +0 -0
  24. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/sdk.py +0 -0
  25. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/types.py +0 -0
  26. {copilotkit-0.1.81 → copilotkit-0.1.82}/copilotkit/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: copilotkit
3
- Version: 0.1.81
3
+ Version: 0.1.82
4
4
  Summary: CopilotKit python SDK
5
5
  License: MIT
6
6
  Keywords: copilot,copilotkit,langgraph,langchain,ai,langsmith,langserve
@@ -65,20 +65,65 @@ class CopilotKitMiddleware(AgentMiddleware[StateSchema, Any]):
65
65
  def _fix_messages_for_bedrock(messages: list) -> list:
66
66
  """Fix messages loaded from checkpoint before sending to Bedrock.
67
67
 
68
- Handles three issues caused by CopilotKit's after_agent restoring
68
+ Handles four issues caused by CopilotKit's after_agent restoring
69
69
  frontend tool_calls to the checkpoint:
70
70
  1. Strip unanswered tool_calls (no matching ToolMessage) — Bedrock
71
71
  rejects toolUse without a corresponding toolResult.
72
72
  2. Sync msg.content tool_use blocks with msg.tool_calls.
73
73
  3. Fix tool_use content blocks with string input (must be dict).
74
+ 4. Deduplicate ToolMessages by tool_call_id — patch_orphan_tool_calls
75
+ injects a placeholder with a new random ID on every checkpoint load;
76
+ when the real result is later appended alongside it, Bedrock rejects
77
+ the duplicate toolResult IDs. We keep the real result (non-interrupted)
78
+ over the placeholder, falling back to the last occurrence if both look
79
+ real.
74
80
  """
75
- # Collect all tool_call_ids that have a ToolMessage answer
76
- answered_tc_ids = {
77
- m.tool_call_id for m in messages
78
- if isinstance(m, ToolMessage) and hasattr(m, 'tool_call_id')
79
- }
81
+ # 4. Deduplicate ToolMessages by tool_call_id before all other processing.
82
+ # patch_orphan_tool_calls adds "…was interrupted before completion."
83
+ # placeholders with fresh random IDs on every checkpoint load. The real
84
+ # result comes in as a separate message with a different ID, so both end
85
+ # up in the list. Keep the real (non-interrupted) one; if multiple real
86
+ # ones exist, keep the last.
87
+ _INTERRUPTED_PAT = re.compile(
88
+ r"^Tool call '.+' with id '.+' was interrupted before completion\.$"
89
+ )
90
+ # Group ToolMessages by tool_call_id, preserving position
91
+ tc_groups: dict[str, list] = {}
92
+ for i, msg in enumerate(messages):
93
+ if isinstance(msg, ToolMessage):
94
+ tc_id = getattr(msg, 'tool_call_id', None)
95
+ if tc_id:
96
+ tc_groups.setdefault(tc_id, []).append(i)
97
+
98
+ drop_indices: set = set()
99
+ for tc_id, indices in tc_groups.items():
100
+ if len(indices) <= 1:
101
+ continue
102
+ # Separate interrupted placeholders from real results
103
+ real_indices = [
104
+ i for i in indices
105
+ if not (isinstance(messages[i].content, str)
106
+ and _INTERRUPTED_PAT.match(messages[i].content))
107
+ ]
108
+ interrupted_indices = [i for i in indices if i not in real_indices]
109
+ if real_indices and interrupted_indices:
110
+ # Replace the first placeholder (correct position, adjacent to AI
111
+ # message) with the last real result (likely appended at the end).
112
+ # This keeps the tool result in the right position for Bedrock.
113
+ messages[interrupted_indices[0]] = messages[real_indices[-1]]
114
+ drop_indices.update(interrupted_indices[1:])
115
+ drop_indices.update(real_indices) # drop all originals (we moved one)
116
+ elif real_indices:
117
+ # No placeholders, multiple real — keep only the last
118
+ drop_indices.update(real_indices[:-1])
119
+ else:
120
+ # All interrupted — keep only the last
121
+ drop_indices.update(interrupted_indices[:-1])
80
122
 
81
- for msg in messages:
123
+ if drop_indices:
124
+ messages[:] = [msg for i, msg in enumerate(messages) if i not in drop_indices]
125
+
126
+ for idx, msg in enumerate(messages):
82
127
  if not isinstance(msg, AIMessage):
83
128
  continue
84
129
 
@@ -106,11 +151,23 @@ class CopilotKitMiddleware(AgentMiddleware[StateSchema, Any]):
106
151
  if not tool_calls:
107
152
  continue
108
153
 
109
- # 2. Strip unanswered tool_calls (no matching ToolMessage)
110
- unanswered = [tc for tc in tool_calls if tc.get('id') not in answered_tc_ids]
154
+ # 2. Strip unanswered tool_calls only consider ToolMessages that
155
+ # are ADJACENT (immediately following this AIMessage, before the
156
+ # next non-ToolMessage). A ToolMessage at the wrong position
157
+ # won't satisfy Bedrock's Converse API requirement that toolResult
158
+ # blocks appear in the user turn right after the assistant turn.
159
+ adjacent_tc_ids: set = set()
160
+ j = idx + 1
161
+ while j < len(messages) and isinstance(messages[j], ToolMessage):
162
+ tc_id = getattr(messages[j], 'tool_call_id', None)
163
+ if tc_id:
164
+ adjacent_tc_ids.add(tc_id)
165
+ j += 1
166
+
167
+ unanswered = [tc for tc in tool_calls if tc.get('id') not in adjacent_tc_ids]
111
168
  if unanswered:
112
169
  unanswered_ids = {tc['id'] for tc in unanswered}
113
- msg.tool_calls = [tc for tc in tool_calls if tc.get('id') in answered_tc_ids]
170
+ msg.tool_calls = [tc for tc in tool_calls if tc.get('id') in adjacent_tc_ids]
114
171
 
115
172
  # Also strip matching content blocks
116
173
  if isinstance(msg.content, list):
@@ -142,6 +199,22 @@ class CopilotKitMiddleware(AgentMiddleware[StateSchema, Any]):
142
199
  elif inp is None:
143
200
  block['input'] = {}
144
201
 
202
+ # 5. Remove orphan ToolMessages whose tool_call_id no longer matches
203
+ # any remaining tool_call in any AIMessage. These can be left over
204
+ # after stripping unanswered tool_calls above.
205
+ remaining_tc_ids: set = set()
206
+ for msg in messages:
207
+ if isinstance(msg, AIMessage):
208
+ for tc in (getattr(msg, 'tool_calls', None) or []):
209
+ tc_id = tc.get('id')
210
+ if tc_id:
211
+ remaining_tc_ids.add(tc_id)
212
+ messages[:] = [
213
+ msg for msg in messages
214
+ if not isinstance(msg, ToolMessage)
215
+ or getattr(msg, 'tool_call_id', None) in remaining_tc_ids
216
+ ]
217
+
145
218
  return messages
146
219
 
147
220
  async def awrap_model_call(
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "copilotkit"
3
- version = "0.1.81"
3
+ version = "0.1.82"
4
4
  description = "CopilotKit python SDK"
5
5
  authors = ["Markus Ecker <markus.ecker@gmail.com>"]
6
6
  license = "MIT"
File without changes