langroid 0.59.0b3__py3-none-any.whl → 0.59.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.
- langroid/agent/done_sequence_parser.py +46 -11
- langroid/agent/special/doc_chat_task.py +0 -0
- langroid/agent/task.py +44 -7
- langroid/language_models/model_info.py +51 -0
- langroid/mcp/__init__.py +1 -0
- langroid/mcp/server/__init__.py +1 -0
- langroid/pydantic_v1/__init__.py +1 -1
- {langroid-0.59.0b3.dist-info → langroid-0.59.1.dist-info}/METADATA +4 -1
- {langroid-0.59.0b3.dist-info → langroid-0.59.1.dist-info}/RECORD +11 -47
- langroid/agent/base.py-e +0 -2216
- langroid/agent/chat_agent.py-e +0 -2086
- langroid/agent/chat_document.py-e +0 -513
- langroid/agent/openai_assistant.py-e +0 -882
- langroid/agent/special/arangodb/arangodb_agent.py-e +0 -648
- langroid/agent/special/lance_tools.py-e +0 -61
- langroid/agent/special/neo4j/neo4j_chat_agent.py-e +0 -430
- langroid/agent/task.py-e +0 -2418
- langroid/agent/tool_message.py-e +0 -400
- langroid/agent/tools/file_tools.py-e +0 -234
- langroid/agent/tools/mcp/fastmcp_client.py-e +0 -584
- langroid/agent/tools/orchestration.py-e +0 -301
- langroid/agent/tools/task_tool.py-e +0 -249
- langroid/agent/xml_tool_message.py-e +0 -392
- langroid/embedding_models/models.py-e +0 -563
- langroid/language_models/azure_openai.py-e +0 -134
- langroid/language_models/base.py-e +0 -812
- langroid/language_models/config.py-e +0 -18
- langroid/language_models/model_info.py-e +0 -483
- langroid/language_models/openai_gpt.py-e +0 -2280
- langroid/language_models/provider_params.py-e +0 -153
- langroid/mytypes.py-e +0 -132
- langroid/parsing/file_attachment.py-e +0 -246
- langroid/parsing/md_parser.py-e +0 -574
- langroid/parsing/parser.py-e +0 -410
- langroid/parsing/repo_loader.py-e +0 -812
- langroid/parsing/url_loader.py-e +0 -683
- langroid/parsing/urls.py-e +0 -279
- langroid/pydantic_v1/__init__.py-e +0 -36
- langroid/pydantic_v1/main.py-e +0 -11
- langroid/utils/configuration.py-e +0 -141
- langroid/utils/constants.py-e +0 -32
- langroid/utils/globals.py-e +0 -49
- langroid/utils/html_logger.py-e +0 -825
- langroid/utils/object_registry.py-e +0 -66
- langroid/utils/pydantic_utils.py-e +0 -602
- langroid/utils/types.py-e +0 -113
- langroid/vector_store/lancedb.py-e +0 -404
- langroid/vector_store/pineconedb.py-e +0 -427
- {langroid-0.59.0b3.dist-info → langroid-0.59.1.dist-info}/WHEEL +0 -0
- {langroid-0.59.0b3.dist-info → langroid-0.59.1.dist-info}/licenses/LICENSE +0 -0
langroid/agent/tool_message.py-e
DELETED
@@ -1,400 +0,0 @@
|
|
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
|
@@ -1,234 +0,0 @@
|
|
1
|
-
from contextlib import chdir
|
2
|
-
from pathlib import Path
|
3
|
-
from textwrap import dedent
|
4
|
-
from typing import Callable, List, Tuple, Type
|
5
|
-
|
6
|
-
import git
|
7
|
-
|
8
|
-
from langroid.agent.tool_message import ToolMessage
|
9
|
-
from langroid.agent.xml_tool_message import XMLToolMessage
|
10
|
-
from pydantic import Field
|
11
|
-
from langroid.utils.git_utils import git_commit_file
|
12
|
-
from langroid.utils.system import create_file, list_dir, read_file
|
13
|
-
|
14
|
-
|
15
|
-
class ReadFileTool(ToolMessage):
|
16
|
-
request: str = "read_file_tool"
|
17
|
-
purpose: str = "Read the contents of a <file_path>"
|
18
|
-
file_path: str
|
19
|
-
|
20
|
-
_line_nums: bool = True # whether to add line numbers to the content
|
21
|
-
_curr_dir: Callable[[], str] | None = None
|
22
|
-
|
23
|
-
@classmethod
|
24
|
-
def create(
|
25
|
-
cls,
|
26
|
-
get_curr_dir: Callable[[], str] | None,
|
27
|
-
) -> Type["ReadFileTool"]:
|
28
|
-
"""
|
29
|
-
Create a subclass of ReadFileTool for a specific directory
|
30
|
-
|
31
|
-
Args:
|
32
|
-
get_curr_dir (callable): A function that returns the current directory.
|
33
|
-
|
34
|
-
Returns:
|
35
|
-
Type[ReadFileTool]: A subclass of the ReadFileTool class, specifically
|
36
|
-
for the current directory.
|
37
|
-
"""
|
38
|
-
|
39
|
-
class CustomReadFileTool(cls): # type: ignore
|
40
|
-
_curr_dir: Callable[[], str] | None = (
|
41
|
-
staticmethod(get_curr_dir) if get_curr_dir else None
|
42
|
-
)
|
43
|
-
|
44
|
-
return CustomReadFileTool
|
45
|
-
|
46
|
-
@classmethod
|
47
|
-
def examples(cls) -> List[ToolMessage | tuple[str, ToolMessage]]:
|
48
|
-
return [
|
49
|
-
cls(file_path="src/lib.rs"),
|
50
|
-
(
|
51
|
-
"I want to read the contents of src/main.rs",
|
52
|
-
cls(file_path="src/main.rs"),
|
53
|
-
),
|
54
|
-
]
|
55
|
-
|
56
|
-
def handle(self) -> str:
|
57
|
-
# return contents as str for LLM to read
|
58
|
-
# ASSUME: file_path should be relative to the curr_dir
|
59
|
-
try:
|
60
|
-
dir = (self._curr_dir and self._curr_dir()) or Path.cwd()
|
61
|
-
with chdir(dir):
|
62
|
-
# if file doesn't exist, return an error message
|
63
|
-
content = read_file(self.file_path, self._line_nums)
|
64
|
-
line_num_str = ""
|
65
|
-
if self._line_nums:
|
66
|
-
line_num_str = "(Line numbers added for reference only!)"
|
67
|
-
return f"""
|
68
|
-
CONTENTS of {self.file_path}:
|
69
|
-
{line_num_str}
|
70
|
-
---------------------------
|
71
|
-
{content}
|
72
|
-
"""
|
73
|
-
except FileNotFoundError:
|
74
|
-
return f"File not found: {self.file_path}"
|
75
|
-
|
76
|
-
|
77
|
-
class WriteFileTool(XMLToolMessage):
|
78
|
-
request: str = "write_file_tool"
|
79
|
-
purpose: str = """
|
80
|
-
Tool for writing <content> in a certain <language> to a <file_path>
|
81
|
-
"""
|
82
|
-
|
83
|
-
file_path: str = Field(..., description="The path to the file to write the content")
|
84
|
-
|
85
|
-
language: str = Field(
|
86
|
-
default="",
|
87
|
-
description="""
|
88
|
-
The language of the content; could be human language or programming language
|
89
|
-
""",
|
90
|
-
)
|
91
|
-
content: str = Field(
|
92
|
-
...,
|
93
|
-
description="The content to write to the file",
|
94
|
-
verbatim=True, # preserve the content as is; uses CDATA section in XML
|
95
|
-
)
|
96
|
-
_curr_dir: Callable[[], str] | None = None
|
97
|
-
_git_repo: Callable[[], git.Repo] | None = None
|
98
|
-
_commit_message: str = "Agent write file tool"
|
99
|
-
|
100
|
-
@classmethod
|
101
|
-
def create(
|
102
|
-
cls,
|
103
|
-
get_curr_dir: Callable[[], str] | None,
|
104
|
-
get_git_repo: Callable[[], str] | None,
|
105
|
-
) -> Type["WriteFileTool"]:
|
106
|
-
"""
|
107
|
-
Create a subclass of WriteFileTool with the current directory and git repo.
|
108
|
-
|
109
|
-
Args:
|
110
|
-
get_curr_dir (callable): A function that returns the current directory.
|
111
|
-
get_git_repo (callable): A function that returns the git repo.
|
112
|
-
|
113
|
-
Returns:
|
114
|
-
Type[WriteFileTool]: A subclass of the WriteFileTool class, specifically
|
115
|
-
for the current directory and git repo.
|
116
|
-
"""
|
117
|
-
|
118
|
-
class CustomWriteFileTool(cls): # type: ignore
|
119
|
-
_curr_dir: Callable[[], str] | None = (
|
120
|
-
staticmethod(get_curr_dir) if get_curr_dir else None
|
121
|
-
)
|
122
|
-
_git_repo: Callable[[], str] | None = (
|
123
|
-
staticmethod(get_git_repo) if get_git_repo else None
|
124
|
-
)
|
125
|
-
|
126
|
-
return CustomWriteFileTool
|
127
|
-
|
128
|
-
@classmethod
|
129
|
-
def examples(cls) -> List[ToolMessage | Tuple[str, ToolMessage]]:
|
130
|
-
return [
|
131
|
-
(
|
132
|
-
"""
|
133
|
-
I want to define a simple hello world python function
|
134
|
-
in a file "mycode/hello.py"
|
135
|
-
""",
|
136
|
-
cls(
|
137
|
-
file_path="mycode/hello.py",
|
138
|
-
language="python",
|
139
|
-
content="""
|
140
|
-
def hello():
|
141
|
-
print("Hello, World!")
|
142
|
-
""",
|
143
|
-
),
|
144
|
-
),
|
145
|
-
cls(
|
146
|
-
file_path="src/lib.rs",
|
147
|
-
language="rust",
|
148
|
-
content="""
|
149
|
-
fn main() {
|
150
|
-
println!("Hello, World!");
|
151
|
-
}
|
152
|
-
""",
|
153
|
-
),
|
154
|
-
cls(
|
155
|
-
file_path="docs/intro.txt",
|
156
|
-
content="""
|
157
|
-
# Introduction
|
158
|
-
This is the first sentence of the introduction.
|
159
|
-
""",
|
160
|
-
),
|
161
|
-
]
|
162
|
-
|
163
|
-
def handle(self) -> str:
|
164
|
-
curr_dir = (self._curr_dir and self._curr_dir()) or Path.cwd()
|
165
|
-
with chdir(curr_dir):
|
166
|
-
create_file(self.file_path, self.content)
|
167
|
-
msg = f"Content written to {self.file_path}"
|
168
|
-
# possibly commit the file
|
169
|
-
if self._git_repo:
|
170
|
-
git_commit_file(
|
171
|
-
self._git_repo(),
|
172
|
-
self.file_path,
|
173
|
-
self._commit_message,
|
174
|
-
)
|
175
|
-
msg += " and committed"
|
176
|
-
return msg
|
177
|
-
|
178
|
-
|
179
|
-
class ListDirTool(ToolMessage):
|
180
|
-
request: str = "list_dir_tool"
|
181
|
-
purpose: str = "List the contents of a <dir_path>"
|
182
|
-
dir_path: str
|
183
|
-
|
184
|
-
_curr_dir: Callable[[], str] | None = None
|
185
|
-
|
186
|
-
@classmethod
|
187
|
-
def create(
|
188
|
-
cls,
|
189
|
-
get_curr_dir: Callable[[], str] | None,
|
190
|
-
) -> Type["ReadFileTool"]:
|
191
|
-
"""
|
192
|
-
Create a subclass of ListDirTool for a specific directory
|
193
|
-
|
194
|
-
Args:
|
195
|
-
get_curr_dir (callable): A function that returns the current directory.
|
196
|
-
|
197
|
-
Returns:
|
198
|
-
Type[ReadFileTool]: A subclass of the ReadFileTool class, specifically
|
199
|
-
for the current directory.
|
200
|
-
"""
|
201
|
-
|
202
|
-
class CustomListDirTool(cls): # type: ignore
|
203
|
-
_curr_dir: Callable[[], str] | None = (
|
204
|
-
staticmethod(get_curr_dir) if get_curr_dir else None
|
205
|
-
)
|
206
|
-
|
207
|
-
return CustomListDirTool
|
208
|
-
|
209
|
-
@classmethod
|
210
|
-
def examples(cls) -> List[ToolMessage | tuple[str, ToolMessage]]:
|
211
|
-
return [
|
212
|
-
cls(dir_path="src"),
|
213
|
-
(
|
214
|
-
"I want to list the contents of src",
|
215
|
-
cls(dir_path="src"),
|
216
|
-
),
|
217
|
-
]
|
218
|
-
|
219
|
-
def handle(self) -> str:
|
220
|
-
# ASSUME: dir_path should be relative to the curr_dir_path
|
221
|
-
dir = (self._curr_dir and self._curr_dir()) or Path.cwd()
|
222
|
-
with chdir(dir):
|
223
|
-
contents = list_dir(self.dir_path)
|
224
|
-
|
225
|
-
if not contents:
|
226
|
-
return f"Directory not found or empty: {self.dir_path}"
|
227
|
-
contents_str = "\n".join(contents)
|
228
|
-
return dedent(
|
229
|
-
f"""
|
230
|
-
LISTING of directory {self.dir_path}:
|
231
|
-
---------------------------
|
232
|
-
{contents_str}
|
233
|
-
""".strip()
|
234
|
-
)
|