vectara-agentic 0.2.4__py3-none-any.whl → 0.2.6__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.

Potentially problematic release.


This version of vectara-agentic might be problematic. Click here for more details.

vectara_agentic/agent.py CHANGED
@@ -8,33 +8,39 @@ from datetime import date
8
8
  import time
9
9
  import json
10
10
  import logging
11
- import traceback
12
11
  import asyncio
13
-
12
+ import importlib
14
13
  from collections import Counter
15
14
 
16
15
  import cloudpickle as pickle
17
16
 
18
17
  from dotenv import load_dotenv
19
18
 
20
- from retrying import retry
21
- from pydantic import Field, create_model
19
+ from pydantic import Field, create_model, ValidationError
22
20
 
23
21
  from llama_index.core.memory import ChatMemoryBuffer
24
22
  from llama_index.core.llms import ChatMessage, MessageRole
25
23
  from llama_index.core.tools import FunctionTool
26
- from llama_index.core.agent import ReActAgent
24
+ from llama_index.core.agent import ReActAgent, StructuredPlannerAgent
27
25
  from llama_index.core.agent.react.formatter import ReActChatFormatter
28
26
  from llama_index.agent.llm_compiler import LLMCompilerAgentWorker
29
27
  from llama_index.agent.lats import LATSAgentWorker
30
28
  from llama_index.core.callbacks import CallbackManager, TokenCountingHandler
31
29
  from llama_index.core.callbacks.base_handler import BaseCallbackHandler
32
30
  from llama_index.agent.openai import OpenAIAgent
33
-
34
-
35
- from .types import AgentType, AgentStatusType, LLMRole, ToolType, AgentResponse, AgentStreamingResponse
31
+ from llama_index.core.agent.runner.base import AgentRunner
32
+ from llama_index.core.agent.types import BaseAgent
33
+ from llama_index.core.workflow import Workflow
34
+
35
+ from .types import (
36
+ AgentType, AgentStatusType, LLMRole, ToolType,
37
+ AgentResponse, AgentStreamingResponse, AgentConfigType
38
+ )
36
39
  from .utils import get_llm, get_tokenizer_for_model
37
- from ._prompts import REACT_PROMPT_TEMPLATE, GENERAL_PROMPT_TEMPLATE, GENERAL_INSTRUCTIONS
40
+ from ._prompts import (
41
+ REACT_PROMPT_TEMPLATE, GENERAL_PROMPT_TEMPLATE, GENERAL_INSTRUCTIONS,
42
+ STRUCTURED_PLANNER_PLAN_REFINE_PROMPT, STRUCTURED_PLANNER_INITIAL_PLAN_PROMPT
43
+ )
38
44
  from ._callback import AgentCallbackHandler
39
45
  from ._observability import setup_observer, eval_fcs
40
46
  from .tools import VectaraToolFactory, VectaraTool, ToolsFactory
@@ -97,10 +103,6 @@ def _get_llm_compiler_prompt(prompt: str, topic: str, custom_instructions: str)
97
103
  prompt += f"Today is {date.today().strftime('%A, %B %d, %Y')}"
98
104
  return prompt
99
105
 
100
- def _retry_if_exception(exception):
101
- # Define the condition to retry on certain exceptions
102
- return isinstance(exception, (TimeoutError))
103
-
104
106
 
105
107
  def get_field_type(field_schema: dict) -> Any:
106
108
  """
@@ -141,12 +143,16 @@ class Agent:
141
143
  topic: str = "general",
142
144
  custom_instructions: str = "",
143
145
  verbose: bool = True,
146
+ use_structured_planning: bool = False,
144
147
  update_func: Optional[Callable[[AgentStatusType, str], None]] = None,
145
148
  agent_progress_callback: Optional[Callable[[AgentStatusType, str], None]] = None,
146
149
  query_logging_callback: Optional[Callable[[str, str], None]] = None,
147
150
  agent_config: Optional[AgentConfig] = None,
151
+ fallback_agent_config: Optional[AgentConfig] = None,
148
152
  chat_history: Optional[list[Tuple[str, str]]] = None,
149
153
  validate_tools: bool = False,
154
+ workflow_cls: Workflow = None,
155
+ workflow_timeout: int = 120,
150
156
  ) -> None:
151
157
  """
152
158
  Initialize the agent with the specified type, tools, topic, and system message.
@@ -157,26 +163,37 @@ class Agent:
157
163
  topic (str, optional): The topic for the agent. Defaults to 'general'.
