grasp_agents 0.2.7__tar.gz → 0.2.8__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 (47) hide show
  1. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/PKG-INFO +1 -1
  2. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/pyproject.toml +1 -1
  3. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/cloud_llm.py +3 -6
  4. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/llm.py +17 -5
  5. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/llm_agent.py +2 -2
  6. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/openai/message_converters.py +1 -1
  7. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/prompt_builder.py +12 -12
  8. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/utils.py +36 -70
  9. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/.gitignore +0 -0
  10. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/LICENSE.md +0 -0
  11. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/README.md +0 -0
  12. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/__init__.py +0 -0
  13. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/agent_message.py +0 -0
  14. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/agent_message_pool.py +0 -0
  15. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/base_agent.py +0 -0
  16. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/comm_agent.py +0 -0
  17. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/costs_dict.yaml +0 -0
  18. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/generics_utils.py +0 -0
  19. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/grasp_logging.py +0 -0
  20. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/http_client.py +0 -0
  21. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/llm_agent_state.py +0 -0
  22. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/memory.py +0 -0
  23. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/openai/__init__.py +0 -0
  24. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/openai/completion_converters.py +0 -0
  25. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/openai/content_converters.py +0 -0
  26. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/openai/converters.py +0 -0
  27. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/openai/openai_llm.py +0 -0
  28. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/openai/tool_converters.py +0 -0
  29. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/printer.py +0 -0
  30. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/rate_limiting/__init__.py +0 -0
  31. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/rate_limiting/rate_limiter_chunked.py +0 -0
  32. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/rate_limiting/types.py +0 -0
  33. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/rate_limiting/utils.py +0 -0
  34. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/run_context.py +0 -0
  35. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/tool_orchestrator.py +0 -0
  36. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/typing/__init__.py +0 -0
  37. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/typing/completion.py +0 -0
  38. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/typing/content.py +0 -0
  39. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/typing/converters.py +0 -0
  40. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/typing/io.py +0 -0
  41. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/typing/message.py +0 -0
  42. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/typing/tool.py +0 -0
  43. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/usage_tracker.py +0 -0
  44. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/workflow/__init__.py +0 -0
  45. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/workflow/looped_agent.py +0 -0
  46. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/workflow/sequential_agent.py +0 -0
  47. {grasp_agents-0.2.7 → grasp_agents-0.2.8}/src/grasp_agents/workflow/workflow_agent.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grasp_agents
3
- Version: 0.2.7
3
+ Version: 0.2.8
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.2.7"
3
+ version = "0.2.8"
4
4
  description = "Grasp Agents Library"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11.4,<4"
@@ -2,12 +2,12 @@ import fnmatch
2
2
  import logging
3
3
  import os
4
4
  from abc import abstractmethod
5
- from collections.abc import AsyncIterator, Sequence
5
+ from collections.abc import AsyncIterator, Mapping, Sequence
6
6
  from copy import deepcopy
7
7
  from typing import Any, Generic, Literal
8
8
 
9
9
  import httpx
10
- from pydantic import BaseModel, TypeAdapter
10
+ from pydantic import BaseModel
11
11
  from tenacity import (
12
12
  RetryCallState,
13
13
  retry,
@@ -104,7 +104,7 @@ class CloudLLM(LLM[SettingsT, ConvertT], Generic[SettingsT, ConvertT]):
104
104
  llm_settings: SettingsT | None = None,
105
105
  model_id: str | None = None,
106
106
  tools: list[BaseTool[BaseModel, Any, Any]] | None = None,
107
- response_format: type | None = None,
107
+ response_format: type | Mapping[str, type] | None = None,
108
108
  # Connection settings
109
109
  async_http_client_params: (
110
110
  dict[str, Any] | AsyncHTTPClientParams | None
@@ -148,9 +148,6 @@ class CloudLLM(LLM[SettingsT, ConvertT], Generic[SettingsT, ConvertT]):
148
148
  fnmatch.fnmatch(self._model_name, pat)
149
149
  for pat in PROVIDERS[api_provider]["struct_output_support"]
150
150
  )
151
- self._response_format_pyd: TypeAdapter[Any] | None = (
152
- TypeAdapter(self._response_format) if response_format else None
153
- )
154
151
  if (
155
152
  self._llm_settings.get("use_structured_outputs")
156
153
  and not self._struct_output_support
@@ -1,10 +1,10 @@
1
1
  import logging
2
2
  from abc import ABC, abstractmethod
3
- from collections.abc import AsyncIterator, Sequence
3
+ from collections.abc import AsyncIterator, Mapping, Sequence
4
4
  from typing import Any, Generic, TypeVar, cast
5
5
  from uuid import uuid4
6
6
 
7
- from pydantic import BaseModel
7
+ from pydantic import BaseModel, TypeAdapter
8
8
  from typing_extensions import TypedDict
9
9
 
10
10
  from .memory import MessageHistory
@@ -33,7 +33,7 @@ class LLM(ABC, Generic[SettingsT, ConvertT]):
33
33
  model_id: str | None = None,
34
34
  llm_settings: SettingsT | None = None,
35
35
  tools: list[BaseTool[BaseModel, Any, Any]] | None = None,
36
- response_format: type | None = None,
36
+ response_format: type | Mapping[str, type] | None = None,
37
37
  **kwargs: Any,
38
38
  ) -> None:
39
39
  super().__init__()
@@ -42,9 +42,21 @@ class LLM(ABC, Generic[SettingsT, ConvertT]):
42
42
  self._model_id = model_id or str(uuid4())[:8]
43
43
  self._model_name = model_name
44
44
  self._tools = {t.name: t for t in tools} if tools else None
45
- self._response_format = response_format
46
45
  self._llm_settings: SettingsT = llm_settings or cast("SettingsT", {})
47
46
 
47
+ self._response_format = response_format
48
+ self._response_format_pyd: (
49
+ TypeAdapter[Any] | Mapping[str, TypeAdapter[Any]] | None
50
+ )
51
+ if isinstance(response_format, type):
52
+ self._response_format_pyd = TypeAdapter(response_format)
53
+ elif isinstance(response_format, Mapping):
54
+ self._response_format_pyd = {
55
+ k: TypeAdapter(v) for k, v in response_format.items()
56
+ }
57
+ else:
58
+ self._response_format_pyd = None
59
+
48
60
  @property
49
61
  def model_id(self) -> str:
50
62
  return self._model_id
@@ -62,7 +74,7 @@ class LLM(ABC, Generic[SettingsT, ConvertT]):
62
74
  return self._tools
63
75
 
64
76
  @property
65
- def response_format(self) -> type | None:
77
+ def response_format(self) -> type | Mapping[str, type] | None:
66
78
  return self._response_format
67
79
 
68
80
  @tools.setter
@@ -441,8 +441,8 @@ class LLMAgent(
441
441
  def _format_in_args(
442
442
  self,
443
443
  *,
444
- usr_args: LLMPromptArgs | None = None,
445
- in_args: InT | None = None,
444
+ usr_args: LLMPromptArgs,
445
+ in_args: InT,
446
446
  batch_idx: int = 0,
447
447
  ctx: RunContextWrapper[CtxT] | None = None,
448
448
  ) -> LLMFormattedArgs:
@@ -114,7 +114,7 @@ def to_api_assistant_message(
114
114
  tool_calls=api_tool_calls or [],
115
115
  refusal=message.refusal,
116
116
  )
117
- if message.content is None and not api_tool_calls:
117
+ if message.content is None:
118
118
  # Some API providers return None in the generated content without errors,
119
119
  # even though None in the input content is not accepted.
120
120
  api_message["content"] = "<empty>"
@@ -34,8 +34,8 @@ class FormatInputArgsHandler(Protocol[InT, CtxT]):
34
34
  def __call__(
35
35
  self,
36
36
  *,
37
- usr_args: LLMPromptArgs | None,
38
- in_args: InT | None,
37
+ usr_args: LLMPromptArgs,
38
+ in_args: InT,
39
39
  batch_idx: int,
40
40
  ctx: RunContextWrapper[CtxT] | None,
41
41
  ) -> LLMFormattedArgs: ...
@@ -78,8 +78,8 @@ class PromptBuilder(AutoInstanceAttributesMixin, Generic[InT, CtxT]):
78
78
  def _format_in_args(
79
79
  self,
80
80
  *,
81
- usr_args: LLMPromptArgs | None = None,
82
- in_args: InT | None = None,
81
+ usr_args: LLMPromptArgs,
82
+ in_args: InT,
83
83
  batch_idx: int = 0,
84
84
  ctx: RunContextWrapper[CtxT] | None = None,
85
85
  ) -> LLMFormattedArgs:
@@ -93,7 +93,7 @@ class PromptBuilder(AutoInstanceAttributesMixin, Generic[InT, CtxT]):
93
93
  "Cannot apply default formatting to non-BaseModel received arguments."
94
94
  )
95
95
 
96
- usr_args_ = DummySchema() if usr_args is None else usr_args
96
+ usr_args_ = usr_args
97
97
  in_args_ = DummySchema() if in_args is None else in_args
98
98
 
99
99
  usr_args_dump = usr_args_.model_dump(exclude_unset=True)
@@ -149,12 +149,10 @@ class PromptBuilder(AutoInstanceAttributesMixin, Generic[InT, CtxT]):
149
149
  usr_args_batch_, in_args_batch_ = self._make_batched(usr_args, in_args_batch)
150
150
 
151
151
  val_usr_args_batch_ = [
152
- self.usr_args_schema.model_validate(u) if u is not None else None
153
- for u in usr_args_batch_
152
+ self.usr_args_schema.model_validate(u) for u in usr_args_batch_
154
153
  ]
155
154
  val_in_args_batch_ = [
156
- self._in_args_type_adapter.validate_python(inp) if inp is not None else None
157
- for inp in in_args_batch_
155
+ self._in_args_type_adapter.validate_python(inp) for inp in in_args_batch_
158
156
  ]
159
157
 
160
158
  formatted_in_args_batch = [
@@ -222,9 +220,11 @@ class PromptBuilder(AutoInstanceAttributesMixin, Generic[InT, CtxT]):
222
220
  self,
223
221
  usr_args: UserRunArgs | None = None,
224
222
  in_args_batch: Sequence[InT] | None = None,
225
- ) -> tuple[Sequence[LLMPromptArgs | None], Sequence[InT | None]]:
226
- usr_args_batch_ = usr_args if isinstance(usr_args, list) else [usr_args]
227
- in_args_batch_ = in_args_batch or [None]
223
+ ) -> tuple[Sequence[LLMPromptArgs | DummySchema], Sequence[InT | DummySchema]]:
224
+ usr_args_batch_ = (
225
+ usr_args if isinstance(usr_args, list) else [usr_args or DummySchema()]
226
+ )
227
+ in_args_batch_ = in_args_batch or [DummySchema()]
228
228
 
229
229
  # Broadcast singleton → match lengths
230
230
  if len(usr_args_batch_) == 1 and len(in_args_batch_) > 1:
@@ -6,14 +6,9 @@ from collections.abc import Coroutine, Mapping
6
6
  from datetime import datetime
7
7
  from logging import getLogger
8
8
  from pathlib import Path
9
- from typing import Any, TypeVar
10
-
11
- from pydantic import (
12
- GetCoreSchemaHandler,
13
- TypeAdapter,
14
- ValidationError,
15
- )
16
- from pydantic_core import core_schema
9
+ from typing import Any, TypeVar, overload
10
+
11
+ from pydantic import TypeAdapter, ValidationError
17
12
  from tqdm.autonotebook import tqdm
18
13
 
19
14
  logger = getLogger(__name__)
@@ -62,9 +57,37 @@ def parse_json_or_py_substring(
62
57
  )
63
58
 
64
59
 
60
+ @overload
61
+ def validate_obj_from_json_or_py_string(
62
+ s: str,
63
+ adapter: TypeAdapter[T],
64
+ from_substring: bool = False,
65
+ ) -> T: ...
66
+
67
+
68
+ @overload
69
+ def validate_obj_from_json_or_py_string(
70
+ s: str,
71
+ adapter: Mapping[str, TypeAdapter[T]],
72
+ from_substring: bool = False,
73
+ ) -> T | str: ...
74
+
75
+
65
76
  def validate_obj_from_json_or_py_string(
66
- s: str, adapter: TypeAdapter[T], from_substring: bool = False
67
- ) -> T:
77
+ s: str,
78
+ adapter: TypeAdapter[T] | Mapping[str, TypeAdapter[T]],
79
+ from_substring: bool = False,
80
+ ) -> T | str:
81
+ _selected_adapter: TypeAdapter[T] | None = None
82
+ if isinstance(adapter, Mapping):
83
+ for _marker, _adapter in adapter.items():
84
+ if _marker in s:
85
+ _selected_adapter = _adapter
86
+ if _selected_adapter is None:
87
+ return s
88
+ else:
89
+ _selected_adapter = adapter
90
+
68
91
  try:
69
92
  if from_substring:
70
93
  parsed = parse_json_or_py_substring(s, return_none_on_failure=True)
@@ -72,10 +95,11 @@ def validate_obj_from_json_or_py_string(
72
95
  parsed = parse_json_or_py_string(s, return_none_on_failure=True)
73
96
  if parsed is None:
74
97
  parsed = s
75
- return adapter.validate_python(parsed)
98
+ return _selected_adapter.validate_python(parsed)
76
99
  except (json.JSONDecodeError, ValidationError) as exc:
77
100
  raise ValueError(
78
- f"Invalid JSON or Python string:\n{s}\nExpected type: {adapter._type}", # type: ignore[arg-type]
101
+ f"Invalid JSON or Python string:\n{s}\n"
102
+ f"Expected type: {_selected_adapter._type}", # type: ignore[arg-type]
79
103
  ) from exc
80
104
 
81
105
 
@@ -89,64 +113,6 @@ def extract_xml_list(text: str) -> list[str]:
89
113
  return chunks
90
114
 
91
115
 
92
- def build_marker_json_parser_type(
93
- marker_to_model: Mapping[str, type],
94
- ) -> type:
95
- """
96
- Return a Pydantic-compatible *type* that, when given a **str**, searches for
97
- the first marker substring and validates the JSON that follows with the
98
- corresponding Pydantic model.
99
-
100
- If no marker is found, the raw string is returned unchanged.
101
-
102
- Example:
103
- -------
104
- >>> Todo = build_marker_json_parser_type({'```json': MyModel})
105
- >>> Todo.validate('```json {"a": 1}')
106
- MyModel(a=1)
107
-
108
- """
109
-
110
- class MarkerParsedOutput:
111
- """String → (Model | str) parser generated by build_marker_json_parser_type."""
112
-
113
- @classmethod
114
- def __get_pydantic_core_schema__(
115
- cls,
116
- _source_type: Any,
117
- _handler: GetCoreSchemaHandler,
118
- ) -> core_schema.CoreSchema:
119
- def _validate(value: Any) -> Any:
120
- if not isinstance(value, str):
121
- raise TypeError("MarkerParsedOutput expects a string")
122
-
123
- for marker, model in marker_to_model.items():
124
- if marker in value:
125
- adapter = TypeAdapter[Any](model)
126
- return validate_obj_from_json_or_py_string(
127
- value, adapter=adapter, from_substring=True
128
- )
129
-
130
- return value
131
-
132
- return core_schema.no_info_after_validator_function(
133
- _validate, core_schema.any_schema()
134
- )
135
-
136
- @classmethod
137
- def __get_pydantic_json_schema__(
138
- cls,
139
- schema: core_schema.CoreSchema,
140
- handler: GetCoreSchemaHandler,
141
- ):
142
- return handler(schema)
143
-
144
- unique_suffix = "_".join(sorted(marker_to_model))[:40]
145
- MarkerParsedOutput.__name__ = f"MarkerParsedOutput_{unique_suffix}"
146
-
147
- return MarkerParsedOutput
148
-
149
-
150
116
  def read_txt(file_path: str | Path, encoding: str = "utf-8") -> str:
151
117
  return Path(file_path).read_text(encoding=encoding)
152
118
 
File without changes
File without changes
File without changes