langroid 0.58.2__py3-none-any.whl → 0.59.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. langroid/agent/base.py +39 -17
  2. langroid/agent/base.py-e +2216 -0
  3. langroid/agent/callbacks/chainlit.py +2 -1
  4. langroid/agent/chat_agent.py +73 -55
  5. langroid/agent/chat_agent.py-e +2086 -0
  6. langroid/agent/chat_document.py +7 -7
  7. langroid/agent/chat_document.py-e +513 -0
  8. langroid/agent/openai_assistant.py +9 -9
  9. langroid/agent/openai_assistant.py-e +882 -0
  10. langroid/agent/special/arangodb/arangodb_agent.py +10 -18
  11. langroid/agent/special/arangodb/arangodb_agent.py-e +648 -0
  12. langroid/agent/special/arangodb/tools.py +3 -3
  13. langroid/agent/special/doc_chat_agent.py +16 -14
  14. langroid/agent/special/lance_rag/critic_agent.py +2 -2
  15. langroid/agent/special/lance_rag/query_planner_agent.py +4 -4
  16. langroid/agent/special/lance_tools.py +6 -5
  17. langroid/agent/special/lance_tools.py-e +61 -0
  18. langroid/agent/special/neo4j/neo4j_chat_agent.py +3 -7
  19. langroid/agent/special/neo4j/neo4j_chat_agent.py-e +430 -0
  20. langroid/agent/special/relevance_extractor_agent.py +1 -1
  21. langroid/agent/special/sql/sql_chat_agent.py +11 -3
  22. langroid/agent/task.py +9 -87
  23. langroid/agent/task.py-e +2418 -0
  24. langroid/agent/tool_message.py +33 -17
  25. langroid/agent/tool_message.py-e +400 -0
  26. langroid/agent/tools/file_tools.py +4 -2
  27. langroid/agent/tools/file_tools.py-e +234 -0
  28. langroid/agent/tools/mcp/fastmcp_client.py +19 -6
  29. langroid/agent/tools/mcp/fastmcp_client.py-e +584 -0
  30. langroid/agent/tools/orchestration.py +22 -17
  31. langroid/agent/tools/orchestration.py-e +301 -0
  32. langroid/agent/tools/recipient_tool.py +3 -3
  33. langroid/agent/tools/task_tool.py +22 -16
  34. langroid/agent/tools/task_tool.py-e +249 -0
  35. langroid/agent/xml_tool_message.py +90 -35
  36. langroid/agent/xml_tool_message.py-e +392 -0
  37. langroid/cachedb/base.py +1 -1
  38. langroid/embedding_models/base.py +2 -2
  39. langroid/embedding_models/models.py +3 -7
  40. langroid/embedding_models/models.py-e +563 -0
  41. langroid/exceptions.py +4 -1
  42. langroid/language_models/azure_openai.py +2 -2
  43. langroid/language_models/azure_openai.py-e +134 -0
  44. langroid/language_models/base.py +6 -4
  45. langroid/language_models/base.py-e +812 -0
  46. langroid/language_models/client_cache.py +64 -0
  47. langroid/language_models/config.py +2 -4
  48. langroid/language_models/config.py-e +18 -0
  49. langroid/language_models/model_info.py +9 -1
  50. langroid/language_models/model_info.py-e +483 -0
  51. langroid/language_models/openai_gpt.py +119 -20
  52. langroid/language_models/openai_gpt.py-e +2280 -0
  53. langroid/language_models/provider_params.py +3 -22
  54. langroid/language_models/provider_params.py-e +153 -0
  55. langroid/mytypes.py +11 -4
  56. langroid/mytypes.py-e +132 -0
  57. langroid/parsing/code_parser.py +1 -1
  58. langroid/parsing/file_attachment.py +1 -1
  59. langroid/parsing/file_attachment.py-e +246 -0
  60. langroid/parsing/md_parser.py +14 -4
  61. langroid/parsing/md_parser.py-e +574 -0
  62. langroid/parsing/parser.py +22 -7
  63. langroid/parsing/parser.py-e +410 -0
  64. langroid/parsing/repo_loader.py +3 -1
  65. langroid/parsing/repo_loader.py-e +812 -0
  66. langroid/parsing/search.py +1 -1
  67. langroid/parsing/url_loader.py +17 -51
  68. langroid/parsing/url_loader.py-e +683 -0
  69. langroid/parsing/urls.py +5 -4
  70. langroid/parsing/urls.py-e +279 -0
  71. langroid/prompts/prompts_config.py +1 -1
  72. langroid/pydantic_v1/__init__.py +45 -6
  73. langroid/pydantic_v1/__init__.py-e +36 -0
  74. langroid/pydantic_v1/main.py +11 -4
  75. langroid/pydantic_v1/main.py-e +11 -0
  76. langroid/utils/configuration.py +13 -11
  77. langroid/utils/configuration.py-e +141 -0
  78. langroid/utils/constants.py +1 -1
  79. langroid/utils/constants.py-e +32 -0
  80. langroid/utils/globals.py +21 -5
  81. langroid/utils/globals.py-e +49 -0
  82. langroid/utils/html_logger.py +2 -1
  83. langroid/utils/html_logger.py-e +825 -0
  84. langroid/utils/object_registry.py +1 -1
  85. langroid/utils/object_registry.py-e +66 -0
  86. langroid/utils/pydantic_utils.py +55 -28
  87. langroid/utils/pydantic_utils.py-e +602 -0
  88. langroid/utils/types.py +2 -2
  89. langroid/utils/types.py-e +113 -0
  90. langroid/vector_store/base.py +3 -3
  91. langroid/vector_store/lancedb.py +5 -5
  92. langroid/vector_store/lancedb.py-e +404 -0
  93. langroid/vector_store/meilisearch.py +2 -2
  94. langroid/vector_store/pineconedb.py +4 -4
  95. langroid/vector_store/pineconedb.py-e +427 -0
  96. langroid/vector_store/postgres.py +1 -1
  97. langroid/vector_store/qdrantdb.py +3 -3
  98. langroid/vector_store/weaviatedb.py +1 -1
  99. {langroid-0.58.2.dist-info → langroid-0.59.0b1.dist-info}/METADATA +3 -2
  100. langroid-0.59.0b1.dist-info/RECORD +181 -0
  101. langroid/agent/special/doc_chat_task.py +0 -0
  102. langroid/mcp/__init__.py +0 -1
  103. langroid/mcp/server/__init__.py +0 -1
  104. langroid-0.58.2.dist-info/RECORD +0 -145
  105. {langroid-0.58.2.dist-info → langroid-0.59.0b1.dist-info}/WHEEL +0 -0
  106. {langroid-0.58.2.dist-info → langroid-0.59.0b1.dist-info}/licenses/LICENSE +0 -0
