langroid 0.1.85__py3-none-any.whl → 0.1.219__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 (107) hide show
  1. langroid/__init__.py +95 -0
  2. langroid/agent/__init__.py +40 -0
  3. langroid/agent/base.py +222 -91
  4. langroid/agent/batch.py +264 -0
  5. langroid/agent/callbacks/chainlit.py +608 -0
  6. langroid/agent/chat_agent.py +247 -101
  7. langroid/agent/chat_document.py +41 -4
  8. langroid/agent/openai_assistant.py +842 -0
  9. langroid/agent/special/__init__.py +50 -0
  10. langroid/agent/special/doc_chat_agent.py +837 -141
  11. langroid/agent/special/lance_doc_chat_agent.py +258 -0
  12. langroid/agent/special/lance_rag/__init__.py +9 -0
  13. langroid/agent/special/lance_rag/critic_agent.py +136 -0
  14. langroid/agent/special/lance_rag/lance_rag_task.py +80 -0
  15. langroid/agent/special/lance_rag/query_planner_agent.py +180 -0
  16. langroid/agent/special/lance_tools.py +44 -0
  17. langroid/agent/special/neo4j/__init__.py +0 -0
  18. langroid/agent/special/neo4j/csv_kg_chat.py +174 -0
  19. langroid/agent/special/neo4j/neo4j_chat_agent.py +370 -0
  20. langroid/agent/special/neo4j/utils/__init__.py +0 -0
  21. langroid/agent/special/neo4j/utils/system_message.py +46 -0
  22. langroid/agent/special/relevance_extractor_agent.py +127 -0
  23. langroid/agent/special/retriever_agent.py +32 -198
  24. langroid/agent/special/sql/__init__.py +11 -0
  25. langroid/agent/special/sql/sql_chat_agent.py +47 -23
  26. langroid/agent/special/sql/utils/__init__.py +22 -0
  27. langroid/agent/special/sql/utils/description_extractors.py +95 -46
  28. langroid/agent/special/sql/utils/populate_metadata.py +28 -21
  29. langroid/agent/special/table_chat_agent.py +43 -9
  30. langroid/agent/task.py +475 -122
  31. langroid/agent/tool_message.py +75 -13
  32. langroid/agent/tools/__init__.py +13 -0
  33. langroid/agent/tools/duckduckgo_search_tool.py +66 -0
  34. langroid/agent/tools/google_search_tool.py +11 -0
  35. langroid/agent/tools/metaphor_search_tool.py +67 -0
  36. langroid/agent/tools/recipient_tool.py +16 -29
  37. langroid/agent/tools/run_python_code.py +60 -0
  38. langroid/agent/tools/sciphi_search_rag_tool.py +79 -0
  39. langroid/agent/tools/segment_extract_tool.py +36 -0
  40. langroid/cachedb/__init__.py +9 -0
  41. langroid/cachedb/base.py +22 -2
  42. langroid/cachedb/momento_cachedb.py +26 -2
  43. langroid/cachedb/redis_cachedb.py +78 -11
  44. langroid/embedding_models/__init__.py +34 -0
  45. langroid/embedding_models/base.py +21 -2
  46. langroid/embedding_models/models.py +120 -18
  47. langroid/embedding_models/protoc/embeddings.proto +19 -0
  48. langroid/embedding_models/protoc/embeddings_pb2.py +33 -0
  49. langroid/embedding_models/protoc/embeddings_pb2.pyi +50 -0
  50. langroid/embedding_models/protoc/embeddings_pb2_grpc.py +79 -0
  51. langroid/embedding_models/remote_embeds.py +153 -0
  52. langroid/language_models/__init__.py +45 -0
  53. langroid/language_models/azure_openai.py +80 -27
  54. langroid/language_models/base.py +117 -12
  55. langroid/language_models/config.py +5 -0
  56. langroid/language_models/openai_assistants.py +3 -0
  57. langroid/language_models/openai_gpt.py +558 -174
  58. langroid/language_models/prompt_formatter/__init__.py +15 -0
  59. langroid/language_models/prompt_formatter/base.py +4 -6
  60. langroid/language_models/prompt_formatter/hf_formatter.py +135 -0
  61. langroid/language_models/utils.py +18 -21
  62. langroid/mytypes.py +25 -8
  63. langroid/parsing/__init__.py +46 -0
  64. langroid/parsing/document_parser.py +260 -63
  65. langroid/parsing/image_text.py +32 -0
  66. langroid/parsing/parse_json.py +143 -0
  67. langroid/parsing/parser.py +122 -59
  68. langroid/parsing/repo_loader.py +114 -52
  69. langroid/parsing/search.py +68 -63
  70. langroid/parsing/spider.py +3 -2
  71. langroid/parsing/table_loader.py +44 -0
  72. langroid/parsing/url_loader.py +59 -11
  73. langroid/parsing/urls.py +85 -37
  74. langroid/parsing/utils.py +298 -4
  75. langroid/parsing/web_search.py +73 -0
  76. langroid/prompts/__init__.py +11 -0
  77. langroid/prompts/chat-gpt4-system-prompt.md +68 -0
  78. langroid/prompts/prompts_config.py +1 -1
  79. langroid/utils/__init__.py +17 -0
  80. langroid/utils/algorithms/__init__.py +3 -0
  81. langroid/utils/algorithms/graph.py +103 -0
  82. langroid/utils/configuration.py +36 -5
  83. langroid/utils/constants.py +4 -0
  84. langroid/utils/globals.py +2 -2
  85. langroid/utils/logging.py +2 -5
  86. langroid/utils/output/__init__.py +21 -0
  87. langroid/utils/output/printing.py +47 -1
  88. langroid/utils/output/status.py +33 -0
  89. langroid/utils/pandas_utils.py +30 -0
  90. langroid/utils/pydantic_utils.py +616 -2
  91. langroid/utils/system.py +98 -0
  92. langroid/vector_store/__init__.py +40 -0
  93. langroid/vector_store/base.py +203 -6
  94. langroid/vector_store/chromadb.py +59 -32
  95. langroid/vector_store/lancedb.py +463 -0
  96. langroid/vector_store/meilisearch.py +10 -7
  97. langroid/vector_store/momento.py +262 -0
  98. langroid/vector_store/qdrantdb.py +104 -22
  99. {langroid-0.1.85.dist-info → langroid-0.1.219.dist-info}/METADATA +329 -149
  100. langroid-0.1.219.dist-info/RECORD +127 -0
  101. {langroid-0.1.85.dist-info → langroid-0.1.219.dist-info}/WHEEL +1 -1
  102. langroid/agent/special/recipient_validator_agent.py +0 -157
  103. langroid/parsing/json.py +0 -64
  104. langroid/utils/web/selenium_login.py +0 -36
  105. langroid-0.1.85.dist-info/RECORD +0 -94
  106. /langroid/{scripts → agent/callbacks}/__init__.py +0 -0
  107. {langroid-0.1.85.dist-info → langroid-0.1.219.dist-info}/LICENSE +0 -0
