langroid 0.33.6__py3-none-any.whl → 0.33.7__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 (129) hide show
  1. langroid/__init__.py +106 -0
  2. langroid/agent/__init__.py +41 -0
  3. langroid/agent/base.py +1983 -0
  4. langroid/agent/batch.py +398 -0
  5. langroid/agent/callbacks/__init__.py +0 -0
  6. langroid/agent/callbacks/chainlit.py +598 -0
  7. langroid/agent/chat_agent.py +1899 -0
  8. langroid/agent/chat_document.py +454 -0
  9. langroid/agent/openai_assistant.py +882 -0
  10. langroid/agent/special/__init__.py +59 -0
  11. langroid/agent/special/arangodb/__init__.py +0 -0
  12. langroid/agent/special/arangodb/arangodb_agent.py +656 -0
  13. langroid/agent/special/arangodb/system_messages.py +186 -0
  14. langroid/agent/special/arangodb/tools.py +107 -0
  15. langroid/agent/special/arangodb/utils.py +36 -0
  16. langroid/agent/special/doc_chat_agent.py +1466 -0
  17. langroid/agent/special/lance_doc_chat_agent.py +262 -0
  18. langroid/agent/special/lance_rag/__init__.py +9 -0
  19. langroid/agent/special/lance_rag/critic_agent.py +198 -0
  20. langroid/agent/special/lance_rag/lance_rag_task.py +82 -0
  21. langroid/agent/special/lance_rag/query_planner_agent.py +260 -0
  22. langroid/agent/special/lance_tools.py +61 -0
  23. langroid/agent/special/neo4j/__init__.py +0 -0
  24. langroid/agent/special/neo4j/csv_kg_chat.py +174 -0
  25. langroid/agent/special/neo4j/neo4j_chat_agent.py +433 -0
  26. langroid/agent/special/neo4j/system_messages.py +120 -0
  27. langroid/agent/special/neo4j/tools.py +32 -0
  28. langroid/agent/special/relevance_extractor_agent.py +127 -0
  29. langroid/agent/special/retriever_agent.py +56 -0
  30. langroid/agent/special/sql/__init__.py +17 -0
  31. langroid/agent/special/sql/sql_chat_agent.py +654 -0
  32. langroid/agent/special/sql/utils/__init__.py +21 -0
  33. langroid/agent/special/sql/utils/description_extractors.py +190 -0
  34. langroid/agent/special/sql/utils/populate_metadata.py +85 -0
  35. langroid/agent/special/sql/utils/system_message.py +35 -0
  36. langroid/agent/special/sql/utils/tools.py +64 -0
  37. langroid/agent/special/table_chat_agent.py +263 -0
  38. langroid/agent/task.py +2095 -0
  39. langroid/agent/tool_message.py +393 -0
  40. langroid/agent/tools/__init__.py +38 -0
  41. langroid/agent/tools/duckduckgo_search_tool.py +50 -0
  42. langroid/agent/tools/file_tools.py +234 -0
  43. langroid/agent/tools/google_search_tool.py +39 -0
  44. langroid/agent/tools/metaphor_search_tool.py +68 -0
  45. langroid/agent/tools/orchestration.py +303 -0
  46. langroid/agent/tools/recipient_tool.py +235 -0
  47. langroid/agent/tools/retrieval_tool.py +32 -0
  48. langroid/agent/tools/rewind_tool.py +137 -0
  49. langroid/agent/tools/segment_extract_tool.py +41 -0
  50. langroid/agent/xml_tool_message.py +382 -0
  51. langroid/cachedb/__init__.py +17 -0
  52. langroid/cachedb/base.py +58 -0
  53. langroid/cachedb/momento_cachedb.py +108 -0
  54. langroid/cachedb/redis_cachedb.py +153 -0
  55. langroid/embedding_models/__init__.py +39 -0
  56. langroid/embedding_models/base.py +74 -0
  57. langroid/embedding_models/models.py +461 -0
  58. langroid/embedding_models/protoc/__init__.py +0 -0
  59. langroid/embedding_models/protoc/embeddings.proto +19 -0
  60. langroid/embedding_models/protoc/embeddings_pb2.py +33 -0
  61. langroid/embedding_models/protoc/embeddings_pb2.pyi +50 -0
  62. langroid/embedding_models/protoc/embeddings_pb2_grpc.py +79 -0
  63. langroid/embedding_models/remote_embeds.py +153 -0
  64. langroid/exceptions.py +71 -0
  65. langroid/language_models/__init__.py +53 -0
  66. langroid/language_models/azure_openai.py +153 -0
  67. langroid/language_models/base.py +678 -0
  68. langroid/language_models/config.py +18 -0
  69. langroid/language_models/mock_lm.py +124 -0
  70. langroid/language_models/openai_gpt.py +1964 -0
  71. langroid/language_models/prompt_formatter/__init__.py +16 -0
  72. langroid/language_models/prompt_formatter/base.py +40 -0
  73. langroid/language_models/prompt_formatter/hf_formatter.py +132 -0
  74. langroid/language_models/prompt_formatter/llama2_formatter.py +75 -0
  75. langroid/language_models/utils.py +151 -0
  76. langroid/mytypes.py +84 -0
  77. langroid/parsing/__init__.py +52 -0
  78. langroid/parsing/agent_chats.py +38 -0
  79. langroid/parsing/code_parser.py +121 -0
  80. langroid/parsing/document_parser.py +718 -0
  81. langroid/parsing/para_sentence_split.py +62 -0
  82. langroid/parsing/parse_json.py +155 -0
  83. langroid/parsing/parser.py +313 -0
  84. langroid/parsing/repo_loader.py +790 -0
  85. langroid/parsing/routing.py +36 -0
  86. langroid/parsing/search.py +275 -0
  87. langroid/parsing/spider.py +102 -0
  88. langroid/parsing/table_loader.py +94 -0
  89. langroid/parsing/url_loader.py +111 -0
  90. langroid/parsing/urls.py +273 -0
  91. langroid/parsing/utils.py +373 -0
  92. langroid/parsing/web_search.py +156 -0
  93. langroid/prompts/__init__.py +9 -0
  94. langroid/prompts/dialog.py +17 -0
  95. langroid/prompts/prompts_config.py +5 -0
  96. langroid/prompts/templates.py +141 -0
  97. langroid/pydantic_v1/__init__.py +10 -0
  98. langroid/pydantic_v1/main.py +4 -0
  99. langroid/utils/__init__.py +19 -0
  100. langroid/utils/algorithms/__init__.py +3 -0
  101. langroid/utils/algorithms/graph.py +103 -0
  102. langroid/utils/configuration.py +98 -0
  103. langroid/utils/constants.py +30 -0
  104. langroid/utils/git_utils.py +252 -0
  105. langroid/utils/globals.py +49 -0
  106. langroid/utils/logging.py +135 -0
  107. langroid/utils/object_registry.py +66 -0
  108. langroid/utils/output/__init__.py +20 -0
  109. langroid/utils/output/citations.py +41 -0
  110. langroid/utils/output/printing.py +99 -0
  111. langroid/utils/output/status.py +40 -0
  112. langroid/utils/pandas_utils.py +30 -0
  113. langroid/utils/pydantic_utils.py +602 -0
  114. langroid/utils/system.py +286 -0
  115. langroid/utils/types.py +93 -0
  116. langroid/vector_store/__init__.py +50 -0
  117. langroid/vector_store/base.py +359 -0
  118. langroid/vector_store/chromadb.py +214 -0
  119. langroid/vector_store/lancedb.py +406 -0
  120. langroid/vector_store/meilisearch.py +299 -0
  121. langroid/vector_store/momento.py +278 -0
  122. langroid/vector_store/qdrantdb.py +468 -0
  123. {langroid-0.33.6.dist-info → langroid-0.33.7.dist-info}/METADATA +95 -94
  124. langroid-0.33.7.dist-info/RECORD +127 -0
  125. {langroid-0.33.6.dist-info → langroid-0.33.7.dist-info}/WHEEL +1 -1
  126. langroid-0.33.6.dist-info/RECORD +0 -7
  127. langroid-0.33.6.dist-info/entry_points.txt +0 -4
  128. pyproject.toml +0 -356
  129. {langroid-0.33.6.dist-info → langroid-0.33.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,598 @@
1
+ """
2
+ Callbacks for Chainlit integration.
3
+ """
4
+
5
+ import json
6
+ import logging
7
+ import textwrap
8
+ from typing import Any, Callable, Dict, List, Literal, Optional, no_type_check
9
+
10
+ from langroid.exceptions import LangroidImportError
11
+ from langroid.pydantic_v1 import BaseSettings
12
+
13
+ try:
14
+ import chainlit as cl
15
+ except ImportError:
16
+ raise LangroidImportError("chainlit", "chainlit")
17
+
18
+ from chainlit import run_sync
19
+ from chainlit.logger import logger
20
+
21
+ import langroid as lr
22
+ import langroid.language_models as lm
23
+ from langroid.utils.configuration import settings
24
+ from langroid.utils.constants import NO_ANSWER
25
+
26
+ # Attempt to reconfigure the root logger to your desired settings
27
+ log_level = logging.INFO if settings.debug else logging.WARNING
28
+ logger.setLevel(log_level)
29
+ logging.basicConfig(level=log_level)
30
+
31
+ logging.getLogger().setLevel(log_level)
32
+
33
+ USER_TIMEOUT = 60_000
34
+ SYSTEM = "System 🖥️"
35
+ LLM = "LLM 🧠"
36
+ AGENT = "Agent <>"
37
+ YOU = "You 😃"
38
+ ERROR = "Error 🚫"
39
+
40
+
41
+ @no_type_check
42
+ async def ask_helper(func, **kwargs):
43
+ res = await func(**kwargs).send()
44
+ while not res:
45
+ res = await func(**kwargs).send()
46
+ return res
47
+
48
+
49
+ @no_type_check
50
+ async def setup_llm() -> None:
51
+ """From the session `llm_settings`, create new LLMConfig and LLM objects,
52
+ save them in session state."""
53
+ llm_settings = cl.user_session.get("llm_settings", {})
54
+ model = llm_settings.get("chat_model")
55
+ context_length = llm_settings.get("context_length", 16_000)
56
+ temperature = llm_settings.get("temperature", 0.2)
57
+ timeout = llm_settings.get("timeout", 90)
58
+ logger.info(f"Using model: {model}")
59
+ llm_config = lm.OpenAIGPTConfig(
60
+ chat_model=model or lm.OpenAIChatModel.GPT4o,
61
+ # or, other possibilities for example:
62
+ # "litellm/ollama_chat/mistral"
63
+ # "litellm/ollama_chat/mistral:7b-instruct-v0.2-q8_0"
64
+ # "litellm/ollama/llama2"
65
+ # "local/localhost:8000/v1"
66
+ # "local/localhost:8000"
67
+ chat_context_length=context_length, # adjust based on model
68
+ temperature=temperature,
69
+ timeout=timeout,
70
+ )
71
+ llm = lm.OpenAIGPT(llm_config)
72
+ cl.user_session.set("llm_config", llm_config)
73
+ cl.user_session.set("llm", llm)
74
+
75
+
76
+ @no_type_check
77
+ async def update_llm(new_settings: Dict[str, Any]) -> None:
78
+ """Update LLMConfig and LLM from settings, and save in session state."""
79
+ cl.user_session.set("llm_settings", new_settings)
80
+ await inform_llm_settings()
81
+ await setup_llm()
82
+
83
+
84
+ async def make_llm_settings_widgets(
85
+ config: lm.OpenAIGPTConfig | None = None,
86
+ ) -> None:
87
+ config = config or lm.OpenAIGPTConfig()
88
+ await cl.ChatSettings(
89
+ [
90
+ cl.input_widget.TextInput(
91
+ id="chat_model",
92
+ label="Model Name (Default GPT-4o)",
93
+ initial="",
94
+ placeholder="E.g. ollama/mistral or " "local/localhost:8000/v1",
95
+ ),
96
+ cl.input_widget.NumberInput(
97
+ id="context_length",
98
+ label="Chat Context Length",
99
+ initial=config.chat_context_length,
100
+ placeholder="E.g. 16000",
101
+ ),
102
+ cl.input_widget.Slider(
103
+ id="temperature",
104
+ label="LLM temperature",
105
+ min=0.0,
106
+ max=1.0,
107
+ step=0.1,
108
+ initial=config.temperature,
109
+ tooltip="Adjust based on model",
110
+ ),
111
+ cl.input_widget.Slider(
112
+ id="timeout",
113
+ label="Timeout (seconds)",
114
+ min=10,
115
+ max=200,
116
+ step=10,
117
+ initial=config.timeout,
118
+ tooltip="Timeout for LLM response, in seconds.",
119
+ ),
120
+ ]
121
+ ).send() # type: ignore
122
+
123
+
124
+ @no_type_check
125
+ async def inform_llm_settings() -> None:
126
+ llm_settings: Dict[str, Any] = cl.user_session.get("llm_settings", {})
127
+ settings_dict = dict(
128
+ model=llm_settings.get("chat_model"),
129
+ context_length=llm_settings.get("context_length"),
130
+ temperature=llm_settings.get("temperature"),
131
+ timeout=llm_settings.get("timeout"),
132
+ )
133
+ await cl.Message(
134
+ author=SYSTEM,
135
+ content="LLM settings updated",
136
+ elements=[
137
+ cl.Text(
138
+ name="settings",
139
+ display="side",
140
+ content=json.dumps(settings_dict, indent=4),
141
+ language="json",
142
+ )
143
+ ],
144
+ ).send()
145
+
146
+
147
+ async def add_instructions(
148
+ title: str = "Instructions",
149
+ content: str = "Enter your question/response in the dialog box below.",
150
+ display: Literal["side", "inline", "page"] = "inline",
151
+ ) -> None:
152
+ await cl.Message(
153
+ author="",
154
+ content=title if display == "side" else "",
155
+ elements=[
156
+ cl.Text(
157
+ name=title,
158
+ content=content,
159
+ display=display,
160
+ )
161
+ ],
162
+ ).send()
163
+
164
+
165
+ async def add_image(
166
+ path: str,
167
+ name: str,
168
+ display: Literal["side", "inline", "page"] = "inline",
169
+ ) -> None:
170
+ await cl.Message(
171
+ author="",
172
+ content=name if display == "side" else "",
173
+ elements=[
174
+ cl.Image(
175
+ name=name,
176
+ path=path,
177
+ display=display,
178
+ )
179
+ ],
180
+ ).send()
181
+
182
+
183
+ async def get_text_files(
184
+ message: cl.Message,
185
+ extensions: List[str] = [".txt", ".pdf", ".doc", ".docx"],
186
+ ) -> Dict[str, str]:
187
+ """Get dict (file_name -> file_path) from files uploaded in chat msg"""
188
+
189
+ files = [file for file in message.elements if file.path.endswith(tuple(extensions))]
190
+ return {file.name: file.path for file in files}
191
+
192
+
193
+ def wrap_text_preserving_structure(text: str, width: int = 90) -> str:
194
+ """Wrap text preserving paragraph breaks. Typically used to
195
+ format an agent_response output, which may have long lines
196
+ with no newlines or paragraph breaks."""
197
+
198
+ paragraphs = text.split("\n\n") # Split the text into paragraphs
199
+ wrapped_text = []
200
+
201
+ for para in paragraphs:
202
+ if para.strip(): # If the paragraph is not just whitespace
203
+ # Wrap this paragraph and add it to the result
204
+ wrapped_paragraph = textwrap.fill(para, width=width)
205
+ wrapped_text.append(wrapped_paragraph)
206
+ else:
207
+ # Preserve paragraph breaks
208
+ wrapped_text.append("")
209
+
210
+ return "\n\n".join(wrapped_text)
211
+
212
+
213
+ class ChainlitCallbackConfig(BaseSettings):
214
+ user_has_agent_name: bool = True # show agent name in front of "YOU" ?
215
+ show_subtask_response: bool = True # show sub-task response as a step?
216
+
217
+
218
+ class ChainlitAgentCallbacks:
219
+ """Inject Chainlit callbacks into a Langroid Agent"""
220
+
221
+ last_step: Optional[cl.Step] = None # used to display sub-steps under this
222
+ curr_step: Optional[cl.Step] = None # used to update an initiated step
223
+ stream: Optional[cl.Step] = None # pushed into openai_gpt.py to stream tokens
224
+ parent_agent: Optional[lr.Agent] = None # used to get parent id, for step nesting
225
+
226
+ def __init__(
227
+ self,
228
+ agent: lr.Agent,
229
+ config: ChainlitCallbackConfig = ChainlitCallbackConfig(),
230
+ ):
231
+ """Add callbacks to the agent, and save the initial message,
232
+ so we can alter the display of the first user message.
233
+ """
234
+ agent.callbacks.start_llm_stream = self.start_llm_stream
235
+ agent.callbacks.start_llm_stream_async = self.start_llm_stream_async
236
+ agent.callbacks.cancel_llm_stream = self.cancel_llm_stream
237
+ agent.callbacks.finish_llm_stream = self.finish_llm_stream
238
+ agent.callbacks.show_llm_response = self.show_llm_response
239
+ agent.callbacks.show_agent_response = self.show_agent_response
240
+ agent.callbacks.get_user_response = self.get_user_response
241
+ agent.callbacks.get_user_response_async = self.get_user_response_async
242
+ agent.callbacks.get_last_step = self.get_last_step
243
+ agent.callbacks.set_parent_agent = self.set_parent_agent
244
+ agent.callbacks.show_error_message = self.show_error_message
245
+ agent.callbacks.show_start_response = self.show_start_response
246
+ self.config = config
247
+ self.agent: lr.Agent = agent
248
+ if self.agent.llm is not None:
249
+ # We don't want to suppress LLM output in async + streaming,
250
+ # since we often use chainlit async callbacks to display LLM output
251
+ self.agent.llm.config.async_stream_quiet = False
252
+
253
+ def _get_parent_id(self) -> str | None:
254
+ """Get step id under which we need to nest the current step:
255
+ This should be the parent Agent's last_step.
256
+ """
257
+ if self.parent_agent is None:
258
+ logger.info(f"No parent agent found for {self.agent.config.name}")
259
+ return None
260
+ logger.info(
261
+ f"Parent agent found for {self.agent.config.name} = "
262
+ f"{self.parent_agent.config.name}"
263
+ )
264
+ last_step = self.parent_agent.callbacks.get_last_step()
265
+ if last_step is None:
266
+ logger.info(f"No last step found for {self.parent_agent.config.name}")
267
+ return None
268
+ logger.info(
269
+ f"Last step found for {self.parent_agent.config.name} = {last_step.id}"
270
+ )
271
+ return last_step.id # type: ignore
272
+
273
+ def set_parent_agent(self, parent: lr.Agent) -> None:
274
+ self.parent_agent = parent
275
+
276
+ def get_last_step(self) -> Optional[cl.Step]:
277
+ return self.last_step
278
+
279
+ def start_llm_stream(self) -> Callable[[str], None]:
280
+ """Returns a streaming fn that can be passed to the LLM class"""
281
+ self.stream = cl.Message(
282
+ content="",
283
+ id=self.curr_step.id if self.curr_step is not None else None,
284
+ author=self._entity_name("llm"),
285
+ type="assistant_message",
286
+ parent_id=self._get_parent_id(),
287
+ )
288
+ self.last_step = self.stream
289
+ self.curr_step = None
290
+ logger.info(
291
+ f"""
292
+ Starting LLM stream for {self.agent.config.name}
293
+ id = {self.stream.id}
294
+ under parent {self._get_parent_id()}
295
+ """
296
+ )
297
+
298
+ def stream_token(t: str) -> None:
299
+ if self.stream is None:
300
+ raise ValueError("Stream not initialized")
301
+ run_sync(self.stream.stream_token(t))
302
+
303
+ return stream_token
304
+
305
+ async def start_llm_stream_async(self) -> Callable[[str], None]:
306
+ """Returns a streaming fn that can be passed to the LLM class"""
307
+ self.stream = cl.Message(
308
+ content="",
309
+ id=self.curr_step.id if self.curr_step is not None else None,
310
+ author=self._entity_name("llm"),
311
+ type="assistant_message",
312
+ parent_id=self._get_parent_id(),
313
+ )
314
+ self.last_step = self.stream
315
+ self.curr_step = None
316
+ logger.info(
317
+ f"""
318
+ Starting LLM stream for {self.agent.config.name}
319
+ id = {self.stream.id}
320
+ under parent {self._get_parent_id()}
321
+ """
322
+ )
323
+
324
+ async def stream_token(t: str) -> None:
325
+ if self.stream is None:
326
+ raise ValueError("Stream not initialized")
327
+ await self.stream.stream_token(t)
328
+
329
+ return stream_token
330
+
331
+ def cancel_llm_stream(self) -> None:
332
+ """Called when cached response found."""
333
+ self.last_step = None
334
+ if self.stream is not None:
335
+ run_sync(self.stream.remove()) # type: ignore
336
+
337
+ def finish_llm_stream(self, content: str, is_tool: bool = False) -> None:
338
+ """Update the stream, and display entire response in the right language."""
339
+ if self.agent.llm is None or self.stream is None:
340
+ raise ValueError("LLM or stream not initialized")
341
+ if content == "":
342
+ run_sync(self.stream.remove()) # type: ignore
343
+ else:
344
+ run_sync(self.stream.update()) # type: ignore
345
+ stream_id = self.stream.id if content else None
346
+ step = cl.Message(
347
+ content=textwrap.dedent(content) or NO_ANSWER,
348
+ id=stream_id,
349
+ author=self._entity_name("llm", tool=is_tool),
350
+ type="assistant_message",
351
+ parent_id=self._get_parent_id(),
352
+ language="json" if is_tool else None,
353
+ )
354
+ logger.info(
355
+ f"""
356
+ Finish STREAM LLM response for {self.agent.config.name}
357
+ id = {step.id}
358
+ under parent {self._get_parent_id()}
359
+ """
360
+ )
361
+ run_sync(step.update()) # type: ignore
362
+
363
+ def show_llm_response(
364
+ self,
365
+ content: str,
366
+ is_tool: bool = False,
367
+ cached: bool = False,
368
+ language: str | None = None,
369
+ ) -> None:
370
+ """Show non-streaming LLM response."""
371
+ step = cl.Message(
372
+ content=textwrap.dedent(content) or NO_ANSWER,
373
+ id=self.curr_step.id if self.curr_step is not None else None,
374
+ author=self._entity_name("llm", tool=is_tool, cached=cached),
375
+ type="assistant_message",
376
+ language=language or ("json" if is_tool else None),
377
+ parent_id=self._get_parent_id(),
378
+ )
379
+ self.last_step = step
380
+ self.curr_step = None
381
+ logger.info(
382
+ f"""
383
+ Showing NON-STREAM LLM response for {self.agent.config.name}
384
+ id = {step.id}
385
+ under parent {self._get_parent_id()}
386
+ """
387
+ )
388
+ run_sync(step.send()) # type: ignore
389
+
390
+ def show_error_message(self, error: str) -> None:
391
+ """Show error message."""
392
+ step = cl.Message(
393
+ content=error,
394
+ author=self.agent.config.name + f"({ERROR})",
395
+ type="run",
396
+ language="text",
397
+ parent_id=self._get_parent_id(),
398
+ )
399
+ self.last_step = step
400
+ run_sync(step.send())
401
+
402
+ def show_agent_response(self, content: str, language="text") -> None:
403
+ """Show message from agent (typically tool handler)."""
404
+ if language == "text":
405
+ content = wrap_text_preserving_structure(content, width=90)
406
+ step = cl.Message(
407
+ content=content,
408
+ id=self.curr_step.id if self.curr_step is not None else None,
409
+ author=self._entity_name("agent"),
410
+ type="tool",
411
+ language=language,
412
+ parent_id=self._get_parent_id(),
413
+ )
414
+ self.last_step = step
415
+ self.curr_step = None
416
+ logger.info(
417
+ f"""
418
+ Showing AGENT response for {self.agent.config.name}
419
+ id = {step.id}
420
+ under parent {self._get_parent_id()}
421
+ """
422
+ )
423
+ run_sync(step.send()) # type: ignore
424
+
425
+ def show_start_response(self, entity: str) -> None:
426
+ """When there's a potentially long-running process, start a step,
427
+ so that the UI displays a spinner while the process is running."""
428
+ if self.curr_step is not None:
429
+ run_sync(self.curr_step.remove()) # type: ignore
430
+ step = cl.Message(
431
+ content="",
432
+ author=self._entity_name(entity),
433
+ type="run",
434
+ parent_id=self._get_parent_id(),
435
+ language="text",
436
+ )
437
+ self.last_step = step
438
+ self.curr_step = step
439
+ logger.info(
440
+ f"""
441
+ Showing START response for {self.agent.config.name} ({entity})
442
+ id = {step.id}
443
+ under parent {self._get_parent_id()}
444
+ """
445
+ )
446
+ run_sync(step.send()) # type: ignore
447
+
448
+ def _entity_name(
449
+ self, entity: str, tool: bool = False, cached: bool = False
450
+ ) -> str:
451
+ """Construct name of entity to display as Author of a step"""
452
+ tool_indicator = " => 🛠️" if tool else ""
453
+ cached = "(cached)" if cached else ""
454
+ match entity:
455
+ case "llm":
456
+ model = self.agent.config.llm.chat_model
457
+ return (
458
+ self.agent.config.name + f"({LLM} {model} {tool_indicator}){cached}"
459
+ )
460
+ case "agent":
461
+ return self.agent.config.name + f"({AGENT})"
462
+ case "user":
463
+ if self.config.user_has_agent_name:
464
+ return self.agent.config.name + f"({YOU})"
465
+ else:
466
+ return YOU
467
+ case _:
468
+ return self.agent.config.name + f"({entity})"
469
+
470
+ def _get_user_response_buttons(self, prompt: str) -> str:
471
+ """Not used. Save for future reference"""
472
+ res = run_sync(
473
+ ask_helper(
474
+ cl.AskActionMessage,
475
+ content="Continue, exit or say something?",
476
+ actions=[
477
+ cl.Action(
478
+ name="continue",
479
+ value="continue",
480
+ label="✅ Continue",
481
+ ),
482
+ cl.Action(
483
+ name="feedback",
484
+ value="feedback",
485
+ label="💬 Say something",
486
+ ),
487
+ cl.Action(name="exit", value="exit", label="🔚 Exit Conversation"),
488
+ ],
489
+ )
490
+ )
491
+ if res.get("value") == "continue":
492
+ return ""
493
+ if res.get("value") == "exit":
494
+ return "x"
495
+ if res.get("value") == "feedback":
496
+ return self.get_user_response(prompt)
497
+ return "" # process the "feedback" case here
498
+
499
+ def get_user_response(self, prompt: str) -> str:
500
+ """Ask for user response, wait for it, and return it"""
501
+
502
+ return run_sync(self.ask_user(prompt=prompt, suppress_values=["c"]))
503
+
504
+ async def get_user_response_async(self, prompt: str) -> str:
505
+ """Ask for user response, wait for it, and return it"""
506
+
507
+ return await self.ask_user(prompt=prompt, suppress_values=["c"])
508
+
509
+ async def ask_user(
510
+ self,
511
+ prompt: str,
512
+ timeout: int = USER_TIMEOUT,
513
+ suppress_values: List[str] = ["c"],
514
+ ) -> str:
515
+ """
516
+ Ask user for input.
517
+
518
+ Args:
519
+ prompt (str): Prompt to display to user
520
+ timeout (int): Timeout in seconds
521
+ suppress_values (List[str]): List of values to suppress from display
522
+ (e.g. "c" for continue)
523
+
524
+ Returns:
525
+ str: User response
526
+ """
527
+ ask_msg = cl.AskUserMessage(
528
+ content=prompt,
529
+ author=f"{self.agent.config.name}(Awaiting user input...)",
530
+ type="assistant_message",
531
+ timeout=timeout,
532
+ )
533
+ res = await ask_msg.send()
534
+ if prompt == "":
535
+ # if there was no actual prompt, clear the row from the UI for clarity.
536
+ await ask_msg.remove()
537
+
538
+ if res is None:
539
+ run_sync(
540
+ cl.Message(
541
+ content=f"Timed out after {USER_TIMEOUT} seconds. Exiting."
542
+ ).send()
543
+ )
544
+ return "x"
545
+
546
+ # Finally, reproduce the user response at right nesting level
547
+ if res["output"] in suppress_values:
548
+ return ""
549
+
550
+ return res["output"]
551
+
552
+
553
+ class ChainlitTaskCallbacks(ChainlitAgentCallbacks):
554
+ """
555
+ Recursively inject ChainlitAgentCallbacks into a Langroid Task's agent and
556
+ agents of sub-tasks.
557
+ """
558
+
559
+ def __init__(
560
+ self,
561
+ task: lr.Task,
562
+ config: ChainlitCallbackConfig = ChainlitCallbackConfig(),
563
+ ):
564
+ """Inject callbacks recursively, ensuring msg is passed to the
565
+ top-level agent"""
566
+
567
+ super().__init__(task.agent, config)
568
+ self._inject_callbacks(task)
569
+ self.task = task
570
+ if config.show_subtask_response:
571
+ self.task.callbacks.show_subtask_response = self.show_subtask_response
572
+
573
+ @classmethod
574
+ def _inject_callbacks(
575
+ cls, task: lr.Task, config: ChainlitCallbackConfig = ChainlitCallbackConfig()
576
+ ) -> None:
577
+ # recursively apply ChainlitAgentCallbacks to agents of sub-tasks
578
+ for t in task.sub_tasks:
579
+ cls(t, config=config)
580
+ # ChainlitTaskCallbacks(t, config=config)
581
+
582
+ def show_subtask_response(
583
+ self, task: lr.Task, content: str, is_tool: bool = False
584
+ ) -> None:
585
+ """Show sub-task response as a step, nested at the right level."""
586
+
587
+ # The step should nest under the calling agent's last step
588
+ step = cl.Message(
589
+ content=content or NO_ANSWER,
590
+ author=(
591
+ self.task.agent.config.name + f"( ⏎ From {task.agent.config.name})"
592
+ ),
593
+ type="run",
594
+ parent_id=self._get_parent_id(),
595
+ language="json" if is_tool else None,
596
+ )
597
+ self.last_step = step
598
+ run_sync(step.send())