158
164
  custom_instructions (str, optional): Custom instructions for the agent. Defaults to ''.
159
165
  verbose (bool, optional): Whether the agent should print its steps. Defaults to True.
166
+ use_structured_planning (bool, optional)
167
+ Whether or not we want to wrap the agent with LlamaIndex StructuredPlannerAgent.
160
168
  agent_progress_callback (Callable): A callback function the code calls on any agent updates.
161
169
  update_func (Callable): old name for agent_progress_callback. Will be deprecated in future.
162
170
  query_logging_callback (Callable): A callback function the code calls upon completion of a query
163
171
  agent_config (AgentConfig, optional): The configuration of the agent.
164
172
  Defaults to AgentConfig(), which reads from environment variables.
173
+ fallback_agent_config (AgentConfig, optional): The fallback configuration of the agent.
174
+ This config is used when the main agent config fails multiple times.
165
175
  chat_history (Tuple[str, str], optional): A list of user/agent chat pairs to initialize the agent memory.
166
176
  validate_tools (bool, optional): Whether to validate tool inconsistency with instructions.
167
177
  Defaults to False.
178
+ workflow_cls (Workflow, optional): The workflow class to be used with run(). Defaults to None.
179
+ workflow_timeout (int, optional): The timeout for the workflow in seconds. Defaults to 120.
168
180
  """
169
181
  self.agent_config = agent_config or AgentConfig()
170
- self.agent_type = self.agent_config.agent_type
182
+ self.agent_config_type = AgentConfigType.DEFAULT
171
183
  self.tools = tools
172
184
  if not any(tool.metadata.name == 'get_current_date' for tool in self.tools):
173
185
  self.tools += [ToolsFactory().create_tool(get_current_date)]
186
+ self.agent_type = self.agent_config.agent_type
187
+ self.use_structured_planning = use_structured_planning
174
188
  self.llm = get_llm(LLMRole.MAIN, config=self.agent_config)
175
189
  self._custom_instructions = custom_instructions
176
190
  self._topic = topic
177
191
  self.agent_progress_callback = agent_progress_callback if agent_progress_callback else update_func
178
192
  self.query_logging_callback = query_logging_callback
179
193
 
194
+ self.workflow_cls = workflow_cls
195
+ self.workflow_timeout = workflow_timeout
196
+
180
197
  # Validate tools
181
198
  # Check for:
182
199
  # 1. multiple copies of the same tool
@@ -214,7 +231,6 @@ class Agent:
214
231
  if self.tool_token_counter:
215
232
  callbacks.append(self.tool_token_counter)
216
233
  callback_manager = CallbackManager(callbacks) # type: ignore
217
- self.llm.callback_manager = callback_manager
218
234
  self.verbose = verbose
219
235
 
220
236
  if chat_history:
@@ -225,70 +241,119 @@ class Agent:
225
241
  self.memory = ChatMemoryBuffer.from_defaults(token_limit=128000, chat_history=msg_history)
226
242
  else:
227
243
  self.memory = ChatMemoryBuffer.from_defaults(token_limit=128000)
228
- if self.agent_type == AgentType.REACT:
229
- prompt = _get_prompt(REACT_PROMPT_TEMPLATE, topic, custom_instructions)
230
- self.agent = ReActAgent.from_tools(
244
+
245
+ # Set up main agent and fallback agent
246
+ self.agent = self._create_agent(self.agent_config, callback_manager)
247
+ self.fallback_agent_config = fallback_agent_config
248
+ if self.fallback_agent_config:
249
+ self.fallback_agent = self._create_agent(self.fallback_agent_config, callback_manager)
250
+ else:
251
+ self.fallback_agent_config = None
252
+
253
+ # Setup observability
254
+ try:
255
+ self.observability_enabled = setup_observer(self.agent_config)
256
+ except Exception as e:
257
+ print(f"Failed to set up observer ({e}), ignoring")
258
+ self.observability_enabled = False
259
+
260
+ def _create_agent(
261
+ self,
262
+ config: AgentConfig,
263
+ llm_callback_manager: CallbackManager
264
+ ) -> Union[BaseAgent, AgentRunner]:
265
+ """
266
+ Creates the agent based on the configuration object.
267
+
268
+ Args:
269
+
270
+ config: The configuration of the agent.
271
+ llm_callback_manager: The callback manager for the agent's llm.
272
+
273
+ Returns:
274
+ Union[BaseAgent, AgentRunner]: The configured agent object.
275
+ """
276
+ agent_type = config.agent_type
277
+ llm = get_llm(LLMRole.MAIN, config=config)
278
+ llm.callback_manager = llm_callback_manager
279
+
280
+ if agent_type == AgentType.REACT:
281
+ prompt = _get_prompt(REACT_PROMPT_TEMPLATE, self._topic, self._custom_instructions)
282
+ agent = ReActAgent.from_tools(
231
283
  tools=self.tools,
232
- llm=self.llm,
284
+ llm=llm,
233
285
  memory=self.memory,
234
- verbose=verbose,
286
+ verbose=self.verbose,
235
287
  react_chat_formatter=ReActChatFormatter(system_header=prompt),
236
- max_iterations=self.agent_config.max_reasoning_steps,
237
- callable_manager=callback_manager,
288
+ max_iterations=config.max_reasoning_steps,
289
+ callable_manager=llm_callback_manager,
238
290
  )
239
- elif self.agent_type == AgentType.OPENAI:
240
- prompt = _get_prompt(GENERAL_PROMPT_TEMPLATE, topic, custom_instructions)
241
- self.agent = OpenAIAgent.from_tools(
291
+ elif agent_type == AgentType.OPENAI:
292
+ prompt = _get_prompt(GENERAL_PROMPT_TEMPLATE, self._topic, self._custom_instructions)
293
+ agent = OpenAIAgent.from_tools(
242
294
  tools=self.tools,
243
- llm=self.llm,
295
+ llm=llm,
244
296
  memory=self.memory,
245
- verbose=verbose,
246
- callable_manager=callback_manager,
247
- max_function_calls=self.agent_config.max_reasoning_steps,
297
+ verbose=self.verbose,
298
+ callable_manager=llm_callback_manager,
299
+ max_function_calls=config.max_reasoning_steps,
248
300
  system_prompt=prompt,
249
301
  )
250
- elif self.agent_type == AgentType.LLMCOMPILER:
302
+ elif agent_type == AgentType.LLMCOMPILER:
251
303
  agent_worker = LLMCompilerAgentWorker.from_tools(
252
304
  tools=self.tools,
253
- llm=self.llm,
254
- verbose=verbose,
255
- callable_manager=callback_manager,
305
+ llm=llm,
306
+ verbose=self.verbose,
307
+ callable_manager=llm_callback_manager,
256
308
  )
257
309
  agent_worker.system_prompt = _get_prompt(
258
- _get_llm_compiler_prompt(agent_worker.system_prompt, topic, custom_instructions),
259
- topic, custom_instructions
310
+ _get_llm_compiler_prompt(agent_worker.system_prompt, self._topic, self._custom_instructions),
311
+ self._topic, self._custom_instructions
260
312
  )
261
313
  agent_worker.system_prompt_replan = _get_prompt(
262
- _get_llm_compiler_prompt(agent_worker.system_prompt_replan, topic, custom_instructions),
263
- topic, custom_instructions
314
+ _get_llm_compiler_prompt(agent_worker.system_prompt_replan, self._topic, self._custom_instructions),
315
+ self._topic, self._custom_instructions
264
316
  )
265
- self.agent = agent_worker.as_agent()
266
- elif self.agent_type == AgentType.LATS:
317
+ agent = agent_worker.as_agent()
318
+ elif agent_type == AgentType.LATS:
267
319
  agent_worker = LATSAgentWorker.from_tools(
268
320
  tools=self.tools,
269
- llm=self.llm,
321
+ llm=llm,
270
322
  num_expansions=3,
271
323
  max_rollouts=-1,
272
- verbose=verbose,
273
- callable_manager=callback_manager,
324
+ verbose=self.verbose,
325
+ callable_manager=llm_callback_manager,
274
326
  )
275
- prompt = _get_prompt(REACT_PROMPT_TEMPLATE, topic, custom_instructions)
327
+ prompt = _get_prompt(REACT_PROMPT_TEMPLATE, self._topic, self._custom_instructions)
276
328
  agent_worker.chat_formatter = ReActChatFormatter(system_header=prompt)
277
- self.agent = agent_worker.as_agent()
329
+ agent = agent_worker.as_agent()
278
330
  else:
279
- raise ValueError(f"Unknown agent type: {self.agent_type}")
331
+ raise ValueError(f"Unknown agent type: {agent_type}")
280
332
 
281
- try:
282
- self.observability_enabled = setup_observer(self.agent_config)
283
- except Exception as e:
284
- print(f"Failed to set up observer ({e}), ignoring")
285
- self.observability_enabled = False
333
+ # Set up structured planner if needed
334
+ if (self.use_structured_planning
335
+ or self.agent_type in [AgentType.LLMCOMPILER, AgentType.LATS]):
336
+ agent = StructuredPlannerAgent(
337
+ agent_worker=agent.agent_worker,
338
+ tools=self.tools,
339
+ memory=self.memory,
340
+ verbose=self.verbose,
341
+ initial_plan_prompt=STRUCTURED_PLANNER_INITIAL_PLAN_PROMPT,
342
+ plan_refine_prompt=STRUCTURED_PLANNER_PLAN_REFINE_PROMPT,
343
+ )
344
+
345
+ return agent
286
346
 
287
347
  def clear_memory(self) -> None:
288
348
  """