@@ -1,14 +1,15 @@
1
+ import copy
1
2
  import inspect
2
- import json
3
3
  import logging
4
4
  import textwrap
5
5
  from contextlib import ExitStack
6
- from typing import Dict, List, Optional, Set, Tuple, Type, cast, no_type_check
6
+ from typing import Dict, List, Optional, Set, Tuple, Type, cast
7
7
 
8
8
  from rich import print
9
9
  from rich.console import Console
10
+ from rich.markup import escape
10
11
 
11
- from langroid.agent.base import Agent, AgentConfig
12
+ from langroid.agent.base import Agent, AgentConfig, noop_fn
12
13
  from langroid.agent.chat_document import ChatDocument
13
14
  from langroid.agent.tool_message import ToolMessage
14
15
  from langroid.language_models.base import (
@@ -17,7 +18,9 @@ from langroid.language_models.base import (
17
18
  Role,
18
19
  StreamingIfAllowed,
19
20
  )
21
+ from langroid.language_models.openai_gpt import OpenAIGPT
20
22
  from langroid.utils.configuration import settings
23
+ from langroid.utils.output import status
21
24
 
22
25
  console = Console()
23
26
 
@@ -40,8 +43,36 @@ class ChatAgentConfig(AgentConfig):
40
43
 
41
44
  system_message: str = "You are a helpful assistant."
42
45
  user_message: Optional[str] = None
43
- use_tools: bool = True
44
- use_functions_api: bool = False
46
+ use_tools: bool = False
47
+ use_functions_api: bool = True
48
+
49
+ def _set_fn_or_tools(self, fn_available: bool) -> None:
50
+ """
51
+ Enable Langroid Tool or OpenAI-like fn-calling,
52
+ depending on config settings and availability of fn-calling.
53
+ """
54
+ if self.use_functions_api and not fn_available:
55
+ logger.debug(
56
+ """
57
+ You have enabled `use_functions_api` but the LLM does not support it.
58
+ So we will enable `use_tools` instead, so we can use
59
+ Langroid's ToolMessage mechanism.
60
+ """
61
+ )
62
+ self.use_functions_api = False
63
+ self.use_tools = True
64
+
65
+ if not self.use_functions_api or not self.use_tools:
66
+ return
67
+ if self.use_functions_api and self.use_tools:
68
+ logger.debug(
69
+ """
70
+ You have enabled both `use_tools` and `use_functions_api`.
71
+ Turning off `use_tools`, since the LLM supports function-calling.
72
+ """
73
+ )
74
+ self.use_tools = False
75
+ self.use_functions_api = True
45
76
 
46
77
 
47
78
  class ChatAgent(Agent):
@@ -61,7 +92,9 @@ class ChatAgent(Agent):
61
92
  """
62
93
 
63
94
  def __init__(
64
- self, config: ChatAgentConfig, task: Optional[List[LLMMessage]] = None
95
+ self,
96
+ config: ChatAgentConfig = ChatAgentConfig(),
97
+ task: Optional[List[LLMMessage]] = None,
65
98
  ):
66
99
  """
67
100
  Chat-mode agent initialized with task spec as the initial message sequence
@@ -71,6 +104,7 @@ class ChatAgent(Agent):
71
104
  """
72
105
  super().__init__(config)
73
106
  self.config: ChatAgentConfig = config
107
+ self.config._set_fn_or_tools(self._fn_call_available())
74
108
  self.message_history: List[LLMMessage] = []
75
109
  self.tool_instructions_added: bool = False
76
110
  # An agent's "task" is defined by a system msg and an optional user msg;
@@ -102,8 +136,42 @@ class ChatAgent(Agent):
102
136
  self.llm_functions_usable: Set[str] = set()
103
137
  self.llm_function_force: Optional[Dict[str, str]] = None
104
138
 
139
+ def clone(self, i: int = 0) -> "ChatAgent":
140
+ """Create i'th clone of this agent, ensuring tool use/handling is cloned.
141
+ Important: We assume all member variables are in the __init__ method here
142
+ and in the Agent class.
143
+ TODO: We are attempting to close an agent after its state has been
144
+ changed in possibly many ways. Below is an imperfect solution. Caution advised.
145
+ Revisit later.
146
+ """
147
+ agent_cls = type(self)
148
+ config_copy = copy.deepcopy(self.config)
149
+ config_copy.name = f"{config_copy.name}-{i}"
150
+ new_agent = agent_cls(config_copy)
151
+ new_agent.system_tool_instructions = self.system_tool_instructions
152
+ new_agent.system_json_tool_instructions = self.system_json_tool_instructions
153
+ new_agent.llm_tools_map = self.llm_tools_map
154
+ new_agent.llm_functions_map = self.llm_functions_map
155
+ new_agent.llm_functions_handled = self.llm_functions_handled
156
+ new_agent.llm_functions_usable = self.llm_functions_usable
157
+ new_agent.llm_function_force = self.llm_function_force
158
+ # Caution - we are copying the vector-db, maybe we don't always want this?
159
+ new_agent.vecdb = self.vecdb
160
+ return new_agent
161
+
162
+ def _fn_call_available(self) -> bool:
163
+ """Does this agent's LLM support function calling?"""
164
+ return (
165
+ self.llm is not None
166
+ and isinstance(self.llm, OpenAIGPT)
167
+ and self.llm.is_openai_chat_model()
168
+ )
169
+
105
170
  def set_system_message(self, msg: str) -> None:
106
171
  self.system_message = msg
172
+ if len(self.message_history) > 0:
173
+ # if there is message history, update the system message in it
174
+ self.message_history[0].content = msg
107
175
 
108
176
  def set_user_message(self, msg: str) -> None:
109
177
  self.user_message = msg
@@ -160,46 +228,24 @@ class ChatAgent(Agent):
160
228
  enabled_classes: List[Type[ToolMessage]] = list(self.llm_tools_map.values())
161
229
  if len(enabled_classes) == 0:
162
230
  return "You can ask questions in natural language."
163
-
164
231
  json_instructions = "\n\n".join(
165
232
  [
166
- textwrap.dedent(
167
- f"""
168
- TOOL: {msg_cls.default_value("request")}
169
- PURPOSE: {msg_cls.default_value("purpose")}
170
- JSON FORMAT: {
171
- json.dumps(
172
- msg_cls.llm_function_schema(request=True).parameters,
173
- indent=4,
174
- )
175
- }
176
- {"EXAMPLE: " + msg_cls.usage_example() if msg_cls.examples() else ""}
177
- """.lstrip()
178
- )
179
- for i, msg_cls in enumerate(enabled_classes)
233
+ msg_cls.json_instructions(tool=self.config.use_tools)
234
+ for _, msg_cls in enumerate(enabled_classes)
180
235
  if msg_cls.default_value("request") in self.llm_tools_usable
181
236
  ]
182
237
  )
183
- return textwrap.dedent(
184
- f"""
185
- === ALL AVAILABLE TOOLS and THEIR JSON FORMAT INSTRUCTIONS ===
186
- You have access to the following TOOLS to accomplish your task:
187
-
188
- {json_instructions}
189
-
190
- When one of the above TOOLs is applicable, you must express your
191
- request as "TOOL:" followed by the request in the above JSON format.
192
- """
193
- + """
194
- The JSON format will be:
195
- \\{
196
- "request": "<tool_name>",
197
- "<arg1>": <value1>,
198
- "<arg2>": <value2>,
199
- ...
200
- \\}
201
- ----------------------------
202
- """.lstrip()
238
+ # if any of the enabled classes has json_group_instructions, then use that,
239
+ # else fall back to ToolMessage.json_group_instructions
240
+ for msg_cls in enabled_classes:
241
+ if hasattr(msg_cls, "json_group_instructions") and callable(
242
+ getattr(msg_cls, "json_group_instructions")
243
+ ):
244
+ return msg_cls.json_group_instructions().format(
245
+ json_instructions=json_instructions
246
+ )
247
+ return ToolMessage.json_group_instructions().format(
248
+ json_instructions=json_instructions
203
249
  )
204
250
 
205
251
  def tool_instructions(self) -> str:
@@ -260,13 +306,20 @@ class ChatAgent(Agent):
260
306
  """
261
307
  self.system_message += "\n\n" + message
262
308
 
309
+ def last_message_with_role(self, role: Role) -> LLMMessage | None:
310
+ """from `message_history`, return the last message with role `role`"""
311
+ for i in range(len(self.message_history) - 1, -1, -1):
312
+ if self.message_history[i].role == role:
313
+ return self.message_history[i]
314
+ return None
315
+
263
316
  def update_last_message(self, message: str, role: str = Role.USER) -> None:
264
317
  """
265
318
  Update the last message that has role `role` in the message history.
266
319
  Useful when we want to replace a long user prompt, that may contain context
267
320
  documents plus a question, with just the question.
268
321
  Args:
269
- message (str): user message
322
+ message (str): new message to replace with
270
323
  role (str): role of message to replace
271
324
  """
272
325
  if len(self.message_history) == 0:
@@ -302,7 +355,8 @@ class ChatAgent(Agent):
302
355
 
303
356
  """.lstrip()
304
357
  )
305
- return LLMMessage(role=Role.SYSTEM, content=content)
358
+ # remove leading and trailing newlines and other whitespace
359
+ return LLMMessage(role=Role.SYSTEM, content=content.strip())
306
360
 
307
361
  def enable_message(
308
362
  self,
@@ -311,6 +365,7 @@ class ChatAgent(Agent):
311
365
  handle: bool = True,
312
366
  force: bool = False,
313
367
  require_recipient: bool = False,
368
+ include_defaults: bool = True,
314
369
  ) -> None:
315
370
  """
316
371
  Add the tool (message class) to the agent, and enable either
@@ -331,7 +386,11 @@ class ChatAgent(Agent):
331
386
  `force` is ignored if `message_class` is None.
332
387
  require_recipient: whether to require that recipient be specified
333
388
  when using the tool message (only applies if `use` is True).
334
-
389
+ require_defaults: whether to include fields that have default values,
390
+ in the "properties" section of the JSON format instructions.
391
+ (Normally the OpenAI completion API ignores these fields,
392
+ but the Assistant fn-calling seems to pay attn to these,
393
+ and if we don't want this, we should set this to False.)
335
394
  """
336
395
  super().enable_message_handling(message_class) # enables handling only
337
396
  tools = self._get_tool_list(message_class)
@@ -339,7 +398,7 @@ class ChatAgent(Agent):
339
398
  if require_recipient:
340
399
  message_class = message_class.require_recipient()
341
400
  request = message_class.default_value("request")
342
- llm_function = message_class.llm_function_schema()
401
+ llm_function = message_class.llm_function_schema(defaults=include_defaults)
343
402
  self.llm_functions_map[request] = llm_function
344
403
  if force:
345
404
  self.llm_function_force = dict(name=request)
@@ -403,12 +462,11 @@ class ChatAgent(Agent):
403
462
  message_class: The only ToolMessage class to allow
404
463
  """
405
464
  request = message_class.__fields__["request"].default
406
- for r in self.llm_functions_usable:
407
- if r != request:
408
- self.llm_tools_usable.discard(r)
409
- self.llm_functions_usable.discard(r)
465
+ to_remove = [r for r in self.llm_tools_usable if r != request]
466
+ for r in to_remove:
467
+ self.llm_tools_usable.discard(r)
468
+ self.llm_functions_usable.discard(r)
410
469
 
411
- @no_type_check
412
470
  def llm_response(
413
471
  self, message: Optional[str | ChatDocument] = None
414
472
  ) -> Optional[ChatDocument]:
@@ -421,32 +479,51 @@ class ChatAgent(Agent):
421
479
  Returns:
422
480
  LLM response as a ChatDocument object
423
481
  """
482
+ if self.llm is None:
483
+ return None
424
484
  hist, output_len = self._prep_llm_messages(message)
485
+ if len(hist) == 0:
486
+ return None
425
487
  with StreamingIfAllowed(self.llm, self.llm.get_stream()):
426
488
  response = self.llm_response_messages(hist, output_len)
427
489
  # TODO - when response contains function_call we should include
428
490
  # that (and related fields) in the message_history
429
491
  self.message_history.append(ChatDocument.to_LLMMessage(response))
492
+ # Preserve trail of tool_ids for OpenAI Assistant fn-calls
493
+ response.metadata.tool_ids = (
494
+ []
495
+ if isinstance(message, str)
496
+ else message.metadata.tool_ids if message is not None else []
497
+ )
430
498
  return response
431
499
 
432
- @no_type_check
433
500
  async def llm_response_async(
434
501
  self, message: Optional[str | ChatDocument] = None
435
502
  ) -> Optional[ChatDocument]:
436
503
  """
437
504
  Async version of `llm_response`. See there for details.
438
505
  """
506
+ if self.llm is None:
507
+ return None
508
+
439
509
  hist, output_len = self._prep_llm_messages(message)
440
510
  with StreamingIfAllowed(self.llm, self.llm.get_stream()):
441
511
  response = await self.llm_response_messages_async(hist, output_len)
442
512
  # TODO - when response contains function_call we should include
443
513
  # that (and related fields) in the message_history
444
514
  self.message_history.append(ChatDocument.to_LLMMessage(response))
515
+ # Preserve trail of tool_ids for OpenAI Assistant fn-calls
516
+ response.metadata.tool_ids = (
517
+ []
518
+ if isinstance(message, str)
519
+ else message.metadata.tool_ids if message is not None else []
520
+ )
445
521
  return response
446
522
 
447
- @no_type_check
448
523
  def _prep_llm_messages(
449
- self, message: Optional[str | ChatDocument] = None
524
+ self,
525
+ message: Optional[str | ChatDocument] = None,
526
+ truncate: bool = True,
450
527
  ) -> Tuple[List[LLMMessage], int]:
451
528
  """
452
529
  Prepare messages to be sent to self.llm_response_messages,
@@ -458,12 +535,21 @@ class ChatAgent(Agent):
458
535
  output_len = max expected number of tokens in response
459
536
  """
460
537
 
461
- if not self.llm_can_respond(message):
462
- return None
538
+ if (
539
+ not self.llm_can_respond(message)
540
+ or self.config.llm is None
541
+ or self.llm is None
542
+ ):
543
+ return [], 0
463
544
 
464
- assert (
465
- message is not None or len(self.message_history) == 0
466
- ), "message can be None only if message_history is empty, i.e. at start."
545
+ if message is None and len(self.message_history) > 0:
546
+ # this means agent has been used to get LLM response already,
547
+ # and so the last message is an "assistant" response.
548
+ # We delete this last assistant response and re-generate it.
549
+ self.clear_history(-1)
550
+ logger.warning(
551
+ "Re-generating the last assistant response since message is None"
552
+ )
467
553
 
468
554
  if len(self.message_history) == 0:
469
555
  # initial messages have not yet been loaded, so load them
@@ -477,8 +563,9 @@ class ChatAgent(Agent):
477
563
  if settings.debug:
478
564
  print(
479
565
  f"""
480
- [red]LLM Initial Msg History:
481
- {self.message_history_str()}
566
+ [grey37]LLM Initial Msg History:
567
+ {escape(self.message_history_str())}
568
+ [/grey37]
482
569
  """
483
570
  )
484
571
  else:
@@ -493,7 +580,8 @@ class ChatAgent(Agent):
493
580
  hist = self.message_history
494
581
  output_len = self.config.llm.max_output_tokens
495
582
  if (
496
- self.chat_num_tokens(hist)
583
+ truncate
584
+ and self.chat_num_tokens(hist)
497
585
  > self.llm.chat_context_length() - self.config.llm.max_output_tokens
498
586
  ):
499
587
  # chat + output > max context length,
@@ -517,7 +605,10 @@ class ChatAgent(Agent):
517
605
  raise ValueError(
518
606
  """
519
607
  The message history is longer than the max chat context
520
- length allowed, and we have run out of messages to drop."""
608
+ length allowed, and we have run out of messages to drop.
609
+ HINT: In your `OpenAIGPTConfig` object, try increasing
610
+ `chat_context_length` or decreasing `max_output_tokens`.
611
+ """
521
612
  )
522
613
  # drop the second message, i.e. first msg after the sys msg
523
614
  # (typically user msg).
@@ -559,6 +650,18 @@ class ChatAgent(Agent):
559
650
  )
560
651
  return hist, output_len
561
652
 
653
+ def _function_args(
654
+ self,
655
+ ) -> Tuple[Optional[List[LLMFunctionSpec]], str | Dict[str, str]]:
656
+ functions: Optional[List[LLMFunctionSpec]] = None
657
+ fun_call: str | Dict[str, str] = "none"
658
+ if self.config.use_functions_api and len(self.llm_functions_usable) > 0:
659
+ functions = [self.llm_functions_map[f] for f in self.llm_functions_usable]
660
+ fun_call = (
661
+ "auto" if self.llm_function_force is None else self.llm_function_force
662
+ )
663
+ return functions, fun_call
664
+
562
665
  def llm_response_messages(
563
666
  self, messages: List[LLMMessage], output_len: Optional[int] = None
564
667
  ) -> ChatDocument:
@@ -573,24 +676,21 @@ class ChatAgent(Agent):
573
676
  """
574
677
  assert self.config.llm is not None and self.llm is not None
575
678
  output_len = output_len or self.config.llm.max_output_tokens
679
+ streamer = noop_fn
680
+ if self.llm.get_stream():
681
+ streamer = self.callbacks.start_llm_stream()
682
+ self.llm.config.streamer = streamer
576
683
  with ExitStack() as stack: # for conditionally using rich spinner
577
684
  if not self.llm.get_stream():
578
685
  # show rich spinner only if not streaming!
579
- cm = console.status("LLM responding to messages...")
686
+ cm = status(
687
+ "LLM responding to messages...",
688
+ log_if_quiet=False,
689
+ )
580
690
  stack.enter_context(cm)
581
- if self.llm.get_stream():
691
+ if self.llm.get_stream() and not settings.quiet:
582
692
  console.print(f"[green]{self.indent}", end="")
583
- functions: Optional[List[LLMFunctionSpec]] = None
584
- fun_call: str | Dict[str, str] = "none"
585
- if self.config.use_functions_api and len(self.llm_functions_usable) > 0:
586
- functions = [
587
- self.llm_functions_map[f] for f in self.llm_functions_usable
588
- ]
589
- fun_call = (
590
- "auto"
591
- if self.llm_function_force is None
592
- else self.llm_function_force
593
- )
693
+ functions, fun_call = self._function_args()
594
694
  assert self.llm is not None
595
695
  response = self.llm.chat(
596
696
  messages,
@@ -598,22 +698,39 @@ class ChatAgent(Agent):
598
698
  functions=functions,
599
699
  function_call=fun_call,
600
700
  )
601
- displayed = False
701
+ if self.llm.get_stream():
702
+ self.callbacks.finish_llm_stream(
703
+ content=str(response),
704
+ is_tool=self.has_tool_message_attempt(
705
+ ChatDocument.from_LLMResponse(response, displayed=True)
706
+ ),
707
+ )
708
+ self.llm.config.streamer = noop_fn
709
+ if response.cached:
710
+ self.callbacks.cancel_llm_stream()
711
+
602
712
  if not self.llm.get_stream() or response.cached:
603
- displayed = True
713
+ # We would have already displayed the msg "live" ONLY if
714
+ # streaming was enabled, AND we did not find a cached response.
715
+ # If we are here, it means the response has not yet been displayed.
604
716
  cached = f"[red]{self.indent}(cached)[/red]" if response.cached else ""
605
- if response.function_call is not None:
606
- response_str = str(response.function_call)
607
- else:
608
- response_str = response.message
609
- print(cached + "[green]" + response_str)
717
+ if not settings.quiet:
718
+ print(cached + "[green]" + escape(str(response)))
719
+ self.callbacks.show_llm_response(
720
+ content=str(response),
721
+ is_tool=self.has_tool_message_attempt(
722
+ ChatDocument.from_LLMResponse(response, displayed=True)
723
+ ),
724
+ cached=response.cached,
725
+ )
610
726
  self.update_token_usage(
611
727
  response,
612
728
  messages,
613
729
  self.llm.get_stream(),
614
- print_response_stats=True,
730
+ chat=True,
731
+ print_response_stats=self.config.show_stats and not settings.quiet,
615
732
  )
616
- return ChatDocument.from_LLMResponse(response, displayed)
733
+ return ChatDocument.from_LLMResponse(response, displayed=True)
617
734
 
618
735
  async def llm_response_messages_async(
619
736
  self, messages: List[LLMMessage], output_len: Optional[int] = None
@@ -631,26 +748,51 @@ class ChatAgent(Agent):
631
748
  "auto" if self.llm_function_force is None else self.llm_function_force
632
749
  )
633
750
  assert self.llm is not None
751
+
752
+ streamer = noop_fn
753
+ if self.llm.get_stream():
754
+ streamer = self.callbacks.start_llm_stream()
755
+ self.llm.config.streamer = streamer
756
+
634
757
  response = await self.llm.achat(
635
758
  messages,
636
759
  output_len,
637
760
  functions=functions,
638
761
  function_call=fun_call,
639
762
  )
640
- displayed = True
641
- cached = f"[red]{self.indent}(cached)[/red]" if response.cached else ""
642
- if response.function_call is not None:
643
- response_str = str(response.function_call)
644
- else:
645
- response_str = response.message
646
- print(cached + "[green]" + response_str)
763
+ if self.llm.get_stream():
764
+ self.callbacks.finish_llm_stream(
765
+ content=str(response),
766
+ is_tool=self.has_tool_message_attempt(
767
+ ChatDocument.from_LLMResponse(response, displayed=True)
768
+ ),
769
+ )
770
+ self.llm.config.streamer = noop_fn
771
+ if response.cached:
772
+ self.callbacks.cancel_llm_stream()
773
+ if not self.llm.get_stream() or response.cached:
774
+ # We would have already displayed the msg "live" ONLY if
775
+ # streaming was enabled, AND we did not find a cached response.
776
+ # If we are here, it means the response has not yet been displayed.
777
+ cached = f"[red]{self.indent}(cached)[/red]" if response.cached else ""
778
+ if not settings.quiet:
779
+ print(cached + "[green]" + escape(str(response)))
780
+ self.callbacks.show_llm_response(
781
+ content=str(response),
782
+ is_tool=self.has_tool_message_attempt(
783
+ ChatDocument.from_LLMResponse(response, displayed=True)
784
+ ),
785
+ cached=response.cached,
786
+ )
787
+
647
788
  self.update_token_usage(
648
789
  response,
649
790
  messages,
650
791
  self.llm.get_stream(),
651
- print_response_stats=True,
792
+ chat=True,
793
+ print_response_stats=self.config.show_stats and not settings.quiet,
652
794
  )
653
- return ChatDocument.from_LLMResponse(response, displayed)
795
+ return ChatDocument.from_LLMResponse(response, displayed=True)
654
796
 
655
797
  def _llm_response_temp_context(self, message: str, prompt: str) -> ChatDocument:
656
798
  """
@@ -703,12 +845,14 @@ class ChatAgent(Agent):
703
845
  """
704
846
  # explicitly call THIS class's respond method,
705
847
  # not a derived class's (or else there would be infinite recursion!)
848
+ n_msgs = len(self.message_history)
706
849
  with StreamingIfAllowed(self.llm, self.llm.get_stream()): # type: ignore
707
850
  response = cast(ChatDocument, ChatAgent.llm_response(self, message))
708
- # clear the last two messages, which are the
709
- # user message and the assistant response
710
- self.message_history.pop()
711
- self.message_history.pop()
851
+ # If there is a response, then we will have two additional
852
+ # messages in the message history, i.e. the user message and the
853
+ # assistant response. We want to (carefully) remove these two messages.
854
+ self.message_history.pop() if len(self.message_history) > n_msgs else None
855
+ self.message_history.pop() if len(self.message_history) > n_msgs else None
712
856
  return response
713
857
 
714
858
  async def llm_response_forget_async(self, message: str) -> ChatDocument:
@@ -717,14 +861,16 @@ class ChatAgent(Agent):
717
861
  """
718
862
  # explicitly call THIS class's respond method,
719
863
  # not a derived class's (or else there would be infinite recursion!)
864
+ n_msgs = len(self.message_history)
720
865
  with StreamingIfAllowed(self.llm, self.llm.get_stream()): # type: ignore
721
866
  response = cast(
722
867
  ChatDocument, await ChatAgent.llm_response_async(self, message)
723
868
  )
724
- # clear the last two messages, which are the
725
- # user message and the assistant response
726
- self.message_history.pop()
727
- self.message_history.pop()
869
+ # If there is a response, then we will have two additional
870
+ # messages in the message history, i.e. the user message and the
871
+ # assistant response. We want to (carefully) remove these two messages.
872
+ self.message_history.pop() if len(self.message_history) > n_msgs else None
873
+ self.message_history.pop() if len(self.message_history) > n_msgs else None
728
874
  return response
729
875
 
730
876
  def chat_num_tokens(self, messages: Optional[List[LLMMessage]] = None) -> int: