grasp_agents 0.5.4__tar.gz → 0.5.5__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 (57) hide show
  1. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/PKG-INFO +1 -1
  2. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/pyproject.toml +1 -1
  3. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/__init__.py +1 -2
  4. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/llm_agent.py +83 -101
  5. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/llm_agent_memory.py +1 -1
  6. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/llm_policy_executor.py +15 -13
  7. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/printer.py +1 -1
  8. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/processor.py +37 -31
  9. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/prompt_builder.py +22 -60
  10. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/run_context.py +3 -8
  11. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/typing/io.py +0 -7
  12. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/workflow/workflow_processor.py +16 -1
  13. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/.gitignore +0 -0
  14. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/LICENSE.md +0 -0
  15. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/README.md +0 -0
  16. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/cloud_llm.py +0 -0
  17. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/costs_dict.yaml +0 -0
  18. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/errors.py +0 -0
  19. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/generics_utils.py +0 -0
  20. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/grasp_logging.py +0 -0
  21. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/http_client.py +0 -0
  22. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/litellm/__init__.py +0 -0
  23. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/litellm/completion_chunk_converters.py +0 -0
  24. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/litellm/completion_converters.py +0 -0
  25. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/litellm/converters.py +0 -0
  26. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/litellm/lite_llm.py +0 -0
  27. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/litellm/message_converters.py +0 -0
  28. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/llm.py +0 -0
  29. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/memory.py +0 -0
  30. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/openai/__init__.py +0 -0
  31. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/openai/completion_chunk_converters.py +0 -0
  32. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/openai/completion_converters.py +0 -0
  33. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/openai/content_converters.py +0 -0
  34. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/openai/converters.py +0 -0
  35. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/openai/message_converters.py +0 -0
  36. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/openai/openai_llm.py +0 -0
  37. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/openai/tool_converters.py +0 -0
  38. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/packet.py +0 -0
  39. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/packet_pool.py +0 -0
  40. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/rate_limiting/__init__.py +0 -0
  41. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/rate_limiting/rate_limiter_chunked.py +0 -0
  42. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/rate_limiting/types.py +0 -0
  43. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/rate_limiting/utils.py +0 -0
  44. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/runner.py +0 -0
  45. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/typing/__init__.py +0 -0
  46. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/typing/completion.py +0 -0
  47. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/typing/completion_chunk.py +0 -0
  48. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/typing/content.py +0 -0
  49. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/typing/converters.py +0 -0
  50. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/typing/events.py +0 -0
  51. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/typing/message.py +0 -0
  52. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/typing/tool.py +0 -0
  53. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/usage_tracker.py +0 -0
  54. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/utils.py +0 -0
  55. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/workflow/__init__.py +0 -0
  56. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/workflow/looped_workflow.py +0 -0
  57. {grasp_agents-0.5.4 → grasp_agents-0.5.5}/src/grasp_agents/workflow/sequential_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grasp_agents
3
- Version: 0.5.4
3
+ Version: 0.5.5
4
4
  Summary: Grasp Agents Library
5
5
  License-File: LICENSE.md
6
6
  Requires-Python: <4,>=3.11.4
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "grasp_agents"
3
- version = "0.5.4"
3
+ version = "0.5.5"
4
4
  description = "Grasp Agents Library"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11.4,<4"
@@ -10,7 +10,7 @@ from .processor import Processor
10
10
  from .run_context import RunContext
11
11
  from .typing.completion import Completion
12
12
  from .typing.content import Content, ImageData
13
- from .typing.io import LLMPrompt, LLMPromptArgs, ProcName
13
+ from .typing.io import LLMPrompt, ProcName
14
14
  from .typing.message import AssistantMessage, Messages, SystemMessage, UserMessage
15
15
  from .typing.tool import BaseTool
16
16
 
@@ -24,7 +24,6 @@ __all__ = [
24
24
  "LLMAgent",
25
25
  "LLMAgentMemory",
26
26
  "LLMPrompt",
27
- "LLMPromptArgs",
28
27
  "LLMSettings",
29
28
  "Memory",
30
29
  "Messages",
@@ -1,21 +1,21 @@
1
1
  from collections.abc import AsyncIterator, Sequence
2
2
  from pathlib import Path
3
- from typing import Any, ClassVar, Generic, Protocol, TypeVar, cast
3
+ from typing import Any, ClassVar, Generic, Protocol, TypeVar, cast, final
4
4
 
5
5
  from pydantic import BaseModel
6
6
 
7
7
  from .llm import LLM, LLMSettings
8
- from .llm_agent_memory import LLMAgentMemory, PrepareMemoryHandler
8
+ from .llm_agent_memory import LLMAgentMemory, MemoryPreparator
9
9
  from .llm_policy_executor import (
10
- ExitToolCallLoopHandler,
11
10
  LLMPolicyExecutor,
12
- ManageMemoryHandler,
11
+ MemoryManager,
12
+ ToolCallLoopTerminator,
13
13
  )
14
14
  from .processor import Processor
15
15
  from .prompt_builder import (
16
- MakeInputContentHandler,
17
- MakeSystemPromptHandler,
16
+ InputContentBuilder,
18
17
  PromptBuilder,
18
+ SystemPromptBuilder,
19
19
  )
20
20
  from .run_context import CtxT, RunContext
21
21
  from .typing.content import Content, ImageData
@@ -26,7 +26,7 @@ from .typing.events import (
26
26
  SystemMessageEvent,
27
27
  UserMessageEvent,
28
28
  )
29
- from .typing.io import InT, LLMPrompt, LLMPromptArgs, OutT, ProcName
29
+ from .typing.io import InT, LLMPrompt, OutT, ProcName
30
30
  from .typing.message import Message, Messages, SystemMessage, UserMessage
31
31
  from .typing.tool import BaseTool
32
32
  from .utils import get_prompt, validate_obj_from_json_or_py_string
@@ -35,7 +35,7 @@ _InT_contra = TypeVar("_InT_contra", contravariant=True)
35
35
  _OutT_co = TypeVar("_OutT_co", covariant=True)
36
36
 
37
37
 
38
- class ParseOutputHandler(Protocol[_InT_contra, _OutT_co, CtxT]):
38
+ class OutputParser(Protocol[_InT_contra, _OutT_co, CtxT]):
39
39
  def __call__(
40
40
  self,
41
41
  conversation: Messages,
@@ -68,8 +68,6 @@ class LLMAgent(
68
68
  # System prompt template
69
69
  sys_prompt: LLMPrompt | None = None,
70
70
  sys_prompt_path: str | Path | None = None,
71
- # System args (static args provided via RunContext)
72
- sys_args_schema: type[LLMPromptArgs] | None = None,
73
71
  # Agent loop settings
74
72
  max_turns: int = 100,
75
73
  react_mode: bool = False,
@@ -88,10 +86,22 @@ class LLMAgent(
88
86
  self._memory: LLMAgentMemory = LLMAgentMemory()
89
87
  self._reset_memory_on_run = reset_memory_on_run
90
88
 
89
+ self.memory_preparator: MemoryPreparator | None
90
+ if not hasattr(type(self), "memory_preparator"):
91
+ self.memory_preparator = None
92
+
93
+ self.output_parser: OutputParser[InT, OutT, CtxT] | None
94
+ if not hasattr(type(self), "output_parser"):
95
+ self.output_parser = None
96
+
91
97
  # LLM policy executor
92
98
 
93
99
  self._used_default_llm_response_schema: bool = False
94
- if llm.response_schema is None and tools is None:
100
+ if (
101
+ llm.response_schema is None
102
+ and tools is None
103
+ and not hasattr(type(self), "output_parser")
104
+ ):
95
105
  llm.response_schema = self.out_type
96
106
  self._used_default_llm_response_schema = True
97
107
 
@@ -119,17 +129,11 @@ class LLMAgent(
119
129
 
120
130
  sys_prompt = get_prompt(prompt_text=sys_prompt, prompt_path=sys_prompt_path)
121
131
  in_prompt = get_prompt(prompt_text=in_prompt, prompt_path=in_prompt_path)
122
- self._prompt_builder: PromptBuilder[InT, CtxT] = PromptBuilder[
123
- self.in_type, CtxT
124
- ](
125
- agent_name=self._name,
126
- sys_prompt=sys_prompt,
127
- in_prompt=in_prompt,
128
- sys_args_schema=sys_args_schema,
132
+
133
+ self._prompt_builder = PromptBuilder[self.in_type, CtxT](
134
+ agent_name=self._name, sys_prompt=sys_prompt, in_prompt=in_prompt
129
135
  )
130
136
 
131
- self._prepare_memory_impl: PrepareMemoryHandler | None = None
132
- self._parse_output_impl: ParseOutputHandler[InT, OutT, CtxT] | None = None
133
137
  self._register_overridden_handlers()
134
138
 
135
139
  @property
@@ -144,10 +148,6 @@ class LLMAgent(
144
148
  def max_turns(self) -> int:
145
149
  return self._policy_executor.max_turns
146
150
 
147
- @property
148
- def sys_args_schema(self) -> type[LLMPromptArgs] | None:
149
- return self._prompt_builder.sys_args_schema
150
-
151
151
  @property
152
152
  def sys_prompt(self) -> LLMPrompt | None:
153
153
  return self._prompt_builder.sys_prompt
@@ -156,6 +156,7 @@ class LLMAgent(
156
156
  def in_prompt(self) -> LLMPrompt | None:
157
157
  return self._prompt_builder.in_prompt
158
158
 
159
+ @final
159
160
  def _prepare_memory(
160
161
  self,
161
162
  memory: LLMAgentMemory,
@@ -163,8 +164,8 @@ class LLMAgent(
163
164
  sys_prompt: LLMPrompt | None = None,
164
165
  ctx: RunContext[Any] | None = None,
165
166
  ) -> None:
166
- if self._prepare_memory_impl:
167
- return self._prepare_memory_impl(
167
+ if self.memory_preparator:
168
+ return self.memory_preparator(
168
169
  memory=memory, in_args=in_args, sys_prompt=sys_prompt, ctx=ctx
169
170
  )
170
171
 
@@ -175,16 +176,7 @@ class LLMAgent(
175
176
  in_args: InT | None = None,
176
177
  ctx: RunContext[CtxT] | None = None,
177
178
  ) -> tuple[SystemMessage | None, UserMessage | None]:
178
- # 1. Get system arguments
179
- sys_args = ctx.sys_args.get(self.name) if ctx and ctx.sys_args else None
180
-
181
- # 2. Make system prompt (can be None)
182
-
183
- formatted_sys_prompt = self._prompt_builder.make_system_prompt(
184
- sys_args=sys_args, ctx=ctx
185
- )
186
-
187
- # 3. Set agent memory
179
+ formatted_sys_prompt = self._prompt_builder.build_system_prompt(ctx=ctx)
188
180
 
189
181
  system_message: SystemMessage | None = None
190
182
  if self._reset_memory_on_run or memory.is_empty:
@@ -196,9 +188,7 @@ class LLMAgent(
196
188
  memory=memory, in_args=in_args, sys_prompt=formatted_sys_prompt, ctx=ctx
197
189
  )
198
190
 
199
- # 3. Make and add input messages
200
-
201
- input_message = self._prompt_builder.make_input_message(
191
+ input_message = self._prompt_builder.build_input_message(
202
192
  chat_inputs=chat_inputs, in_args=in_args, ctx=ctx
203
193
  )
204
194
  if input_message:
@@ -213,8 +203,8 @@ class LLMAgent(
213
203
  in_args: InT | None = None,
214
204
  ctx: RunContext[CtxT] | None = None,
215
205
  ) -> OutT:
216
- if self._parse_output_impl:
217
- return self._parse_output_impl(
206
+ if self.output_parser:
207
+ return self.output_parser(
218
208
  conversation=conversation, in_args=in_args, ctx=ctx
219
209
  )
220
210
 
@@ -302,108 +292,100 @@ class LLMAgent(
302
292
  cur_cls = type(self)
303
293
  base_cls = LLMAgent[Any, Any, Any]
304
294
 
305
- # Packet routing
306
- if cur_cls._select_recipients is not base_cls._select_recipients: # noqa: SLF001
307
- self.select_recipients_impl = self._select_recipients
308
-
309
295
  # Prompt builder
310
296
 
311
- if cur_cls._make_system_prompt is not base_cls._make_system_prompt: # noqa: SLF001
312
- self._prompt_builder.make_system_prompt_impl = self._make_system_prompt
297
+ if cur_cls.system_prompt_builder is not base_cls.system_prompt_builder:
298
+ self._prompt_builder.system_prompt_builder = self.system_prompt_builder
313
299
 
314
- if cur_cls._make_input_content is not base_cls._make_input_content: # noqa: SLF001
315
- self._prompt_builder.make_input_content_impl = self._make_input_content
300
+ if cur_cls.input_content_builder is not base_cls.input_content_builder:
301
+ self._prompt_builder.input_content_builder = self.input_content_builder
316
302
 
317
303
  # Policy executor
318
304
 
319
- if (
320
- cur_cls._exit_tool_call_loop is not base_cls._exit_tool_call_loop # noqa: SLF001
321
- ):
322
- self._policy_executor.exit_tool_call_loop_impl = self._exit_tool_call_loop
323
-
324
- if cur_cls._manage_memory is not base_cls._manage_memory: # noqa: SLF001
325
- self._policy_executor.manage_memory_impl = self._manage_memory
305
+ if cur_cls.tool_call_loop_terminator is not base_cls.tool_call_loop_terminator:
306
+ self._policy_executor.tool_call_loop_terminator = (
307
+ self.tool_call_loop_terminator
308
+ )
326
309
 
327
- # Make sure default LLM response schema is not used when custom output
328
- # parsing is provided
329
- if (
330
- cur_cls._parse_output is not base_cls._parse_output # noqa: SLF001
331
- and self._used_default_llm_response_schema
332
- ):
333
- self._policy_executor.llm.response_schema = None
310
+ if cur_cls.memory_manager is not base_cls.memory_manager:
311
+ self._policy_executor.memory_manager = self.memory_manager
334
312
 
335
- def _make_system_prompt(
336
- self, sys_args: LLMPromptArgs | None, *, ctx: RunContext[CtxT] | None = None
337
- ) -> str | None:
338
- return self._prompt_builder.make_system_prompt(sys_args=sys_args, ctx=ctx)
313
+ def system_prompt_builder(self, ctx: RunContext[CtxT] | None = None) -> str | None:
314
+ if self._prompt_builder.system_prompt_builder is not None:
315
+ return self._prompt_builder.system_prompt_builder(ctx=ctx)
316
+ raise NotImplementedError("System prompt builder is not implemented.")
339
317
 
340
- def _make_input_content(
318
+ def input_content_builder(
341
319
  self, in_args: InT | None = None, *, ctx: RunContext[CtxT] | None = None
342
320
  ) -> Content:
343
- return self._prompt_builder.make_input_content(in_args=in_args, ctx=ctx)
321
+ if self._prompt_builder.input_content_builder is not None:
322
+ return self._prompt_builder.input_content_builder(in_args=in_args, ctx=ctx)
323
+ raise NotImplementedError("Input content builder is not implemented.")
344
324
 
345
- def _exit_tool_call_loop(
325
+ def tool_call_loop_terminator(
346
326
  self,
347
327
  conversation: Messages,
348
328
  *,
349
329
  ctx: RunContext[CtxT] | None = None,
350
330
  **kwargs: Any,
351
331
  ) -> bool:
352
- return self._policy_executor._exit_tool_call_loop( # type: ignore[return-value]
353
- conversation=conversation, ctx=ctx, **kwargs
354
- )
332
+ if self._policy_executor.tool_call_loop_terminator is not None:
333
+ return self._policy_executor.tool_call_loop_terminator(
334
+ conversation=conversation, ctx=ctx, **kwargs
335
+ )
336
+ raise NotImplementedError("Tool call loop terminator is not implemented.")
355
337
 
356
- def _manage_memory(
338
+ def memory_manager(
357
339
  self,
358
340
  memory: LLMAgentMemory,
359
341
  *,
360
342
  ctx: RunContext[CtxT] | None = None,
361
343
  **kwargs: Any,
362
344
  ) -> None:
363
- return self._policy_executor._manage_memory( # type: ignore[return-value]
364
- memory=memory, ctx=ctx, **kwargs
365
- )
345
+ if self._policy_executor.memory_manager is not None:
346
+ return self._policy_executor.memory_manager(
347
+ memory=memory, ctx=ctx, **kwargs
348
+ )
349
+ raise NotImplementedError("Memory manager is not implemented.")
366
350
 
367
351
  # Decorators for custom implementations as an alternative to overriding methods
368
352
 
369
- def make_system_prompt(
370
- self, func: MakeSystemPromptHandler[CtxT]
371
- ) -> MakeSystemPromptHandler[CtxT]:
372
- self._prompt_builder.make_system_prompt_impl = func
353
+ def add_system_prompt_builder(
354
+ self, func: SystemPromptBuilder[CtxT]
355
+ ) -> SystemPromptBuilder[CtxT]:
356
+ self._prompt_builder.system_prompt_builder = func
373
357
 
374
358
  return func
375
359
 
376
- def make_input_content(
377
- self, func: MakeInputContentHandler[InT, CtxT]
378
- ) -> MakeInputContentHandler[InT, CtxT]:
379
- self._prompt_builder.make_input_content_impl = func
360
+ def add_input_content_builder(
361
+ self, func: InputContentBuilder[InT, CtxT]
362
+ ) -> InputContentBuilder[InT, CtxT]:
363
+ self._prompt_builder.input_content_builder = func
380
364
 
381
365
  return func
382
366
 
383
- def parse_output(
384
- self, func: ParseOutputHandler[InT, OutT, CtxT]
385
- ) -> ParseOutputHandler[InT, OutT, CtxT]:
386
- if self._used_default_llm_response_schema:
387
- self._policy_executor.llm.response_schema = None
388
- self._parse_output_impl = func
367
+ def add_memory_manager(self, func: MemoryManager[CtxT]) -> MemoryManager[CtxT]:
368
+ self._policy_executor.memory_manager = func
389
369
 
390
370
  return func
391
371
 
392
- def prepare_memory(self, func: PrepareMemoryHandler) -> PrepareMemoryHandler:
393
- self._prepare_memory_impl = func
372
+ def add_tool_call_loop_terminator(
373
+ self, func: ToolCallLoopTerminator[CtxT]
374
+ ) -> ToolCallLoopTerminator[CtxT]:
375
+ self._policy_executor.tool_call_loop_terminator = func
394
376
 
395
377
  return func
396
378
 
397
- def manage_memory(
398
- self, func: ManageMemoryHandler[CtxT]
399
- ) -> ManageMemoryHandler[CtxT]:
400
- self._policy_executor.manage_memory_impl = func
379
+ def add_output_parser(
380
+ self, func: OutputParser[InT, OutT, CtxT]
381
+ ) -> OutputParser[InT, OutT, CtxT]:
382
+ if self._used_default_llm_response_schema:
383
+ self._policy_executor.llm.response_schema = None
384
+ self.output_parser = func
401
385
 
402
386
  return func
403
387
 
404
- def exit_tool_call_loop(
405
- self, func: ExitToolCallLoopHandler[CtxT]
406
- ) -> ExitToolCallLoopHandler[CtxT]:
407
- self._policy_executor.exit_tool_call_loop_impl = func
388
+ def add_memory_preparator(self, func: MemoryPreparator) -> MemoryPreparator:
389
+ self.memory_preparator = func
408
390
 
409
391
  return func
@@ -9,7 +9,7 @@ from .typing.io import LLMPrompt
9
9
  from .typing.message import Message, Messages, SystemMessage
10
10
 
11
11
 
12
- class PrepareMemoryHandler(Protocol):
12
+ class MemoryPreparator(Protocol):
13
13
  def __call__(
14
14
  self,
15
15
  memory: "LLMAgentMemory",
@@ -3,7 +3,7 @@ import json
3
3
  from collections.abc import AsyncIterator, Coroutine, Sequence
4
4
  from itertools import starmap
5
5
  from logging import getLogger
6
- from typing import Any, Generic, Protocol
6
+ from typing import Any, Generic, Protocol, final
7
7
 
8
8
  from pydantic import BaseModel
9
9
 
@@ -29,7 +29,7 @@ from .typing.tool import BaseTool, NamedToolChoice, ToolCall, ToolChoice
29
29
  logger = getLogger(__name__)
30
30
 
31
31
 
32
- class ExitToolCallLoopHandler(Protocol[CtxT]):
32
+ class ToolCallLoopTerminator(Protocol[CtxT]):
33
33
  def __call__(
34
34
  self,
35
35
  conversation: Messages,
@@ -39,7 +39,7 @@ class ExitToolCallLoopHandler(Protocol[CtxT]):
39
39
  ) -> bool: ...
40
40
 
41
41
 
42
- class ManageMemoryHandler(Protocol[CtxT]):
42
+ class MemoryManager(Protocol[CtxT]):
43
43
  def __call__(
44
44
  self,
45
45
  memory: LLMAgentMemory,
@@ -78,8 +78,8 @@ class LLMPolicyExecutor(Generic[CtxT]):
78
78
  self._max_turns = max_turns
79
79
  self._react_mode = react_mode
80
80
 
81
- self.exit_tool_call_loop_impl: ExitToolCallLoopHandler[CtxT] | None = None
82
- self.manage_memory_impl: ManageMemoryHandler[CtxT] | None = None
81
+ self.tool_call_loop_terminator: ToolCallLoopTerminator[CtxT] | None = None
82
+ self.memory_manager: MemoryManager[CtxT] | None = None
83
83
 
84
84
  @property
85
85
  def agent_name(self) -> str:
@@ -97,18 +97,20 @@ class LLMPolicyExecutor(Generic[CtxT]):
97
97
  def max_turns(self) -> int:
98
98
  return self._max_turns
99
99
 
100
- def _exit_tool_call_loop(
100
+ @final
101
+ def _terminate_tool_call_loop(
101
102
  self,
102
103
  conversation: Messages,
103
104
  *,
104
105
  ctx: RunContext[CtxT] | None = None,
105
106
  **kwargs: Any,
106
107
  ) -> bool:
107
- if self.exit_tool_call_loop_impl:
108
- return self.exit_tool_call_loop_impl(conversation, ctx=ctx, **kwargs)
108
+ if self.tool_call_loop_terminator:
109
+ return self.tool_call_loop_terminator(conversation, ctx=ctx, **kwargs)
109
110
 
110
111
  return False
111
112
 
113
+ @final
112
114
  def _manage_memory(
113
115
  self,
114
116
  memory: LLMAgentMemory,
@@ -116,8 +118,8 @@ class LLMPolicyExecutor(Generic[CtxT]):
116
118
  ctx: RunContext[CtxT] | None = None,
117
119
  **kwargs: Any,
118
120
  ) -> None:
119
- if self.manage_memory_impl:
120
- self.manage_memory_impl(memory=memory, ctx=ctx, **kwargs)
121
+ if self.memory_manager:
122
+ self.memory_manager(memory=memory, ctx=ctx, **kwargs)
121
123
 
122
124
  async def generate_message(
123
125
  self,
@@ -309,8 +311,8 @@ class LLMPolicyExecutor(Generic[CtxT]):
309
311
  # 2. Check if we should exit the tool call loop
310
312
 
311
313
  # If a final answer is not provided via a tool call, we use
312
- # exit_tool_call_loop to determine whether to exit the loop.
313
- if not self._final_answer_as_tool_call and self._exit_tool_call_loop(
314
+ # _terminate_tool_call_loop to determine whether to exit the loop.
315
+ if not self._final_answer_as_tool_call and self._terminate_tool_call_loop(
314
316
  memory.message_history, ctx=ctx, num_turns=turns
315
317
  ):
316
318
  return gen_message
@@ -390,7 +392,7 @@ class LLMPolicyExecutor(Generic[CtxT]):
390
392
  turns = 0
391
393
 
392
394
  while True:
393
- if not self._final_answer_as_tool_call and self._exit_tool_call_loop(
395
+ if not self._final_answer_as_tool_call and self._terminate_tool_call_loop(
394
396
  memory.message_history, ctx=ctx, num_turns=turns
395
397
  ):
396
398
  return
@@ -119,7 +119,7 @@ class Printer:
119
119
  # Thinking
120
120
  if isinstance(message, AssistantMessage) and message.reasoning_content:
121
121
  thinking = message.reasoning_content.strip(" \n")
122
- out += f"\n<thinking>\n{thinking}\n</thinking>\n"
122
+ out += f"<thinking>\n{thinking}\n</thinking>\n"
123
123
 
124
124
  # Content
125
125
  content = self.content_to_str(message.content or "", message.role)
@@ -34,7 +34,7 @@ logger = logging.getLogger(__name__)
34
34
  _OutT_contra = TypeVar("_OutT_contra", contravariant=True)
35
35
 
36
36
 
37
- class SelectRecipientsHandler(Protocol[_OutT_contra, CtxT]):
37
+ class RecipientSelector(Protocol[_OutT_contra, CtxT]):
38
38
  def __call__(
39
39
  self, output: _OutT_contra, ctx: RunContext[CtxT] | None
40
40
  ) -> list[ProcName] | None: ...
@@ -61,8 +61,13 @@ class Processor(AutoInstanceAttributesMixin, ABC, Generic[InT, OutT, MemT, CtxT]
61
61
  self._name: ProcName = name
62
62
  self._memory: MemT = cast("MemT", DummyMemory())
63
63
  self._max_retries: int = max_retries
64
+
64
65
  self.recipients = recipients
65
- self.select_recipients_impl: SelectRecipientsHandler[OutT, CtxT] | None = None
66
+
67
+ self.recipient_selector: RecipientSelector[OutT, CtxT] | None
68
+ if not hasattr(type(self), "recipient_selector"):
69
+ # Set to None if not defined in the subclass
70
+ self.recipient_selector = None
66
71
 
67
72
  @property
68
73
  def in_type(self) -> type[InT]:
@@ -94,8 +99,8 @@ class Processor(AutoInstanceAttributesMixin, ABC, Generic[InT, OutT, MemT, CtxT]
94
99
  call_id: str,
95
100
  chat_inputs: Any | None = None,
96
101
  in_packet: Packet[InT] | None = None,
97
- in_args: InT | Sequence[InT] | None = None,
98
- ) -> Sequence[InT] | None:
102
+ in_args: InT | list[InT] | None = None,
103
+ ) -> list[InT] | None:
99
104
  mult_inputs_err_message = (
100
105
  "Only one of chat_inputs, in_args, or in_message must be provided."
101
106
  )
@@ -126,14 +131,14 @@ class Processor(AutoInstanceAttributesMixin, ABC, Generic[InT, OutT, MemT, CtxT]
126
131
  if chat_inputs is not None:
127
132
  return None
128
133
 
129
- resolved_args: Sequence[InT]
134
+ resolved_args: list[InT]
130
135
 
131
- if isinstance(in_args, Sequence):
132
- _in_args = cast("Sequence[Any]", in_args)
136
+ if isinstance(in_args, list):
137
+ _in_args = cast("list[Any]", in_args)
133
138
  if all(isinstance(x, self.in_type) for x in _in_args):
134
- resolved_args = cast("Sequence[InT]", _in_args)
139
+ resolved_args = cast("list[InT]", _in_args)
135
140
  elif isinstance(_in_args, self.in_type):
136
- resolved_args = cast("Sequence[InT]", [_in_args])
141
+ resolved_args = cast("list[InT]", [_in_args])
137
142
  else:
138
143
  raise ProcInputValidationError(
139
144
  message=f"in_args are neither of type {self.in_type} "
@@ -142,11 +147,11 @@ class Processor(AutoInstanceAttributesMixin, ABC, Generic[InT, OutT, MemT, CtxT]
142
147
  )
143
148
 
144
149
  elif in_args is not None:
145
- resolved_args = cast("Sequence[InT]", [in_args])
150
+ resolved_args = cast("list[InT]", [in_args])
146
151
 
147
152
  else:
148
153
  assert in_packet is not None
149
- resolved_args = in_packet.payloads
154
+ resolved_args = cast("list[InT]", in_packet.payloads)
150
155
 
151
156
  try:
152
157
  for args in resolved_args:
@@ -167,7 +172,7 @@ class Processor(AutoInstanceAttributesMixin, ABC, Generic[InT, OutT, MemT, CtxT]
167
172
  ) from err
168
173
 
169
174
  def _validate_recipients(
170
- self, recipients: Sequence[ProcName] | None, call_id: str
175
+ self, recipients: list[ProcName] | None, call_id: str
171
176
  ) -> None:
172
177
  for r in recipients or []:
173
178
  if r not in (self.recipients or []):
@@ -191,6 +196,22 @@ class Processor(AutoInstanceAttributesMixin, ABC, Generic[InT, OutT, MemT, CtxT]
191
196
  f"[proc_name={self.name}; call_id={call_id}]",
192
197
  )
193
198
 
199
+ @final
200
+ def _select_recipients(
201
+ self, output: OutT, ctx: RunContext[CtxT] | None = None
202
+ ) -> list[ProcName] | None:
203
+ if self.recipient_selector:
204
+ return self.recipient_selector(output=output, ctx=ctx)
205
+
206
+ return self.recipients
207
+
208
+ def add_recipient_selector(
209
+ self, func: RecipientSelector[OutT, CtxT]
210
+ ) -> RecipientSelector[OutT, CtxT]:
211
+ self.recipient_selector = func
212
+
213
+ return func
214
+
194
215
  async def _process(
195
216
  self,
196
217
  chat_inputs: Any | None = None,
@@ -275,7 +296,7 @@ class Processor(AutoInstanceAttributesMixin, ABC, Generic[InT, OutT, MemT, CtxT]
275
296
  raise ProcRunError(proc_name=self.name, call_id=call_id)
276
297
 
277
298
  async def _run_par(
278
- self, in_args: Sequence[InT], call_id: str, ctx: RunContext[CtxT] | None = None
299
+ self, in_args: list[InT], call_id: str, ctx: RunContext[CtxT] | None = None
279
300
  ) -> Packet[OutT]:
280
301
  tasks = [
281
302
  self._run_single(
@@ -298,7 +319,7 @@ class Processor(AutoInstanceAttributesMixin, ABC, Generic[InT, OutT, MemT, CtxT]
298
319
  chat_inputs: Any | None = None,
299
320
  *,
300
321
  in_packet: Packet[InT] | None = None,
301
- in_args: InT | Sequence[InT] | None = None,
322
+ in_args: InT | list[InT] | None = None,
302
323
  forgetful: bool = False,
303
324
  call_id: str | None = None,
304
325
  ctx: RunContext[CtxT] | None = None,
@@ -406,7 +427,7 @@ class Processor(AutoInstanceAttributesMixin, ABC, Generic[InT, OutT, MemT, CtxT]
406
427
 
407
428
  async def _run_par_stream(
408
429
  self,
409
- in_args: Sequence[InT],
430
+ in_args: list[InT],
410
431
  call_id: str,
411
432
  ctx: RunContext[CtxT] | None = None,
412
433
  ) -> AsyncIterator[Event[Any]]:
@@ -442,7 +463,7 @@ class Processor(AutoInstanceAttributesMixin, ABC, Generic[InT, OutT, MemT, CtxT]
442
463
  chat_inputs: Any | None = None,
443
464
  *,
444
465
  in_packet: Packet[InT] | None = None,
445
- in_args: InT | Sequence[InT] | None = None,
466
+ in_args: InT | list[InT] | None = None,
446
467
  forgetful: bool = False,
447
468
  call_id: str | None = None,
448
469
  ctx: RunContext[CtxT] | None = None,
@@ -469,21 +490,6 @@ class Processor(AutoInstanceAttributesMixin, ABC, Generic[InT, OutT, MemT, CtxT]
469
490
  async for event in stream:
470
491
  yield event
471
492
 
472
- def _select_recipients(
473
- self, output: OutT, ctx: RunContext[CtxT] | None = None
474
- ) -> list[ProcName] | None:
475
- if self.select_recipients_impl:
476
- return self.select_recipients_impl(output=output, ctx=ctx)
477
-
478
- return self.recipients
479
-
480
- def select_recipients(
481
- self, func: SelectRecipientsHandler[OutT, CtxT]
482
- ) -> SelectRecipientsHandler[OutT, CtxT]:
483
- self.select_recipients_impl = func
484
-
485
- return func
486
-
487
493
  @final
488
494
  def as_tool(
489
495
  self, tool_name: str, tool_description: str
@@ -1,34 +1,26 @@
1
1
  import json
2
2
  from collections.abc import Sequence
3
- from typing import ClassVar, Generic, Protocol, TypeAlias, TypeVar
3
+ from typing import ClassVar, Generic, Protocol, TypeAlias, TypeVar, final
4
4
 
5
5
  from pydantic import BaseModel, TypeAdapter
6
6
 
7
- from .errors import InputPromptBuilderError, SystemPromptBuilderError
7
+ from .errors import InputPromptBuilderError
8
8
  from .generics_utils import AutoInstanceAttributesMixin
9
9
  from .run_context import CtxT, RunContext
10
10
  from .typing.content import Content, ImageData
11
- from .typing.io import InT, LLMPrompt, LLMPromptArgs
11
+ from .typing.io import InT, LLMPrompt
12
12
  from .typing.message import UserMessage
13
13
 
14
14
  _InT_contra = TypeVar("_InT_contra", contravariant=True)
15
15
 
16
16
 
17
- class MakeSystemPromptHandler(Protocol[CtxT]):
18
- def __call__(
19
- self,
20
- sys_args: LLMPromptArgs | None,
21
- *,
22
- ctx: RunContext[CtxT] | None,
23
- ) -> str | None: ...
17
+ class SystemPromptBuilder(Protocol[CtxT]):
18
+ def __call__(self, ctx: RunContext[CtxT] | None) -> str | None: ...
24
19
 
25
20
 
26
- class MakeInputContentHandler(Protocol[_InT_contra, CtxT]):
21
+ class InputContentBuilder(Protocol[_InT_contra, CtxT]):
27
22
  def __call__(
28
- self,
29
- in_args: _InT_contra | None,
30
- *,
31
- ctx: RunContext[CtxT] | None,
23
+ self, in_args: _InT_contra | None, *, ctx: RunContext[CtxT] | None
32
24
  ) -> Content: ...
33
25
 
34
26
 
@@ -39,11 +31,7 @@ class PromptBuilder(AutoInstanceAttributesMixin, Generic[InT, CtxT]):
39
31
  _generic_arg_to_instance_attr_map: ClassVar[dict[int, str]] = {0: "_in_type"}
40
32
 
41
33
  def __init__(
42
- self,
43
- agent_name: str,
44
- sys_prompt: LLMPrompt | None,
45
- in_prompt: LLMPrompt | None,
46
- sys_args_schema: type[LLMPromptArgs] | None = None,
34
+ self, agent_name: str, sys_prompt: LLMPrompt | None, in_prompt: LLMPrompt | None
47
35
  ):
48
36
  self._in_type: type[InT]
49
37
  super().__init__()
@@ -51,45 +39,17 @@ class PromptBuilder(AutoInstanceAttributesMixin, Generic[InT, CtxT]):
51
39
  self._agent_name = agent_name
52
40
  self.sys_prompt = sys_prompt
53
41
  self.in_prompt = in_prompt
54
- self.sys_args_schema = sys_args_schema
55
- self.make_system_prompt_impl: MakeSystemPromptHandler[CtxT] | None = None
56
- self.make_input_content_impl: MakeInputContentHandler[InT, CtxT] | None = None
42
+ self.system_prompt_builder: SystemPromptBuilder[CtxT] | None = None
43
+ self.input_content_builder: InputContentBuilder[InT, CtxT] | None = None
57
44
 
58
45
  self._in_args_type_adapter: TypeAdapter[InT] = TypeAdapter(self._in_type)
59
46
 
60
- def _validate_sys_args(
61
- self, sys_args: LLMPromptArgs | None
62
- ) -> LLMPromptArgs | None:
63
- val_sys_args = sys_args
64
- if sys_args is not None:
65
- if self.sys_args_schema is not None:
66
- val_sys_args = self.sys_args_schema.model_validate(sys_args)
67
- else:
68
- raise SystemPromptBuilderError(
69
- proc_name=self._agent_name,
70
- message="System prompt template and arguments is set, "
71
- "but system arguments schema is not provided "
72
- f"[agent_name={self._agent_name}]",
73
- )
74
-
75
- return val_sys_args
76
-
77
- def make_system_prompt(
78
- self,
79
- sys_args: LLMPromptArgs | None = None,
80
- ctx: RunContext[CtxT] | None = None,
81
- ) -> str | None:
82
- if self.sys_prompt is None:
83
- return None
84
-
85
- val_sys_args = self._validate_sys_args(sys_args=sys_args)
86
-
87
- if self.make_system_prompt_impl:
88
- return self.make_system_prompt_impl(sys_args=val_sys_args, ctx=ctx)
89
-
90
- sys_args_dict = val_sys_args.model_dump() if val_sys_args else {}
47
+ @final
48
+ def build_system_prompt(self, ctx: RunContext[CtxT] | None = None) -> str | None:
49
+ if self.system_prompt_builder:
50
+ return self.system_prompt_builder(ctx=ctx)
91
51
 
92
- return self.sys_prompt.format(**sys_args_dict)
52
+ return self.sys_prompt
93
53
 
94
54
  def _validate_input_args(self, in_args: InT) -> InT:
95
55
  val_in_args = self._in_args_type_adapter.validate_python(in_args)
@@ -111,7 +71,8 @@ class PromptBuilder(AutoInstanceAttributesMixin, Generic[InT, CtxT]):
111
71
 
112
72
  return val_in_args
113
73
 
114
- def make_input_content(
74
+ @final
75
+ def _build_input_content(
115
76
  self,
116
77
  in_args: InT | None = None,
117
78
  ctx: RunContext[CtxT] | None = None,
@@ -120,8 +81,8 @@ class PromptBuilder(AutoInstanceAttributesMixin, Generic[InT, CtxT]):
120
81
  if in_args is not None:
121
82
  val_in_args = self._validate_input_args(in_args=in_args)
122
83
 
123
- if self.make_input_content_impl:
124
- return self.make_input_content_impl(in_args=val_in_args, ctx=ctx)
84
+ if self.input_content_builder:
85
+ return self.input_content_builder(in_args=val_in_args, ctx=ctx)
125
86
 
126
87
  if val_in_args is None:
127
88
  raise InputPromptBuilderError(
@@ -141,7 +102,8 @@ class PromptBuilder(AutoInstanceAttributesMixin, Generic[InT, CtxT]):
141
102
  ).decode("utf-8")
142
103
  return Content.from_text(fmt_in_args)
143
104
 
144
- def make_input_message(
105
+ @final
106
+ def build_input_message(
145
107
  self,
146
108
  chat_inputs: LLMPrompt | Sequence[str | ImageData] | None = None,
147
109
  in_args: InT | None = None,
@@ -160,7 +122,7 @@ class PromptBuilder(AutoInstanceAttributesMixin, Generic[InT, CtxT]):
160
122
  return UserMessage.from_content_parts(chat_inputs, name=self._agent_name)
161
123
 
162
124
  return UserMessage(
163
- content=self.make_input_content(in_args=in_args, ctx=ctx),
125
+ content=self._build_input_content(in_args=in_args, ctx=ctx),
164
126
  name=self._agent_name,
165
127
  )
166
128
 
@@ -6,7 +6,7 @@ from pydantic import BaseModel, ConfigDict, Field
6
6
  from grasp_agents.typing.completion import Completion
7
7
 
8
8
  from .printer import ColoringMode, Printer
9
- from .typing.io import LLMPromptArgs, ProcName
9
+ from .typing.io import ProcName
10
10
  from .usage_tracker import UsageTracker
11
11
 
12
12
  CtxT = TypeVar("CtxT")
@@ -15,22 +15,17 @@ CtxT = TypeVar("CtxT")
15
15
  class RunContext(BaseModel, Generic[CtxT]):
16
16
  state: CtxT | None = None
17
17
 
18
- sys_args: dict[ProcName, LLMPromptArgs] = Field(default_factory=dict)
19
-
20
- is_streaming: bool = False
21
- result: Any | None = None
22
-
23
18
  completions: dict[ProcName, list[Completion]] = Field(
24
19
  default_factory=lambda: defaultdict(list)
25
20
  )
26
21
  usage_tracker: UsageTracker = Field(default_factory=UsageTracker)
27
22
 
28
23
  printer: Printer | None = None
29
- print_messages: bool = False
24
+ log_messages: bool = False
30
25
  color_messages_by: ColoringMode = "role"
31
26
 
32
27
  def model_post_init(self, context: Any) -> None: # noqa: ARG002
33
- if self.print_messages:
28
+ if self.log_messages:
34
29
  self.printer = Printer(color_by=self.color_messages_by)
35
30
 
36
31
  model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
@@ -1,16 +1,9 @@
1
1
  from typing import TypeAlias, TypeVar
2
2
 
3
- from pydantic import BaseModel
4
-
5
3
  ProcName: TypeAlias = str
6
4
 
7
5
 
8
6
  InT = TypeVar("InT")
9
7
  OutT = TypeVar("OutT")
10
8
 
11
-
12
- class LLMPromptArgs(BaseModel):
13
- pass
14
-
15
-
16
9
  LLMPrompt: TypeAlias = str
@@ -4,7 +4,7 @@ from typing import Any, Generic
4
4
 
5
5
  from ..errors import WorkflowConstructionError
6
6
  from ..packet import Packet
7
- from ..processor import Processor
7
+ from ..processor import Processor, RecipientSelector
8
8
  from ..run_context import CtxT, RunContext
9
9
  from ..typing.events import Event
10
10
  from ..typing.io import InT, OutT, ProcName
@@ -44,6 +44,21 @@ class WorkflowProcessor(
44
44
  self._start_proc = start_proc
45
45
  self._end_proc = end_proc
46
46
 
47
+ self.recipients = end_proc.recipients
48
+ if recipients is not None:
49
+ self.recipients = recipients
50
+
51
+ if hasattr(type(self), "recipient_selector"):
52
+ self._end_proc.recipient_selector = self.recipient_selector
53
+
54
+ def add_recipient_selector(
55
+ self, func: RecipientSelector[OutT, CtxT]
56
+ ) -> RecipientSelector[OutT, CtxT]:
57
+ self._end_proc.recipient_selector = func
58
+ self.recipient_selector = func
59
+
60
+ return func
61
+
47
62
  @property
48
63
  def subprocs(self) -> Sequence[Processor[Any, Any, Any, CtxT]]:
49
64
  return self._subprocs
File without changes
File without changes
File without changes