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.
- grasp_agents/cloud_llm.py +191 -218
- grasp_agents/comm_processor.py +101 -100
- grasp_agents/errors.py +69 -9
- grasp_agents/litellm/__init__.py +106 -0
- grasp_agents/litellm/completion_chunk_converters.py +68 -0
- grasp_agents/litellm/completion_converters.py +72 -0
- grasp_agents/litellm/converters.py +138 -0
- grasp_agents/litellm/lite_llm.py +210 -0
- grasp_agents/litellm/message_converters.py +66 -0
- grasp_agents/llm.py +84 -49
- grasp_agents/llm_agent.py +136 -120
- grasp_agents/llm_agent_memory.py +3 -3
- grasp_agents/llm_policy_executor.py +167 -174
- grasp_agents/memory.py +4 -0
- grasp_agents/openai/__init__.py +24 -9
- grasp_agents/openai/completion_chunk_converters.py +6 -6
- grasp_agents/openai/completion_converters.py +12 -14
- grasp_agents/openai/content_converters.py +1 -3
- grasp_agents/openai/converters.py +6 -8
- grasp_agents/openai/message_converters.py +21 -3
- grasp_agents/openai/openai_llm.py +155 -103
- grasp_agents/openai/tool_converters.py +4 -6
- grasp_agents/packet.py +5 -2
- grasp_agents/packet_pool.py +14 -13
- grasp_agents/printer.py +234 -72
- grasp_agents/processor.py +228 -88
- grasp_agents/prompt_builder.py +2 -2
- grasp_agents/run_context.py +11 -20
- grasp_agents/runner.py +42 -0
- grasp_agents/typing/completion.py +16 -9
- grasp_agents/typing/completion_chunk.py +51 -22
- grasp_agents/typing/events.py +95 -19
- grasp_agents/typing/message.py +25 -1
- grasp_agents/typing/tool.py +2 -0
- grasp_agents/usage_tracker.py +31 -37
- grasp_agents/utils.py +95 -84
- grasp_agents/workflow/looped_workflow.py +60 -11
- grasp_agents/workflow/sequential_workflow.py +43 -11
- grasp_agents/workflow/workflow_processor.py +25 -24
- {grasp_agents-0.4.6.dist-info → grasp_agents-0.5.0.dist-info}/METADATA +7 -6
- grasp_agents-0.5.0.dist-info/RECORD +57 -0
- grasp_agents-0.4.6.dist-info/RECORD +0 -50
- {grasp_agents-0.4.6.dist-info → grasp_agents-0.5.0.dist-info}/WHEEL +0 -0
- {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
|
-
|
5
|
-
from
|
6
|
-
|
7
|
-
|
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,
|
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: "
|
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.
|
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
|
85
|
-
self,
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
110
|
-
out += f"\n{message.name} | {message.tool_call_id}"
|
117
|
+
out = f"<{agent_name}> [{call_id}]\n"
|
111
118
|
|
112
|
-
#
|
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
|
-
|
124
|
+
# Content
|
125
|
+
content = self.content_to_str(message.content or "", message.role)
|
126
|
+
if content:
|
115
127
|
try:
|
116
|
-
|
128
|
+
content = json.dumps(json.loads(content), indent=2)
|
117
129
|
except Exception:
|
118
130
|
pass
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
-
|
131
|
-
|
132
|
-
|
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
|
-
#
|
141
|
-
|
151
|
+
# Usage
|
142
152
|
if usage is not None:
|
143
|
-
usage_str =
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
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
|
-
|
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.
|
169
|
-
_message, usage=_usage, agent_name=agent_name,
|
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)
|