289
349
  Clear the agent's memory.
290
350
  """
291
- self.agent.memory.reset()
351
+ if self.agent_config_type == AgentConfigType.DEFAULT:
352
+ self.agent.memory.reset()
353
+ elif self.agent_config_type == AgentConfigType.FALLBACK and self.fallback_agent_config:
354
+ self.fallback_agent.memory.reset()
355
+ else:
356
+ raise ValueError(f"Invalid agent config type {self.agent_config_type}")
292
357
 
293
358
  def __eq__(self, other):
294
359
  if not isinstance(other, Agent):
@@ -296,10 +361,10 @@ class Agent:
296
361
  return False
297
362
 
298
363
  # Compare agent_type
299
- if self.agent_type != other.agent_type:
364
+ if self.agent_config.agent_type != other.agent_config.agent_type:
300
365
  print(
301
- f"Comparison failed: agent_type differs. (self.agent_type: {self.agent_type}, "
302
- f"other.agent_type: {other.agent_type})"
366
+ f"Comparison failed: agent_type differs. (self.agent_config.agent_type: {self.agent_config.agent_type},"
367
+ f" other.agent_config.agent_type: {other.agent_config.agent_type})"
303
368
  )
304
369
  return False
305
370
 
@@ -329,7 +394,7 @@ class Agent:
329
394
  print(f"Comparison failed: verbose differs. (self.verbose: {self.verbose}, other.verbose: {other.verbose})")
330
395
  return False
331
396
 
332
- # Compare agent
397
+ # Compare agent memory
333
398
  if self.agent.memory.chat_store != other.agent.memory.chat_store:
334
399
  print(
335
400
  f"Comparison failed: agent memory differs. (self.agent: {repr(self.agent.memory.chat_store)}, "
@@ -352,7 +417,11 @@ class Agent:
352
417
  agent_progress_callback: Optional[Callable[[AgentStatusType, str], None]] = None,
353
418
  query_logging_callback: Optional[Callable[[str, str], None]] = None,
354
419
  agent_config: AgentConfig = AgentConfig(),
420
+ validate_tools: bool = False,
421
+ fallback_agent_config: Optional[AgentConfig] = None,
355
422
  chat_history: Optional[list[Tuple[str, str]]] = None,
423
+ workflow_cls: Workflow = None,
424
+ workflow_timeout: int = 120,
356
425
  ) -> "Agent":
357
426
  """
