grasp_agents 0.4.6__py3-none-any.whl → 0.5.0__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 -218
  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 +4 -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 +234 -72
  26. grasp_agents/processor.py +228 -88
  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.6.dist-info → grasp_agents-0.5.0.dist-info}/METADATA +7 -6
  41. grasp_agents-0.5.0.dist-info/RECORD +57 -0
  42. grasp_agents-0.4.6.dist-info/RECORD +0 -50
  43. {grasp_agents-0.4.6.dist-info → grasp_agents-0.5.0.dist-info}/WHEEL +0 -0
  44. {grasp_agents-0.4.6.dist-info → grasp_agents-0.5.0.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,238 @@ 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
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 += (
140
+ f"<tool result> [{message.tool_call_id}]{content}\n</tool result>"
141
+ )
127
142
 
143
+ # Tool calls
128
144
  if isinstance(message, AssistantMessage) and message.tool_calls is not None:
129
145
  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
146
+ out += (
147
+ f"<tool call> {tool_call.tool_name} [{tool_call.id}]\n"
148
+ f"{tool_call.tool_arguments}\n</tool call>\n"
138
149
  )
139
150
 
140
- # Print usage
141
-
151
+ # Usage
142
152
  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
- )
153
+ usage_str = f"I/O/R/C tokens: {usage.input_tokens}/{usage.output_tokens}"
154
+ usage_str += f"/{usage.reasoning_tokens or '-'}"
155
+ usage_str += f"/{usage.cached_tokens or '-'}"
156
+
157
+ out += f"\n------------------------------------\n{usage_str}"
154
158
 
