langroid 0.59.0b2__py3-none-any.whl → 0.59.1__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 (50) hide show
  1. langroid/agent/done_sequence_parser.py +46 -11
  2. langroid/agent/special/doc_chat_task.py +0 -0
  3. langroid/agent/task.py +44 -7
  4. langroid/language_models/model_info.py +51 -0
  5. langroid/mcp/__init__.py +1 -0
  6. langroid/mcp/server/__init__.py +1 -0
  7. langroid/pydantic_v1/__init__.py +7 -0
  8. {langroid-0.59.0b2.dist-info → langroid-0.59.1.dist-info}/METADATA +4 -1
  9. {langroid-0.59.0b2.dist-info → langroid-0.59.1.dist-info}/RECORD +11 -47
  10. langroid/agent/base.py-e +0 -2216
  11. langroid/agent/chat_agent.py-e +0 -2086
  12. langroid/agent/chat_document.py-e +0 -513
  13. langroid/agent/openai_assistant.py-e +0 -882
  14. langroid/agent/special/arangodb/arangodb_agent.py-e +0 -648
  15. langroid/agent/special/lance_tools.py-e +0 -61
  16. langroid/agent/special/neo4j/neo4j_chat_agent.py-e +0 -430
  17. langroid/agent/task.py-e +0 -2418
  18. langroid/agent/tool_message.py-e +0 -400
  19. langroid/agent/tools/file_tools.py-e +0 -234
  20. langroid/agent/tools/mcp/fastmcp_client.py-e +0 -584
  21. langroid/agent/tools/orchestration.py-e +0 -301
  22. langroid/agent/tools/task_tool.py-e +0 -249
  23. langroid/agent/xml_tool_message.py-e +0 -392
  24. langroid/embedding_models/models.py-e +0 -563
  25. langroid/language_models/azure_openai.py-e +0 -134
  26. langroid/language_models/base.py-e +0 -812
  27. langroid/language_models/config.py-e +0 -18
  28. langroid/language_models/model_info.py-e +0 -483
  29. langroid/language_models/openai_gpt.py-e +0 -2280
  30. langroid/language_models/provider_params.py-e +0 -153
  31. langroid/mytypes.py-e +0 -132
  32. langroid/parsing/file_attachment.py-e +0 -246
  33. langroid/parsing/md_parser.py-e +0 -574
  34. langroid/parsing/parser.py-e +0 -410
  35. langroid/parsing/repo_loader.py-e +0 -812
  36. langroid/parsing/url_loader.py-e +0 -683
  37. langroid/parsing/urls.py-e +0 -279
  38. langroid/pydantic_v1/__init__.py-e +0 -36
  39. langroid/pydantic_v1/main.py-e +0 -11
  40. langroid/utils/configuration.py-e +0 -141
  41. langroid/utils/constants.py-e +0 -32
  42. langroid/utils/globals.py-e +0 -49
  43. langroid/utils/html_logger.py-e +0 -825
  44. langroid/utils/object_registry.py-e +0 -66
  45. langroid/utils/pydantic_utils.py-e +0 -602
  46. langroid/utils/types.py-e +0 -113
  47. langroid/vector_store/lancedb.py-e +0 -404
  48. langroid/vector_store/pineconedb.py-e +0 -427
  49. {langroid-0.59.0b2.dist-info → langroid-0.59.1.dist-info}/WHEEL +0 -0
  50. {langroid-0.59.0b2.dist-info → langroid-0.59.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,513 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import copy
4
- import json
5
- from collections import OrderedDict
6
- from enum import Enum
7
- from typing import Any, Dict, List, Optional, Union, cast
8
-
9
- from langroid.agent.tool_message import ToolMessage
10
- from langroid.agent.xml_tool_message import XMLToolMessage
11
- from langroid.language_models.base import (
12
- LLMFunctionCall,
13
- LLMMessage,
14
- LLMResponse,
15
- LLMTokenUsage,
16
- OpenAIToolCall,
17
- Role,
18
- ToolChoiceTypes,
19
- )
20
- from langroid.mytypes import DocMetaData, Document, Entity
21
- from langroid.parsing.agent_chats import parse_message
22
- from langroid.parsing.file_attachment import FileAttachment
23
- from langroid.parsing.parse_json import extract_top_level_json, top_level_json_field
24
- from pydantic import BaseModel, ConfigDict
25
- from langroid.utils.object_registry import ObjectRegistry
26
- from langroid.utils.output.printing import shorten_text
27
- from langroid.utils.types import to_string
28
-
29
-
30
- class ChatDocAttachment(BaseModel):
31
- # any additional data that should be attached to the document
32
- model_config = ConfigDict(extra="allow")
33
-
34
-
35
- class StatusCode(str, Enum):
36
- """Codes meant to be returned by task.run(). Some are not used yet."""
37
-
38
- OK = "OK"
39
- ERROR = "ERROR"
40
- DONE = "DONE"
41
- STALLED = "STALLED"
42
- INF_LOOP = "INF_LOOP"
43
- KILL = "KILL"
44
- FIXED_TURNS = "FIXED_TURNS" # reached intended number of turns
45
- MAX_TURNS = "MAX_TURNS" # hit max-turns limit
46
- MAX_COST = "MAX_COST"
47
- MAX_TOKENS = "MAX_TOKENS"
48
- TIMEOUT = "TIMEOUT"
49
- NO_ANSWER = "NO_ANSWER"
50
- USER_QUIT = "USER_QUIT"
51
-
52
-
53
- class ChatDocMetaData(DocMetaData):
54
- parent_id: str = "" # msg (ChatDocument) to which this is a response
55
- child_id: str = "" # ChatDocument that has response to this message
56
- agent_id: str = "" # ChatAgent that generated this message
57
- msg_idx: int = -1 # index of this message in the agent `message_history`
58
- sender: Entity # sender of the message
59
- # tool_id corresponding to single tool result in ChatDocument.content
60
- oai_tool_id: str | None = None
61
- tool_ids: List[str] = [] # stack of tool_ids; used by OpenAIAssistant
62
- block: None | Entity = None
63
- sender_name: str = ""
64
- recipient: str = ""
65
- usage: Optional[LLMTokenUsage] = None
66
- cached: bool = False
67
- displayed: bool = False
68
- has_citation: bool = False
69
- status: Optional[StatusCode] = None
70
-
71
- @property
72
- def parent(self) -> Optional["ChatDocument"]:
73
- return ChatDocument.from_id(self.parent_id)
74
-
75
- @property
76
- def child(self) -> Optional["ChatDocument"]:
77
- return ChatDocument.from_id(self.child_id)
78
-
79
-
80
- class ChatDocLoggerFields(BaseModel):
81
- sender_entity: Entity = Entity.USER
82
- sender_name: str = ""
83
- recipient: str = ""
84
- block: Entity | None = None
85
- tool_type: str = ""
86
- tool: str = ""
87
- content: str = ""
88
-
89
- @classmethod
90
- def tsv_header(cls) -> str:
91
- field_names = cls().model_dump().keys()
92
- return "\t".join(field_names)
93
-
94
-
95
- class ChatDocument(Document):
96
- """
97
- Represents a message in a conversation among agents. All responders of an agent
98
- have signature ChatDocument -> ChatDocument (modulo None, str, etc),
99
- and so does the Task.run() method.
100
-
101
- Attributes:
102
- oai_tool_calls (Optional[List[OpenAIToolCall]]):
103
- Tool-calls from an OpenAI-compatible API
104
- oai_tool_id2results (Optional[OrderedDict[str, str]]):
105
- Results of tool-calls from OpenAI (dict is a map of tool_id -> result)
106
- oai_tool_choice: ToolChoiceTypes | Dict[str, str]: Param controlling how the
107
- LLM should choose tool-use in its response
108
- (auto, none, required, or a specific tool)
109
- function_call (Optional[LLMFunctionCall]):
110
- Function-call from an OpenAI-compatible API
111
- (deprecated by OpenAI, in favor of tool-calls)
112
- tool_messages (List[ToolMessage]): Langroid ToolMessages extracted from
113
- - `content` field (via JSON parsing),
114
- - `oai_tool_calls`, or
115
- - `function_call`
116
- metadata (ChatDocMetaData): Metadata for the message, e.g. sender, recipient.
117
- attachment (None | ChatDocAttachment): Any additional data attached.
118
- """
119
-
120
- reasoning: str = "" # reasoning produced by a reasoning LLM
121
- content_any: Any = None # to hold arbitrary data returned by responders
122
- files: List[FileAttachment] = [] # list of file attachments
123
- oai_tool_calls: Optional[List[OpenAIToolCall]] = None
124
- oai_tool_id2result: Optional[OrderedDict[str, str]] = None
125
- oai_tool_choice: ToolChoiceTypes | Dict[str, Dict[str, str] | str] = "auto"
126
- function_call: Optional[LLMFunctionCall] = None
127
- # tools that are explicitly added by agent response/handler,
128
- # or tools recognized in the ChatDocument as handle-able tools
129
- tool_messages: List[ToolMessage] = []
130
- # all known tools in the msg that are in an agent's llm_tools_known list,
131
- # even if non-used/handled
132
- all_tool_messages: List[ToolMessage] = []
133
-
134
- metadata: ChatDocMetaData
135
- attachment: None | ChatDocAttachment = None
136
-
137
- def __init__(self, **data: Any):
138
- super().__init__(**data)
139
- ObjectRegistry.register_object(self)
140
-
141
- @property
142
- def parent(self) -> Optional["ChatDocument"]:
143
- return ChatDocument.from_id(self.metadata.parent_id)
144
-
145
- @property
146
- def child(self) -> Optional["ChatDocument"]:
147
- return ChatDocument.from_id(self.metadata.child_id)
148
-
149
- @staticmethod
150
- def deepcopy(doc: ChatDocument) -> ChatDocument:
151
- new_doc = copy.deepcopy(doc)
152
- new_doc.metadata.id = ObjectRegistry.new_id()
153
- new_doc.metadata.child_id = ""
154
- new_doc.metadata.parent_id = ""
155
- ObjectRegistry.register_object(new_doc)
156
- return new_doc
157
-
158
- @staticmethod
159
- def from_id(id: str) -> Optional["ChatDocument"]:
160
- return cast(ChatDocument, ObjectRegistry.get(id))
161
-
162
- @staticmethod
163
- def delete_id(id: str) -> None:
164
- """Remove ChatDocument with given id from ObjectRegistry,
165
- and all its descendants.
166
- """
167
- chat_doc = ChatDocument.from_id(id)
168
- # first delete all descendants
169
- while chat_doc is not None:
170
- next_chat_doc = chat_doc.child
171
- ObjectRegistry.remove(chat_doc.id())
172
- chat_doc = next_chat_doc
173
-
174
- def __str__(self) -> str:
175
- fields = self.log_fields()
176
- tool_str = ""
177
- if fields.tool_type != "":
178
- tool_str = f"{fields.tool_type}[{fields.tool}]: "
179
- recipient_str = ""
180
- if fields.recipient != "":
181
- recipient_str = f"=>{fields.recipient}: "
182
- return (
183
- f"{fields.sender_entity}[{fields.sender_name}] "
184
- f"{recipient_str}{tool_str}{fields.content}"
185
- )
186
-
187
- def get_tool_names(self) -> List[str]:
188
- """
189
- Get names of attempted tool usages (JSON or non-JSON) in the content
190
- of the message.
191
- Returns:
192
- List[str]: list of *attempted* tool names
193
- (We say "attempted" since we ONLY look at the `request` component of the
194
- tool-call representation, and we're not fully parsing it into the
195
- corresponding tool message class)
196
-
197
- """
198
- tool_candidates = XMLToolMessage.find_candidates(self.content)
199
- if len(tool_candidates) == 0:
200
- tool_candidates = extract_top_level_json(self.content)
201
- if len(tool_candidates) == 0:
202
- return []
203
- tools = [json.loads(tc).get("request") for tc in tool_candidates]
204
- else:
205
- tool_dicts = [
206
- XMLToolMessage.extract_field_values(tc) for tc in tool_candidates
207
- ]
208
- tools = [td.get("request") for td in tool_dicts if td is not None]
209
- return [str(tool) for tool in tools if tool is not None]
210
-
211
- def log_fields(self) -> ChatDocLoggerFields:
212
- """
213
- Fields for logging in csv/tsv logger
214
- Returns:
215
- List[str]: list of fields
216
- """
217
- tool_type = "" # FUNC or TOOL
218
- tool = "" # tool name or function name
219
-
220
- # Skip tool detection for system messages - they contain tool instructions,
221
- # not actual tool calls
222
- if self.metadata.sender != Entity.SYSTEM:
223
- oai_tools = (
224
- []
225
- if self.oai_tool_calls is None
226
- else [t for t in self.oai_tool_calls if t.function is not None]
227
- )
228
- if self.function_call is not None:
229
- tool_type = "FUNC"
230
- tool = self.function_call.name
231
- elif len(oai_tools) > 0:
232
- tool_type = "OAI_TOOL"
233
- tool = ",".join(t.function.name for t in oai_tools) # type: ignore
234
- else:
235
- try:
236
- json_tools = self.get_tool_names()
237
- except Exception:
238
- json_tools = []
239
- if json_tools != []:
240
- tool_type = "TOOL"
241
- tool = json_tools[0]
242
- recipient = self.metadata.recipient
243
- content = self.content
244
- sender_entity = self.metadata.sender
245
- sender_name = self.metadata.sender_name
246
- if tool_type == "FUNC":
247
- content += str(self.function_call)
248
- return ChatDocLoggerFields(
249
- sender_entity=sender_entity,
250
- sender_name=sender_name,
251
- recipient=recipient,
252
- block=self.metadata.block,
253
- tool_type=tool_type,
254
- tool=tool,
255
- content=content,
256
- )
257
-
258
- def tsv_str(self) -> str:
259
- fields = self.log_fields()
260
- fields.content = shorten_text(fields.content, 80)
261
- field_values = fields.model_dump().values()
262
- return "\t".join(str(v) for v in field_values)
263
-
264
- def pop_tool_ids(self) -> None:
265
- """
266
- Pop the last tool_id from the stack of tool_ids.
267
- """
268
- if len(self.metadata.tool_ids) > 0:
269
- self.metadata.tool_ids.pop()
270
-
271
- @staticmethod
272
- def _clean_fn_call(fc: LLMFunctionCall | None) -> None:
273
- # Sometimes an OpenAI LLM (esp gpt-4o) may generate a function-call
274
- # with oddities:
275
- # (a) the `name` is set, as well as `arguments.request` is set,
276
- # and in langroid we use the `request` value as the `name`.
277
- # In this case we override the `name` with the `request` value.
278
- # (b) the `name` looks like "functions blah" or just "functions"
279
- # In this case we strip the "functions" part.
280
- if fc is None:
281
- return
282
- fc.name = fc.name.replace("functions", "").strip()
283
- if fc.arguments is not None:
284
- request = fc.arguments.get("request")
285
- if request is not None and request != "":
286
- fc.name = request
287
- fc.arguments.pop("request")
288
-
289
- @staticmethod
290
- def from_LLMResponse(
291
- response: LLMResponse,
292
- displayed: bool = False,
293
- ) -> "ChatDocument":
294
- """
295
- Convert LLMResponse to ChatDocument.
296
- Args:
297
- response (LLMResponse): LLMResponse to convert.
298
- displayed (bool): Whether this response was displayed to the user.
299
- Returns:
300
- ChatDocument: ChatDocument representation of this LLMResponse.
301
- """
302
- recipient, message = response.get_recipient_and_message()
303
- message = message.strip()
304
- if message in ["''", '""']:
305
- message = ""
306
- if response.function_call is not None:
307
- ChatDocument._clean_fn_call(response.function_call)
308
- if response.oai_tool_calls is not None:
309
- # there must be at least one if it's not None
310
- for oai_tc in response.oai_tool_calls:
311
- ChatDocument._clean_fn_call(oai_tc.function)
312
- return ChatDocument(
313
- content=message,
314
- reasoning=response.reasoning,
315
- content_any=message,
316
- oai_tool_calls=response.oai_tool_calls,
317
- function_call=response.function_call,
318
- metadata=ChatDocMetaData(
319
- source=Entity.LLM,
320
- sender=Entity.LLM,
321
- usage=response.usage,
322
- displayed=displayed,
323
- cached=response.cached,
324
- recipient=recipient,
325
- ),
326
- )
327
-
328
- @staticmethod
329
- def from_str(msg: str) -> "ChatDocument":
330
- # first check whether msg is structured as TO <recipient>: <message>
331
- recipient, message = parse_message(msg)
332
- if recipient == "":
333
- # check if any top level json specifies a 'recipient'
334
- recipient = top_level_json_field(msg, "recipient")
335
- message = msg # retain the whole msg in this case
336
- return ChatDocument(
337
- content=message,
338
- content_any=message,
339
- metadata=ChatDocMetaData(
340
- source=Entity.USER,
341
- sender=Entity.USER,
342
- recipient=recipient,
343
- ),
344
- )
345
-
346
- @staticmethod
347
- def from_LLMMessage(
348
- message: LLMMessage,
349
- sender_name: str = "",
350
- recipient: str = "",
351
- ) -> "ChatDocument":
352
- """
353
- Convert LLMMessage to ChatDocument.
354
-
355
- Args:
356
- message (LLMMessage): LLMMessage to convert.
357
- sender_name (str): Name of the sender. Defaults to "".
358
- recipient (str): Name of the recipient. Defaults to "".
359
-
360
- Returns:
361
- ChatDocument: ChatDocument representation of this LLMMessage.
362
- """
363
- # Map LLMMessage Role to ChatDocument Entity
364
- role_to_entity = {
365
- Role.USER: Entity.USER,
366
- Role.SYSTEM: Entity.SYSTEM,
367
- Role.ASSISTANT: Entity.LLM,
368
- Role.FUNCTION: Entity.LLM,
369
- Role.TOOL: Entity.LLM,
370
- }
371
-
372
- sender_entity = role_to_entity.get(message.role, Entity.USER)
373
-
374
- return ChatDocument(
375
- content=message.content or "",
376
- content_any=message.content,
377
- files=message.files,
378
- function_call=message.function_call,
379
- oai_tool_calls=message.tool_calls,
380
- metadata=ChatDocMetaData(
381
- source=sender_entity,
382
- sender=sender_entity,
383
- sender_name=sender_name,
384
- recipient=recipient,
385
- oai_tool_id=message.tool_call_id,
386
- tool_ids=[message.tool_id] if message.tool_id else [],
387
- ),
388
- )
389
-
390
- @staticmethod
391
- def to_LLMMessage(
392
- message: Union[str, "ChatDocument"],
393
- oai_tools: Optional[List[OpenAIToolCall]] = None,
394
- ) -> List[LLMMessage]:
395
- """
396
- Convert to list of LLMMessage, to incorporate into msg-history sent to LLM API.
397
- Usually there will be just a single LLMMessage, but when the ChatDocument
398
- contains results from multiple OpenAI tool-calls, we would have a sequence
399
- LLMMessages, one per tool-call result.
400
-
401
- Args:
402
- message (str|ChatDocument): Message to convert.
403
- oai_tools (Optional[List[OpenAIToolCall]]): Tool-calls currently awaiting
404
- response, from the ChatAgent's latest message.
405
- Returns:
406
- List[LLMMessage]: list of LLMMessages corresponding to this ChatDocument.
407
- """
408
-
409
- sender_role = Role.USER
410
- if isinstance(message, str):
411
- message = ChatDocument.from_str(message)
412
- content = message.content or to_string(message.content_any) or ""
413
- fun_call = message.function_call
414
- oai_tool_calls = message.oai_tool_calls
415
- if message.metadata.sender == Entity.USER and fun_call is not None:
416
- # This may happen when a (parent agent's) LLM generates a
417
- # a Function-call, and it ends up being sent to the current task's
418
- # LLM (possibly because the function-call is mis-named or has other
419
- # issues and couldn't be handled by handler methods).
420
- # But a function-call can only be generated by an entity with
421
- # Role.ASSISTANT, so we instead put the content of the function-call
422
- # in the content of the message.
423
- content += " " + str(fun_call)
424
- fun_call = None
425
- if message.metadata.sender == Entity.USER and oai_tool_calls is not None:
426
- # same reasoning as for function-call above
427
- content += " " + "\n\n".join(str(tc) for tc in oai_tool_calls)
428
- oai_tool_calls = None
429
- # some LLM APIs (e.g. gemini) don't like empty msg
430
- content = content or " "
431
- sender_name = message.metadata.sender_name
432
- tool_ids = message.metadata.tool_ids
433
- tool_id = tool_ids[-1] if len(tool_ids) > 0 else ""
434
- chat_document_id = message.id()
435
- if message.metadata.sender == Entity.SYSTEM:
436
- sender_role = Role.SYSTEM
437
- if (
438
- message.metadata.parent is not None
439
- and message.metadata.parent.function_call is not None
440
- ):
441
- # This is a response to a function call, so set the role to FUNCTION.
442
- sender_role = Role.FUNCTION
443
- sender_name = message.metadata.parent.function_call.name
444
- elif oai_tools is not None and len(oai_tools) > 0:
445
- pending_tool_ids = [tc.id for tc in oai_tools]
446
- # The ChatAgent has pending OpenAI tool-call(s),
447
- # so the current ChatDocument contains
448
- # results for some/all/none of them.
449
-
450
- if len(oai_tools) == 1:
451
- # Case 1:
452
- # There was exactly 1 pending tool-call, and in this case
453
- # the result would be a plain string in `content`
454
- return [
455
- LLMMessage(
456
- role=Role.TOOL,
457
- tool_call_id=oai_tools[0].id,
458
- content=content,
459
- files=message.files,
460
- chat_document_id=chat_document_id,
461
- )
462
- ]
463
-
464
- elif (
465
- message.metadata.oai_tool_id is not None
466
- and message.metadata.oai_tool_id in pending_tool_ids
467
- ):
468
- # Case 2:
469
- # ChatDocument.content has result of a single tool-call
470
- return [
471
- LLMMessage(
472
- role=Role.TOOL,
473
- tool_call_id=message.metadata.oai_tool_id,
474
- content=content,
475
- files=message.files,
476
- chat_document_id=chat_document_id,
477
- )
478
- ]
479
- elif message.oai_tool_id2result is not None:
480
- # Case 2:
481
- # There were > 1 tool-calls awaiting response,
482
- assert (
483
- len(message.oai_tool_id2result) > 1
484
- ), "oai_tool_id2result must have more than 1 item."
485
- return [
486
- LLMMessage(
487
- role=Role.TOOL,
488
- tool_call_id=tool_id,
489
- content=result or " ",
490
- files=message.files,
491
- chat_document_id=chat_document_id,
492
- )
493
- for tool_id, result in message.oai_tool_id2result.items()
494
- ]
495
- elif message.metadata.sender == Entity.LLM:
496
- sender_role = Role.ASSISTANT
497
-
498
- return [
499
- LLMMessage(
500
- role=sender_role,
501
- tool_id=tool_id, # for OpenAI Assistant
502
- content=content,
503
- files=message.files,
504
- function_call=fun_call,
505
- tool_calls=oai_tool_calls,
506
- name=sender_name,
507
- chat_document_id=chat_document_id,
508
- )
509
- ]
510
-
511
-
512
- LLMMessage.update_forward_refs()
513
- ChatDocMetaData.update_forward_refs()