358
427
  Create an agent from tools, agent type, and language model.
@@ -367,7 +436,12 @@ class Agent:
367
436
  update_func (Callable): old name for agent_progress_callback. Will be deprecated in future.
368
437
  query_logging_callback (Callable): A callback function the code calls upon completion of a query
369
438
  agent_config (AgentConfig, optional): The configuration of the agent.
439
+ fallback_agent_config (AgentConfig, optional): The fallback configuration of the agent.
370
440
  chat_history (Tuple[str, str], optional): A list of user/agent chat pairs to initialize the agent memory.
441
+ validate_tools (bool, optional): Whether to validate tool inconsistency with instructions.
442
+ Defaults to False.
443
+ workflow_cls (Workflow, optional): The workflow class to be used with run(). Defaults to None.
444
+ workflow_timeout (int, optional): The timeout for the workflow in seconds. Defaults to 120.
371
445
 
372
446
  Returns:
373
447
  Agent: An instance of the Agent class.
@@ -378,6 +452,9 @@ class Agent:
378
452
  query_logging_callback=query_logging_callback,
379
453
  update_func=update_func, agent_config=agent_config,
380
454
  chat_history=chat_history,
455
+ validate_tools=validate_tools,
456
+ fallback_agent_config=fallback_agent_config,
457
+ workflow_cls = workflow_cls, workflow_timeout = workflow_timeout,
381
458
  )