155
- def print_llm_messages(
159
+ logger.debug(out, **log_kwargs) # type: ignore
160
+
161
+ def print_messages(
156
162
  self,
157
163
  messages: Sequence[Message],
158
164
  agent_name: str,
159
- run_id: str,
165
+ call_id: str,
160
166
  usages: Sequence[Usage | None] | None = None,
161
167
  ) -> None:
162
- if not self.print_messages:
163
- return
164
-
165
168
  _usages: Sequence[Usage | None] = usages or [None] * len(messages)
166
169
 
167
170
  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
171
+ self.print_message(
172
+ _message, usage=_usage, agent_name=agent_name, call_id=call_id
170
173
  )
174
+
175
+
176
+ def stream_text(new_text: str, color: Color) -> None:
177
+ sys.stdout.write(colored(new_text, color))
178
+ sys.stdout.flush()
179
+
180
+
181
+ async def print_event_stream(
182
+ event_generator: AsyncIterator[Event[Any]],
183
+ color_by: ColoringMode = "role",
184
+ trunc_len: int = 1000,
185
+ ) -> AsyncIterator[Event[Any]]:
186
+ prev_chunk_id: str | None = None
187
+ thinking_open = False
188
+ response_open = False
189
+ open_tool_calls: set[str] = set()
190
+
191
+ color = Printer.get_role_color(Role.ASSISTANT)
192
+
193
+ def _close_blocks(
194
+ _thinking_open: bool, _response_open: bool, color: Color
195
+ ) -> tuple[bool, bool]:
196
+ closing_text = ""
197
+ while open_tool_calls:
198
+ open_tool_calls.pop()
199
+ closing_text += "\n</tool call>\n"
200
+
201
+ if _thinking_open:
202
+ closing_text += "</thinking>\n"
203
+ _thinking_open = False
204
+
205
+ if _response_open:
206
+ closing_text += "</response>\n"
207
+ _response_open = False
208
+
209
+ if closing_text:
210
+ stream_text(closing_text, color)
211
+
212
+ return _thinking_open, _response_open
213
+
214
+ def _get_color(event: Event[Any], role: Role = Role.ASSISTANT) -> Color:
215
+ if color_by == "agent":
216
+ return Printer.get_agent_color(event.proc_name or "")
217
+ return Printer.get_role_color(role)
218
+
219
+ def _print_packet(
220
+ event: ProcPacketOutputEvent | WorkflowResultEvent | RunResultEvent,
221
+ ) -> None:
222
+ color = _get_color(event, Role.ASSISTANT)
223
+
224
+ if isinstance(event, ProcPacketOutputEvent):
225
+ src = "processor"
226
+ elif isinstance(event, WorkflowResultEvent):
227
+ src = "workflow"
228
+ else:
229
+ src = "run"
230
+
231
+ text = f"\n<{event.proc_name}> [{event.call_id}]\n"
232
+
233
+ if event.data.payloads:
234
+ text += f"<{src} output>\n"
235
+ for p in event.data.payloads:
236
+ if isinstance(p, BaseModel):
237
+ p_str = p.model_dump_json(indent=2)
238
+ else:
239
+ try:
240
+ p_str = json.dumps(p, indent=2)
241
+ except TypeError:
242
+ p_str = str(p)
243
+ text += f"{p_str}\n"
244
+ text += f"</{src} output>\n"
245
+
246
+ stream_text(text, color)
247
+
248
+ async for event in event_generator:
249
+ yield event
250
+
251
+ if isinstance(event, CompletionChunkEvent):
252
+ delta = event.data.choices[0].delta
253
+ chunk_id = event.data.id
254
+ new_completion = chunk_id != prev_chunk_id
255
+ color = _get_color(event, Role.ASSISTANT)
256
+
257
+ text = ""
258
+
259
+ if new_completion:
260
+ thinking_open, response_open = _close_blocks(
261
+ thinking_open, response_open, color
262
+ )
263
+ text += f"\n<{event.proc_name}> [{event.call_id}]\n"
264
+
265
+ if delta.reasoning_content:
266
+ if not thinking_open:
267
+ text += "<thinking>\n"
268
+ thinking_open = True
269
+ text += delta.reasoning_content
270
+ elif thinking_open:
271
+ text += "\n</thinking>\n"
272
+ thinking_open = False
273
+
274
+ if delta.content:
275
+ if not response_open:
276
+ text += "<response>\n"
277
+ response_open = True
278
+ text += delta.content
279
+ elif response_open:
280
+ text += "\n</response>\n"
281
+ response_open = False
282
+
283
+ if delta.tool_calls:
284
+ for tc in delta.tool_calls:
285
+ if tc.id and tc.id not in open_tool_calls:
286
+ open_tool_calls.add(tc.id) # type: ignore
287
+ text += f"<tool call> {tc.tool_name} [{tc.id}]\n"
288
+
289
+ if tc.tool_arguments:
290
+ text += tc.tool_arguments
291
+
292
+ stream_text(text, color)
293
+ prev_chunk_id = chunk_id
294
+
295
+ else:
296
+ thinking_open, response_open = _close_blocks(
297
+ thinking_open, response_open, color
298
+ )
299
+
300
+ if isinstance(event, MessageEvent) and not isinstance(event, GenMessageEvent):
301
+ message = event.data
302
+ role = message.role
303
+ content = Printer.content_to_str(message.content, role=role)
304
+ color = _get_color(event, role)
305
+
306
+ text = f"\n<{event.proc_name}> [{event.call_id}]\n"
307
+
308
+ if isinstance(event, (SystemMessageEvent, UserMessageEvent)):
309
+ content = Printer.truncate_content_str(content, trunc_len=trunc_len)
310
+
311
+ if isinstance(event, SystemMessageEvent):
312
+ text += f"<system>\n{content}\n</system>\n"
313
+
314
+ elif isinstance(event, UserMessageEvent):
315
+ text += f"<input>\n{content}\n</input>\n"
316
+
317
+ elif isinstance(event, ToolMessageEvent):
318
+ try:
319
+ content = json.dumps(json.loads(content), indent=2)
320
+ except Exception:
321
+ pass
322
+ text += (
323
+ f"<tool result> [{message.tool_call_id}]\n"
324
+ f"{content}\n</tool result>\n"
325
+ )
326
+
327
+ stream_text(text, color)
328
+
329
+ if isinstance(
330
+ event, (ProcPacketOutputEvent, WorkflowResultEvent, RunResultEvent)
331
+ ):
332
+ _print_packet(event)