langroid/parsing/urls.py CHANGED
@@ -9,11 +9,10 @@ from urllib.parse import urldefrag, urljoin, urlparse
9
9
  import fire
10
10
  import requests
11
11
  from bs4 import BeautifulSoup
12
+ from pydantic import BaseModel, HttpUrl, TypeAdapter, ValidationError
12
13
  from rich import print
13
14
  from rich.prompt import Prompt
14
15
 
15
- from langroid.pydantic_v1 import BaseModel, HttpUrl, ValidationError, parse_obj_as
16
-
17
16
  logger = logging.getLogger(__name__)
18
17
 
19
18
 
@@ -106,7 +105,8 @@ class Url(BaseModel):
106
105
 
107
106
  def is_url(s: str) -> bool:
108
107
  try:
109
- Url(url=parse_obj_as(HttpUrl, s))
108
+ url_adapter = TypeAdapter(HttpUrl)
109
+ Url(url=url_adapter.validate_python(s))
110
110
  return True
111
111
  except ValidationError:
112
112
  return False
@@ -133,7 +133,8 @@ def get_urls_paths_bytes_indices(
133
133
  byte_list.append(i)
134
134
  continue
135
135
  try:
136
- Url(url=parse_obj_as(HttpUrl, item))
136
+ url_adapter = TypeAdapter(HttpUrl)
137
+ Url(url=url_adapter.validate_python(item))
137
138
  urls.append(i)
138
139
  except ValidationError:
139
140
  if os.path.exists(item):
@@ -0,0 +1,279 @@
1
+ import logging
2
+ import os
3
+ import tempfile
4
+ import urllib.parse
5
+ import urllib.robotparser
6
+ from typing import List, Optional, Set, Tuple
7
+ from urllib.parse import urldefrag, urljoin, urlparse
8
+
9
+ import fire
10
+ import requests
11
+ from bs4 import BeautifulSoup
12
+ from rich import print
13
+ from rich.prompt import Prompt
14
+
15
+ from pydantic import BaseModel, HttpUrl, ValidationError, parse_obj_as
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def url_to_tempfile(url: str) -> str:
21
+ """
22
+ Fetch content from the given URL and save it to a temporary local file.
23
+
24
+ Args:
25
+ url (str): The URL of the content to fetch.
26
+
27
+ Returns:
28
+ str: The path to the temporary file where the content is saved.
29
+
30
+ Raises:
31
+ HTTPError: If there's any issue fetching the content.
32
+ """
33
+
34
+ response = requests.get(url)
35
+ response.raise_for_status() # Raise an exception for HTTP errors
36
+
37
+ # Create a temporary file and write the content
38
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".tmp") as temp_file:
39
+ temp_file.write(response.content)
40
+ return temp_file.name
41
+
42
+
43
+ def get_user_input(msg: str, color: str = "blue") -> str:
44
+ """
45
+ Prompt the user for input.
46
+ Args:
47
+ msg: printed prompt
48
+ color: color of the prompt
49
+ Returns:
50
+ user input
51
+ """
52
+ color_str = f"[{color}]{msg} " if color else msg + " "
53
+ print(color_str, end="")
54
+ return input("")
55
+
56
+
57
+ def get_list_from_user(
58
+ prompt: str = "Enter input (type 'done' or hit return to finish)",
59
+ n: int | None = None,
60
+ ) -> List[str]:
61
+ """
62
+ Prompt the user for inputs.
63
+ Args:
64
+ prompt: printed prompt
65
+ n: how many inputs to prompt for. If None, then prompt until done, otherwise
66
+ quit after n inputs.
67
+ Returns:
68
+ list of input strings
69
+ """
70
+ # Create an empty set to store the URLs.
71
+ input_set = set()
72
+
73
+ # Use a while loop to continuously ask the user for URLs.
74
+ for _ in range(n or 1000):
75
+ # Prompt the user for input.
76
+ input_str = Prompt.ask(f"[blue]{prompt}")
77
+
78
+ # Check if the user wants to exit the loop.
79
+ if input_str.lower() == "done" or input_str == "":
80
+ break
81
+
82
+ # if it is a URL, ask how many to crawl
83
+ if is_url(input_str):
84
+ url = input_str
85
+ input_str = Prompt.ask("[blue] How many new URLs to crawl?", default="0")
86
+ max_urls = int(input_str) + 1
87
+ tot_urls = list(find_urls(url, max_links=max_urls, max_depth=2))
88
+ tot_urls_str = "\n".join(tot_urls)
89
+ print(
90
+ f"""
91
+ Found these {len(tot_urls)} links upto depth 2:
92
+ {tot_urls_str}
93
+ """
94
+ )
95
+
96
+ input_set.update(tot_urls)
97
+ else:
98
+ input_set.add(input_str.strip())
99
+
100
+ return list(input_set)
101
+
102
+
103
+ class Url(BaseModel):
104
+ url: HttpUrl
105
+
106
+
107
+ def is_url(s: str) -> bool:
108
+ try:
109
+ Url(url=parse_obj_as(HttpUrl, s))
110
+ return True
111
+ except ValidationError:
112
+ return False
113
+
114
+
115
+ def get_urls_paths_bytes_indices(
116
+ inputs: List[str | bytes],
117
+ ) -> Tuple[List[int], List[int], List[int]]:
118
+ """
119
+ Given a list of inputs, return a
120
+ list of indices of URLs, list of indices of paths, list of indices of byte-contents.
121
+ Args:
122
+ inputs: list of strings or bytes
123
+ Returns:
124
+ list of Indices of URLs,
125
+ list of indices of paths,
126
+ list of indices of byte-contents
127
+ """
128
+ urls = []
129
+ paths = []
130
+ byte_list = []
131
+ for i, item in enumerate(inputs):
132
+ if isinstance(item, bytes):
133
+ byte_list.append(i)
134
+ continue
135
+ try:
136
+ Url(url=parse_obj_as(HttpUrl, item))
137
+ urls.append(i)
138
+ except ValidationError:
139
+ if os.path.exists(item):
140
+ paths.append(i)
141
+ else:
142
+ logger.warning(f"{item} is neither a URL nor a path.")
143
+ return urls, paths, byte_list
144
+
145
+
146
+ def crawl_url(url: str, max_urls: int = 1) -> List[str]:
147
+ """
148
+ Crawl starting at the url and return a list of URLs to be parsed,
149
+ up to a maximum of `max_urls`.
150
+ This has not been tested to work as intended. Ignore.
151
+ """
152
+ from trafilatura.spider import focused_crawler
153
+
154
+ if max_urls == 1:
155
+ # no need to crawl, just return the original list
156
+ return [url]
157
+
158
+ to_visit = None
159
+ known_urls = None
160
+
161
+ # Create a RobotFileParser object
162
+ robots = urllib.robotparser.RobotFileParser()
163
+ while True:
164
+ if known_urls is not None and len(known_urls) >= max_urls:
165
+ break
166
+ # Set the RobotFileParser object to the website's robots.txt file
167
+ robots.set_url(url + "/robots.txt")
168
+ robots.read()
169
+
170
+ if robots.can_fetch("*", url):
171
+ # Start or resume the crawl
172
+ to_visit, known_urls = focused_crawler(
173
+ url,
174
+ max_seen_urls=max_urls,
175
+ max_known_urls=max_urls,
176
+ todo=to_visit,
177
+ known_links=known_urls,
178
+ rules=robots,
179
+ )
180
+ if to_visit is None:
181
+ break
182
+
183
+ if known_urls is None:
184
+ return [url]
185
+ final_urls = [s.strip() for s in known_urls]
186
+ return list(final_urls)[:max_urls]
187
+
188
+
189
+ def find_urls(
190
+ url: str = "https://en.wikipedia.org/wiki/Generative_pre-trained_transformer",
191
+ max_links: int = 20,
192
+ visited: Optional[Set[str]] = None,
193
+ depth: int = 0,
194
+ max_depth: int = 2,
195
+ match_domain: bool = True,
196
+ ) -> Set[str]:
197
+ """
198
+ Recursively find all URLs on a given page.
199
+
200
+ Args:
201
+ url (str): The URL to start from.
202
+ max_links (int): The maximum number of links to find.
203
+ visited (set): A set of URLs that have already been visited.
204
+ depth (int): The current depth of the recursion.
205
+ max_depth (int): The maximum depth of the recursion.
206
+ match_domain (bool): Whether to only return URLs that are on the same domain.
207
+
208
+ Returns:
209
+ set: A set of URLs found on the page.
210
+ """
211
+
212
+ if visited is None:
213
+ visited = set()
214
+
215
+ if url in visited or depth > max_depth:
216
+ return visited
217
+
218
+ visited.add(url)
219
+ base_domain = urlparse(url).netloc
220
+
221
+ try:
222
+ response = requests.get(url, timeout=5)
223
+ response.raise_for_status()
224
+ soup = BeautifulSoup(response.text, "html.parser")
225
+ links = [
226
+ urljoin(url, a["href"]) # type: ignore
227
+ for a in soup.find_all("a", href=True)
228
+ ]
229
+
230
+ # Defrag links: discard links that are to portions of same page
231
+ defragged_links = list(
232
+ set(urldefrag(link).url for link in links) # type: ignore
233
+ )
234
+
235
+ # Filter links based on domain matching requirement
236
+ domain_matching_links = [
237
+ link for link in defragged_links if urlparse(link).netloc == base_domain
238
+ ]
239
+
240
+ # ensure url is first, since below we are taking first max_links urls
241
+ domain_matching_links = [url] + [x for x in domain_matching_links if x != url]
242
+
243
+ # If found links exceed max_links, return immediately
244
+ if len(domain_matching_links) >= max_links:
245
+ return set(domain_matching_links[:max_links])
246
+
247
+ for link in domain_matching_links:
248
+ if len(visited) >= max_links:
249
+ break
250
+
251
+ if link not in visited:
252
+ visited.update(
253
+ find_urls(
254
+ link,
255
+ max_links,
256
+ visited,
257
+ depth + 1,
258
+ max_depth,
259
+ match_domain,
260
+ )
261
+ )
262
+
263
+ except (requests.RequestException, Exception) as e:
264
+ print(f"Error fetching {url}. Error: {e}")
265
+
266
+ return set(list(visited)[:max_links])
267
+
268
+
269
+ def org_user_from_github(url: str) -> str:
270
+ parsed = urllib.parse.urlparse(url)
271
+ org, user = parsed.path.lstrip("/").split("/")
272
+ return f"{org}-{user}"
273
+
274
+
275
+ if __name__ == "__main__":
276
+ # Example usage
277
+ found_urls = set(fire.Fire(find_urls))
278
+ for url in found_urls:
279
+ print(url)
@@ -1,4 +1,4 @@
1
- from langroid.pydantic_v1 import BaseSettings
1
+ from pydantic_settings import BaseSettings
2
2
 