382
459
 
383
460
  @classmethod
@@ -390,6 +467,9 @@ class Agent:
390
467
  vectara_api_key: str = str(os.environ.get("VECTARA_API_KEY", "")),
391
468
  agent_progress_callback: Optional[Callable[[AgentStatusType, str], None]] = None,
392
469
  query_logging_callback: Optional[Callable[[str, str], None]] = None,
470
+ agent_config: AgentConfig = AgentConfig(),
471
+ fallback_agent_config: Optional[AgentConfig] = None,
472
+ chat_history: Optional[list[Tuple[str, str]]] = None,
393
473
  verbose: bool = False,
394
474
  vectara_filter_fields: list[dict] = [],
395
475
  vectara_offset: int = 0,
@@ -425,6 +505,9 @@ class Agent:
425
505
  vectara_api_key (str): The Vectara API key.
426
506
  agent_progress_callback (Callable): A callback function the code calls on any agent updates.
427
507
  query_logging_callback (Callable): A callback function the code calls upon completion of a query
508
+ agent_config (AgentConfig, optional): The configuration of the agent.
509
+ fallback_agent_config (AgentConfig, optional): The fallback configuration of the agent.
510
+ chat_history (Tuple[str, str], optional): A list of user/agent chat pairs to initialize the agent memory.
428
511
  data_description (str): The description of the data.
429
512
  assistant_specialty (str): The specialty of the assistant.
430
513
  verbose (bool, optional): Whether to print verbose output.
@@ -526,22 +609,41 @@ class Agent:
526
609
  verbose=verbose,
527
610
  agent_progress_callback=agent_progress_callback,
528
611
  query_logging_callback=query_logging_callback,
612
+ agent_config=agent_config,
613
+ fallback_agent_config=fallback_agent_config,
614
+ chat_history=chat_history,
529
615
  )
530
616
 
531
- def report(self) -> None:
617
+ def _switch_agent_config(self) -> None:
618
+ """"
619
+ Switch the configuration type of the agent.
620
+ This function is called automatically to switch the agent configuration if the current configuration fails.
621
+ """
622
+ if self.agent_config_type == AgentConfigType.DEFAULT:
623
+ self.agent_config_type = AgentConfigType.FALLBACK
624
+ else:
625
+ self.agent_config_type = AgentConfigType.DEFAULT
626
+
627
+ def report(self, detailed: bool = False) -> None:
532
628
  """
533
629
  Get a report from the agent.
534
630
 
631
+ Args:
632
+ detailed (bool, optional): Whether to include detailed information. Defaults to False.
633
+
535
634
  Returns:
536
635
  str: The report from the agent.
537
636
  """
538
637
  print("Vectara agentic Report:")
539
- print(f"Agent Type = {self.agent_type}")
638
+ print(f"Agent Type = {self.agent_config.agent_type}")
540
639
  print(f"Topic = {self._topic}")
541
640
  print("Tools:")
542
641
  for tool in self.tools:
543
642
  if hasattr(tool, 'metadata'):
544
- print(f"- {tool.metadata.name}")
643
+ if detailed:
644
+ print(f"- {tool.metadata.name} - {tool.metadata.description}")
645
+ else:
646
+ print(f"- {tool.metadata.name}")
545
647
  else:
546
648
  print("- tool without metadata")
547
649
  print(f"Agent LLM = {get_llm(LLMRole.MAIN, config=self.agent_config).metadata.model_name}")
@@ -559,13 +661,27 @@ class Agent:
559
661
  "tool token count": self.tool_token_counter.total_llm_token_count if self.tool_token_counter else -1,
560
662
  }
561
663
 
664
+ def _get_current_agent(self):
665
+ return self.agent if self.agent_config_type == AgentConfigType.DEFAULT else self.fallback_agent
666
+
667
+ def _get_current_agent_type(self):
668
+ return (
669
+ self.agent_config.agent_type if self.agent_config_type == AgentConfigType.DEFAULT
670
+ else self.fallback_agent_config.agent_type
671
+ )
672
+
562
673
  async def _aformat_for_lats(self, prompt, agent_response):
563
674
  llm_prompt = f"""
564
675
  Given the question '{prompt}', and agent response '{agent_response.response}',
565
676
  Please provide a well formatted final response to the query.
566
677
  final response:
567
678
  """
568
- agent_response.response = str(self.llm.acomplete(llm_prompt))
679
+ agent_type = self._get_current_agent_type()
680
+ if agent_type != AgentType.LATS:
681
+ return
682
+
683
+ agent = self._get_current_agent()
684
+ agent_response.response = str(agent.llm.acomplete(llm_prompt))
569
685
 
570
686
  def chat(self, prompt: str) -> AgentResponse: # type: ignore
571
687
  """
@@ -579,12 +695,7 @@ class Agent:
579
695
  """
580
696
  return asyncio.run(self.achat(prompt))
581
697
 
582
- @retry(
583
- retry_on_exception=_retry_if_exception,
584
- stop_max_attempt_number=3,
585
- wait_fixed=2000,
586
- )
587
- async def achat(self, prompt: str) -> AgentResponse: # type: ignore
698
+ async def achat(self, prompt: str) -> AgentResponse: # type: ignore
588
699
  """
589
700
  Interact with the agent using a chat prompt.
590
701
 
@@ -594,26 +705,30 @@ class Agent:
594
705
  Returns:
595
706
  AgentResponse: The response from the agent.
596
707
  """
597
-
598
- try:
599
- st = time.time()
600
- agent_response = await self.agent.achat(prompt)
601
- if self.agent_type == AgentType.LATS:
708
+ max_attempts = 4 if self.fallback_agent_config else 2
709
+ attempt = 0
710
+ while attempt < max_attempts:
711
+ try:
712
+ current_agent = self._get_current_agent()
713
+ agent_response = await current_agent.achat(prompt)
602
714
  await self._aformat_for_lats(prompt, agent_response)
603
- if self.verbose:
604
- print(f"Time taken: {time.time() - st}")
605
- if self.observability_enabled:
606
- eval_fcs()
607
- if self.query_logging_callback:
608
- self.query_logging_callback(prompt, agent_response.response)
609
- return agent_response
610
- except Exception as e:
611
- return AgentResponse(
612
- response = (
613
- f"Vectara Agentic: encountered an exception ({e}) at ({traceback.format_exc()})"
614
- ", and can't respond."
615
- )
616
- )
715
+ if self.observability_enabled:
716
+ eval_fcs()
717
+ if self.query_logging_callback:
718
+ self.query_logging_callback(prompt, agent_response.response)
719
+ return agent_response
720
+
721
+ except Exception:
722
+ if attempt >= 2:
723
+ if self.verbose:
724
+ print(f"LLM call failed on attempt {attempt+1}. Switching agent configuration.")
725
+ self._switch_agent_config()
726
+ time.sleep(1)
727
+ attempt += 1
728
+
729
+ return AgentResponse(
730
+ response=f"LLM failure can't be resolved after {max_attempts} attempts."
731
+ )
617
732
 
618
733
  def stream_chat(self, prompt: str) -> AgentStreamingResponse: # type: ignore
619
734
  """
@@ -625,11 +740,6 @@ class Agent:
625
740
  """
626
741
  return asyncio.run(self.astream_chat(prompt))
627
742
 
628
- @retry(
629
- retry_on_exception=_retry_if_exception,
630
- stop_max_attempt_number=3,
631
- wait_fixed=2000,
632
- )
633
743
  async def astream_chat(self, prompt: str) -> AgentStreamingResponse: # type: ignore
634
744
  """
635
745
  Interact with the agent using a chat prompt asynchronously with streaming.
@@ -638,29 +748,85 @@ class Agent:
638
748
  Returns:
639
749
  AgentStreamingResponse: The streaming response from the agent.
640
750
  """
641
- try:
642
- agent_response = await self.agent.astream_chat(prompt)
643
- original_async_response_gen = agent_response.async_response_gen
751
+ max_attempts = 4 if self.fallback_agent_config else 2
752
+ attempt = 0
753
+ while attempt < max_attempts:
754
+ try:
755
+ current_agent = self._get_current_agent()
756
+ agent_response = await current_agent.astream_chat(prompt)
757
+ original_async_response_gen = agent_response.async_response_gen
758
+
759
+ # Define a wrapper to preserve streaming behavior while executing post-stream logic.
760
+ async def _stream_response_wrapper():
761
+ async for token in original_async_response_gen():
762
+ yield token # Yield tokens as they are generated
763
+ # Post-streaming additional logic:
764
+ await self._aformat_for_lats(prompt, agent_response)
765
+ if self.query_logging_callback:
766
+ self.query_logging_callback(prompt, agent_response.response)
767
+ if self.observability_enabled:
768
+ eval_fcs()
769
+
770
+ agent_response.async_response_gen = _stream_response_wrapper # Override the generator
771
+ return agent_response
772
+
773
+ except Exception:
774
+ if attempt >= 2:
775
+ if self.verbose:
776
+ print("LLM call failed. Switching agent configuration.")
777
+ self._switch_agent_config()
778
+ time.sleep(1)
779
+ attempt += 1
780
+
781
+ return AgentResponse(
782
+ response=f"LLM failure can't be resolved after {max_attempts} attempts."
783
+ )
644
784
 
645
- # Wrap async_response_gen
646
- async def _stream_response_wrapper():
647
- async for token in original_async_response_gen():
648
- yield token # Yield async token to keep streaming behavior
785
+ #
786
+ # run() method for running a workflow
787
+ # workflow will always get these arguments in the StartEvent: agent, tools, llm, verbose
788
+ # the inputs argument comes from the call to run()
789
+ #
790
+ async def run(
791
+ self,
792
+ inputs: Any,
793
+ verbose: bool = False
794
+ ) -> Any:
795
+ """
796
+ Run a workflow using the agent.
797
+ workflow class must be provided in the agent constructor.
798
+ Args:
799
+ inputs (Any): The inputs to the workflow.
800
+ verbose (bool, optional): Whether to print verbose output. Defaults to False.
801
+ Returns:
802
+ Any: The output of the workflow.
803
+ """
804
+ # Create workflow
805
+ if self.workflow_cls:
806
+ workflow = self.workflow_cls(timeout=self.workflow_timeout, verbose=verbose)
807
+ else:
808
+ raise ValueError("Workflow is not defined.")
649
809
 
650
- # After streaming completes, execute additional logic
651
- if self.agent_type == AgentType.LATS:
652
- await self._aformat_for_lats(prompt, agent_response)
653
- if self.query_logging_callback:
654
- self.query_logging_callback(prompt, agent_response.response)
655
- if self.observability_enabled:
656
- eval_fcs()
810
+ # Validate inputs is in the form of workflow.InputsModel
811
+ if not isinstance(inputs, self.workflow_cls.InputsModel):
812
+ raise ValueError(f"Inputs must be an instance of {workflow.InputsModel}.")
657
813
 
658
- agent_response.async_response_gen = _stream_response_wrapper # Override method
659
- return agent_response
660
- except Exception as e:
661
- raise ValueError(
662
- f"Vectara Agentic: encountered an exception ({e}) at ({traceback.format_exc()}), and can't respond."
663
- ) from e
814
+ # run workflow
815
+ result = await workflow.run(
816
+ agent=self,
817
+ tools=self.tools,
818
+ llm=self.llm,
819
+ verbose=verbose,
820
+ inputs=inputs,
821
+ )
822
+
823
+ # return output in the form of workflow.OutputsModel
824
+ try:
825
+ output = workflow.OutputsModel.model_validate(result)
826
+ except ValidationError as e:
827
+ raise ValueError(f"Failed to map workflow output to model: {e}") from e
828
+
829
+ return output
664
830
 
665
831
  #
666
832
  # Serialization methods
@@ -679,56 +845,85 @@ class Agent:
679
845
  tool_info = []
680
846
 
681
847
  for tool in self.tools:
682
- # Serialize each tool's metadata, function, and dynamic model schema (QueryArgs)
848
+ if hasattr(tool.metadata, "fn_schema"):
849
+ fn_schema_cls = tool.metadata.fn_schema
850
+ fn_schema_serialized = {
851
+ "schema": (
852
+ fn_schema_cls.model_json_schema()
853
+ if hasattr(fn_schema_cls, "model_json_schema")
854
+ else None
855
+ ),
856
+ "metadata": {
857
+ "module": fn_schema_cls.__module__,
858
+ "class": fn_schema_cls.__name__,
859
+ }
860
+ }
861
+ else:
862
+ fn_schema_serialized = None
863
+
683
864
  tool_dict = {
684
865
  "tool_type": tool.metadata.tool_type.value,
685
866
  "name": tool.metadata.name,
686
867
  "description": tool.metadata.description,
687
- "fn": pickle.dumps(tool.fn).decode("latin-1") if tool.fn else None, # Serialize fn
688
- "async_fn": pickle.dumps(tool.async_fn).decode("latin-1")
689
- if tool.async_fn
690
- else None, # Serialize async_fn
691
- "fn_schema": tool.metadata.fn_schema.model_json_schema()
692
- if hasattr(tool.metadata, "fn_schema")
693
- else None, # Serialize schema if available
868
+ "fn": pickle.dumps(getattr(tool, 'fn', None)).decode("latin-1") if getattr(tool, 'fn', None) else None,
869
+ "async_fn": pickle.dumps(getattr(tool, 'async_fn', None)).decode("latin-1")
870
+ if getattr(tool, 'async_fn', None) else None,
871
+ "fn_schema": fn_schema_serialized,
694
872
  }
