langroid 0.16.7__py3-none-any.whl → 0.17.0__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/base.py +45 -21
- langroid/agent/chat_agent.py +22 -14
- langroid/agent/chat_document.py +22 -13
- langroid/agent/tool_message.py +11 -11
- langroid/agent/tools/file_tools.py +234 -0
- langroid/agent/xml_tool_message.py +178 -45
- langroid/utils/constants.py +2 -0
- langroid/utils/git_utils.py +251 -0
- langroid/utils/system.py +78 -0
- {langroid-0.16.7.dist-info → langroid-0.17.0.dist-info}/METADATA +6 -3
- {langroid-0.16.7.dist-info → langroid-0.17.0.dist-info}/RECORD +14 -89
- pyproject.toml +3 -2
- langroid/agent/md_tool_message_grammar.py +0 -455
- langroid/agent/tools/code_file_tool_parse.py +0 -150
- langroid/agent/tools/code_file_tool_pyparsing.py +0 -194
- langroid/agent/tools/code_file_tool_pyparsing2.py +0 -199
- langroid/agent/tools/extract_tool.py +0 -96
- langroid/agent/tools/formatted_model_custom.py +0 -150
- langroid/agent/tools/formatted_model_custom2.py +0 -168
- langroid/agent/tools/formatted_model_custom3.py +0 -279
- langroid/agent/tools/formatted_model_custom4.py +0 -395
- langroid/agent/tools/formatted_model_jinja.py +0 -133
- langroid/agent/tools/formatted_model_jinja.py-e +0 -122
- langroid/agent/tools/formatted_model_jinja2.py +0 -145
- langroid/agent/tools/formatted_model_jinja2.py-e +0 -135
- langroid/agent/tools/formatted_model_lark.py +0 -0
- langroid/agent/tools/formatted_model_lark2.py +0 -168
- langroid/agent/tools/formatted_model_parse.py +0 -105
- langroid/agent/tools/formatted_model_parse.py-e +0 -98
- langroid/agent/tools/formatted_model_parse2.py +0 -113
- langroid/agent/tools/formatted_model_parse2.py-e +0 -109
- langroid/agent/tools/formatted_model_parse3.py +0 -114
- langroid/agent/tools/formatted_model_parse3.py-e +0 -110
- langroid/agent/tools/formatted_model_parsimon.py +0 -194
- langroid/agent/tools/formatted_model_parsimon.py-e +0 -186
- langroid/agent/tools/formatted_model_pyparsing.py +0 -169
- langroid/agent/tools/formatted_model_pyparsing.py-e +0 -149
- langroid/agent/tools/formatted_model_pyparsing2.py +0 -159
- langroid/agent/tools/formatted_model_pyparsing2.py-e +0 -143
- langroid/agent/tools/formatted_model_pyparsing3.py +0 -133
- langroid/agent/tools/formatted_model_pyparsing3.py-e +0 -121
- langroid/agent/tools/formatted_model_pyparsing4.py +0 -213
- langroid/agent/tools/formatted_model_pyparsing4.py-e +0 -176
- langroid/agent/tools/formatted_model_pyparsing5.py +0 -173
- langroid/agent/tools/formatted_model_pyparsing5.py-e +0 -142
- langroid/agent/tools/formatted_model_regex.py +0 -246
- langroid/agent/tools/formatted_model_regex.py-e +0 -248
- langroid/agent/tools/formatted_model_regex2.py +0 -250
- langroid/agent/tools/formatted_model_regex2.py-e +0 -253
- langroid/agent/tools/formatted_model_tatsu.py +0 -172
- langroid/agent/tools/formatted_model_tatsu.py-e +0 -160
- langroid/agent/tools/formatted_model_template.py +0 -217
- langroid/agent/tools/formatted_model_template.py-e +0 -200
- langroid/agent/tools/formatted_model_xml.py +0 -178
- langroid/agent/tools/formatted_model_xml2.py +0 -178
- langroid/agent/tools/formatted_model_xml3.py +0 -132
- langroid/agent/tools/formatted_model_xml4.py +0 -130
- langroid/agent/tools/formatted_model_xml5.py +0 -130
- langroid/agent/tools/formatted_model_xml6.py +0 -113
- langroid/agent/tools/formatted_model_xml7.py +0 -117
- langroid/agent/tools/formatted_model_xml8.py +0 -164
- langroid/agent/tools/generator_tool.py +0 -20
- langroid/agent/tools/generic_tool.py +0 -165
- langroid/agent/tools/generic_tool_tatsu.py +0 -275
- langroid/agent/tools/grammar_based_model.py +0 -132
- langroid/agent/tools/grammar_based_model.py-e +0 -128
- langroid/agent/tools/grammar_based_model_lark.py +0 -156
- langroid/agent/tools/grammar_based_model_lark.py-e +0 -153
- langroid/agent/tools/grammar_based_model_parse.py +0 -86
- langroid/agent/tools/grammar_based_model_parse.py-e +0 -80
- langroid/agent/tools/grammar_based_model_parsimonious.py +0 -129
- langroid/agent/tools/grammar_based_model_parsimonious.py-e +0 -120
- langroid/agent/tools/grammar_based_model_pyparsing.py +0 -105
- langroid/agent/tools/grammar_based_model_pyparsing.py-e +0 -103
- langroid/agent/tools/grammar_based_model_regex.py +0 -139
- langroid/agent/tools/grammar_based_model_regex.py-e +0 -130
- langroid/agent/tools/grammar_based_model_regex2.py +0 -124
- langroid/agent/tools/grammar_based_model_regex2.py-e +0 -116
- langroid/agent/tools/grammar_based_model_tatsu.py +0 -80
- langroid/agent/tools/grammar_based_model_tatsu.py-e +0 -77
- langroid/agent/tools/lark_earley_example.py +0 -135
- langroid/agent/tools/lark_earley_example.py-e +0 -117
- langroid/agent/tools/lark_example.py +0 -72
- langroid/agent/tools/note_tool.py +0 -0
- langroid/agent/tools/parse_example.py +0 -76
- langroid/agent/tools/parse_example2.py +0 -87
- langroid/agent/tools/parse_example3.py +0 -42
- langroid/agent/tools/parse_test.py +0 -791
- langroid/agent/tools/run_python_code.py +0 -60
- {langroid-0.16.7.dist-info → langroid-0.17.0.dist-info}/LICENSE +0 -0
- {langroid-0.16.7.dist-info → langroid-0.17.0.dist-info}/WHEEL +0 -0
@@ -1,13 +1,24 @@
|
|
1
|
-
from typing import
|
1
|
+
from typing import Any, Dict, List, Optional
|
2
2
|
|
3
3
|
from lxml import etree
|
4
4
|
|
5
5
|
from langroid.agent.tool_message import ToolMessage
|
6
6
|
|
7
7
|
|
8
|
-
class
|
8
|
+
class XMLToolMessage(ToolMessage):
|
9
9
|
"""
|
10
10
|
Abstract class for tools formatted using XML instead of JSON.
|
11
|
+
Mainly tested for non-nested tool structures.
|
12
|
+
|
13
|
+
When a subclass defines a field with the attribute `verbatim=True`,
|
14
|
+
its value will be:
|
15
|
+
- preserved as is, including whitespace, indents, quotes, newlines, etc
|
16
|
+
with no escaping, and
|
17
|
+
- enclosed in a CDATA section in the XML output.
|
18
|
+
This is useful for LLMs sending code as part of a tool;
|
19
|
+
results can be far superior compared to sending code in JSON-formatted tools,
|
20
|
+
where code needs to confirm to JSON's strict rules and escaping requirements.
|
21
|
+
(see test_xml_tool_message.py for an example).
|
11
22
|
"""
|
12
23
|
|
13
24
|
request: str
|
@@ -19,72 +30,191 @@ class XmlToolMessage(ToolMessage):
|
|
19
30
|
root_element = "tool"
|
20
31
|
|
21
32
|
@classmethod
|
22
|
-
def
|
33
|
+
def extract_field_values(cls, formatted_string: str) -> Optional[Dict[str, Any]]:
|
34
|
+
"""
|
35
|
+
Extracts field values from an XML-formatted string.
|
36
|
+
|
37
|
+
Args:
|
38
|
+
formatted_string (str): The XML-formatted string to parse.
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
Optional[Dict[str, Any]]: A dictionary containing the extracted field
|
42
|
+
values, where keys are the XML element names and values are their
|
43
|
+
corresponding contents.
|
44
|
+
Returns None if parsing fails or the root element is not a dictionary.
|
45
|
+
|
46
|
+
Raises:
|
47
|
+
etree.XMLSyntaxError: If the input string is not valid XML.
|
48
|
+
"""
|
23
49
|
parser = etree.XMLParser(strip_cdata=False)
|
24
50
|
root = etree.fromstring(formatted_string.encode("utf-8"), parser=parser)
|
25
|
-
|
26
|
-
|
27
|
-
|
51
|
+
|
52
|
+
def parse_element(element: etree._Element) -> Any | Dict[str, Any] | str:
|
53
|
+
# Skip elements starting with underscore
|
54
|
+
field_info = cls.__fields__.get(element.tag)
|
55
|
+
is_verbatim = field_info and field_info.field_info.extra.get(
|
56
|
+
"verbatim", False
|
28
57
|
)
|
29
58
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
59
|
+
if is_verbatim:
|
60
|
+
# For code elements, preserve the content as is, including whitespace
|
61
|
+
content = element.text if element.text else ""
|
62
|
+
# Strip leading and trailing triple backticks if present,
|
63
|
+
# accounting for whitespace
|
64
|
+
return (
|
65
|
+
content.strip().removeprefix("```").removesuffix("```").strip()
|
66
|
+
if content.strip().startswith("```")
|
67
|
+
and content.strip().endswith("```")
|
68
|
+
else content
|
69
|
+
)
|
70
|
+
elif len(element) == 0:
|
71
|
+
# For non-code leaf elements, strip whitespace
|
72
|
+
return element.text.strip() if element.text else ""
|
73
|
+
else:
|
74
|
+
# For branch elements, recurse
|
75
|
+
return {child.tag: parse_element(child) for child in element}
|
76
|
+
|
77
|
+
result = parse_element(root)
|
78
|
+
if not isinstance(result, dict):
|
79
|
+
return None
|
80
|
+
# Filter out empty dictionaries from skipped underscore fields
|
81
|
+
return {k: v for k, v in result.items() if v != {}}
|
82
|
+
|
83
|
+
@classmethod
|
84
|
+
def parse(cls, formatted_string: str) -> Optional["XMLToolMessage"]:
|
85
|
+
"""
|
86
|
+
Parses the XML-formatted string and returns an instance of the class.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
formatted_string (str): The XML-formatted string to parse.
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
Optional["XMLToolMessage"]: An instance of the class if parsing succeeds,
|
93
|
+
None otherwise.
|
94
|
+
"""
|
95
|
+
parsed_data = cls.extract_field_values(formatted_string)
|
96
|
+
if parsed_data is None:
|
97
|
+
return None
|
98
|
+
|
99
|
+
# Use Pydantic's parse_obj to create and validate the instance
|
100
|
+
return cls.parse_obj(parsed_data)
|
50
101
|
|
51
102
|
@classmethod
|
52
|
-
def
|
103
|
+
def format_instructions(cls, tool: bool = False) -> str:
|
104
|
+
"""
|
105
|
+
Instructions to the LLM showing how to use the XML tool.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
tool: Not used in this implementation, kept for compatibility.
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
str: instructions on how to use the XML message
|
112
|
+
"""
|
53
113
|
fields = [
|
54
114
|
f
|
55
115
|
for f in cls.__fields__.keys()
|
56
116
|
if f not in cls.Config.schema_extra.get("exclude", set())
|
57
117
|
]
|
58
118
|
|
119
|
+
instructions = """
|
120
|
+
To use this tool, please provide the required information in an XML-like
|
121
|
+
format. Here's how to structure your input:\n\n
|
122
|
+
"""
|
123
|
+
|
59
124
|
preamble = "Placeholders:\n"
|
60
125
|
for field in fields:
|
61
126
|
preamble += f"{field.upper()} = [value for {field}]\n"
|
62
127
|
|
63
|
-
|
128
|
+
verbatim_fields = [
|
129
|
+
field
|
130
|
+
for field, field_info in cls.__fields__.items()
|
131
|
+
if field_info.field_info.extra.get("verbatim", False)
|
132
|
+
]
|
133
|
+
|
134
|
+
verbatim_alert = ""
|
135
|
+
if len(verbatim_fields) > 0:
|
136
|
+
verbatim_alert = f"""
|
137
|
+
EXTREMELY IMPORTANT: For these fields:
|
138
|
+
{', '.join(verbatim_fields)},
|
139
|
+
the contents MUST be wrapped in a CDATA section, and the content
|
140
|
+
must be written verbatim WITHOUT any modifications or escaping,
|
141
|
+
such as spaces, tabs, indents, newlines, quotes, etc.
|
142
|
+
"""
|
143
|
+
xml_format = f"Formatting example:\n\n<{cls.Config.root_element}>\n"
|
64
144
|
for field in fields:
|
65
145
|
if field == "code":
|
66
|
-
|
146
|
+
xml_format += f" <{field}><![CDATA[{{{field.upper()}}}]]></{field}>\n"
|
67
147
|
else:
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
148
|
+
xml_format += f" <{field}>{{{field.upper()}}}</{field}>\n"
|
149
|
+
xml_format += f"</{cls.Config.root_element}>"
|
150
|
+
|
151
|
+
examples_str = ""
|
152
|
+
if cls.examples():
|
153
|
+
examples_str = "EXAMPLES:\n" + cls.usage_examples()
|
154
|
+
|
155
|
+
return f"""
|
156
|
+
TOOL: {cls.default_value("request")}
|
157
|
+
PURPOSE: {cls.default_value("purpose")}
|
158
|
+
|
159
|
+
{instructions}
|
160
|
+
{preamble}
|
161
|
+
{xml_format}
|
162
|
+
|
163
|
+
Make sure to replace the placeholders with actual values
|
164
|
+
when using the tool.
|
165
|
+
{verbatim_alert}
|
166
|
+
{examples_str}
|
167
|
+
""".lstrip()
|
168
|
+
|
169
|
+
def format_example(self) -> str:
|
170
|
+
"""
|
171
|
+
Format the current instance as an XML example.
|
172
|
+
|
173
|
+
Returns:
|
174
|
+
str: A string representation of the current instance in XML format.
|
175
|
+
|
176
|
+
Raises:
|
177
|
+
ValueError: If the result from etree.tostring is not a string.
|
178
|
+
"""
|
179
|
+
root = etree.Element(self.Config.root_element)
|
180
|
+
exclude_fields = self.Config.schema_extra.get("exclude", set())
|
181
|
+
for name, value in self.dict().items():
|
78
182
|
if name not in exclude_fields:
|
79
183
|
elem = etree.SubElement(root, name)
|
80
|
-
|
184
|
+
field_info = self.__class__.__fields__[name]
|
185
|
+
is_verbatim = field_info.field_info.extra.get("verbatim", False)
|
186
|
+
if is_verbatim:
|
81
187
|
elem.text = etree.CDATA(str(value))
|
82
188
|
else:
|
83
189
|
elem.text = str(value)
|
84
|
-
|
190
|
+
result = etree.tostring(root, encoding="unicode", pretty_print=True)
|
191
|
+
if not isinstance(result, str):
|
192
|
+
raise ValueError("Unexpected non-string result from etree.tostring")
|
193
|
+
return result
|
85
194
|
|
86
195
|
@classmethod
|
87
|
-
def find_candidates(cls, text: str) -> str:
|
196
|
+
def find_candidates(cls, text: str) -> List[str]:
|
197
|
+
"""
|
198
|
+
Find and extract all potential XML tool messages from the given text.
|
199
|
+
|
200
|
+
This method searches for XML-like structures in the input text that match
|
201
|
+
the expected format of the tool message. It looks for opening and closing
|
202
|
+
tags that correspond to the root element defined in the XMLToolMessage class,
|
203
|
+
which is by default <tool>.
|
204
|
+
|
205
|
+
Args:
|
206
|
+
text (str): The input text to search for XML tool messages.
|
207
|
+
|
208
|
+
Returns:
|
209
|
+
List[str]: A list of strings, each representing a potential XML tool
|
210
|
+
message.
|
211
|
+
These candidates include both the opening and
|
212
|
+
closing tags, so that they are individually parseable.
|
213
|
+
|
214
|
+
Note:
|
215
|
+
This method ensures that all candidates are valid and parseable by
|
216
|
+
inserting a closing tag if it's missing for the last candidate.
|
217
|
+
"""
|
88
218
|
root_tag = cls.Config.root_element
|
89
219
|
opening_tag = f"<{root_tag}>"
|
90
220
|
closing_tag = f"</{root_tag}>"
|
@@ -97,10 +227,13 @@ class XmlToolMessage(ToolMessage):
|
|
97
227
|
break
|
98
228
|
end = text.find(closing_tag, start)
|
99
229
|
if end == -1:
|
100
|
-
# For the last candidate,
|
101
|
-
|
230
|
+
# For the last candidate, insert the closing tag if it's missing
|
231
|
+
candidate = text[start:]
|
232
|
+
if not candidate.strip().endswith(closing_tag):
|
233
|
+
candidate += closing_tag
|
234
|
+
candidates.append(candidate)
|
102
235
|
break
|
103
236
|
candidates.append(text[start : end + len(closing_tag)])
|
104
237
|
start = end + len(closing_tag)
|
105
238
|
|
106
|
-
return
|
239
|
+
return candidates
|
langroid/utils/constants.py
CHANGED
@@ -0,0 +1,251 @@
|
|
1
|
+
import fnmatch
|
2
|
+
import logging
|
3
|
+
import textwrap
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import List
|
6
|
+
|
7
|
+
import git
|
8
|
+
from github import Github, GithubException
|
9
|
+
|
10
|
+
from langroid.utils.system import create_file
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
def git_read_file(repo: str, filepath: str) -> str:
|
16
|
+
"""
|
17
|
+
Read the contents of a file from a GitHub repository.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
repo (str): The GitHub repository in the format "owner/repo"
|
21
|
+
filepath (str): The file path relative to the repository root
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
str: The contents of the file as a string
|
25
|
+
"""
|
26
|
+
try:
|
27
|
+
g = Github()
|
28
|
+
github_repo = g.get_repo(repo)
|
29
|
+
file_content = github_repo.get_contents(filepath)
|
30
|
+
if isinstance(file_content, list) and len(file_content) > 0:
|
31
|
+
return file_content[0].decoded_content.decode("utf-8")
|
32
|
+
elif hasattr(file_content, "decoded_content"):
|
33
|
+
return file_content.decoded_content.decode("utf-8")
|
34
|
+
else:
|
35
|
+
logger.error(f"Unexpected file_content type: {type(file_content)}")
|
36
|
+
return ""
|
37
|
+
except GithubException as e:
|
38
|
+
logger.error(f"An error occurred while reading file {filepath}: {e}")
|
39
|
+
return ""
|
40
|
+
|
41
|
+
|
42
|
+
def get_file_list(repo: str, dir: str, pat: str = "") -> List[str]:
|
43
|
+
"""
|
44
|
+
Get a list of files in a specified directory of a GitHub repository.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
repo (str): The GitHub repository in the format "owner/repo"
|
48
|
+
dir (str): The directory path relative to the repository root
|
49
|
+
pat (str): Optional wildcard pattern to filter file names (default: "")
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
List[str]: A list of file paths in the specified directory
|
53
|
+
"""
|
54
|
+
try:
|
55
|
+
g = Github()
|
56
|
+
github_repo = g.get_repo(repo)
|
57
|
+
contents = github_repo.get_contents(dir)
|
58
|
+
|
59
|
+
file_list = []
|
60
|
+
if isinstance(contents, list):
|
61
|
+
file_list = [content.path for content in contents if content.type == "file"]
|
62
|
+
elif hasattr(contents, "path") and hasattr(contents, "type"):
|
63
|
+
if contents.type == "file":
|
64
|
+
file_list = [contents.path]
|
65
|
+
|
66
|
+
if pat:
|
67
|
+
file_list = [file for file in file_list if fnmatch.fnmatch(file, pat)]
|
68
|
+
return sorted(file_list)
|
69
|
+
|
70
|
+
except GithubException as e:
|
71
|
+
logger.error(f"An error occurred while fetching file list: {e}")
|
72
|
+
return []
|
73
|
+
|
74
|
+
|
75
|
+
def git_init_repo(dir: str) -> git.Repo | None:
|
76
|
+
"""
|
77
|
+
Set up a Git repository in the specified directory.
|
78
|
+
|
79
|
+
Args:
|
80
|
+
dir (str): Path to the directory where the Git repository should be initialized
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
git.Repo: The initialized Git repository object
|
84
|
+
"""
|
85
|
+
repo_path = Path(dir).expanduser()
|
86
|
+
try:
|
87
|
+
repo = git.Repo.init(repo_path)
|
88
|
+
logger.info(f"Git repository initialized in {repo_path}")
|
89
|
+
|
90
|
+
gitignore_content = textwrap.dedent(
|
91
|
+
"""
|
92
|
+
/target/
|
93
|
+
**/*.rs.bk
|
94
|
+
Cargo.lock
|
95
|
+
"""
|
96
|
+
).strip()
|
97
|
+
|
98
|
+
gitignore_path = repo_path / ".gitignore"
|
99
|
+
create_file(gitignore_path, gitignore_content)
|
100
|
+
logger.info(f"Created .gitignore file in {repo_path}")
|
101
|
+
|
102
|
+
# Ensure the default branch is 'main'
|
103
|
+
# Check if we're on the master branch
|
104
|
+
if repo.active_branch.name == "master":
|
105
|
+
# Rename the branch
|
106
|
+
repo.git.branch("-m", "master", "main")
|
107
|
+
print("Branch renamed from 'master' to 'main'")
|
108
|
+
else:
|
109
|
+
print("Current branch is not 'master'. No changes made.")
|
110
|
+
return repo
|
111
|
+
except git.GitCommandError as e:
|
112
|
+
logger.error(f"An error occurred while initializing the repository: {e}")
|
113
|
+
return None
|
114
|
+
|
115
|
+
|
116
|
+
def git_commit_file(repo: git.Repo, filepath: str, msg: str) -> None:
|
117
|
+
"""
|
118
|
+
Commit a file to a Git repository.
|
119
|
+
|
120
|
+
Args:
|
121
|
+
repo (git.Repo): The Git repository object
|
122
|
+
filepath (str): Path to the file to be committed
|
123
|
+
msg (str): The commit message
|
124
|
+
|
125
|
+
Returns:
|
126
|
+
None
|
127
|
+
"""
|
128
|
+
try:
|
129
|
+
repo.index.add([filepath])
|
130
|
+
repo.index.commit(f"{msg}; Updated {filepath}")
|
131
|
+
logger.info(f"Successfully committed {filepath}")
|
132
|
+
except git.GitCommandError as e:
|
133
|
+
logger.error(f"An error occurred while committing: {e}")
|
134
|
+
|
135
|
+
|
136
|
+
def git_commit_mods(repo: git.Repo, msg: str = "commit all changes") -> None:
|
137
|
+
"""
|
138
|
+
Commit all modifications in the Git repository.
|
139
|
+
Does not raise an error if there's nothing to commit.
|
140
|
+
|
141
|
+
Args:
|
142
|
+
repo (git.Repo): The Git repository object
|
143
|
+
|
144
|
+
Returns:
|
145
|
+
None
|
146
|
+
"""
|
147
|
+
try:
|
148
|
+
if repo.is_dirty():
|
149
|
+
repo.git.add(update=True)
|
150
|
+
repo.index.commit(msg)
|
151
|
+
logger.info("Successfully committed all modifications")
|
152
|
+
else:
|
153
|
+
logger.info("No changes to commit")
|
154
|
+
except git.GitCommandError as e:
|
155
|
+
logger.error(f"An error occurred while committing modifications: {e}")
|
156
|
+
|
157
|
+
|
158
|
+
def git_restore_repo(repo: git.Repo) -> None:
|
159
|
+
"""
|
160
|
+
Restore all unstaged, uncommitted changes in the Git repository.
|
161
|
+
This function undoes any dirty files to the last commit.
|
162
|
+
|
163
|
+
Args:
|
164
|
+
repo (git.Repo): The Git repository object
|
165
|
+
|
166
|
+
Returns:
|
167
|
+
None
|
168
|
+
"""
|
169
|
+
try:
|
170
|
+
if repo.is_dirty():
|
171
|
+
repo.git.restore(".")
|
172
|
+
logger.info("Successfully restored all unstaged changes")
|
173
|
+
else:
|
174
|
+
logger.info("No unstaged changes to restore")
|
175
|
+
except git.GitCommandError as e:
|
176
|
+
logger.error(f"An error occurred while restoring changes: {e}")
|
177
|
+
|
178
|
+
|
179
|
+
def git_restore_file(repo: git.Repo, file_path: str) -> None:
|
180
|
+
"""
|
181
|
+
Restore a specific file in the Git repository to its state in the last commit.
|
182
|
+
This function undoes changes to the specified file.
|
183
|
+
|
184
|
+
Args:
|
185
|
+
repo (git.Repo): The Git repository object
|
186
|
+
file_path (str): Path to the file to be restored
|
187
|
+
|
188
|
+
Returns:
|
189
|
+
None
|
190
|
+
"""
|
191
|
+
try:
|
192
|
+
repo.git.restore(file_path)
|
193
|
+
logger.info(f"Successfully restored file: {file_path}")
|
194
|
+
except git.GitCommandError as e:
|
195
|
+
logger.error(f"An error occurred while restoring file {file_path}: {e}")
|
196
|
+
|
197
|
+
|
198
|
+
def git_create_checkout_branch(repo: git.Repo, branch: str) -> None:
|
199
|
+
"""
|
200
|
+
Create and checkout a new branch in the given Git repository.
|
201
|
+
If the branch already exists, it will be checked out.
|
202
|
+
If we're already on the specified branch, no action is taken.
|
203
|
+
|
204
|
+
Args:
|
205
|
+
repo (git.Repo): The Git repository object
|
206
|
+
branch (str): The name of the branch to create or checkout
|
207
|
+
|
208
|
+
Returns:
|
209
|
+
None
|
210
|
+
"""
|
211
|
+
try:
|
212
|
+
if repo.active_branch.name == branch:
|
213
|
+
logger.info(f"Already on branch: {branch}")
|
214
|
+
return
|
215
|
+
|
216
|
+
if branch in repo.heads:
|
217
|
+
repo.heads[branch].checkout()
|
218
|
+
logger.info(f"Checked out existing branch: {branch}")
|
219
|
+
else:
|
220
|
+
new_branch = repo.create_head(branch)
|
221
|
+
new_branch.checkout()
|
222
|
+
logger.info(f"Created and checked out new branch: {branch}")
|
223
|
+
except git.GitCommandError as e:
|
224
|
+
logger.error(f"An error occurred while creating/checking out branch: {e}")
|
225
|
+
|
226
|
+
|
227
|
+
def git_diff_file(repo: git.Repo, filepath: str) -> str:
|
228
|
+
"""
|
229
|
+
Show diffs of file between the latest commit and the previous one if any.
|
230
|
+
|
231
|
+
Args:
|
232
|
+
repo (git.Repo): The Git repository object
|
233
|
+
filepath (str): Path to the file to be diffed
|
234
|
+
|
235
|
+
Returns:
|
236
|
+
str: The diff output as a string
|
237
|
+
"""
|
238
|
+
try:
|
239
|
+
# Get the two most recent commits
|
240
|
+
commits = list(repo.iter_commits(paths=filepath, max_count=2))
|
241
|
+
|
242
|
+
if len(commits) < 2:
|
243
|
+
return "No previous commit found for comparison."
|
244
|
+
|
245
|
+
# Get the diff between the two commits for the specific file
|
246
|
+
diff = repo.git.diff(commits[1].hexsha, commits[0].hexsha, filepath)
|
247
|
+
|
248
|
+
return str(diff)
|
249
|
+
except git.GitCommandError as e:
|
250
|
+
logger.error(f"An error occurred while getting diff: {e}")
|
251
|
+
return f"Error: {str(e)}"
|
langroid/utils/system.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import difflib
|
1
2
|
import getpass
|
2
3
|
import hashlib
|
3
4
|
import importlib
|
@@ -8,6 +9,7 @@ import shutil
|
|
8
9
|
import socket
|
9
10
|
import traceback
|
10
11
|
import uuid
|
12
|
+
from pathlib import Path
|
11
13
|
from typing import Any
|
12
14
|
|
13
15
|
logger = logging.getLogger(__name__)
|
@@ -182,3 +184,79 @@ def hash(s: str) -> str:
|
|
182
184
|
def generate_unique_id() -> str:
|
183
185
|
"""Generate a unique ID using UUID4."""
|
184
186
|
return str(uuid.uuid4())
|
187
|
+
|
188
|
+
|
189
|
+
def create_file(filepath: str | Path, content: str = "") -> None:
|
190
|
+
"""
|
191
|
+
Create a file with the given content in the specified directory.
|
192
|
+
If content is empty, it will simply touch to create an empty file.
|
193
|
+
|
194
|
+
Args:
|
195
|
+
filepath (str|Path): The relative path of the file to be created
|
196
|
+
content (str): The content to be written to the file
|
197
|
+
"""
|
198
|
+
Path(filepath).parent.mkdir(parents=True, exist_ok=True)
|
199
|
+
if content == "":
|
200
|
+
Path(filepath).touch()
|
201
|
+
else:
|
202
|
+
# the newline = '\n` argument is used to ensure that
|
203
|
+
# newlines in the content are written as actual line breaks
|
204
|
+
with open(filepath, "w", newline="\n") as f:
|
205
|
+
f.write(content)
|
206
|
+
logger.warning(f"File created/updated: {filepath}")
|
207
|
+
|
208
|
+
|
209
|
+
def read_file(path: str, line_numbers: bool = False) -> str:
|
210
|
+
"""
|
211
|
+
Read the contents of a file.
|
212
|
+
|
213
|
+
Args:
|
214
|
+
path (str): The path to the file to be read.
|
215
|
+
line_numbers (bool, optional): If True, prepend line numbers to each line.
|
216
|
+
Defaults to False.
|
217
|
+
|
218
|
+
Returns:
|
219
|
+
str: The contents of the file, optionally with line numbers.
|
220
|
+
|
221
|
+
Raises:
|
222
|
+
FileNotFoundError: If the specified file does not exist.
|
223
|
+
"""
|
224
|
+
# raise an error if the file does not exist
|
225
|
+
if not Path(path).exists():
|
226
|
+
raise FileNotFoundError(f"File not found: {path}")
|
227
|
+
file = Path(path).expanduser()
|
228
|
+
content = file.read_text()
|
229
|
+
if line_numbers:
|
230
|
+
lines = content.splitlines()
|
231
|
+
numbered_lines = [f"{i+1}: {line}" for i, line in enumerate(lines)]
|
232
|
+
return "\n".join(numbered_lines)
|
233
|
+
return content
|
234
|
+
|
235
|
+
|
236
|
+
def diff_files(file1: str, file2: str) -> str:
|
237
|
+
"""
|
238
|
+
Find the diffs between two files, in unified diff format.
|
239
|
+
"""
|
240
|
+
with open(file1, "r") as f1, open(file2, "r") as f2:
|
241
|
+
lines1 = f1.readlines()
|
242
|
+
lines2 = f2.readlines()
|
243
|
+
|
244
|
+
differ = difflib.unified_diff(lines1, lines2, fromfile=file1, tofile=file2)
|
245
|
+
diff_result = "".join(differ)
|
246
|
+
return diff_result
|
247
|
+
|
248
|
+
|
249
|
+
def list_dir(path: str | Path) -> list[str]:
|
250
|
+
"""
|
251
|
+
List the contents of a directory.
|
252
|
+
|
253
|
+
Args:
|
254
|
+
path (str): The path to the directory.
|
255
|
+
|
256
|
+
Returns:
|
257
|
+
list[str]: A list of the files and directories in the specified directory.
|
258
|
+
"""
|
259
|
+
dir_path = Path(path)
|
260
|
+
if not dir_path.is_dir():
|
261
|
+
raise NotADirectoryError(f"Path is not a directory: {dir_path}")
|
262
|
+
return [str(p) for p in dir_path.iterdir()]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: langroid
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.17.0
|
4
4
|
Summary: Harness LLMs with Multi-Agent Programming
|
5
5
|
License: MIT
|
6
6
|
Author: Prasad Chalasani
|
@@ -48,6 +48,7 @@ Requires-Dist: faker (>=18.9.0,<19.0.0)
|
|
48
48
|
Requires-Dist: fakeredis (>=2.12.1,<3.0.0)
|
49
49
|
Requires-Dist: fastembed (>=0.3.1,<0.4.0) ; extra == "all" or extra == "fastembed"
|
50
50
|
Requires-Dist: fire (>=0.5.0,<0.6.0)
|
51
|
+
Requires-Dist: gitpython (>=3.1.43,<4.0.0)
|
51
52
|
Requires-Dist: google-api-python-client (>=2.95.0,<3.0.0)
|
52
53
|
Requires-Dist: google-generativeai (>=0.5.2,<0.6.0)
|
53
54
|
Requires-Dist: groq (>=0.5.0,<0.6.0)
|
@@ -55,7 +56,7 @@ Requires-Dist: grpcio (>=1.62.1,<2.0.0)
|
|
55
56
|
Requires-Dist: halo (>=0.0.31,<0.0.32)
|
56
57
|
Requires-Dist: huggingface-hub (>=0.21.2,<0.22.0) ; extra == "hf-transformers" or extra == "all" or extra == "transformers"
|
57
58
|
Requires-Dist: jinja2 (>=3.1.2,<4.0.0)
|
58
|
-
Requires-Dist: json-repair (>=0.29.
|
59
|
+
Requires-Dist: json-repair (>=0.29.9,<0.30.0)
|
59
60
|
Requires-Dist: lancedb (>=0.8.2,<0.9.0) ; extra == "vecdbs" or extra == "lancedb"
|
60
61
|
Requires-Dist: litellm (>=1.30.1,<2.0.0) ; extra == "all" or extra == "litellm"
|
61
62
|
Requires-Dist: lxml (>=4.9.3,<5.0.0)
|
@@ -143,7 +144,7 @@ Description-Content-Type: text/markdown
|
|
143
144
|
</h3>
|
144
145
|
|
145
146
|
`Langroid` is an intuitive, lightweight, extensible and principled
|
146
|
-
Python framework to easily build LLM-powered applications, from
|
147
|
+
Python framework to easily build LLM-powered applications, from CMU and UW-Madison researchers.
|
147
148
|
You set up Agents, equip them with optional components (LLM,
|
148
149
|
vector-store and tools/functions), assign them tasks, and have them
|
149
150
|
collaboratively solve a problem by exchanging messages.
|
@@ -242,6 +243,8 @@ teacher_task.run()
|
|
242
243
|
<details>
|
243
244
|
<summary> <b>Click to expand</b></summary>
|
244
245
|
|
246
|
+
- **Oct 2024:**
|
247
|
+
- **[0.17.0]** XML-based tools, see [docs](https://langroid.github.io/langroid/tutorials/xml-tools/).
|
245
248
|
- **Sep 2024:**
|
246
249
|
- **[0.16.0](https://github.com/langroid/langroid/releases/tag/0.16.0)** Support for OpenAI `o1-mini` and `o1-preview` models.
|
247
250
|
- **[0.15.0](https://github.com/langroid/langroid/releases/tag/0.15.0)** Cerebras API support -- run llama-3.1 models hosted on Cerebras Cloud (very fast inference).
|