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.
Files changed (91) hide show
  1. langroid/agent/base.py +45 -21
  2. langroid/agent/chat_agent.py +22 -14
  3. langroid/agent/chat_document.py +22 -13
  4. langroid/agent/tool_message.py +11 -11
  5. langroid/agent/tools/file_tools.py +234 -0
  6. langroid/agent/xml_tool_message.py +178 -45
  7. langroid/utils/constants.py +2 -0
  8. langroid/utils/git_utils.py +251 -0
  9. langroid/utils/system.py +78 -0
  10. {langroid-0.16.7.dist-info → langroid-0.17.0.dist-info}/METADATA +6 -3
  11. {langroid-0.16.7.dist-info → langroid-0.17.0.dist-info}/RECORD +14 -89
  12. pyproject.toml +3 -2
  13. langroid/agent/md_tool_message_grammar.py +0 -455
  14. langroid/agent/tools/code_file_tool_parse.py +0 -150
  15. langroid/agent/tools/code_file_tool_pyparsing.py +0 -194
  16. langroid/agent/tools/code_file_tool_pyparsing2.py +0 -199
  17. langroid/agent/tools/extract_tool.py +0 -96
  18. langroid/agent/tools/formatted_model_custom.py +0 -150
  19. langroid/agent/tools/formatted_model_custom2.py +0 -168
  20. langroid/agent/tools/formatted_model_custom3.py +0 -279
  21. langroid/agent/tools/formatted_model_custom4.py +0 -395
  22. langroid/agent/tools/formatted_model_jinja.py +0 -133
  23. langroid/agent/tools/formatted_model_jinja.py-e +0 -122
  24. langroid/agent/tools/formatted_model_jinja2.py +0 -145
  25. langroid/agent/tools/formatted_model_jinja2.py-e +0 -135
  26. langroid/agent/tools/formatted_model_lark.py +0 -0
  27. langroid/agent/tools/formatted_model_lark2.py +0 -168
  28. langroid/agent/tools/formatted_model_parse.py +0 -105
  29. langroid/agent/tools/formatted_model_parse.py-e +0 -98
  30. langroid/agent/tools/formatted_model_parse2.py +0 -113
  31. langroid/agent/tools/formatted_model_parse2.py-e +0 -109
  32. langroid/agent/tools/formatted_model_parse3.py +0 -114
  33. langroid/agent/tools/formatted_model_parse3.py-e +0 -110
  34. langroid/agent/tools/formatted_model_parsimon.py +0 -194
  35. langroid/agent/tools/formatted_model_parsimon.py-e +0 -186
  36. langroid/agent/tools/formatted_model_pyparsing.py +0 -169
  37. langroid/agent/tools/formatted_model_pyparsing.py-e +0 -149
  38. langroid/agent/tools/formatted_model_pyparsing2.py +0 -159
  39. langroid/agent/tools/formatted_model_pyparsing2.py-e +0 -143
  40. langroid/agent/tools/formatted_model_pyparsing3.py +0 -133
  41. langroid/agent/tools/formatted_model_pyparsing3.py-e +0 -121
  42. langroid/agent/tools/formatted_model_pyparsing4.py +0 -213
  43. langroid/agent/tools/formatted_model_pyparsing4.py-e +0 -176
  44. langroid/agent/tools/formatted_model_pyparsing5.py +0 -173
  45. langroid/agent/tools/formatted_model_pyparsing5.py-e +0 -142
  46. langroid/agent/tools/formatted_model_regex.py +0 -246
  47. langroid/agent/tools/formatted_model_regex.py-e +0 -248
  48. langroid/agent/tools/formatted_model_regex2.py +0 -250
  49. langroid/agent/tools/formatted_model_regex2.py-e +0 -253
  50. langroid/agent/tools/formatted_model_tatsu.py +0 -172
  51. langroid/agent/tools/formatted_model_tatsu.py-e +0 -160
  52. langroid/agent/tools/formatted_model_template.py +0 -217
  53. langroid/agent/tools/formatted_model_template.py-e +0 -200
  54. langroid/agent/tools/formatted_model_xml.py +0 -178
  55. langroid/agent/tools/formatted_model_xml2.py +0 -178
  56. langroid/agent/tools/formatted_model_xml3.py +0 -132
  57. langroid/agent/tools/formatted_model_xml4.py +0 -130
  58. langroid/agent/tools/formatted_model_xml5.py +0 -130
  59. langroid/agent/tools/formatted_model_xml6.py +0 -113
  60. langroid/agent/tools/formatted_model_xml7.py +0 -117
  61. langroid/agent/tools/formatted_model_xml8.py +0 -164
  62. langroid/agent/tools/generator_tool.py +0 -20
  63. langroid/agent/tools/generic_tool.py +0 -165
  64. langroid/agent/tools/generic_tool_tatsu.py +0 -275
  65. langroid/agent/tools/grammar_based_model.py +0 -132
  66. langroid/agent/tools/grammar_based_model.py-e +0 -128
  67. langroid/agent/tools/grammar_based_model_lark.py +0 -156
  68. langroid/agent/tools/grammar_based_model_lark.py-e +0 -153
  69. langroid/agent/tools/grammar_based_model_parse.py +0 -86
  70. langroid/agent/tools/grammar_based_model_parse.py-e +0 -80
  71. langroid/agent/tools/grammar_based_model_parsimonious.py +0 -129
  72. langroid/agent/tools/grammar_based_model_parsimonious.py-e +0 -120
  73. langroid/agent/tools/grammar_based_model_pyparsing.py +0 -105
  74. langroid/agent/tools/grammar_based_model_pyparsing.py-e +0 -103
  75. langroid/agent/tools/grammar_based_model_regex.py +0 -139
  76. langroid/agent/tools/grammar_based_model_regex.py-e +0 -130
  77. langroid/agent/tools/grammar_based_model_regex2.py +0 -124
  78. langroid/agent/tools/grammar_based_model_regex2.py-e +0 -116
  79. langroid/agent/tools/grammar_based_model_tatsu.py +0 -80
  80. langroid/agent/tools/grammar_based_model_tatsu.py-e +0 -77
  81. langroid/agent/tools/lark_earley_example.py +0 -135
  82. langroid/agent/tools/lark_earley_example.py-e +0 -117
  83. langroid/agent/tools/lark_example.py +0 -72
  84. langroid/agent/tools/note_tool.py +0 -0
  85. langroid/agent/tools/parse_example.py +0 -76
  86. langroid/agent/tools/parse_example2.py +0 -87
  87. langroid/agent/tools/parse_example3.py +0 -42
  88. langroid/agent/tools/parse_test.py +0 -791
  89. langroid/agent/tools/run_python_code.py +0 -60
  90. {langroid-0.16.7.dist-info → langroid-0.17.0.dist-info}/LICENSE +0 -0
  91. {langroid-0.16.7.dist-info → langroid-0.17.0.dist-info}/WHEEL +0 -0
