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.
- grasp_agents/cloud_llm.py +191 -224
- 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 +23 -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 +233 -73
- grasp_agents/processor.py +229 -91
- 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.7.dist-info → grasp_agents-0.5.1.dist-info}/METADATA +7 -6
- grasp_agents-0.5.1.dist-info/RECORD +57 -0
- grasp_agents-0.4.7.dist-info/RECORD +0 -50
- {grasp_agents-0.4.7.dist-info → grasp_agents-0.5.1.dist-info}/WHEEL +0 -0
- {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
|
-
|
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,236 @@ 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
|
-
|
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
|
-
|
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
|
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
|
-
#
|
141
|
-
|
149
|
+
# Usage
|
142
150
|
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
|
-
)
|
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
|
-
|
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
|
-
|
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.
|
169
|
-
_message, usage=_usage, agent_name=agent_name,
|
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)
|