3
3
 
4
4
  class PromptsConfig(BaseSettings):
@@ -1,10 +1,49 @@
1
1
  """
2
- If we're on Pydantic v2, use the v1 namespace, else just use the main namespace.
2
+ Compatibility layer for Pydantic v2 migration.
3
3
 
4
- This allows compatibility with both Pydantic v1 and v2
4
+ This module now imports directly from Pydantic v2 since all internal code
5
+ has been migrated to use Pydantic v2 patterns.
5
6
  """
6
7
 
7
- try:
8
- from pydantic.v1 import * # noqa: F403, F401
9
- except ImportError:
10
- from pydantic import * # type: ignore # noqa: F403, F401
8
+ # Import everything from pydantic v2
9
+ from pydantic import * # noqa: F403, F401
10
+
11
+ # Import BaseSettings and SettingsConfigDict from pydantic-settings v2
12
+ from pydantic_settings import BaseSettings, SettingsConfigDict # noqa: F401
13
+
14
+ # Explicitly re-export commonly used items for better IDE support and type checking
15
+ from pydantic import ( # noqa: F401
16
+ BaseModel,
17
+ Field,
18
+ ConfigDict,
19
+ ValidationError,
20
+ field_validator,
21
+ model_validator,
22
+ create_model,
23
+ HttpUrl,
24
+ AnyUrl,
25
+ TypeAdapter,
26
+ parse_obj_as,
27
+ )
28
+
29
+ # Legacy names are already provided by pydantic v2 for backward compatibility
30
+ # No need to redefine validator and root_validator as they are already imported above
31
+
32
+ # Explicitly export all items for mypy
33
+ __all__ = [
34
+ "BaseModel",
35
+ "BaseSettings",
36
+ "SettingsConfigDict",
37
+ "Field",
38
+ "ConfigDict",
39
+ "ValidationError",
40
+ "field_validator",
41
+ "model_validator",
42
+ "create_model",
43
+ "HttpUrl",
44
+ "AnyUrl",
45
+ "TypeAdapter",
46
+ "parse_obj_as",
47
+ "validator",
48
+ "root_validator",
49
+ ]
@@ -0,0 +1,36 @@
1
+ """
2
+ Compatibility layer for Pydantic v2 migration.
3
+
4
+ This module now imports directly from Pydantic v2 since all internal code
5
+ has been migrated to use Pydantic v2 patterns.
6
+ """
7
+
8
+ # Import everything from pydantic v2
9
+ from pydantic import * # noqa: F403, F401
10
+
11
+ # Import BaseSettings from pydantic-settings v2
12
+ from pydantic_settings import BaseSettings # noqa: F401
13
+
14
+ # Explicitly re-export commonly used items for better IDE support and type checking
15
+ from pydantic import ( # noqa: F401
16
+ BaseModel,
17
+ Field,
18
+ ConfigDict,
19
+ ValidationError,
20
+ field_validator,
21
+ model_validator,
22
+ create_model,
23
+ HttpUrl,
24
+ AnyUrl,
25
+ TypeAdapter,
26
+ )
27
+
28
+ # Legacy names that map to v2 equivalents
29
+ validator = field_validator # noqa: F401
30
+ root_validator = model_validator # noqa: F401
31
+
32
+
33
+ # For parse_obj_as, we need to create a wrapper function
34
+ def parse_obj_as(type_, obj):
35
+ """Compatibility wrapper for parse_obj_as which was removed in Pydantic v2."""
36
+ return TypeAdapter(type_).validate_python(obj)
@@ -1,4 +1,11 @@
1
- try:
2
- from pydantic.v1.main import * # noqa: F403, F401
3
- except ImportError:
4
- from pydantic.main import * # type: ignore # noqa: F403, F401
1
+ """
2
+ Compatibility layer for Pydantic v2 migration.
3
+
4
+ This module now imports directly from Pydantic v2 since all internal code
5
+ has been migrated to use Pydantic v2 patterns.
6
+ """
7
+
8
+ # Explicitly export BaseModel for better type checking
9
+ from pydantic.main import * # noqa: F403, F401
10
+
11
+ from langroid.pydantic_v1 import BaseModel # noqa: F401
@@ -0,0 +1,11 @@
1
+ """
2
+ Compatibility layer for Pydantic v2 migration.
3
+
4
+ This module now imports directly from Pydantic v2 since all internal code
5
+ has been migrated to use Pydantic v2 patterns.
6
+ """
7
+
8
+ # Explicitly export BaseModel for better type checking
9
+ from pydantic.main import * # noqa: F403, F401
10
+
11
+ from pydantic import BaseModel # noqa: F401
@@ -4,8 +4,7 @@ from contextlib import contextmanager
4
4
  from typing import Any, Dict, Iterator, List, Literal, cast
5
5
 
6
6
  from dotenv import find_dotenv, load_dotenv
7
-
8
- from langroid.pydantic_v1 import BaseSettings
7
+ from pydantic_settings import BaseSettings, SettingsConfigDict
9
8
 
10
9
  # Global reentrant lock to serialize any modifications to the global settings.
11
10
  _global_lock = threading.RLock()
@@ -22,8 +21,7 @@ class Settings(BaseSettings):
22
21
  quiet: bool = False # quiet mode (i.e. suppress all output)?
23
22
  notebook: bool = False # running in a notebook?
24
23
 
25
- class Config:
26
- extra = "forbid"
24
+ model_config = SettingsConfigDict(extra="forbid")
27
25
 
28
26
 
29
27
  # Load environment variables from .env file.
@@ -60,8 +58,10 @@ class SettingsProxy:
60
58
  # Return a dict view of the settings as seen by the caller.
61
59
  # Note that temporary overrides are not “merged” with global settings.
62
60
  if hasattr(_thread_local, "override"):
63
- return cast(Dict[str, Any], cast(Settings, _thread_local.override.dict()))
64
- return _global_settings.dict()
61
+ return cast(
62
+ Dict[str, Any], cast(Settings, _thread_local.override.model_dump())
63
+ )
64
+ return _global_settings.model_dump()
65
65
 