695
873
  tool_info.append(tool_dict)
696
874
 
697
875
  return {
698
- "agent_type": self.agent_type.value,
876
+ "agent_type": self.agent_config.agent_type.value,
699
877
  "memory": pickle.dumps(self.agent.memory).decode("latin-1"),
700
878
  "tools": tool_info,
701
879
  "topic": self._topic,
702
880
  "custom_instructions": self._custom_instructions,
703
881
  "verbose": self.verbose,
704
882
  "agent_config": self.agent_config.to_dict(),
883
+ "fallback_agent": self.fallback_agent_config.to_dict() if self.fallback_agent_config else None,
884
+ "workflow_cls": self.workflow_cls if self.workflow_cls else None,
705
885
  }
706
886
 
707
887
  @classmethod
708
888
  def from_dict(cls, data: Dict[str, Any]) -> "Agent":
709
889
  """Create an Agent instance from a dictionary."""
710
890
  agent_config = AgentConfig.from_dict(data["agent_config"])
891
+ fallback_agent_config = (
892
+ AgentConfig.from_dict(data["fallback_agent_config"])
893
+ if data.get("fallback_agent_config")
894
+ else None
895
+ )
711
896
  tools = []
712
897
 
713
898
  for tool_data in data["tools"]:
714
899
  # Recreate the dynamic model using the schema info
715
900
  if tool_data.get("fn_schema"):
716
- field_definitions = {}
717
- for field, values in tool_data["fn_schema"]["properties"].items():
718
- # Instead of checking for 'type', use the helper:
719
- field_type = get_field_type(values)
720
- # If there's a default value, include it.
721
- if "default" in values:
722
- field_definitions[field] = (
723
- field_type,
724
- Field(description=values.get("description", ""), default=values["default"]),
725
- )
726
- else:
727
- field_definitions[field] = (
728
- field_type,
729
- Field(description=values.get("description", "")),
730
- )
731
- query_args_model = create_model("QueryArgs", **field_definitions) # type: ignore
901
+ schema_info = tool_data["fn_schema"]
902
+ try:
903
+ module_name = schema_info["metadata"]["module"]
904
+ class_name = schema_info["metadata"]["class"]
905
+ mod = importlib.import_module(module_name)
906
+ fn_schema_cls = getattr(mod, class_name)
907
+ query_args_model = fn_schema_cls
908
+ except Exception:
909
+ # Fallback: rebuild using the JSON schema
910
+ field_definitions = {}
911
+ for field, values in schema_info.get("schema", {}).get("properties", {}).items():
912
+ field_type = get_field_type(values)
913
+ if "default" in values:
914
+ field_definitions[field] = (
915
+ field_type,
916
+ Field(description=values.get("description", ""), default=values["default"]),
917
+ )
918
+ else:
919
+ field_definitions[field] = (
920
+ field_type,
921
+ Field(description=values.get("description", "")),
922
+ )
923
+ query_args_model = create_model(
924
+ schema_info.get("schema", {}).get("title", "QueryArgs"),
925
+ **field_definitions
926
+ )
732
927
  else:
733
928
  query_args_model = create_model("QueryArgs")
734
929
 
@@ -751,6 +946,8 @@ class Agent:
751
946
  topic=data["topic"],
752
947
  custom_instructions=data["custom_instructions"],
753
948
  verbose=data["verbose"],
949
+ fallback_agent_config=fallback_agent_config,
950
+ workflow_cls=data["workflow_cls"],
754
951
  )
755
952
  memory = pickle.loads(data["memory"].encode("latin-1")) if data.get("memory") else None
756
953
  if memory: