langroid 0.58.2__py3-none-any.whl → 0.59.0b1__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 (106) hide show
  1. langroid/agent/base.py +39 -17
  2. langroid/agent/base.py-e +2216 -0
  3. langroid/agent/callbacks/chainlit.py +2 -1
  4. langroid/agent/chat_agent.py +73 -55
  5. langroid/agent/chat_agent.py-e +2086 -0
  6. langroid/agent/chat_document.py +7 -7
  7. langroid/agent/chat_document.py-e +513 -0
  8. langroid/agent/openai_assistant.py +9 -9
  9. langroid/agent/openai_assistant.py-e +882 -0
  10. langroid/agent/special/arangodb/arangodb_agent.py +10 -18
  11. langroid/agent/special/arangodb/arangodb_agent.py-e +648 -0
  12. langroid/agent/special/arangodb/tools.py +3 -3
  13. langroid/agent/special/doc_chat_agent.py +16 -14
  14. langroid/agent/special/lance_rag/critic_agent.py +2 -2
  15. langroid/agent/special/lance_rag/query_planner_agent.py +4 -4
  16. langroid/agent/special/lance_tools.py +6 -5
  17. langroid/agent/special/lance_tools.py-e +61 -0
  18. langroid/agent/special/neo4j/neo4j_chat_agent.py +3 -7
  19. langroid/agent/special/neo4j/neo4j_chat_agent.py-e +430 -0
  20. langroid/agent/special/relevance_extractor_agent.py +1 -1
  21. langroid/agent/special/sql/sql_chat_agent.py +11 -3
  22. langroid/agent/task.py +9 -87
  23. langroid/agent/task.py-e +2418 -0
  24. langroid/agent/tool_message.py +33 -17
  25. langroid/agent/tool_message.py-e +400 -0
  26. langroid/agent/tools/file_tools.py +4 -2
  27. langroid/agent/tools/file_tools.py-e +234 -0
  28. langroid/agent/tools/mcp/fastmcp_client.py +19 -6
  29. langroid/agent/tools/mcp/fastmcp_client.py-e +584 -0
  30. langroid/agent/tools/orchestration.py +22 -17
  31. langroid/agent/tools/orchestration.py-e +301 -0
  32. langroid/agent/tools/recipient_tool.py +3 -3
  33. langroid/agent/tools/task_tool.py +22 -16
  34. langroid/agent/tools/task_tool.py-e +249 -0
  35. langroid/agent/xml_tool_message.py +90 -35
  36. langroid/agent/xml_tool_message.py-e +392 -0
  37. langroid/cachedb/base.py +1 -1
  38. langroid/embedding_models/base.py +2 -2
  39. langroid/embedding_models/models.py +3 -7
  40. langroid/embedding_models/models.py-e +563 -0
  41. langroid/exceptions.py +4 -1
  42. langroid/language_models/azure_openai.py +2 -2
  43. langroid/language_models/azure_openai.py-e +134 -0
  44. langroid/language_models/base.py +6 -4
  45. langroid/language_models/base.py-e +812 -0
  46. langroid/language_models/client_cache.py +64 -0
  47. langroid/language_models/config.py +2 -4
  48. langroid/language_models/config.py-e +18 -0
  49. langroid/language_models/model_info.py +9 -1
  50. langroid/language_models/model_info.py-e +483 -0
  51. langroid/language_models/openai_gpt.py +119 -20
  52. langroid/language_models/openai_gpt.py-e +2280 -0
  53. langroid/language_models/provider_params.py +3 -22
  54. langroid/language_models/provider_params.py-e +153 -0
  55. langroid/mytypes.py +11 -4
  56. langroid/mytypes.py-e +132 -0
  57. langroid/parsing/code_parser.py +1 -1
  58. langroid/parsing/file_attachment.py +1 -1
  59. langroid/parsing/file_attachment.py-e +246 -0
  60. langroid/parsing/md_parser.py +14 -4
  61. langroid/parsing/md_parser.py-e +574 -0
  62. langroid/parsing/parser.py +22 -7
  63. langroid/parsing/parser.py-e +410 -0
  64. langroid/parsing/repo_loader.py +3 -1
  65. langroid/parsing/repo_loader.py-e +812 -0
  66. langroid/parsing/search.py +1 -1
  67. langroid/parsing/url_loader.py +17 -51
  68. langroid/parsing/url_loader.py-e +683 -0
  69. langroid/parsing/urls.py +5 -4
  70. langroid/parsing/urls.py-e +279 -0
  71. langroid/prompts/prompts_config.py +1 -1
  72. langroid/pydantic_v1/__init__.py +45 -6
  73. langroid/pydantic_v1/__init__.py-e +36 -0
  74. langroid/pydantic_v1/main.py +11 -4
  75. langroid/pydantic_v1/main.py-e +11 -0
  76. langroid/utils/configuration.py +13 -11
  77. langroid/utils/configuration.py-e +141 -0
  78. langroid/utils/constants.py +1 -1
  79. langroid/utils/constants.py-e +32 -0
  80. langroid/utils/globals.py +21 -5
  81. langroid/utils/globals.py-e +49 -0
  82. langroid/utils/html_logger.py +2 -1
  83. langroid/utils/html_logger.py-e +825 -0
  84. langroid/utils/object_registry.py +1 -1
  85. langroid/utils/object_registry.py-e +66 -0
  86. langroid/utils/pydantic_utils.py +55 -28
  87. langroid/utils/pydantic_utils.py-e +602 -0
  88. langroid/utils/types.py +2 -2
  89. langroid/utils/types.py-e +113 -0
  90. langroid/vector_store/base.py +3 -3
  91. langroid/vector_store/lancedb.py +5 -5
  92. langroid/vector_store/lancedb.py-e +404 -0
  93. langroid/vector_store/meilisearch.py +2 -2
  94. langroid/vector_store/pineconedb.py +4 -4
  95. langroid/vector_store/pineconedb.py-e +427 -0
  96. langroid/vector_store/postgres.py +1 -1
  97. langroid/vector_store/qdrantdb.py +3 -3
  98. langroid/vector_store/weaviatedb.py +1 -1
  99. {langroid-0.58.2.dist-info → langroid-0.59.0b1.dist-info}/METADATA +3 -2
  100. langroid-0.59.0b1.dist-info/RECORD +181 -0
  101. langroid/agent/special/doc_chat_task.py +0 -0
  102. langroid/mcp/__init__.py +0 -1
  103. langroid/mcp/server/__init__.py +0 -1
  104. langroid-0.58.2.dist-info/RECORD +0 -145
  105. {langroid-0.58.2.dist-info → langroid-0.59.0b1.dist-info}/WHEEL +0 -0
  106. {langroid-0.58.2.dist-info → langroid-0.59.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -14,9 +14,9 @@ from random import choice
14
14
  from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar
15
15
 
16
16
  from docstring_parser import parse
17
+ from pydantic import BaseModel, ConfigDict
17
18
 
18
19
  from langroid.language_models.base import LLMFunctionSpec
19
- from langroid.pydantic_v1 import BaseModel, Extra
20
20
  from langroid.utils.pydantic_utils import (
21
21
  _recursive_purge_dict_key,
22
22
  generate_simple_schema,
@@ -40,6 +40,14 @@ def format_schema_for_strict(schema: Any) -> None:
40
40
  This may not be equivalent to the original schema.
41
41
  """
42
42
  if isinstance(schema, dict):
43
+ # Handle $ref nodes - they can't have any other properties
44
+ if "$ref" in schema:
45
+ # Keep only the $ref, remove all other properties like description
46
+ ref_value = schema["$ref"]
47
+ schema.clear()
48
+ schema["$ref"] = ref_value
49
+ return
50
+
43
51
  if "type" in schema and schema["type"] == "object":
44
52
  schema["additionalProperties"] = False
45
53
 
@@ -111,14 +119,21 @@ class ToolMessage(ABC, BaseModel):
111
119
  # Optional param to limit number of tokens in the result of the tool.
112
120
  _max_result_tokens: int | None = None
113
121
 
114
- class Config:
115
- extra = Extra.allow
116
- arbitrary_types_allowed = False
117
- validate_all = True
118
- validate_assignment = True
122
+ model_config = ConfigDict(
123
+ extra="allow",
124
+ arbitrary_types_allowed=False,
125
+ validate_default=True,
126
+ validate_assignment=True,
119
127
  # do not include these fields in the generated schema
120
128
  # since we don't require the LLM to specify them
121
- schema_extra = {"exclude": {"purpose", "id"}}
129
+ json_schema_extra={"exclude": ["purpose", "id"]},
130
+ )
131
+
132
+ # Define excluded fields as a class method to avoid Pydantic treating it as
133
+ # a model field
134
+ @classmethod
135
+ def _get_excluded_fields(cls) -> set[str]:
136
+ return {"purpose", "id"}
122
137
 
123
138
  @classmethod
124
139
  def name(cls) -> str:
@@ -196,18 +211,18 @@ class ToolMessage(ABC, BaseModel):
196
211
  return "\n\n".join(formatted_examples)
197
212
 
198
213
  def to_json(self) -> str:
199
- return self.json(indent=4, exclude=self.Config.schema_extra["exclude"])
214
+ return self.model_dump_json(indent=4, exclude=self._get_excluded_fields())
200
215
 
201
216
  def format_example(self) -> str:
202
- return self.json(indent=4, exclude=self.Config.schema_extra["exclude"])
217
+ return self.model_dump_json(indent=4, exclude=self._get_excluded_fields())
203
218
 
204
219
  def dict_example(self) -> Dict[str, Any]:
205
- return self.dict(exclude=self.Config.schema_extra["exclude"])
220
+ return self.model_dump(exclude=self._get_excluded_fields())
206
221
 
207
222
  def get_value_of_type(self, target_type: Type[Any]) -> Any:
208
223
  """Try to find a value of a desired type in the fields of the ToolMessage."""
209
- ignore_fields = self.Config.schema_extra["exclude"].union(["request"])
210
- for field_name in set(self.dict().keys()) - ignore_fields:
224
+ ignore_fields = self._get_excluded_fields().union({"request"})
225
+ for field_name in set(self.model_dump().keys()) - ignore_fields:
211
226
  value = getattr(self, field_name)
212
227
  if is_instance_of(value, target_type):
213
228
  return value
@@ -224,7 +239,7 @@ class ToolMessage(ABC, BaseModel):
224
239
  Any: default value of the field, or None if not set or if the
225
240
  field does not exist.
226
241
  """
227
- schema = cls.schema()
242
+ schema = cls.model_json_schema()
228
243
  properties = schema["properties"]
229
244
  return properties.get(f, {}).get("default", None)
230
245
 
@@ -304,7 +319,7 @@ class ToolMessage(ABC, BaseModel):
304
319
  LLMFunctionSpec: the schema as an LLMFunctionSpec
305
320
 
306
321
  """
307
- schema = copy.deepcopy(cls.schema())
322
+ schema = copy.deepcopy(cls.model_json_schema())
308
323
  docstring = parse(cls.__doc__ or "")
309
324
  parameters = {
310
325
  k: v for k, v in schema.items() if k not in ("title", "description")
@@ -316,7 +331,7 @@ class ToolMessage(ABC, BaseModel):
316
331
  if "description" not in parameters["properties"][name]:
317
332
  parameters["properties"][name]["description"] = description
318
333
 
319
- excludes = cls.Config.schema_extra["exclude"]
334
+ excludes = cls._get_excluded_fields().copy()
320
335
  if not request:
321
336
  excludes = excludes.union({"request"})
322
337
  # exclude 'excludes' from parameters["properties"]:
@@ -374,7 +389,8 @@ class ToolMessage(ABC, BaseModel):
374
389
  _recursive_purge_dict_key(parameters, "additionalProperties")
375
390
  return LLMFunctionSpec(
376
391
  name=cls.default_value("request"),
377
- description=cls.default_value("purpose"),
392
+ description=cls.default_value("purpose")
393
+ or f"Tool for {cls.default_value('request')}",
378
394
  parameters=parameters,
379
395
  )
380
396
 
@@ -388,6 +404,6 @@ class ToolMessage(ABC, BaseModel):
388
404
  """
389
405
  schema = generate_simple_schema(
390
406
  cls,
391
- exclude=list(cls.Config.schema_extra["exclude"]),
407
+ exclude=list(cls._get_excluded_fields()),
392
408
  )
393
409
  return schema
@@ -0,0 +1,400 @@
1
+ """
2
+ Structured messages to an agent, typically from an LLM, to be handled by
3
+ an agent. The messages could represent, for example:
4
+ - information or data given to the agent
5
+ - request for information or data from the agent
6
+ - request to run a method of the agent
7
+ """
8
+
9
+ import copy
10
+ import json
11
+ import textwrap
12
+ from abc import ABC
13
+ from random import choice
14
+ from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar
15
+
16
+ from docstring_parser import parse
17
+
18
+ from langroid.language_models.base import LLMFunctionSpec
19
+ from pydantic import BaseModel, ConfigDict
20
+ from langroid.utils.pydantic_utils import (
21
+ _recursive_purge_dict_key,
22
+ generate_simple_schema,
23
+ )
24
+ from langroid.utils.types import is_instance_of
25
+
26
+ K = TypeVar("K")
27
+
28
+
29
+ def remove_if_exists(k: K, d: dict[K, Any]) -> None:
30
+ """Removes key `k` from `d` if present."""
31
+ if k in d:
32
+ d.pop(k)
33
+
34
+
35
+ def format_schema_for_strict(schema: Any) -> None:
36
+ """
37
+ Recursively set additionalProperties to False and replace
38
+ oneOf and allOf with anyOf, required for OpenAI structured outputs.
39
+ Additionally, remove all defaults and set all fields to required.
40
+ This may not be equivalent to the original schema.
41
+ """
42
+ if isinstance(schema, dict):
43
+ if "type" in schema and schema["type"] == "object":
44
+ schema["additionalProperties"] = False
45
+
46
+ if "properties" in schema:
47
+ properties = schema["properties"]
48
+ all_properties = list(properties.keys())
49
+ for k, v in properties.items():
50
+ if "default" in v:
51
+ if k == "request":
52
+ v["enum"] = [v["default"]]
53
+
54
+ v.pop("default")
55
+ schema["required"] = all_properties
56
+ else:
57
+ schema["properties"] = {}
58
+ schema["required"] = []
59
+
60
+ anyOf = (
61
+ schema.get("oneOf", []) + schema.get("allOf", []) + schema.get("anyOf", [])
62
+ )
63
+ if "allOf" in schema or "oneOf" in schema or "anyOf" in schema:
64
+ schema["anyOf"] = anyOf
65
+
66
+ remove_if_exists("allOf", schema)
67
+ remove_if_exists("oneOf", schema)
68
+
69
+ for v in schema.values():
70
+ format_schema_for_strict(v)
71
+ elif isinstance(schema, list):
72
+ for v in schema:
73
+ format_schema_for_strict(v)
74
+
75
+
76
+ class ToolMessage(ABC, BaseModel):
77
+ """
78
+ Abstract Class for a class that defines the structure of a "Tool" message from an
79
+ LLM. Depending on context, "tools" are also referred to as "plugins",
80
+ or "function calls" (in the context of OpenAI LLMs).
81
+ Essentially, they are a way for the LLM to express its intent to run a special
82
+ function or method. Currently these "tools" are handled by methods of the
83
+ agent.
84
+
85
+ Attributes:
86
+ request (str): name of agent method to map to.
87
+ purpose (str): purpose of agent method, expressed in general terms.
88
+ (This is used when auto-generating the tool instruction to the LLM)
89
+ """
90
+
91
+ request: str
92
+ purpose: str
93
+ id: str = "" # placeholder for OpenAI-API tool_call_id
94
+
95
+ # If enabled, forces strict adherence to schema.
96
+ # Currently only supported by OpenAI LLMs. When unset, enables if supported.
97
+ _strict: Optional[bool] = None
98
+ _allow_llm_use: bool = True # allow an LLM to use (i.e. generate) this tool?
99
+
100
+ # Optional param to limit number of result tokens to retain in msg history.
101
+ # Some tools can have large results that we may not want to fully retain,
102
+ # e.g. result of a db query, which the LLM later reduces to a summary, so
103
+ # in subsequent dialog we may only want to retain the summary,
104
+ # and replace this raw result truncated to _max_retained_tokens.
105
+ # Important to note: unlike _max_result_tokens, this param is used
106
+ # NOT used to immediately truncate the result;
107
+ # it is only used to truncate what is retained in msg history AFTER the
108
+ # response to this result.
109
+ _max_retained_tokens: int | None = None
110
+
111
+ # Optional param to limit number of tokens in the result of the tool.
112
+ _max_result_tokens: int | None = None
113
+
114
+ model_config = ConfigDict(
115
+ extra="allow",
116
+ arbitrary_types_allowed=False,
117
+ validate_default=True,
118
+ validate_assignment=True,
119
+ # do not include these fields in the generated schema
120
+ # since we don't require the LLM to specify them
121
+ json_schema_extra={"exclude": {"purpose", "id"}},
122
+ )
123
+
124
+ @classmethod
125
+ def name(cls) -> str:
126
+ return str(cls.default_value("request")) # redundant str() to appease mypy
127
+
128
+ @classmethod
129
+ def instructions(cls) -> str:
130
+ """
131
+ Instructions on tool usage.
132
+ """
133
+ return ""
134
+
135
+ @classmethod
136
+ def langroid_tools_instructions(cls) -> str:
137
+ """
138
+ Instructions on tool usage when `use_tools == True`, i.e.
139
+ when using langroid built-in tools
140
+ (as opposed to OpenAI-like function calls/tools).
141
+ """
142
+ return """
143
+ IMPORTANT: When using this or any other tool/function, you MUST include a
144
+ `request` field and set it equal to the FUNCTION/TOOL NAME you intend to use.
145
+ """
146
+
147
+ @classmethod
148
+ def require_recipient(cls) -> Type["ToolMessage"]:
149
+ class ToolMessageWithRecipient(cls): # type: ignore
150
+ recipient: str # no default, so it is required
151
+
152
+ return ToolMessageWithRecipient
153
+
154
+ @classmethod
155
+ def examples(cls) -> List["ToolMessage" | Tuple[str, "ToolMessage"]]:
156
+ """
157
+ Examples to use in few-shot demos with formatting instructions.
158
+ Each example can be either:
159
+ - just a ToolMessage instance, e.g. MyTool(param1=1, param2="hello"), or
160
+ - a tuple (description, ToolMessage instance), where the description is
161
+ a natural language "thought" that leads to the tool usage,
162
+ e.g. ("I want to find the square of 5", SquareTool(num=5))
163
+ In some scenarios, including such a description can significantly
164
+ enhance reliability of tool use.
165
+ Returns:
166
+ """
167
+ return []
168
+
169
+ @classmethod
170
+ def usage_examples(cls, random: bool = False) -> str:
171
+ """
172
+ Instruction to the LLM showing examples of how to use the tool-message.
173
+
174
+ Args:
175
+ random (bool): whether to pick a random example from the list of examples.
176
+ Set to `true` when using this to illustrate a dialog between LLM and
177
+ user.
178
+ (if false, use ALL examples)
179
+ Returns:
180
+ str: examples of how to use the tool/function-call
181
+ """
182
+ # pick a random example of the fields
183
+ if len(cls.examples()) == 0:
184
+ return ""
185
+ if random:
186
+ examples = [choice(cls.examples())]
187
+ else:
188
+ examples = cls.examples()
189
+ formatted_examples = [
190
+ (
191
+ f"EXAMPLE {i}: (THOUGHT: {ex[0]}) => \n{ex[1].format_example()}"
192
+ if isinstance(ex, tuple)
193
+ else f"EXAMPLE {i}:\n {ex.format_example()}"
194
+ )
195
+ for i, ex in enumerate(examples, 1)
196
+ ]
197
+ return "\n\n".join(formatted_examples)
198
+
199
+ def to_json(self) -> str:
200
+ return self.model_dump_json(
201
+ indent=4, exclude=self.model_config["json_schema_extra"]["exclude"]
202
+ )
203
+
204
+ def format_example(self) -> str:
205
+ return self.model_dump_json(
206
+ indent=4, exclude=self.model_config["json_schema_extra"]["exclude"]
207
+ )
208
+
209
+ def dict_example(self) -> Dict[str, Any]:
210
+ return self.model_dump(
211
+ exclude=self.model_config["json_schema_extra"]["exclude"]
212
+ )
213
+
214
+ def get_value_of_type(self, target_type: Type[Any]) -> Any:
215
+ """Try to find a value of a desired type in the fields of the ToolMessage."""
216
+ ignore_fields = self.Config.schema_extra["exclude"].union(["request"])
217
+ for field_name in set(self.model_dump().keys()) - ignore_fields:
218
+ value = getattr(self, field_name)
219
+ if is_instance_of(value, target_type):
220
+ return value
221
+ return None
222
+
223
+ @classmethod
224
+ def default_value(cls, f: str) -> Any:
225
+ """
226
+ Returns the default value of the given field, for the message-class
227
+ Args:
228
+ f (str): field name
229
+
230
+ Returns:
231
+ Any: default value of the field, or None if not set or if the
232
+ field does not exist.
233
+ """
234
+ schema = cls.model_json_schema()
235
+ properties = schema["properties"]
236
+ return properties.get(f, {}).get("default", None)
237
+
238
+ @classmethod
239
+ def format_instructions(cls, tool: bool = False) -> str:
240
+ """
241
+ Default Instructions to the LLM showing how to use the tool/function-call.
242
+ Works for GPT4 but override this for weaker LLMs if needed.
243
+
244
+ Args:
245
+ tool: instructions for Langroid-native tool use? (e.g. for non-OpenAI LLM)
246
+ (or else it would be for OpenAI Function calls).
247
+ Ignored in the default implementation, but can be used in subclasses.
248
+ Returns:
249
+ str: instructions on how to use the message
250
+ """
251
+ # TODO: when we attempt to use a "simpler schema"
252
+ # (i.e. all nested fields explicit without definitions),
253
+ # we seem to get worse results, so we turn it off for now
254
+ param_dict = (
255
+ # cls.simple_schema() if tool else
256
+ cls.llm_function_schema(request=True).parameters
257
+ )
258
+ examples_str = ""
259
+ if cls.examples():
260
+ examples_str = "EXAMPLES:\n" + cls.usage_examples()
261
+ return textwrap.dedent(
262
+ f"""
263
+ TOOL: {cls.default_value("request")}
264
+ PURPOSE: {cls.default_value("purpose")}
265
+ JSON FORMAT: {
266
+ json.dumps(param_dict, indent=4)
267
+ }
268
+ {examples_str}
269
+ """.lstrip()
270
+ )
271
+
272
+ @staticmethod
273
+ def group_format_instructions() -> str:
274
+ """Template for instructions for a group of tools.
275
+ Works with GPT4 but override this for weaker LLMs if needed.
276
+ """
277
+ return textwrap.dedent(
278
+ """
279
+ === ALL AVAILABLE TOOLS and THEIR FORMAT INSTRUCTIONS ===
280
+ You have access to the following TOOLS to accomplish your task:
281
+
282
+ {format_instructions}
283
+
284
+ When one of the above TOOLs is applicable, you must express your
285
+ request as "TOOL:" followed by the request in the above format.
286
+ """
287
+ )
288
+
289
+ @classmethod
290
+ def llm_function_schema(
291
+ cls,
292
+ request: bool = False,
293
+ defaults: bool = True,
294
+ ) -> LLMFunctionSpec:
295
+ """
296
+ Clean up the schema of the Pydantic class (which can recursively contain
297
+ other Pydantic classes), to create a version compatible with OpenAI
298
+ Function-call API.
299
+
300
+ Adapted from this excellent library:
301
+ https://github.com/jxnl/instructor/blob/main/instructor/function_calls.py
302
+
303
+ Args:
304
+ request: whether to include the "request" field in the schema.
305
+ (we set this to True when using Langroid-native TOOLs as opposed to
306
+ OpenAI Function calls)
307
+ defaults: whether to include fields with default values in the schema,
308
+ in the "properties" section.
309
+
310
+ Returns:
311
+ LLMFunctionSpec: the schema as an LLMFunctionSpec
312
+
313
+ """
314
+ schema = copy.deepcopy(cls.model_json_schema())
315
+ docstring = parse(cls.__doc__ or "")
316
+ parameters = {
317
+ k: v for k, v in schema.items() if k not in ("title", "description")
318
+ }
319
+ for param in docstring.params:
320
+ if (name := param.arg_name) in parameters["properties"] and (
321
+ description := param.description
322
+ ):
323
+ if "description" not in parameters["properties"][name]:
324
+ parameters["properties"][name]["description"] = description
325
+
326
+ excludes = cls.model_config["json_schema_extra"]["exclude"]
327
+ if not request:
328
+ excludes = excludes.union({"request"})
329
+ # exclude 'excludes' from parameters["properties"]:
330
+ parameters["properties"] = {
331
+ field: details
332
+ for field, details in parameters["properties"].items()
333
+ if field not in excludes and (defaults or details.get("default") is None)
334
+ }
335
+ parameters["required"] = sorted(
336
+ k
337
+ for k, v in parameters["properties"].items()
338
+ if ("default" not in v and k not in excludes)
339
+ )
340
+ if request:
341
+ parameters["required"].append("request")
342
+
343
+ # If request is present it must match the default value
344
+ # Similar to defining request as a literal type
345
+ parameters["request"] = {
346
+ "enum": [cls.default_value("request")],
347
+ "type": "string",
348
+ }
349
+
350
+ if "description" not in schema:
351
+ if docstring.short_description:
352
+ schema["description"] = docstring.short_description
353
+ else:
354
+ schema["description"] = (
355
+ f"Correctly extracted `{cls.__name__}` with all "
356
+ f"the required parameters with correct types"
357
+ )
358
+
359
+ # Handle nested ToolMessage fields
360
+ if "definitions" in parameters:
361
+ for v in parameters["definitions"].values():
362
+ if "exclude" in v:
363
+ v.pop("exclude")
364
+
365
+ remove_if_exists("purpose", v["properties"])
366
+ remove_if_exists("id", v["properties"])
367
+ if (
368
+ "request" in v["properties"]
369
+ and "default" in v["properties"]["request"]
370
+ ):
371
+ if "required" not in v:
372
+ v["required"] = []
373
+ v["required"].append("request")
374
+ v["properties"]["request"] = {
375
+ "type": "string",
376
+ "enum": [v["properties"]["request"]["default"]],
377
+ }
378
+
379
+ parameters.pop("exclude")
380
+ _recursive_purge_dict_key(parameters, "title")
381
+ _recursive_purge_dict_key(parameters, "additionalProperties")
382
+ return LLMFunctionSpec(
383
+ name=cls.default_value("request"),
384
+ description=cls.default_value("purpose"),
385
+ parameters=parameters,
386
+ )
387
+
388
+ @classmethod
389
+ def simple_schema(cls) -> Dict[str, Any]:
390
+ """
391
+ Return a simplified schema for the message, with only the request and
392
+ required fields.
393
+ Returns:
394
+ Dict[str, Any]: simplified schema
395
+ """
396
+ schema = generate_simple_schema(
397
+ cls,
398
+ exclude=list(cls.model_config["json_schema_extra"]["exclude"]),
399
+ )
400
+ return schema
@@ -4,10 +4,10 @@ from textwrap import dedent
4
4
  from typing import Callable, List, Tuple, Type
5
5
 
6
6
  import git
7
+ from pydantic import Field
7
8
 
8
9
  from langroid.agent.tool_message import ToolMessage
9
10
  from langroid.agent.xml_tool_message import XMLToolMessage
10
- from langroid.pydantic_v1 import Field
11
11
  from langroid.utils.git_utils import git_commit_file
12
12
  from langroid.utils.system import create_file, list_dir, read_file
13
13
 
@@ -91,7 +91,9 @@ class WriteFileTool(XMLToolMessage):
91
91
  content: str = Field(
92
92
  ...,
93
93
  description="The content to write to the file",
94
- verbatim=True, # preserve the content as is; uses CDATA section in XML
94
+ json_schema_extra={
95
+ "verbatim": True
96
+ }, # preserve the content as is; uses CDATA section in XML
95
97
  )
96
98
  _curr_dir: Callable[[], str] | None = None
97
99
  _git_repo: Callable[[], git.Repo] | None = None