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.
- langroid/agent/base.py +39 -17
- langroid/agent/base.py-e +2216 -0
- langroid/agent/callbacks/chainlit.py +2 -1
- langroid/agent/chat_agent.py +73 -55
- langroid/agent/chat_agent.py-e +2086 -0
- langroid/agent/chat_document.py +7 -7
- langroid/agent/chat_document.py-e +513 -0
- langroid/agent/openai_assistant.py +9 -9
- langroid/agent/openai_assistant.py-e +882 -0
- langroid/agent/special/arangodb/arangodb_agent.py +10 -18
- langroid/agent/special/arangodb/arangodb_agent.py-e +648 -0
- langroid/agent/special/arangodb/tools.py +3 -3
- langroid/agent/special/doc_chat_agent.py +16 -14
- langroid/agent/special/lance_rag/critic_agent.py +2 -2
- langroid/agent/special/lance_rag/query_planner_agent.py +4 -4
- langroid/agent/special/lance_tools.py +6 -5
- langroid/agent/special/lance_tools.py-e +61 -0
- langroid/agent/special/neo4j/neo4j_chat_agent.py +3 -7
- langroid/agent/special/neo4j/neo4j_chat_agent.py-e +430 -0
- langroid/agent/special/relevance_extractor_agent.py +1 -1
- langroid/agent/special/sql/sql_chat_agent.py +11 -3
- langroid/agent/task.py +9 -87
- langroid/agent/task.py-e +2418 -0
- langroid/agent/tool_message.py +33 -17
- langroid/agent/tool_message.py-e +400 -0
- langroid/agent/tools/file_tools.py +4 -2
- langroid/agent/tools/file_tools.py-e +234 -0
- langroid/agent/tools/mcp/fastmcp_client.py +19 -6
- langroid/agent/tools/mcp/fastmcp_client.py-e +584 -0
- langroid/agent/tools/orchestration.py +22 -17
- langroid/agent/tools/orchestration.py-e +301 -0
- langroid/agent/tools/recipient_tool.py +3 -3
- langroid/agent/tools/task_tool.py +22 -16
- langroid/agent/tools/task_tool.py-e +249 -0
- langroid/agent/xml_tool_message.py +90 -35
- langroid/agent/xml_tool_message.py-e +392 -0
- langroid/cachedb/base.py +1 -1
- langroid/embedding_models/base.py +2 -2
- langroid/embedding_models/models.py +3 -7
- langroid/embedding_models/models.py-e +563 -0
- langroid/exceptions.py +4 -1
- langroid/language_models/azure_openai.py +2 -2
- langroid/language_models/azure_openai.py-e +134 -0
- langroid/language_models/base.py +6 -4
- langroid/language_models/base.py-e +812 -0
- langroid/language_models/client_cache.py +64 -0
- langroid/language_models/config.py +2 -4
- langroid/language_models/config.py-e +18 -0
- langroid/language_models/model_info.py +9 -1
- langroid/language_models/model_info.py-e +483 -0
- langroid/language_models/openai_gpt.py +119 -20
- langroid/language_models/openai_gpt.py-e +2280 -0
- langroid/language_models/provider_params.py +3 -22
- langroid/language_models/provider_params.py-e +153 -0
- langroid/mytypes.py +11 -4
- langroid/mytypes.py-e +132 -0
- langroid/parsing/code_parser.py +1 -1
- langroid/parsing/file_attachment.py +1 -1
- langroid/parsing/file_attachment.py-e +246 -0
- langroid/parsing/md_parser.py +14 -4
- langroid/parsing/md_parser.py-e +574 -0
- langroid/parsing/parser.py +22 -7
- langroid/parsing/parser.py-e +410 -0
- langroid/parsing/repo_loader.py +3 -1
- langroid/parsing/repo_loader.py-e +812 -0
- langroid/parsing/search.py +1 -1
- langroid/parsing/url_loader.py +17 -51
- langroid/parsing/url_loader.py-e +683 -0
- langroid/parsing/urls.py +5 -4
- langroid/parsing/urls.py-e +279 -0
- langroid/prompts/prompts_config.py +1 -1
- langroid/pydantic_v1/__init__.py +45 -6
- langroid/pydantic_v1/__init__.py-e +36 -0
- langroid/pydantic_v1/main.py +11 -4
- langroid/pydantic_v1/main.py-e +11 -0
- langroid/utils/configuration.py +13 -11
- langroid/utils/configuration.py-e +141 -0
- langroid/utils/constants.py +1 -1
- langroid/utils/constants.py-e +32 -0
- langroid/utils/globals.py +21 -5
- langroid/utils/globals.py-e +49 -0
- langroid/utils/html_logger.py +2 -1
- langroid/utils/html_logger.py-e +825 -0
- langroid/utils/object_registry.py +1 -1
- langroid/utils/object_registry.py-e +66 -0
- langroid/utils/pydantic_utils.py +55 -28
- langroid/utils/pydantic_utils.py-e +602 -0
- langroid/utils/types.py +2 -2
- langroid/utils/types.py-e +113 -0
- langroid/vector_store/base.py +3 -3
- langroid/vector_store/lancedb.py +5 -5
- langroid/vector_store/lancedb.py-e +404 -0
- langroid/vector_store/meilisearch.py +2 -2
- langroid/vector_store/pineconedb.py +4 -4
- langroid/vector_store/pineconedb.py-e +427 -0
- langroid/vector_store/postgres.py +1 -1
- langroid/vector_store/qdrantdb.py +3 -3
- langroid/vector_store/weaviatedb.py +1 -1
- {langroid-0.58.2.dist-info → langroid-0.59.0b1.dist-info}/METADATA +3 -2
- langroid-0.59.0b1.dist-info/RECORD +181 -0
- langroid/agent/special/doc_chat_task.py +0 -0
- langroid/mcp/__init__.py +0 -1
- langroid/mcp/server/__init__.py +0 -1
- langroid-0.58.2.dist-info/RECORD +0 -145
- {langroid-0.58.2.dist-info → langroid-0.59.0b1.dist-info}/WHEEL +0 -0
- {langroid-0.58.2.dist-info → langroid-0.59.0b1.dist-info}/licenses/LICENSE +0 -0
langroid/agent/task.py
CHANGED
@@ -27,6 +27,7 @@ from typing import (
|
|
27
27
|
)
|
28
28
|
|
29
29
|
import numpy as np
|
30
|
+
from pydantic import BaseModel, ConfigDict
|
30
31
|
from rich import print
|
31
32
|
from rich.markup import escape
|
32
33
|
|
@@ -45,7 +46,6 @@ from langroid.exceptions import InfiniteLoopException
|
|
45
46
|
from langroid.mytypes import Entity
|
46
47
|
from langroid.parsing.parse_json import extract_top_level_json
|
47
48
|
from langroid.parsing.routing import parse_addressed_message
|
48
|
-
from langroid.pydantic_v1 import BaseModel
|
49
49
|
from langroid.utils.configuration import settings
|
50
50
|
from langroid.utils.constants import (
|
51
51
|
DONE,
|
@@ -483,7 +483,7 @@ class Task:
|
|
483
483
|
self.message_counter.clear()
|
484
484
|
# create a unique string that will not likely be in any message,
|
485
485
|
# so we always have a message with count=1
|
486
|
-
self.message_counter.update([hash("___NO_MESSAGE___")])
|
486
|
+
self.message_counter.update([str(hash("___NO_MESSAGE___"))])
|
487
487
|
|
488
488
|
def _cache_session_store(self, key: str, value: str) -> None:
|
489
489
|
"""
|
@@ -2083,7 +2083,7 @@ class Task:
|
|
2083
2083
|
"""
|
2084
2084
|
from langroid.agent.chat_document import ChatDocLoggerFields
|
2085
2085
|
|
2086
|
-
default_values = ChatDocLoggerFields().
|
2086
|
+
default_values = ChatDocLoggerFields().model_dump().values()
|
2087
2087
|
msg_str_tsv = "\t".join(str(v) for v in default_values)
|
2088
2088
|
if msg is not None:
|
2089
2089
|
msg_str_tsv = msg.tsv_str()
|
@@ -2144,7 +2144,7 @@ class Task:
|
|
2144
2144
|
else:
|
2145
2145
|
# Get fields from the message
|
2146
2146
|
fields = msg.log_fields()
|
2147
|
-
fields_dict = fields.
|
2147
|
+
fields_dict = fields.model_dump()
|
2148
2148
|
fields_dict.update(
|
2149
2149
|
{
|
2150
2150
|
"responder": str(resp),
|
@@ -2155,13 +2155,11 @@ class Task:
|
|
2155
2155
|
|
2156
2156
|
# Create a ChatDocLoggerFields-like object for the HTML logger
|
2157
2157
|
# Create a simple BaseModel subclass dynamically
|
2158
|
-
from
|
2158
|
+
from pydantic import BaseModel
|
2159
2159
|
|
2160
2160
|
class LogFields(BaseModel):
|
2161
|
-
|
2162
|
-
extra = "allow" # Allow extra fields
|
2161
|
+
model_config = ConfigDict(extra="allow") # Allow extra fields
|
2163
2162
|
|
2164
|
-
# Create instance with the fields from fields_dict
|
2165
2163
|
log_obj = LogFields(**fields_dict)
|
2166
2164
|
self.html_logger.log(log_obj)
|
2167
2165
|
|
@@ -2173,8 +2171,6 @@ class Task:
|
|
2173
2171
|
"""
|
2174
2172
|
if recipient == "":
|
2175
2173
|
return True
|
2176
|
-
# native responders names are USER, LLM, AGENT,
|
2177
|
-
# and the names of subtasks are from Task.name attribute
|
2178
2174
|
responder_names = [self.name.lower()] + [
|
2179
2175
|
r.name.lower() for r in self.responders
|
2180
2176
|
]
|
@@ -2184,7 +2180,6 @@ class Task:
|
|
2184
2180
|
"""
|
2185
2181
|
Is the recipient explicitly specified and does not match responder "e" ?
|
2186
2182
|
"""
|
2187
|
-
# Note that recipient could be specified as an Entity or a Task name
|
2188
2183
|
return (
|
2189
2184
|
self.pending_message is not None
|
2190
2185
|
and (recipient := self.pending_message.metadata.recipient) != ""
|
@@ -2195,8 +2190,6 @@ class Task:
|
|
2195
2190
|
|
2196
2191
|
def _user_can_respond(self) -> bool:
|
2197
2192
|
return self.interactive or (
|
2198
|
-
# regardless of self.interactive, if a msg is explicitly addressed to
|
2199
|
-
# user, then wait for user response
|
2200
2193
|
self.pending_message is not None
|
2201
2194
|
and self.pending_message.metadata.recipient == Entity.USER
|
2202
2195
|
and not self.agent.has_tool_message_attempt(self.pending_message)
|
@@ -2204,19 +2197,13 @@ class Task:
|
|
2204
2197
|
|
2205
2198
|
def _can_respond(self, e: Responder) -> bool:
|
2206
2199
|
user_can_respond = self._user_can_respond()
|
2207
|
-
|
2208
2200
|
if self.pending_sender == e or (e == Entity.USER and not user_can_respond):
|
2209
|
-
# sender is same as e (an entity cannot respond to its own msg),
|
2210
|
-
# or user cannot respond
|
2211
2201
|
return False
|
2212
|
-
|
2213
2202
|
if self.pending_message is None:
|
2214
2203
|
return True
|
2215
2204
|
if isinstance(e, Task) and not e.agent.can_respond(self.pending_message):
|
2216
2205
|
return False
|
2217
|
-
|
2218
2206
|
if self._recipient_mismatch(e):
|
2219
|
-
# Cannot respond if not addressed to this entity
|
2220
2207
|
return False
|
2221
2208
|
return self.pending_message.metadata.block != e
|
2222
2209
|
|
@@ -2230,7 +2217,6 @@ class Task:
|
|
2230
2217
|
Args:
|
2231
2218
|
enable (bool): value of `self.color_log` to set to,
|
2232
2219
|
which will enable/diable rich logging
|
2233
|
-
|
2234
2220
|
"""
|
2235
2221
|
self.color_log = enable
|
2236
2222
|
|
@@ -2247,16 +2233,13 @@ class Task:
|
|
2247
2233
|
Args:
|
2248
2234
|
msg (ChatDocument|str|None): message to parse
|
2249
2235
|
addressing_prefix (str): prefix to address other agents or entities,
|
2250
|
-
|
2236
|
+
(e.g. "@". See documentation of `TaskConfig` for details).
|
2251
2237
|
Returns:
|
2252
2238
|
Tuple[bool|None, str|None, str|None]:
|
2253
2239
|
bool: true=PASS, false=SEND, or None if neither
|
2254
2240
|
str: recipient, or None
|
2255
2241
|
str: content to send, or None
|
2256
2242
|
"""
|
2257
|
-
# handle routing instruction-strings in result if any,
|
2258
|
-
# such as PASS, PASS_TO, or SEND
|
2259
|
-
|
2260
2243
|
msg_str = msg.content if isinstance(msg, ChatDocument) else msg
|
2261
2244
|
if (
|
2262
2245
|
self.agent.has_tool_message_attempt(msg)
|
@@ -2264,10 +2247,7 @@ class Task:
|
|
2264
2247
|
and not msg_str.startswith(PASS_TO)
|
2265
2248
|
and not msg_str.startswith(SEND_TO)
|
2266
2249
|
):
|
2267
|
-
# if there's an attempted tool-call, we ignore any routing strings,
|
2268
|
-
# unless they are at the start of the msg
|
2269
2250
|
return None, None, None
|
2270
|
-
|
2271
2251
|
content = msg.content if isinstance(msg, ChatDocument) else msg
|
2272
2252
|
content = content.strip()
|
2273
2253
|
if PASS in content and PASS_TO not in content:
|
@@ -2279,10 +2259,7 @@ class Task:
|
|
2279
2259
|
and (addressee_content := parse_addressed_message(content, SEND_TO))[0]
|
2280
2260
|
is not None
|
2281
2261
|
):
|
2282
|
-
# Note this will discard any portion of content BEFORE SEND_TO.
|
2283
|
-
# TODO maybe make this configurable.
|
2284
2262
|
(addressee, content_to_send) = addressee_content
|
2285
|
-
# if no content then treat same as PASS_TO
|
2286
2263
|
if content_to_send == "":
|
2287
2264
|
return True, addressee, None
|
2288
2265
|
else:
|
@@ -2296,12 +2273,10 @@ class Task:
|
|
2296
2273
|
is not None
|
2297
2274
|
):
|
2298
2275
|
(addressee, content_to_send) = addressee_content
|
2299
|
-
# if no content then treat same as PASS_TO
|
2300
2276
|
if content_to_send == "":
|
2301
2277
|
return True, addressee, None
|
2302
2278
|
else:
|
2303
2279
|
return False, addressee, content_to_send
|
2304
|
-
|
2305
2280
|
return None, None, None
|
2306
2281
|
|
2307
2282
|
def _classify_event(
|
@@ -2310,19 +2285,13 @@ class Task:
|
|
2310
2285
|
"""Classify a message into an AgentEvent for sequence matching."""
|
2311
2286
|
if msg is None:
|
2312
2287
|
return AgentEvent(event_type=EventType.NO_RESPONSE)
|
2313
|
-
|
2314
|
-
# Determine the event type based on responder and message content
|
2315
2288
|
event_type = EventType.NO_RESPONSE
|
2316
2289
|
tool_name = None
|
2317
|
-
|
2318
|
-
# Check if there are tool messages
|
2319
2290
|
tool_messages = self.agent.try_get_tool_messages(msg, all_tools=True)
|
2320
2291
|
if tool_messages:
|
2321
2292
|
event_type = EventType.TOOL
|
2322
2293
|
if len(tool_messages) == 1:
|
2323
2294
|
tool_name = tool_messages[0].request
|
2324
|
-
|
2325
|
-
# Check responder type
|
2326
2295
|
if responder == Entity.LLM and not tool_messages:
|
2327
2296
|
event_type = EventType.LLM_RESPONSE
|
2328
2297
|
elif responder == Entity.AGENT:
|
@@ -2330,21 +2299,17 @@ class Task:
|
|
2330
2299
|
elif responder == Entity.USER:
|
2331
2300
|
event_type = EventType.USER_RESPONSE
|
2332
2301
|
elif isinstance(responder, Task):
|
2333
|
-
# For sub-task responses, check the sender in metadata
|
2334
2302
|
if msg.metadata.sender == Entity.LLM:
|
2335
2303
|
event_type = EventType.LLM_RESPONSE
|
2336
2304
|
elif msg.metadata.sender == Entity.AGENT:
|
2337
2305
|
event_type = EventType.AGENT_RESPONSE
|
2338
2306
|
else:
|
2339
2307
|
event_type = EventType.USER_RESPONSE
|
2340
|
-
|
2341
|
-
# Get sender name
|
2342
2308
|
sender_name = None
|
2343
2309
|
if isinstance(responder, Entity):
|
2344
2310
|
sender_name = responder.value
|
2345
2311
|
elif isinstance(responder, Task):
|
2346
2312
|
sender_name = responder.name
|
2347
|
-
|
2348
2313
|
return AgentEvent(
|
2349
2314
|
event_type=event_type,
|
2350
2315
|
tool_name=tool_name,
|
@@ -2356,17 +2321,13 @@ class Task:
|
|
2356
2321
|
) -> List[ChatDocument]:
|
2357
2322
|
"""Get the chain of messages from response sequence."""
|
2358
2323
|
if max_depth is None:
|
2359
|
-
# Get max depth needed from all sequences
|
2360
2324
|
max_depth = 50 # default fallback
|
2361
|
-
|
2362
|
-
|
2363
|
-
|
2364
|
-
# Simply return the last max_depth elements from response_sequence
|
2325
|
+
if self._parsed_done_sequences:
|
2326
|
+
max_depth = max(len(seq.events) for seq in self._parsed_done_sequences)
|
2365
2327
|
return self.response_sequence[-max_depth:]
|
2366
2328
|
|
2367
2329
|
def _matches_event(self, actual: AgentEvent, expected: AgentEvent) -> bool:
|
2368
2330
|
"""Check if an actual event matches an expected event pattern."""
|
2369
|
-
# Check event type
|
2370
2331
|
if expected.event_type == EventType.SPECIFIC_TOOL:
|
2371
2332
|
if actual.event_type != EventType.TOOL:
|
2372
2333
|
return False
|
@@ -2374,53 +2335,36 @@ class Task:
|
|
2374
2335
|
return False
|
2375
2336
|
elif actual.event_type != expected.event_type:
|
2376
2337
|
return False
|
2377
|
-
|
2378
|
-
# Check sender if specified
|
2379
2338
|
if expected.sender and actual.sender != expected.sender:
|
2380
2339
|
return False
|
2381
|
-
|
2382
|
-
# TODO: Add content pattern matching for CONTENT_MATCH type
|
2383
|
-
|
2384
2340
|
return True
|
2385
2341
|
|
2386
2342
|
def _matches_sequence(
|
2387
2343
|
self, msg_chain: List[ChatDocument], sequence: DoneSequence
|
2388
2344
|
) -> bool:
|
2389
2345
|
"""Check if a message chain matches a done sequence.
|
2390
|
-
|
2391
2346
|
We traverse the message chain and try to match the sequence events.
|
2392
2347
|
The events don't have to be consecutive in the chain.
|
2393
2348
|
"""
|
2394
2349
|
if not sequence.events:
|
2395
2350
|
return False
|
2396
|
-
|
2397
|
-
# Convert messages to events
|
2398
2351
|
events = []
|
2399
2352
|
for i, msg in enumerate(msg_chain):
|
2400
|
-
# Determine responder from metadata or by checking previous message
|
2401
2353
|
responder = None
|
2402
2354
|
if msg.metadata.sender:
|
2403
2355
|
responder = msg.metadata.sender
|
2404
2356
|
elif msg.metadata.sender_name:
|
2405
|
-
# Could be a task name - keep as None for now since we can't resolve
|
2406
|
-
# the actual Task object from just the name
|
2407
2357
|
responder = None
|
2408
|
-
|
2409
2358
|
event = self._classify_event(msg, responder)
|
2410
2359
|
if event:
|
2411
2360
|
events.append(event)
|
2412
|
-
|
2413
|
-
# Try to match the sequence
|
2414
2361
|
seq_idx = 0
|
2415
2362
|
for event in events:
|
2416
2363
|
if seq_idx >= len(sequence.events):
|
2417
2364
|
break
|
2418
|
-
|
2419
2365
|
expected = sequence.events[seq_idx]
|
2420
2366
|
if self._matches_event(event, expected):
|
2421
2367
|
seq_idx += 1
|
2422
|
-
|
2423
|
-
# Check if we matched the entire sequence
|
2424
2368
|
return seq_idx == len(sequence.events)
|
2425
2369
|
|
2426
2370
|
def close_loggers(self) -> None:
|
@@ -2438,43 +2382,26 @@ class Task:
|
|
2438
2382
|
current_responder: Optional[Responder],
|
2439
2383
|
) -> bool:
|
2440
2384
|
"""Check if the message chain plus current message matches a done sequence.
|
2441
|
-
|
2442
2385
|
Process messages in reverse order (newest first) and match against
|
2443
2386
|
the sequence events in reverse order.
|
2444
2387
|
"""
|
2445
|
-
# Add current message to chain if not already there
|
2446
2388
|
if not msg_chain or msg_chain[-1].id() != current_msg.id():
|
2447
2389
|
msg_chain = msg_chain + [current_msg]
|
2448
|
-
|
2449
|
-
# If we don't have enough messages for the sequence, can't match
|
2450
2390
|
if len(msg_chain) < len(sequence.events):
|
2451
2391
|
return False
|
2452
|
-
|
2453
|
-
# Process in reverse order - start from the end of both lists
|
2454
2392
|
seq_idx = len(sequence.events) - 1
|
2455
2393
|
msg_idx = len(msg_chain) - 1
|
2456
|
-
|
2457
2394
|
while seq_idx >= 0 and msg_idx >= 0:
|
2458
2395
|
msg = msg_chain[msg_idx]
|
2459
2396
|
expected = sequence.events[seq_idx]
|
2460
|
-
|
2461
|
-
# Determine responder for this message
|
2462
2397
|
if msg_idx == len(msg_chain) - 1 and current_responder is not None:
|
2463
|
-
# For the last message, use the current responder
|
2464
2398
|
responder = current_responder
|
2465
2399
|
else:
|
2466
|
-
# For other messages, determine from metadata
|
2467
2400
|
responder = msg.metadata.sender
|
2468
|
-
|
2469
|
-
# Classify the event
|
2470
2401
|
event = self._classify_event(msg, responder)
|
2471
2402
|
if not event:
|
2472
2403
|
return False
|
2473
|
-
|
2474
|
-
# Check if it matches
|
2475
2404
|
matched = False
|
2476
|
-
|
2477
|
-
# Special handling for CONTENT_MATCH
|
2478
2405
|
if (
|
2479
2406
|
expected.event_type == EventType.CONTENT_MATCH
|
2480
2407
|
and expected.content_pattern
|
@@ -2483,14 +2410,9 @@ class Task:
|
|
2483
2410
|
matched = True
|
2484
2411
|
elif self._matches_event(event, expected):
|
2485
2412
|
matched = True
|
2486
|
-
|
2487
2413
|
if not matched:
|
2488
|
-
# Strict matching - no skipping allowed
|
2489
2414
|
return False
|
2490
2415
|
else:
|
2491
|
-
# Matched! Move to next expected event
|
2492
2416
|
seq_idx -= 1
|
2493
2417
|
msg_idx -= 1
|
2494
|
-
|
2495
|
-
# We matched if we've matched all events in the sequence
|
2496
2418
|
return seq_idx < 0
|