66
66
 
67
67
  settings = SettingsProxy()
@@ -76,7 +76,7 @@ def update_global_settings(cfg: BaseSettings, keys: List[str]) -> None:
76
76
 
77
77
  This updates the global default.
78
78
  """
79
- config_dict = cfg.dict()
79
+ config_dict = cfg.model_dump()
80
80
  filtered_config = {key: config_dict[key] for key in keys if key in config_dict}
81
81
  new_settings = Settings(**filtered_config)
82
82
  _global_settings.__dict__.update(new_settings.__dict__)
@@ -117,7 +117,9 @@ def quiet_mode(quiet: bool = True) -> Iterator[None]:
117
117
  if quiet is already True (from an outer context),
118
118
  then it remains True even if a nested context passes quiet=False.
119
119
  """
120
- current_effective = settings.dict() # get the current thread's effective settings
120
+ current_effective = (
121
+ settings.model_dump()
122
+ ) # get the current thread's effective settings
121
123
  # Create a new settings instance from the current effective state.
122
124
  temp = Settings(**current_effective)
123
125
  # Merge the new flag: once quiet is enabled, it stays enabled.
@@ -132,6 +134,6 @@ def set_env(settings_instance: BaseSettings) -> None:
132
134
 
133
135
  Each field in the settings is written to os.environ.
