langroid 0.16.6__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.
@@ -0,0 +1,239 @@
1
+ from typing import Any, Dict, List, Optional
2
+
3
+ from lxml import etree
4
+
5
+ from langroid.agent.tool_message import ToolMessage
6
+
7
+
8
+ class XMLToolMessage(ToolMessage):
9
+ """
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).
22
+ """
23
+
24
+ request: str
25
+ purpose: str
26
+
27
+ _allow_llm_use = True
28
+
29
+ class Config(ToolMessage.Config):
30
+ root_element = "tool"
31
+
32
+ @classmethod
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
+ """
49
+ parser = etree.XMLParser(strip_cdata=False)
50
+ root = etree.fromstring(formatted_string.encode("utf-8"), parser=parser)
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
57
+ )
58
+
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)
101
+
102
+ @classmethod
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
+ """
113
+ fields = [
114
+ f
115
+ for f in cls.__fields__.keys()
116
+ if f not in cls.Config.schema_extra.get("exclude", set())
117
+ ]
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
+
124
+ preamble = "Placeholders:\n"
125
+ for field in fields:
126
+ preamble += f"{field.upper()} = [value for {field}]\n"
127
+
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"
144
+ for field in fields:
145
+ if field == "code":
146
+ xml_format += f" <{field}><![CDATA[{{{field.upper()}}}]]></{field}>\n"
147
+ else:
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():
182
+ if name not in exclude_fields:
183
+ elem = etree.SubElement(root, name)
184
+ field_info = self.__class__.__fields__[name]
185
+ is_verbatim = field_info.field_info.extra.get("verbatim", False)
186
+ if is_verbatim:
187
+ elem.text = etree.CDATA(str(value))
188
+ else:
189
+ elem.text = str(value)
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
194
+
195
+ @classmethod
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
+ """
218
+ root_tag = cls.Config.root_element
219
+ opening_tag = f"<{root_tag}>"
220
+ closing_tag = f"</{root_tag}>"
221
+
222
+ candidates = []
223
+ start = 0
224
+ while True:
225
+ start = text.find(opening_tag, start)
226
+ if start == -1:
227
+ break
228
+ end = text.find(closing_tag, start)
229
+ if end == -1:
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)
235
+ break
236
+ candidates.append(text[start : end + len(closing_tag)])
237
+ start = end + len(closing_tag)
238
+
239
+ return candidates
@@ -230,6 +230,8 @@ class OpenAICallParams(BaseModel):
230
230
  Various params that can be sent to an OpenAI API chat-completion call.
231
231
  When specified, any param here overrides the one with same name in the
232
232
  OpenAIGPTConfig.
233
+ See OpenAI API Reference for details on the params:
234
+ https://platform.openai.com/docs/api-reference/chat
233
235
  """
234
236
 
235
237
  max_tokens: int = 1024
@@ -239,7 +241,7 @@ class OpenAICallParams(BaseModel):
239
241
  response_format: Dict[str, str] | None = None
240
242
  logit_bias: Dict[int, float] | None = None # token_id -> bias
241
243
  logprobs: bool = False
242
- top_p: int | None = 1
244
+ top_p: float | None = 1.0
243
245
  top_logprobs: int | None = None # if int, requires logprobs=True
244
246
  n: int = 1 # how many completions to generate (n > 1 is NOT handled now)
245
247
  stop: str | List[str] | None = None # (list of) stop sequence(s)
@@ -329,7 +331,7 @@ class OpenAIGPTConfig(LLMConfig):
329
331
  litellm.drop_params = True # drop un-supported params without crashing
330
332
  # modify params to fit the model expectations, and avoid crashing
331
333
  # (e.g. anthropic doesn't like first msg to be system msg)
332
- litellm.modify_params = True
334
+ litellm.modify_params = True
333
335
  self.seed = None # some local mdls don't support seed
334
336
  keys_dict = litellm.utils.validate_environment(self.chat_model)
335
337
  missing_keys = keys_dict.get("missing_keys", [])
@@ -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.6
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).