@@ -1,13 +1,24 @@
1
- from typing import get_type_hints
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 XmlToolMessage(ToolMessage):
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 parse(cls, formatted_string: str) -> "XmlToolMessage":
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
- if root.tag != cls.Config.root_element:
26
- raise ValueError(
27
- f"Invalid root element: expected {cls.Config.root_element}, got {root.tag}"
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
- data = {}
31
- type_hints = get_type_hints(cls)
32
- exclude_fields = cls.Config.schema_extra.get("exclude", set())
33
- for elem in root:
34
- if elem.tag not in exclude_fields:
35
- field_type = type_hints.get(elem.tag, str)
36
- if elem.tag == "code":
37
- data[elem.tag] = elem.text if elem.text else ""
38
- else:
39
- value = elem.text.strip() if elem.text else ""
40
- if field_type == int:
41
- data[elem.tag] = int(value)
42
- elif field_type == float:
43
- data[elem.tag] = float(value)
44
- elif field_type == bool:
45
- data[elem.tag] = value.lower() in ("true", "1", "yes")
46
- else:
47
- data[elem.tag] = value
48
-
49
- return cls(**data)
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 instructions(cls) -> str:
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
- example = f"Formatting example:\n\n<{cls.Config.root_element}>\n"
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
- example += f" <{field}><![CDATA[{{{field.upper()}}}]]></{field}>\n"
146
+ xml_format += f" <{field}><![CDATA[{{{field.upper()}}}]]></{field}>\n"
67
147
  else:
68
- example += f" <{field}>{{{field.upper()}}}</{field}>\n"
69
- example += f"</{cls.Config.root_element}>"
70
-
71
- return f"{preamble}\n{example}"
72
-
73
- @classmethod
74
- def format(cls, instance: "XmlToolMessage") -> str:
75
- root = etree.Element(cls.Config.root_element)
76
- exclude_fields = cls.Config.schema_extra.get("exclude", set())
77
- for name, value in instance.dict().items():
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
- if name == "code":
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
- return etree.tostring(root, encoding="unicode", pretty_print=True)
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, allow missing closing tag
101
- candidates.append(text[start:])
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 "\n".join(candidates)
239
+ return candidates
@@ -26,3 +26,5 @@ TOOL = "TOOL"
26
26
  # it no letters, digits or underscores.
27
27
  # See tests/main/test_msg_routing for example usage
28
28
  AT = "|@|"
29
+ TOOL_BEGIN = "TOOL_BEGIN"
30
+ TOOL_END = "TOOL_END"
@@ -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.16.7
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.0,<0.30.0)
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 ex-CMU and UW-Madison researchers.
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).