134
136
  """
135
- for field_name, field in settings_instance.__class__.__fields__.items():
136
- env_var_name = field.field_info.extra.get("env", field_name).upper()
137
- os.environ[env_var_name] = str(settings_instance.dict()[field_name])
137
+ for field_name, field in settings_instance.__class__.model_fields.items():
138
+ env_var_name = field.alias or field_name.upper()
139
+ os.environ[env_var_name] = str(settings_instance.model_dump()[field_name])
@@ -0,0 +1,141 @@
1
+ import os
2
+ import threading
3
+ from contextlib import contextmanager
4
+ from typing import Any, Dict, Iterator, List, Literal, cast
5
+
6
+ from dotenv import find_dotenv, load_dotenv
7
+ from pydantic_settings import BaseSettings
8
+
9
+ from pydantic import ConfigDict
10
+
11
+ # Global reentrant lock to serialize any modifications to the global settings.
12
+ _global_lock = threading.RLock()
13
+
14
+
15
+ class Settings(BaseSettings):
16
+ debug: bool = False # show debug messages?
17
+ max_turns: int = -1 # maximum number of turns in a task (to avoid inf loop)
18
+ progress: bool = False # show progress spinners/bars?
19
+ stream: bool = True # stream output?
20
+ cache: bool = True # use cache?
21
+ cache_type: Literal["redis", "fakeredis", "none"] = "redis" # cache type
22
+ chat_model: str = "" # language model name, e.g. litellm/ollama/llama2
23
+ quiet: bool = False # quiet mode (i.e. suppress all output)?
24
+ notebook: bool = False # running in a notebook?
25
+
26
+ model_config = ConfigDict(extra="forbid")
27
+
28
+
29
+ # Load environment variables from .env file.
30
+ load_dotenv(find_dotenv(usecwd=True))
31
+
32
+ # The global (default) settings instance.
33
+ # This is updated by update_global_settings() and set_global().
34
+ _global_settings = Settings()
35
+
36
+ # Thread-local storage for temporary (per-thread) settings overrides.
37
+ _thread_local = threading.local()
38
+
39
+
40
+ class SettingsProxy:
41
+ """
42
+ A proxy for the settings that returns a thread‐local override if set,
43
+ or else falls back to the global settings.
44
+ """
45
+
46
+ def __getattr__(self, name: str) -> Any:
47
+ # If the calling thread has set an override, use that.
48
+ if hasattr(_thread_local, "override"):
49
+ return getattr(_thread_local.override, name)
50
+ return getattr(_global_settings, name)
51
+
52
+ def __setattr__(self, name: str, value: Any) -> None:
53
+ # All writes go to the global settings.
54
+ setattr(_global_settings, name, value)
55
+
56
+ def update(self, new_settings: Settings) -> None:
57
+ _global_settings.__dict__.update(new_settings.__dict__)
58
+
59
+ def dict(self) -> Dict[str, Any]:
60
+ # Return a dict view of the settings as seen by the caller.
61
+ # Note that temporary overrides are not “merged” with global settings.
62
+ if hasattr(_thread_local, "override"):
63
+ return cast(
64
+ Dict[str, Any], cast(Settings, _thread_local.override.model_dump())
65
+ )
66
+ return _global_settings.model_dump()
67
+
68
+
69
+ settings = SettingsProxy()
70
+
71
+
72
+ def update_global_settings(cfg: BaseSettings, keys: List[str]) -> None:
73
+ """
74
+ Update global settings so that modules can later access them via, e.g.,
75
+
76
+ from langroid.utils.configuration import settings
77
+ if settings.debug: ...
78
+
79
+ This updates the global default.
80
+ """
81
+ config_dict = cfg.model_dump()
82
+ filtered_config = {key: config_dict[key] for key in keys if key in config_dict}
83
+ new_settings = Settings(**filtered_config)
84
+ _global_settings.__dict__.update(new_settings.__dict__)
85
+
86
+
87
+ def set_global(key_vals: Settings) -> None:
88
+ """
89
+ Update the global settings object.
90
+ """
91
+ _global_settings.__dict__.update(key_vals.__dict__)
92
+
93
+
94
+ @contextmanager
95
+ def temporary_settings(temp_settings: Settings) -> Iterator[None]:
96
+ """
97
+ Temporarily override the settings for the calling thread.
98
+
99
+ Within the context, any access to "settings" will use the provided temporary
100
+ settings. Once the context is exited, the thread reverts to the global settings.
101
+ """
102
+ saved = getattr(_thread_local, "override", None)
103
+ _thread_local.override = temp_settings
104
+ try:
105
+ yield
106
+ finally:
107
+ if saved is not None:
108
+ _thread_local.override = saved
109
+ else:
110
+ del _thread_local.override
111
+
112
+
113
+ @contextmanager
114
+ def quiet_mode(quiet: bool = True) -> Iterator[None]:
115
+ """
116
+ Temporarily override settings.quiet for the current thread.
117
+ This implementation builds on the thread‑local temporary_settings context manager.
118
+ The effective quiet mode is merged:
119
+ if quiet is already True (from an outer context),
120
+ then it remains True even if a nested context passes quiet=False.
121
+ """
122
+ current_effective = (
123
+ settings.model_dump()
124
+ ) # get the current thread's effective settings
125
+ # Create a new settings instance from the current effective state.
126
+ temp = Settings(**current_effective)
127
+ # Merge the new flag: once quiet is enabled, it stays enabled.
128
+ temp.quiet = settings.quiet or quiet
129
+ with temporary_settings(temp):
130
+ yield
131
+
132
+
133
+ def set_env(settings_instance: BaseSettings) -> None:
134
+ """
135
+ Set environment variables from a BaseSettings instance.
136
+
137
+ Each field in the settings is written to os.environ.
138
+ """
139
+ for field_name, field in settings_instance.__class__.__fields__.items():
140
+ env_var_name = field.field_info.extra.get("env", field_name).upper()
141
+ os.environ[env_var_name] = str(settings_instance.model_dump()[field_name])
@@ -1,4 +1,4 @@
1
- from langroid.pydantic_v1 import BaseModel
1
+ from pydantic import BaseModel
2
2
 
3
3
 
4
4
  # Define the ANSI escape sequences for various colors and reset
@@ -0,0 +1,32 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ # Define the ANSI escape sequences for various colors and reset
5
+ class Colors(BaseModel):
6
+ RED: str = "\033[31m"
7
+ BLUE: str = "\033[34m"
8
+ GREEN: str = "\033[32m"
9
+ GREEN_DIMMER: str = "\033[38;5;22m" # very dark green
10
+ GREEN_DIM: str = "\033[38;5;28m" # medium-dim green
11
+ ORANGE: str = "\033[33m" # no standard ANSI color for orange; using yellow
12
+ CYAN: str = "\033[36m"
13
+ MAGENTA: str = "\033[35m"
14
+ YELLOW: str = "\033[33m"
15
+ RESET: str = "\033[0m"
16
+
17
+
18
+ NO_ANSWER = "DO-NOT-KNOW"
19
+ DONE = "DONE"
20
+ USER_QUIT_STRINGS = ["q", "x", "quit", "exit", "bye", DONE]
21
+ PASS = "__PASS__"
22
+ PASS_TO = PASS + ":"
23
+ SEND_TO = "__SEND__:"
24
+ TOOL = "TOOL"
25
+ # This is a recommended setting for TaskConfig.addressing_prefix if using it at all;
26
+ # prefer to use `RecipientTool` to allow agents addressing others.
27
+ # Caution the AT string should NOT contain any 'word' characters, i.e.
28
+ # it no letters, digits or underscores.
29
+ # See tests/main/test_msg_routing for example usage
30
+ AT = "|@|"
31
+ TOOL_BEGIN = "TOOL_BEGIN"
32
+ TOOL_END = "TOOL_END"