grasp_agents 0.4.7__py3-none-any.whl → 0.5.1__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 (44) hide show
  1. grasp_agents/cloud_llm.py +191 -224
  2. grasp_agents/comm_processor.py +101 -100
  3. grasp_agents/errors.py +69 -9
  4. grasp_agents/litellm/__init__.py +106 -0
  5. grasp_agents/litellm/completion_chunk_converters.py +68 -0
  6. grasp_agents/litellm/completion_converters.py +72 -0
  7. grasp_agents/litellm/converters.py +138 -0
  8. grasp_agents/litellm/lite_llm.py +210 -0
  9. grasp_agents/litellm/message_converters.py +66 -0
  10. grasp_agents/llm.py +84 -49
  11. grasp_agents/llm_agent.py +136 -120
  12. grasp_agents/llm_agent_memory.py +3 -3
  13. grasp_agents/llm_policy_executor.py +167 -174
  14. grasp_agents/memory.py +23 -0
  15. grasp_agents/openai/__init__.py +24 -9
  16. grasp_agents/openai/completion_chunk_converters.py +6 -6
  17. grasp_agents/openai/completion_converters.py +12 -14
  18. grasp_agents/openai/content_converters.py +1 -3
  19. grasp_agents/openai/converters.py +6 -8
  20. grasp_agents/openai/message_converters.py +21 -3
  21. grasp_agents/openai/openai_llm.py +155 -103
  22. grasp_agents/openai/tool_converters.py +4 -6
  23. grasp_agents/packet.py +5 -2
  24. grasp_agents/packet_pool.py +14 -13
  25. grasp_agents/printer.py +233 -73
  26. grasp_agents/processor.py +229 -91
  27. grasp_agents/prompt_builder.py +2 -2
  28. grasp_agents/run_context.py +11 -20
  29. grasp_agents/runner.py +42 -0
  30. grasp_agents/typing/completion.py +16 -9
  31. grasp_agents/typing/completion_chunk.py +51 -22
  32. grasp_agents/typing/events.py +95 -19
  33. grasp_agents/typing/message.py +25 -1
  34. grasp_agents/typing/tool.py +2 -0
  35. grasp_agents/usage_tracker.py +31 -37
  36. grasp_agents/utils.py +95 -84
  37. grasp_agents/workflow/looped_workflow.py +60 -11
  38. grasp_agents/workflow/sequential_workflow.py +43 -11
  39. grasp_agents/workflow/workflow_processor.py +25 -24
  40. {grasp_agents-0.4.7.dist-info → grasp_agents-0.5.1.dist-info}/METADATA +7 -6
  41. grasp_agents-0.5.1.dist-info/RECORD +57 -0
  42. grasp_agents-0.4.7.dist-info/RECORD +0 -50
  43. {grasp_agents-0.4.7.dist-info → grasp_agents-0.5.1.dist-info}/WHEEL +0 -0
  44. {grasp_agents-0.4.7.dist-info → grasp_agents-0.5.1.dist-info}/licenses/LICENSE.md +0 -0
grasp_agents/printer.py CHANGED
@@ -1,25 +1,39 @@
1
1
  import hashlib
2
2
  import json
3
3
  import logging
4
- from collections.abc import Mapping, Sequence
5
- from typing import Literal, TypeAlias
6
-
7
- from termcolor._types import Color # type: ignore[import]
4
+ import sys
5
+ from collections.abc import AsyncIterator, Mapping, Sequence
6
+ from typing import Any, Literal, TypeAlias
7
+
8
+ from pydantic import BaseModel
9
+ from termcolor import colored
10
+ from termcolor._types import Color
11
+
12
+ from grasp_agents.typing.events import (
13
+ CompletionChunkEvent,
14
+ Event,
15
+ GenMessageEvent,
16
+ MessageEvent,
17
+ ProcPacketOutputEvent,
18
+ RunResultEvent,
19
+ SystemMessageEvent,
20
+ ToolMessageEvent,
21
+ UserMessageEvent,
22
+ WorkflowResultEvent,
23
+ )
8
24
 
9
25
  from .typing.completion import Usage
10
26
  from .typing.content import Content, ContentPartText
11
- from .typing.message import AssistantMessage, Message, Role, ToolMessage
27
+ from .typing.message import AssistantMessage, Message, Role, SystemMessage, UserMessage
12
28
 
13
29
  logger = logging.getLogger(__name__)
14
30
 
15
31
 
16
- ColoringMode: TypeAlias = Literal["agent", "role"]
17
-
18
32
  ROLE_TO_COLOR: Mapping[Role, Color] = {
19
33
  Role.SYSTEM: "magenta",
20
34
  Role.USER: "green",
21
35
  Role.ASSISTANT: "light_blue",
22
- Role.TOOL: "light_cyan",
36
+ Role.TOOL: "blue",
23
37
  }
24
38
 
25
39
  AVAILABLE_COLORS: list[Color] = [
@@ -32,17 +46,17 @@ AVAILABLE_COLORS: list[Color] = [
32
46
  "red",
33
47
  ]
34
48
 
49
+ ColoringMode: TypeAlias = Literal["agent", "role"]
50
+ CompletionBlockType: TypeAlias = Literal["response", "thinking", "tool_call"]
51
+
35
52
 
36
53
  class Printer:
37
54
  def __init__(
38
- self,
39
- color_by: ColoringMode = "role",
40
- msg_trunc_len: int = 20000,
41
- print_messages: bool = False,
55
+ self, color_by: ColoringMode = "role", msg_trunc_len: int = 20000
42
56
  ) -> None:
43
57
  self.color_by = color_by
44
58
  self.msg_trunc_len = msg_trunc_len
45
- self.print_messages = print_messages
59
+ self._current_message: str = ""
46
60
 
47
61
  @staticmethod
48
62
  def get_role_color(role: Role) -> Color:
@@ -81,90 +95,236 @@ class Printer:
81
95
 
82
96
  return content_str
83
97
 
84
- def print_llm_message(
85
- self, message: Message, agent_name: str, run_id: str, usage: Usage | None = None
98
+ def print_message(
99
+ self,
100
+ message: Message,
101
+ agent_name: str,
102
+ call_id: str,
103
+ usage: Usage | None = None,
86
104
  ) -> None:
87
- if not self.print_messages:
88
- return
89
-
90
105
  if usage is not None and not isinstance(message, AssistantMessage):
91
106
  raise ValueError(
92
107
  "Usage information can only be printed for AssistantMessage"
93
108
  )
94
109
 
95
- role = message.role
96
- content_str = self.content_to_str(message.content or "", message.role)
97
-
98
- if self.color_by == "agent":
99
- color = self.get_agent_color(agent_name)
100
- elif self.color_by == "role":
101
- color = self.get_role_color(role)
102
-
103
- log_kwargs = {"extra": {"color": color}} # type: ignore
104
-
105
- # Print message title
106
-
107
- out = f"\n[agent: {agent_name} | role: {role.value} | run: {run_id}]"
110
+ color = (
111
+ self.get_agent_color(agent_name)
112
+ if self.color_by == "agent"
113
+ else self.get_role_color(message.role)
114
+ )
115
+ log_kwargs = {"extra": {"color": color}}
108
116
 
109
- if isinstance(message, ToolMessage):
110
- out += f"\n{message.name} | {message.tool_call_id}"
117
+ out = f"<{agent_name}> [{call_id}]\n"
111
118
 
112
- # Print message content
119
+ # Thinking
120
+ if isinstance(message, AssistantMessage) and message.reasoning_content:
121
+ thinking = message.reasoning_content.strip(" \n")
122
+ out += f"\n<thinking>\n{thinking}\n</thinking>\n"
113
123
 
114
- if content_str:
124
+ # Content
125
+ content = self.content_to_str(message.content or "", message.role)
126
+ if content:
115
127
  try:
116
- content_str = json.dumps(json.loads(content_str), indent=2)
128
+ content = json.dumps(json.loads(content), indent=2)
117
129
  except Exception:
118
130
  pass
119
- content_str_truncated = self.truncate_content_str(
120
- content_str, trunc_len=self.msg_trunc_len
121
- )
122
- out += f"\n{content_str_truncated}"
123
-
124
- logger.debug(out, **log_kwargs) # type: ignore
125
-
126
- # Print tool calls
127
-
131
+ content = self.truncate_content_str(content, trunc_len=self.msg_trunc_len)
132
+ if isinstance(message, SystemMessage):
133
+ out += f"<system>\n{content}\n</system>\n"
134
+ elif isinstance(message, UserMessage):
135
+ out += f"<input>\n{content}\n</input>\n"
136
+ elif isinstance(message, AssistantMessage):
137
+ out += f"<response>\n{content}\n</response>\n"
138
+ else:
139
+ out += f"<tool result> [{message.tool_call_id}]\n{content}\n</tool result>\n"
140
+
141
+ # Tool calls
128
142
  if isinstance(message, AssistantMessage) and message.tool_calls is not None:
129
143
  for tool_call in message.tool_calls:
130
- if self.color_by == "agent":
131
- tool_color = self.get_agent_color(agent_name=agent_name)
132
- elif self.color_by == "role":
133
- tool_color = self.get_role_color(role=Role.TOOL)
134
- logger.debug(
135
- f"\n<{agent_name}>[TOOL_CALL]\n{tool_call.tool_name} "
136
- f"| {tool_call.id}\n{tool_call.tool_arguments}",
137
- extra={"color": tool_color}, # type: ignore
144
+ out += (
145
+ f"<tool call> {tool_call.tool_name} [{tool_call.id}]\n"
146
+ f"{tool_call.tool_arguments}\n</tool call>\n"
138
147
  )
139
148
 
140
- # Print usage
141
-
149
+ # Usage
142
150
  if usage is not None:
143
- usage_str = (
144
- f"I/O/(R)/(C) tokens: {usage.input_tokens}/{usage.output_tokens}"
145
- )
146
- if usage.reasoning_tokens is not None:
147
- usage_str += f"/{usage.reasoning_tokens}"
148
- if usage.cached_tokens is not None:
149
- usage_str += f"/{usage.cached_tokens}"
150
- logger.debug(
151
- f"\n------------------------------------\n{usage_str}",
152
- **log_kwargs, # type: ignore
153
- )
151
+ usage_str = f"I/O/R/C tokens: {usage.input_tokens}/{usage.output_tokens}"
152
+ usage_str += f"/{usage.reasoning_tokens or '-'}"
153
+ usage_str += f"/{usage.cached_tokens or '-'}"
154
+
155
+ out += f"\n------------------------------------\n{usage_str}\n"
154
156
 
155
- def print_llm_messages(
157
+ logger.debug(out, **log_kwargs) # type: ignore
158
+
159
+ def print_messages(
156
160
  self,
157
161
  messages: Sequence[Message],
158
162
  agent_name: str,
159
- run_id: str,
163
+ call_id: str,
160
164
  usages: Sequence[Usage | None] | None = None,
161
165
  ) -> None:
162
- if not self.print_messages:
163
- return
164
-
165
166
  _usages: Sequence[Usage | None] = usages or [None] * len(messages)
166
167
 
167
168
  for _message, _usage in zip(messages, _usages, strict=False):
168
- self.print_llm_message(
169
- _message, usage=_usage, agent_name=agent_name, run_id=run_id
169
+ self.print_message(
170
+ _message, usage=_usage, agent_name=agent_name, call_id=call_id
170
171
  )
172
+
173
+
174
+ def stream_text(new_text: str, color: Color) -> None:
175
+ sys.stdout.write(colored(new_text, color))
176
+ sys.stdout.flush()
177
+
178
+
179
+ async def print_event_stream(
180
+ event_generator: AsyncIterator[Event[Any]],
181
+ color_by: ColoringMode = "role",
182
+ trunc_len: int = 1000,
183
+ ) -> AsyncIterator[Event[Any]]:
184
+ prev_chunk_id: str | None = None
185
+ thinking_open = False
186
+ response_open = False
187
+ open_tool_calls: set[str] = set()
188
+
189
+ color = Printer.get_role_color(Role.ASSISTANT)
190
+
191
+ def _close_blocks(
192
+ _thinking_open: bool, _response_open: bool, color: Color
193
+ ) -> tuple[bool, bool]:
194
+ closing_text = ""
195
+ while open_tool_calls:
196
+ open_tool_calls.pop()
197
+ closing_text += "\n</tool call>\n"
198
+
199
+ if _thinking_open:
200
+ closing_text += "</thinking>\n"
201
+ _thinking_open = False
202
+
203
+ if _response_open:
204
+ closing_text += "</response>\n"
205
+ _response_open = False
206
+
207
+ if closing_text:
208
+ stream_text(closing_text, color)
209
+
210
+ return _thinking_open, _response_open
211
+
212
+ def _get_color(event: Event[Any], role: Role = Role.ASSISTANT) -> Color:
213
+ if color_by == "agent":
214
+ return Printer.get_agent_color(event.proc_name or "")
215
+ return Printer.get_role_color(role)
216
+
217
+ def _print_packet(
218
+ event: ProcPacketOutputEvent | WorkflowResultEvent | RunResultEvent,
219
+ ) -> None:
220
+ color = _get_color(event, Role.ASSISTANT)
221
+
222
+ if isinstance(event, ProcPacketOutputEvent):
223
+ src = "processor"
224
+ elif isinstance(event, WorkflowResultEvent):
225
+ src = "workflow"
226
+ else:
227
+ src = "run"
228
+
229
+ text = f"\n<{event.proc_name}> [{event.call_id}]\n"
230
+
231
+ if event.data.payloads:
232
+ text += f"<{src} output>\n"
233
+ for p in event.data.payloads:
234
+ if isinstance(p, BaseModel):
235
+ p_str = p.model_dump_json(indent=2)
236
+ else:
237
+ try:
238
+ p_str = json.dumps(p, indent=2)
239
+ except TypeError:
240
+ p_str = str(p)
241
+ text += f"{p_str}\n"
242
+ text += f"</{src} output>\n"
243
+
244
+ stream_text(text, color)
245
+
246
+ async for event in event_generator:
247
+ yield event
248
+
249
+ if isinstance(event, CompletionChunkEvent):
250
+ delta = event.data.choices[0].delta
251
+ chunk_id = event.data.id
252
+ new_completion = chunk_id != prev_chunk_id
253
+ color = _get_color(event, Role.ASSISTANT)
254
+
255
+ text = ""
256
+
257
+ if new_completion:
258
+ thinking_open, response_open = _close_blocks(
259
+ thinking_open, response_open, color
260
+ )
261
+ text += f"\n<{event.proc_name}> [{event.call_id}]\n"
262
+
263
+ if delta.reasoning_content:
264
+ if not thinking_open:
265
+ text += "<thinking>\n"
266
+ thinking_open = True
267
+ text += delta.reasoning_content
268
+ elif thinking_open:
269
+ text += "\n</thinking>\n"
270
+ thinking_open = False
271
+
272
+ if delta.content:
273
+ if not response_open:
274
+ text += "<response>\n"
275
+ response_open = True
276
+ text += delta.content
277
+ elif response_open:
278
+ text += "\n</response>\n"
279
+ response_open = False
280
+
281
+ if delta.tool_calls:
282
+ for tc in delta.tool_calls:
283
+ if tc.id and tc.id not in open_tool_calls:
284
+ open_tool_calls.add(tc.id) # type: ignore
285
+ text += f"<tool call> {tc.tool_name} [{tc.id}]\n"
286
+
287
+ if tc.tool_arguments:
288
+ text += tc.tool_arguments
289
+
290
+ stream_text(text, color)
291
+ prev_chunk_id = chunk_id
292
+
293
+ else:
294
+ thinking_open, response_open = _close_blocks(
295
+ thinking_open, response_open, color
296
+ )
297
+
298
+ if isinstance(event, MessageEvent) and not isinstance(event, GenMessageEvent):
299
+ message = event.data
300
+ role = message.role
301
+ content = Printer.content_to_str(message.content, role=role)
302
+ color = _get_color(event, role)
303
+
304
+ text = f"\n<{event.proc_name}> [{event.call_id}]\n"
305
+
306
+ if isinstance(event, (SystemMessageEvent, UserMessageEvent)):
307
+ content = Printer.truncate_content_str(content, trunc_len=trunc_len)
308
+
309
+ if isinstance(event, SystemMessageEvent):
310
+ text += f"<system>\n{content}\n</system>\n"
311
+
312
+ elif isinstance(event, UserMessageEvent):
313
+ text += f"<input>\n{content}\n</input>\n"
314
+
315
+ elif isinstance(event, ToolMessageEvent):
316
+ try:
317
+ content = json.dumps(json.loads(content), indent=2)
318
+ except Exception:
319
+ pass
320
+ text += (
321
+ f"<tool result> [{message.tool_call_id}]\n"
322
+ f"{content}\n</tool result>\n"
323
+ )
324
+
325
+ stream_text(text, color)
326
+
327
+ if isinstance(
328
+ event, (ProcPacketOutputEvent, WorkflowResultEvent, RunResultEvent)
329
+ ):
330
+ _print_packet(event)