vectara-agentic 0.3.3__py3-none-any.whl → 0.4.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.

Potentially problematic release.


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

Files changed (56) hide show
  1. tests/__init__.py +7 -0
  2. tests/conftest.py +316 -0
  3. tests/endpoint.py +54 -17
  4. tests/run_tests.py +112 -0
  5. tests/test_agent.py +35 -33
  6. tests/test_agent_fallback_memory.py +270 -0
  7. tests/test_agent_memory_consistency.py +229 -0
  8. tests/test_agent_type.py +86 -143
  9. tests/test_api_endpoint.py +4 -0
  10. tests/test_bedrock.py +50 -31
  11. tests/test_fallback.py +4 -0
  12. tests/test_gemini.py +27 -59
  13. tests/test_groq.py +50 -31
  14. tests/test_private_llm.py +11 -2
  15. tests/test_return_direct.py +6 -2
  16. tests/test_serialization.py +7 -6
  17. tests/test_session_memory.py +252 -0
  18. tests/test_streaming.py +109 -0
  19. tests/test_together.py +62 -0
  20. tests/test_tools.py +10 -82
  21. tests/test_vectara_llms.py +4 -0
  22. tests/test_vhc.py +67 -0
  23. tests/test_workflow.py +13 -28
  24. vectara_agentic/__init__.py +27 -4
  25. vectara_agentic/_callback.py +65 -67
  26. vectara_agentic/_observability.py +30 -30
  27. vectara_agentic/_version.py +1 -1
  28. vectara_agentic/agent.py +565 -859
  29. vectara_agentic/agent_config.py +15 -14
  30. vectara_agentic/agent_core/__init__.py +22 -0
  31. vectara_agentic/agent_core/factory.py +383 -0
  32. vectara_agentic/{_prompts.py → agent_core/prompts.py} +21 -46
  33. vectara_agentic/agent_core/serialization.py +348 -0
  34. vectara_agentic/agent_core/streaming.py +483 -0
  35. vectara_agentic/agent_core/utils/__init__.py +29 -0
  36. vectara_agentic/agent_core/utils/hallucination.py +157 -0
  37. vectara_agentic/agent_core/utils/logging.py +52 -0
  38. vectara_agentic/agent_core/utils/schemas.py +87 -0
  39. vectara_agentic/agent_core/utils/tools.py +125 -0
  40. vectara_agentic/agent_endpoint.py +4 -6
  41. vectara_agentic/db_tools.py +37 -12
  42. vectara_agentic/llm_utils.py +42 -43
  43. vectara_agentic/sub_query_workflow.py +9 -14
  44. vectara_agentic/tool_utils.py +138 -83
  45. vectara_agentic/tools.py +36 -21
  46. vectara_agentic/tools_catalog.py +16 -16
  47. vectara_agentic/types.py +106 -8
  48. {vectara_agentic-0.3.3.dist-info → vectara_agentic-0.4.1.dist-info}/METADATA +111 -31
  49. vectara_agentic-0.4.1.dist-info/RECORD +53 -0
  50. tests/test_agent_planning.py +0 -64
  51. tests/test_hhem.py +0 -100
  52. vectara_agentic/hhem.py +0 -82
  53. vectara_agentic-0.3.3.dist-info/RECORD +0 -39
  54. {vectara_agentic-0.3.3.dist-info → vectara_agentic-0.4.1.dist-info}/WHEEL +0 -0
  55. {vectara_agentic-0.3.3.dist-info → vectara_agentic-0.4.1.dist-info}/licenses/LICENSE +0 -0
  56. {vectara_agentic-0.3.3.dist-info → vectara_agentic-0.4.1.dist-info}/top_level.txt +0 -0
vectara_agentic/agent.py CHANGED
@@ -2,85 +2,70 @@
2
2
  This module contains the Agent class for handling different types of agents and their interactions.
3
3
  """
4
4
 
5
- from typing import List, Callable, Optional, Dict, Any, Union, Tuple
5
+ import warnings
6
+
7
+ warnings.simplefilter("ignore", DeprecationWarning)
8
+
9
+ # pylint: disable=wrong-import-position
10
+ from typing import List, Callable, Optional, Dict, Any, Tuple, TYPE_CHECKING
6
11
  import os
7
- import re
8
12
  from datetime import date
9
- import time
10
13
  import json
11
14
  import logging
12
15
  import asyncio
13
- import importlib
14
- from collections import Counter
15
- import inspect
16
- from inspect import Signature, Parameter, ismethod
17
- from pydantic import Field, create_model, ValidationError, BaseModel
18
- from pydantic_core import PydanticUndefined
19
16
 
20
- import cloudpickle as pickle
17
+ from pydantic import ValidationError
18
+ from pydantic_core import PydanticUndefined
21
19
 
22
20
  from dotenv import load_dotenv
23
21
 
24
- from llama_index.core.memory import ChatMemoryBuffer
25
- from llama_index.core.llms import ChatMessage, MessageRole
26
- from llama_index.core.tools import FunctionTool
27
- from llama_index.core.agent import (
28
- ReActAgent,
29
- StructuredPlannerAgent,
30
- FunctionCallingAgent,
31
- )
32
- from llama_index.core.agent.react.formatter import ReActChatFormatter
33
- from llama_index.agent.llm_compiler import LLMCompilerAgentWorker
34
- from llama_index.agent.lats import LATSAgentWorker
35
- from llama_index.core.callbacks import CallbackManager, TokenCountingHandler
36
- from llama_index.core.callbacks.base_handler import BaseCallbackHandler
37
- from llama_index.agent.openai import OpenAIAgent
38
- from llama_index.core.agent.runner.base import AgentRunner
39
- from llama_index.core.agent.types import BaseAgent
40
- from llama_index.core.workflow import Workflow, Context
22
+ # Runtime imports for components used at module level
23
+ from llama_index.core.llms import MessageRole, ChatMessage
24
+ from llama_index.core.callbacks import CallbackManager
25
+ from llama_index.core.memory import Memory
26
+
27
+ # Heavy llama_index imports moved to TYPE_CHECKING for lazy loading
28
+ if TYPE_CHECKING:
29
+ from llama_index.core.tools import FunctionTool
30
+ from llama_index.core.workflow import Workflow
31
+ from llama_index.core.agent.types import BaseAgent
32
+ from llama_index.core.callbacks.base_handler import BaseCallbackHandler
33
+
41
34
 
42
35
  from .types import (
43
36
  AgentType,
44
37
  AgentStatusType,
45
38
  LLMRole,
46
- ToolType,
47
39
  ModelProvider,
48
40
  AgentResponse,
49
41
  AgentStreamingResponse,
50
42
  AgentConfigType,
51
43
  )
52
- from .llm_utils import get_llm, get_tokenizer_for_model
53
- from ._prompts import (
54
- REACT_PROMPT_TEMPLATE,
55
- GENERAL_PROMPT_TEMPLATE,
56
- GENERAL_INSTRUCTIONS,
57
- STRUCTURED_PLANNER_PLAN_REFINE_PROMPT,
58
- STRUCTURED_PLANNER_INITIAL_PLAN_PROMPT,
59
- )
44
+ from .llm_utils import get_llm
45
+ from .agent_core.prompts import GENERAL_INSTRUCTIONS
60
46
  from ._callback import AgentCallbackHandler
61
- from ._observability import setup_observer, eval_fcs
62
- from .tools import VectaraToolFactory, VectaraTool, ToolsFactory
63
- from .tool_utils import _is_human_readable_output
47
+ from ._observability import setup_observer
48
+ from .tools import ToolsFactory
64
49
  from .tools_catalog import get_current_date
65
50
  from .agent_config import AgentConfig
66
- from .hhem import HHEM
67
-
68
-
69
- class IgnoreUnpickleableAttributeFilter(logging.Filter):
70
- """
71
- Filter to ignore log messages that contain certain strings
72
- """
73
-
74
- def filter(self, record):
75
- msgs_to_ignore = [
76
- "Removing unpickleable private attribute _chunking_tokenizer_fn",
77
- "Removing unpickleable private attribute _split_fns",
78
- "Removing unpickleable private attribute _sub_sentence_split_fns",
79
- ]
80
- return all(msg not in record.getMessage() for msg in msgs_to_ignore)
81
51
 
52
+ # Import utilities from agent core modules
53
+ from .agent_core.streaming import (
54
+ FunctionCallingStreamHandler,
55
+ execute_post_stream_processing,
56
+ )
57
+ from .agent_core.factory import create_agent_from_config, create_agent_from_corpus
58
+ from .agent_core.serialization import (
59
+ serialize_agent_to_dict,
60
+ deserialize_agent_from_dict,
61
+ )
62
+ from .agent_core.utils import (
63
+ sanitize_tools_for_gemini,
64
+ setup_agent_logging,
65
+ )
66
+ from .agent_core.utils.tools import validate_tool_consistency
82
67
 
83
- logging.getLogger().addFilter(IgnoreUnpickleableAttributeFilter())
68
+ setup_agent_logging()
84
69
 
85
70
  logger = logging.getLogger("opentelemetry.exporter.otlp.proto.http.trace_exporter")
86
71
  logger.setLevel(logging.CRITICAL)
@@ -88,113 +73,6 @@ logger.setLevel(logging.CRITICAL)
88
73
  load_dotenv(override=True)
89
74
 
90
75
 
91
- def _get_prompt(
92
- prompt_template: str,
93
- general_instructions: str,
94
- topic: str,
95
- custom_instructions: str,
96
- ):
97
- """
98
- Generate a prompt by replacing placeholders with topic and date.
99
-
100
- Args:
101
- prompt_template (str): The template for the prompt.
102
- general_instructions (str): General instructions to be included in the prompt.
103
- topic (str): The topic to be included in the prompt.
104
- custom_instructions(str): The custom instructions to be included in the prompt.
105
-
106
- Returns:
107
- str: The formatted prompt.
108
- """
109
- return (
110
- prompt_template.replace("{chat_topic}", topic)
111
- .replace("{today}", date.today().strftime("%A, %B %d, %Y"))
112
- .replace("{custom_instructions}", custom_instructions)
113
- .replace("{INSTRUCTIONS}", general_instructions)
114
- )
115
-
116
-
117
- def _get_llm_compiler_prompt(
118
- prompt: str, general_instructions: str, topic: str, custom_instructions: str
119
- ) -> str:
120
- """
121
- Add custom instructions to the prompt.
122
-
123
- Args:
124
- prompt (str): The prompt to which custom instructions should be added.
125
-
126
- Returns:
127
- str: The prompt with custom instructions added.
128
- """
129
- prompt += "\nAdditional Instructions:\n"
130
- prompt += f"You have experise in {topic}.\n"
131
- prompt += general_instructions
132
- prompt += custom_instructions
133
- prompt += f"Today is {date.today().strftime('%A, %B %d, %Y')}"
134
- return prompt
135
-
136
-
137
- def get_field_type(field_schema: dict) -> Any:
138
- """
139
- Convert a JSON schema field definition to a Python type.
140
- Handles 'type' and 'anyOf' cases.
141
- """
142
- json_type_to_python = {
143
- "string": str,
144
- "integer": int,
145
- "boolean": bool,
146
- "array": list,
147
- "object": dict,
148
- "number": float,
149
- "null": type(None),
150
- }
151
- if not field_schema: # Handles empty schema {}
152
- return Any
153
-
154
- if "anyOf" in field_schema:
155
- types = []
156
- for option_schema in field_schema["anyOf"]:
157
- types.append(get_field_type(option_schema)) # Recursive call
158
- if not types:
159
- return Any
160
- return Union[tuple(types)]
161
-
162
- if "type" in field_schema and isinstance(field_schema["type"], list):
163
- types = []
164
- for type_name in field_schema["type"]:
165
- if type_name == "array":
166
- item_schema = field_schema.get("items", {})
167
- types.append(List[get_field_type(item_schema)])
168
- elif type_name in json_type_to_python:
169
- types.append(json_type_to_python[type_name])
170
- else:
171
- types.append(Any) # Fallback for unknown types in the list
172
- if not types:
173
- return Any
174
- return Union[tuple(types)] # type: ignore
175
-
176
- if "type" in field_schema:
177
- schema_type_name = field_schema["type"]
178
- if schema_type_name == "array":
179
- item_schema = field_schema.get(
180
- "items", {}
181
- ) # Default to Any if "items" is missing
182
- return List[get_field_type(item_schema)]
183
-
184
- return json_type_to_python.get(schema_type_name, Any)
185
-
186
- # If only "items" is present (implies array by some conventions, but less standard)
187
- # Or if it's a schema with other keywords like 'properties' (implying object)
188
- # For simplicity, if no "type" or "anyOf" at this point, default to Any or add more specific handling.
189
- # If 'properties' in field_schema or 'additionalProperties' in field_schema, it's likely an object.
190
- if "properties" in field_schema or "additionalProperties" in field_schema:
191
- # This path might need to reconstruct a nested Pydantic model if you encounter such schemas.
192
- # For now, treating as 'dict' or 'Any' might be a simpler placeholder.
193
- return dict # Or Any, or more sophisticated object reconstruction.
194
-
195
- return Any
196
-
197
-
198
76
  class Agent:
199
77
  """
200
78
  Agent class for handling different types of agents and their interactions.
@@ -202,13 +80,11 @@ class Agent:
202
80
 
203
81
  def __init__(
204
82
  self,
205
- tools: List[FunctionTool],
83
+ tools: List["FunctionTool"],
206
84
  topic: str = "general",
207
85
  custom_instructions: str = "",
208
86
  general_instructions: str = GENERAL_INSTRUCTIONS,
209
- verbose: bool = True,
210
- use_structured_planning: bool = False,
211
- update_func: Optional[Callable[[AgentStatusType, dict, str], None]] = None,
87
+ verbose: bool = False,
212
88
  agent_progress_callback: Optional[
213
89
  Callable[[AgentStatusType, dict, str], None]
214
90
  ] = None,
@@ -217,9 +93,10 @@ class Agent:
217
93
  fallback_agent_config: Optional[AgentConfig] = None,
218
94
  chat_history: Optional[list[Tuple[str, str]]] = None,
219
95
  validate_tools: bool = False,
220
- workflow_cls: Optional[Workflow] = None,
96
+ workflow_cls: Optional["Workflow"] = None,
221
97
  workflow_timeout: int = 120,
222
98
  vectara_api_key: Optional[str] = None,
99
+ session_id: Optional[str] = None,
223
100
  ) -> None:
224
101
  """
225
102
  Initialize the agent with the specified type, tools, topic, and system message.
@@ -232,11 +109,8 @@ class Agent:
232
109
  general_instructions (str, optional): General instructions for the agent.
233
110
  The Agent has a default set of instructions that are crafted to help it operate effectively.
234
111
  This allows you to customize the agent's behavior and personality, but use with caution.
235
- verbose (bool, optional): Whether the agent should print its steps. Defaults to True.
236
- use_structured_planning (bool, optional)
237
- Whether or not we want to wrap the agent with LlamaIndex StructuredPlannerAgent.
112
+ verbose (bool, optional): Whether the agent should print its steps. Defaults to False.
238
113
  agent_progress_callback (Callable): A callback function the code calls on any agent updates.
239
- update_func (Callable): old name for agent_progress_callback. Will be deprecated in future.
240
114
  query_logging_callback (Callable): A callback function the code calls upon completion of a query
241
115
  agent_config (AgentConfig, optional): The configuration of the agent.
242
116
  Defaults to AgentConfig(), which reads from environment variables.
@@ -247,103 +121,61 @@ class Agent:
247
121
  Defaults to False.
248
122
  workflow_cls (Workflow, optional): The workflow class to be used with run(). Defaults to None.
249
123
  workflow_timeout (int, optional): The timeout for the workflow in seconds. Defaults to 120.
250
- vectara_api_key (str, optional): The Vectara API key for FCS evaluation. Defaults to None.
124
+ vectara_api_key (str, optional): The Vectara API key for VHC computation. Defaults to None.
125
+ session_id (str, optional): The session ID for memory persistence.
126
+ If None, auto-generates from topic and date. Defaults to None.
251
127
  """
252
128
  self.agent_config = agent_config or AgentConfig()
253
129
  self.agent_config_type = AgentConfigType.DEFAULT
254
130
  self.tools = tools
255
131
  if not any(tool.metadata.name == "get_current_date" for tool in self.tools):
256
- self.tools += [ToolsFactory().create_tool(get_current_date)]
132
+ self.tools += [
133
+ ToolsFactory().create_tool(get_current_date, vhc_eligible=False)
134
+ ]
257
135
  self.agent_type = self.agent_config.agent_type
258
- self.use_structured_planning = use_structured_planning
259
136
  self._llm = None # Lazy loading
260
137
  self._custom_instructions = custom_instructions
261
138
  self._general_instructions = general_instructions
262
139
  self._topic = topic
263
- self.agent_progress_callback = (
264
- agent_progress_callback if agent_progress_callback else update_func
265
- )
266
- self.query_logging_callback = query_logging_callback
140
+ self.agent_progress_callback = agent_progress_callback
267
141
 
142
+ self.query_logging_callback = query_logging_callback
268
143
  self.workflow_cls = workflow_cls
269
144
  self.workflow_timeout = workflow_timeout
270
145
  self.vectara_api_key = vectara_api_key or os.environ.get("VECTARA_API_KEY", "")
271
146
 
272
147
  # Sanitize tools for Gemini if needed
273
148
  if self.agent_config.main_llm_provider == ModelProvider.GEMINI:
274
- self.tools = self._sanitize_tools_for_gemini(self.tools)
149
+ self.tools = sanitize_tools_for_gemini(self.tools)
275
150
 
276
151
  # Validate tools
277
- # Check for:
278
- # 1. multiple copies of the same tool
279
- # 2. Instructions for using tools that do not exist
280
- tool_names = [tool.metadata.name for tool in self.tools]
281
- duplicates = [tool for tool, count in Counter(tool_names).items() if count > 1]
282
- if duplicates:
283
- raise ValueError(f"Duplicate tools detected: {', '.join(duplicates)}")
284
-
285
152
  if validate_tools:
286
- prompt = f"""
287
- You are provided these tools:
288
- <tools>{','.join(tool_names)}</tools>
289
- And these instructions:
290
- <instructions>
291
- {self._custom_instructions}
292
- </instructions>
293
- Your task is to identify invalid tools.
294
- A tool is invalid if it is mentioned in the instructions but not in the tools list.
295
- A tool's name must have at least two characters.
296
- Your response should be a comma-separated list of the invalid tools.
297
- If no invalid tools exist, respond with "<OKAY>" (and nothing else).
298
- """
299
- llm = get_llm(LLMRole.MAIN, config=self.agent_config)
300
- bad_tools_str = llm.complete(prompt).text.strip("\n")
301
- if bad_tools_str and bad_tools_str != "<OKAY>":
302
- bad_tools = [tool.strip() for tool in bad_tools_str.split(",")]
303
- numbered = ", ".join(
304
- f"({i}) {tool}" for i, tool in enumerate(bad_tools, 1)
305
- )
306
- raise ValueError(
307
- f"The Agent custom instructions mention these invalid tools: {numbered}"
308
- )
309
-
310
- # Create token counters for the main and tool LLMs
311
- main_tok = get_tokenizer_for_model(role=LLMRole.MAIN)
312
- self.main_token_counter = (
313
- TokenCountingHandler(tokenizer=main_tok) if main_tok else None
314
- )
315
- tool_tok = get_tokenizer_for_model(role=LLMRole.TOOL)
316
- self.tool_token_counter = (
317
- TokenCountingHandler(tokenizer=tool_tok) if tool_tok else None
318
- )
153
+ validate_tool_consistency(
154
+ self.tools, self._custom_instructions, self.agent_config
155
+ )
319
156
 
320
157
  # Setup callback manager
321
158
  callbacks: list[BaseCallbackHandler] = [
322
159
  AgentCallbackHandler(self.agent_progress_callback)
323
160
  ]
324
- if self.main_token_counter:
325
- callbacks.append(self.main_token_counter)
326
- if self.tool_token_counter:
327
- callbacks.append(self.tool_token_counter)
328
161
  self.callback_manager = CallbackManager(callbacks) # type: ignore
329
162
  self.verbose = verbose
330
163
 
164
+ self.session_id = (
165
+ session_id
166
+ or getattr(self, "session_id", None)
167
+ or f"{topic}:{date.today().isoformat()}"
168
+ )
169
+
170
+ self.memory = Memory.from_defaults(
171
+ session_id=self.session_id, token_limit=65536
172
+ )
331
173
  if chat_history:
332
- msg_history = []
333
- for text_pairs in chat_history:
334
- msg_history.append(
335
- ChatMessage.from_str(content=text_pairs[0], role=MessageRole.USER)
336
- )
337
- msg_history.append(
338
- ChatMessage.from_str(
339
- content=text_pairs[1], role=MessageRole.ASSISTANT
340
- )
341
- )
342
- self.memory = ChatMemoryBuffer.from_defaults(
343
- token_limit=128000, chat_history=msg_history
344
- )
345
- else:
346
- self.memory = ChatMemoryBuffer.from_defaults(token_limit=128000)
174
+ msgs = []
175
+ for u, a in chat_history:
176
+ msgs.append(ChatMessage.from_str(u, role=MessageRole.USER))
177
+ msgs.append(ChatMessage.from_str(a, role=MessageRole.ASSISTANT))
178
+ self.memory.put_messages(msgs)
347
179
 
348
180
  # Set up main agent and fallback agent
349
181
  self._agent = None # Lazy loading
@@ -354,9 +186,15 @@ class Agent:
354
186
  try:
355
187
  self.observability_enabled = setup_observer(self.agent_config, self.verbose)
356
188
  except Exception as e:
357
- print(f"Failed to set up observer ({e}), ignoring")
189
+ logger.warning(f"Failed to set up observer ({e}), ignoring")
358
190
  self.observability_enabled = False
359
191
 
192
+ # VHC state tracking
193
+ self._vhc_cache = {} # Cache VHC results by query hash
194
+ self._last_query = None
195
+ self._last_response = None
196
+ self._current_tool_outputs = [] # Store tool outputs from current query for VHC
197
+
360
198
  @property
361
199
  def llm(self):
362
200
  """Lazy-loads the LLM."""
@@ -380,231 +218,56 @@ class Agent:
380
218
  )
381
219
  return self._fallback_agent
382
220
 
383
- def _sanitize_tools_for_gemini(
384
- self, tools: list[FunctionTool]
385
- ) -> list[FunctionTool]:
386
- """
387
- Strip all default values from:
388
- - tool.fn
389
- - tool.async_fn
390
- - tool.metadata.fn_schema
391
- so Gemini sees *only* required parameters, no defaults.
392
- """
393
- for tool in tools:
394
- # 1) strip defaults off the actual callables
395
- for func in (tool.fn, tool.async_fn):
396
- if not func:
397
- continue
398
- orig_sig = inspect.signature(func)
399
- new_params = [
400
- p.replace(default=Parameter.empty)
401
- for p in orig_sig.parameters.values()
402
- ]
403
- new_sig = Signature(
404
- new_params, return_annotation=orig_sig.return_annotation
405
- )
406
- if ismethod(func):
407
- func.__func__.__signature__ = new_sig
408
- else:
409
- func.__signature__ = new_sig
410
-
411
- # 2) rebuild the Pydantic schema so that *every* field is required
412
- schema_cls = getattr(tool.metadata, "fn_schema", None)
413
- if schema_cls and hasattr(schema_cls, "model_fields"):
414
- # collect (name → (type, Field(...))) for all fields
415
- new_fields: dict[str, tuple[type, Any]] = {}
416
- for name, mf in schema_cls.model_fields.items():
417
- typ = mf.annotation
418
- desc = getattr(mf, "description", "")
419
- # force required (no default) with Field(...)
420
- new_fields[name] = (typ, Field(..., description=desc))
421
-
422
- # make a brand-new schema class where every field is required
423
- no_default_schema = create_model(
424
- f"{schema_cls.__name__}", # new class name
425
- **new_fields, # type: ignore
426
- )
427
-
428
- # give it a clean __signature__ so inspect.signature sees no defaults
429
- params = [
430
- Parameter(n, Parameter.POSITIONAL_OR_KEYWORD, annotation=typ)
431
- for n, (typ, _) in new_fields.items()
432
- ]
433
- no_default_schema.__signature__ = Signature(params)
434
-
435
- # swap it back onto the tool
436
- tool.metadata.fn_schema = no_default_schema
437
-
438
- return tools
439
-
440
221
  def _create_agent(
441
- self, config: AgentConfig, llm_callback_manager: CallbackManager
442
- ) -> Union[BaseAgent, AgentRunner]:
222
+ self, config: AgentConfig, llm_callback_manager: "CallbackManager"
223
+ ) -> "BaseAgent":
443
224
  """
444
225
  Creates the agent based on the configuration object.
445
226
 
446
227
  Args:
447
-
448
228
  config: The configuration of the agent.
449
229
  llm_callback_manager: The callback manager for the agent's llm.
450
230
 
451
231
  Returns:
452
- Union[BaseAgent, AgentRunner]: The configured agent object.
232
+ BaseAgent: The configured agent object.
453
233
  """
454
- agent_type = config.agent_type
455
234
  # Use the same LLM instance for consistency
456
- llm = self.llm if config == self.agent_config else get_llm(LLMRole.MAIN, config=config)
235
+ llm = (
236
+ self.llm
237
+ if config == self.agent_config
238
+ else get_llm(LLMRole.MAIN, config=config)
239
+ )
457
240
  llm.callback_manager = llm_callback_manager
458
241
 
459
- if agent_type == AgentType.FUNCTION_CALLING:
460
- if config.tool_llm_provider == ModelProvider.OPENAI:
461
- raise ValueError(
462
- "Vectara-agentic: Function calling agent type is not supported with the OpenAI LLM."
463
- )
464
- prompt = _get_prompt(
465
- GENERAL_PROMPT_TEMPLATE,
466
- self._general_instructions,
467
- self._topic,
468
- self._custom_instructions,
469
- )
470
- agent = FunctionCallingAgent.from_tools(
471
- tools=self.tools,
472
- llm=llm,
473
- memory=self.memory,
474
- verbose=self.verbose,
475
- max_function_calls=config.max_reasoning_steps,
476
- callback_manager=llm_callback_manager,
477
- system_prompt=prompt,
478
- allow_parallel_tool_calls=True,
479
- )
480
- elif agent_type == AgentType.REACT:
481
- prompt = _get_prompt(
482
- REACT_PROMPT_TEMPLATE,
483
- self._general_instructions,
484
- self._topic,
485
- self._custom_instructions,
486
- )
487
- agent = ReActAgent.from_tools(
488
- tools=self.tools,
489
- llm=llm,
490
- memory=self.memory,
491
- verbose=self.verbose,
492
- react_chat_formatter=ReActChatFormatter(system_header=prompt),
493
- max_iterations=config.max_reasoning_steps,
494
- callable_manager=llm_callback_manager,
495
- )
496
- elif agent_type == AgentType.OPENAI:
497
- if config.tool_llm_provider != ModelProvider.OPENAI:
498
- raise ValueError(
499
- "Vectara-agentic: OPENAI agent type requires the OpenAI LLM."
500
- )
501
- prompt = _get_prompt(
502
- GENERAL_PROMPT_TEMPLATE,
503
- self._general_instructions,
504
- self._topic,
505
- self._custom_instructions,
506
- )
507
- agent = OpenAIAgent.from_tools(
508
- tools=self.tools,
509
- llm=llm,
510
- memory=self.memory,
511
- verbose=self.verbose,
512
- callback_manager=llm_callback_manager,
513
- max_function_calls=config.max_reasoning_steps,
514
- system_prompt=prompt,
515
- )
516
- elif agent_type == AgentType.LLMCOMPILER:
517
- agent_worker = LLMCompilerAgentWorker.from_tools(
518
- tools=self.tools,
519
- llm=llm,
520
- verbose=self.verbose,
521
- callback_manager=llm_callback_manager,
522
- )
523
- agent_worker.system_prompt = _get_prompt(
524
- prompt_template=_get_llm_compiler_prompt(
525
- prompt=agent_worker.system_prompt,
526
- general_instructions=self._general_instructions,
527
- topic=self._topic,
528
- custom_instructions=self._custom_instructions,
529
- ),
530
- general_instructions=self._general_instructions,
531
- topic=self._topic,
532
- custom_instructions=self._custom_instructions,
533
- )
534
- agent_worker.system_prompt_replan = _get_prompt(
535
- prompt_template=_get_llm_compiler_prompt(
536
- prompt=agent_worker.system_prompt_replan,
537
- general_instructions=GENERAL_INSTRUCTIONS,
538
- topic=self._topic,
539
- custom_instructions=self._custom_instructions,
540
- ),
541
- general_instructions=GENERAL_INSTRUCTIONS,
542
- topic=self._topic,
543
- custom_instructions=self._custom_instructions,
544
- )
545
- agent = agent_worker.as_agent()
546
- elif agent_type == AgentType.LATS:
547
- agent_worker = LATSAgentWorker.from_tools(
548
- tools=self.tools,
549
- llm=llm,
550
- num_expansions=3,
551
- max_rollouts=-1,
552
- verbose=self.verbose,
553
- callback_manager=llm_callback_manager,
554
- )
555
- prompt = _get_prompt(
556
- REACT_PROMPT_TEMPLATE,
557
- self._general_instructions,
558
- self._topic,
559
- self._custom_instructions,
560
- )
561
- agent_worker.chat_formatter = ReActChatFormatter(system_header=prompt)
562
- agent = agent_worker.as_agent()
563
- else:
564
- raise ValueError(f"Unknown agent type: {agent_type}")
565
-
566
- # Set up structured planner if needed
567
- if self.use_structured_planning or self.agent_type in [
568
- AgentType.LLMCOMPILER,
569
- AgentType.LATS,
570
- ]:
571
- planner_llm = get_llm(LLMRole.TOOL, config=config)
572
- agent = StructuredPlannerAgent(
573
- agent_worker=agent.agent_worker,
574
- tools=self.tools,
575
- llm=planner_llm,
576
- memory=self.memory,
577
- verbose=self.verbose,
578
- initial_plan_prompt=STRUCTURED_PLANNER_INITIAL_PLAN_PROMPT,
579
- plan_refine_prompt=STRUCTURED_PLANNER_PLAN_REFINE_PROMPT,
580
- )
581
-
582
- return agent
242
+ return create_agent_from_config(
243
+ tools=self.tools,
244
+ llm=llm,
245
+ memory=self.memory,
246
+ config=config,
247
+ callback_manager=llm_callback_manager,
248
+ general_instructions=self._general_instructions,
249
+ topic=self._topic,
250
+ custom_instructions=self._custom_instructions,
251
+ verbose=self.verbose,
252
+ )
583
253
 
584
254
  def clear_memory(self) -> None:
585
- """
586
- Clear the agent's memory.
587
- """
588
- if self.agent_config_type == AgentConfigType.DEFAULT:
589
- self.agent.memory.reset()
590
- elif (
591
- self.agent_config_type == AgentConfigType.FALLBACK
592
- and self.fallback_agent_config
593
- ):
594
- self.fallback_agent.memory.reset()
595
- else:
596
- raise ValueError(f"Invalid agent config type {self.agent_config_type}")
255
+ """Clear the agent's memory and reset agent instances to ensure consistency."""
256
+ self.memory.reset()
257
+ # Clear agent instances so they get recreated with the cleared memory
258
+ self._agent = None
259
+ self._fallback_agent = None
597
260
 
598
261
  def __eq__(self, other):
599
262
  if not isinstance(other, Agent):
600
- print(
263
+ logger.debug(
601
264
  f"Comparison failed: other is not an instance of Agent. (self: {type(self)}, other: {type(other)})"
602
265
  )
603
266
  return False
604
267
 
605
268
  # Compare agent_type
606
269
  if self.agent_config.agent_type != other.agent_config.agent_type:
607
- print(
270
+ logger.debug(
608
271
  f"Comparison failed: agent_type differs. (self.agent_config.agent_type: {self.agent_config.agent_type},"
609
272
  f" other.agent_config.agent_type: {other.agent_config.agent_type})"
610
273
  )
@@ -612,7 +275,7 @@ class Agent:
612
275
 
613
276
  # Compare tools
614
277
  if self.tools != other.tools:
615
- print(
278
+ logger.debug(
616
279
  "Comparison failed: tools differ."
617
280
  f"(self.tools: {[t.metadata.name for t in self.tools]}, "
618
281
  f"other.tools: {[t.metadata.name for t in other.tools]})"
@@ -621,14 +284,14 @@ class Agent:
621
284
 
622
285
  # Compare topic
623
286
  if self._topic != other._topic:
624
- print(
287
+ logger.debug(
625
288
  f"Comparison failed: topic differs. (self.topic: {self._topic}, other.topic: {other._topic})"
626
289
  )
627
290
  return False
628
291
 
629
292
  # Compare custom_instructions
630
293
  if self._custom_instructions != other._custom_instructions:
631
- print(
294
+ logger.debug(
632
295
  "Comparison failed: custom_instructions differ. (self.custom_instructions: "
633
296
  f"{self._custom_instructions}, other.custom_instructions: {other._custom_instructions})"
634
297
  )
@@ -636,31 +299,27 @@ class Agent:
636
299
 
637
300
  # Compare verbose
638
301
  if self.verbose != other.verbose:
639
- print(
302
+ logger.debug(
640
303
  f"Comparison failed: verbose differs. (self.verbose: {self.verbose}, other.verbose: {other.verbose})"
641
304
  )
642
305
  return False
643
306
 
644
307
  # Compare agent memory
645
- if self.agent.memory.chat_store != other.agent.memory.chat_store:
646
- print(
647
- f"Comparison failed: agent memory differs. (self.agent: {repr(self.agent.memory.chat_store)}, "
648
- f"other.agent: {repr(other.agent.memory.chat_store)})"
649
- )
308
+ if self.memory.get() != other.memory.get():
309
+ logger.debug("Comparison failed: agent memory differs.")
650
310
  return False
651
311
 
652
312
  # If all comparisons pass
653
- print("All comparisons passed. Objects are equal.")
313
+ logger.debug("All comparisons passed. Objects are equal.")
654
314
  return True
655
315
 
656
316
  @classmethod
657
317
  def from_tools(
658
318
  cls,
659
- tools: List[FunctionTool],
319
+ tools: List["FunctionTool"],
660
320
  topic: str = "general",
661
321
  custom_instructions: str = "",
662
322
  verbose: bool = True,
663
- update_func: Optional[Callable[[AgentStatusType, dict, str], None]] = None,
664
323
  agent_progress_callback: Optional[
665
324
  Callable[[AgentStatusType, dict, str], None]
666
325
  ] = None,
@@ -669,8 +328,9 @@ class Agent:
669
328
  validate_tools: bool = False,
670
329
  fallback_agent_config: Optional[AgentConfig] = None,
671
330
  chat_history: Optional[list[Tuple[str, str]]] = None,
672
- workflow_cls: Optional[Workflow] = None,
331
+ workflow_cls: Optional["Workflow"] = None,
673
332
  workflow_timeout: int = 120,
333
+ session_id: Optional[str] = None,
674
334
  ) -> "Agent":
675
335
  """
676
336
  Create an agent from tools, agent type, and language model.
@@ -682,7 +342,6 @@ class Agent:
682
342
  custom_instructions (str, optional): custom instructions for the agent. Defaults to ''.
683
343
  verbose (bool, optional): Whether the agent should print its steps. Defaults to True.
684
344
  agent_progress_callback (Callable): A callback function the code calls on any agent updates.
685
- update_func (Callable): old name for agent_progress_callback. Will be deprecated in future.
686
345
  query_logging_callback (Callable): A callback function the code calls upon completion of a query
687
346
  agent_config (AgentConfig, optional): The configuration of the agent.
688
347
  fallback_agent_config (AgentConfig, optional): The fallback configuration of the agent.
@@ -691,6 +350,8 @@ class Agent:
691
350
  Defaults to False.
692
351
  workflow_cls (Workflow, optional): The workflow class to be used with run(). Defaults to None.
693
352
  workflow_timeout (int, optional): The timeout for the workflow in seconds. Defaults to 120.
353
+ session_id (str, optional): The session ID for memory persistence.
354
+ If None, auto-generates from topic and date. Defaults to None.
694
355
 
695
356
  Returns:
696
357
  Agent: An instance of the Agent class.
@@ -702,13 +363,13 @@ class Agent:
702
363
  verbose=verbose,
703
364
  agent_progress_callback=agent_progress_callback,
704
365
  query_logging_callback=query_logging_callback,
705
- update_func=update_func,
706
366
  agent_config=agent_config,
707
367
  chat_history=chat_history,
708
368
  validate_tools=validate_tools,
709
369
  fallback_agent_config=fallback_agent_config,
710
370
  workflow_cls=workflow_cls,
711
371
  workflow_timeout=workflow_timeout,
372
+ session_id=session_id,
712
373
  )
713
374
 
714
375
  @classmethod
@@ -753,141 +414,78 @@ class Agent:
753
414
  vectara_presence_penalty: Optional[float] = None,
754
415
  vectara_save_history: bool = True,
755
416
  return_direct: bool = False,
417
+ session_id: Optional[str] = None,
756
418
  ) -> "Agent":
757
- """
758
- Create an agent from a single Vectara corpus
419
+ """Create an agent from a single Vectara corpus using the factory function.
759
420
 
760
421
  Args:
761
- tool_name (str): The name of Vectara tool used by the agent
762
- vectara_corpus_key (str): The Vectara corpus key (or comma separated list of keys).
763
- vectara_api_key (str): The Vectara API key.
764
- agent_progress_callback (Callable): A callback function the code calls on any agent updates.
765
- query_logging_callback (Callable): A callback function the code calls upon completion of a query
766
- agent_config (AgentConfig, optional): The configuration of the agent.
767
- fallback_agent_config (AgentConfig, optional): The fallback configuration of the agent.
768
- chat_history (Tuple[str, str], optional): A list of user/agent chat pairs to initialize the agent memory.
769
- data_description (str): The description of the data.
770
- assistant_specialty (str): The specialty of the assistant.
771
- general_instructions (str, optional): General instructions for the agent.
772
- The Agent has a default set of instructions that are crafted to help it operate effectively.
773
- This allows you to customize the agent's behavior and personality, but use with caution.
774
- verbose (bool, optional): Whether to print verbose output.
775
- vectara_filter_fields (List[dict], optional): The filterable attributes
776
- (each dict maps field name to Tuple[type, description]).
777
- vectara_offset (int, optional): Number of results to skip.
778
- vectara_lambda_val (float, optional): Lambda value for Vectara hybrid search.
779
- vectara_semantics: (str, optional): Indicates whether the query is intended as a query or response.
780
- vectara_custom_dimensions: (Dict, optional): Custom dimensions for the query.
781
- vectara_reranker (str, optional): The Vectara reranker name (default "slingshot")
782
- vectara_rerank_k (int, optional): The number of results to use with reranking.
783
- vectara_rerank_limit: (int, optional): The maximum number of results to return after reranking.
784
- vectara_rerank_cutoff: (float, optional): The minimum score threshold for results to include after
785
- reranking.
786
- vectara_diversity_bias (float, optional): The MMR diversity bias.
787
- vectara_udf_expression (str, optional): The user defined expression for reranking results.
788
- vectara_rerank_chain (List[Dict], optional): A list of Vectara rerankers to be applied sequentially.
789
- vectara_n_sentences_before (int, optional): The number of sentences before the matching text
790
- vectara_n_sentences_after (int, optional): The number of sentences after the matching text.
791
- vectara_summary_num_results (int, optional): The number of results to use in summarization.
792
- vectara_summarizer (str, optional): The Vectara summarizer name.
793
- vectara_summary_response_language (str, optional): The response language for the Vectara summary.
794
- vectara_summary_prompt_text (str, optional): The custom prompt, using appropriate prompt variables and
795
- functions.
796
- vectara_max_response_chars (int, optional): The desired maximum number of characters for the generated
797
- summary.
798
- vectara_max_tokens (int, optional): The maximum number of tokens to be returned by the LLM.
799
- vectara_temperature (float, optional): The sampling temperature; higher values lead to more randomness.
800
- vectara_frequency_penalty (float, optional): How much to penalize repeating tokens in the response,
801
- higher values reducing likelihood of repeating the same line.
802
- vectara_presence_penalty (float, optional): How much to penalize repeating tokens in the response,
803
- higher values increasing the diversity of topics.
804
- vectara_save_history (bool, optional): Whether to save the query in history.
805
- return_direct (bool, optional): Whether the agent should return the tool's response directly.
806
-
807
- Returns:
808
- Agent: An instance of the Agent class.
422
+ tool_name (str): Name of the tool to be created.
423
+ data_description (str): Description of the data/corpus.
424
+ assistant_specialty (str): The specialty/topic of the assistant.
425
+ session_id (str, optional): The session ID for memory persistence.
426
+ If None, auto-generates from topic and date. Defaults to None.
427
+ ... (other parameters as documented in factory function)
809
428
  """
810
- vec_factory = VectaraToolFactory(
811
- vectara_api_key=vectara_api_key,
429
+ # Use the factory function to avoid code duplication
430
+ config = create_agent_from_corpus(
431
+ tool_name=tool_name,
432
+ data_description=data_description,
433
+ assistant_specialty=assistant_specialty,
434
+ general_instructions=general_instructions,
812
435
  vectara_corpus_key=vectara_corpus_key,
813
- )
814
- field_definitions = {}
815
- field_definitions["query"] = (str, Field(description="The user query")) # type: ignore
816
- for field in vectara_filter_fields:
817
- field_definitions[field["name"]] = (
818
- eval(field["type"]),
819
- Field(description=field["description"]),
820
- ) # type: ignore
821
- query_args = create_model("QueryArgs", **field_definitions) # type: ignore
822
-
823
- # tool name must be valid Python function name
824
- if tool_name:
825
- tool_name = re.sub(r"[^A-Za-z0-9_]", "_", tool_name)
826
-
827
- vectara_tool = vec_factory.create_rag_tool(
828
- tool_name=tool_name or f"vectara_{vectara_corpus_key}",
829
- tool_description=f"""
830
- Given a user query,
831
- returns a response (str) to a user question about {data_description}.
832
- """,
833
- tool_args_schema=query_args,
834
- reranker=vectara_reranker,
835
- rerank_k=vectara_rerank_k,
836
- rerank_limit=vectara_rerank_limit,
837
- rerank_cutoff=vectara_rerank_cutoff,
838
- mmr_diversity_bias=vectara_diversity_bias,
839
- udf_expression=vectara_udf_expression,
840
- rerank_chain=vectara_rerank_chain,
841
- n_sentences_before=vectara_n_sentences_before,
842
- n_sentences_after=vectara_n_sentences_after,
843
- offset=vectara_offset,
844
- lambda_val=vectara_lambda_val,
845
- semantics=vectara_semantics,
846
- custom_dimensions=vectara_custom_dimensions,
847
- summary_num_results=vectara_summary_num_results,
848
- vectara_summarizer=vectara_summarizer,
849
- summary_response_lang=vectara_summary_response_language,
850
- vectara_prompt_text=vectara_summary_prompt_text,
851
- max_response_chars=vectara_max_response_chars,
852
- max_tokens=vectara_max_tokens,
853
- temperature=vectara_temperature,
854
- frequency_penalty=vectara_frequency_penalty,
855
- presence_penalty=vectara_presence_penalty,
856
- save_history=vectara_save_history,
857
- include_citations=True,
436
+ vectara_api_key=vectara_api_key,
437
+ agent_config=agent_config,
438
+ fallback_agent_config=fallback_agent_config,
858
439
  verbose=verbose,
440
+ vectara_filter_fields=vectara_filter_fields,
441
+ vectara_offset=vectara_offset,
442
+ vectara_lambda_val=vectara_lambda_val,
443
+ vectara_semantics=vectara_semantics,
444
+ vectara_custom_dimensions=vectara_custom_dimensions,
445
+ vectara_reranker=vectara_reranker,
446
+ vectara_rerank_k=vectara_rerank_k,
447
+ vectara_rerank_limit=vectara_rerank_limit,
448
+ vectara_rerank_cutoff=vectara_rerank_cutoff,
449
+ vectara_diversity_bias=vectara_diversity_bias,
450
+ vectara_udf_expression=vectara_udf_expression,
451
+ vectara_rerank_chain=vectara_rerank_chain,
452
+ vectara_n_sentences_before=vectara_n_sentences_before,
453
+ vectara_n_sentences_after=vectara_n_sentences_after,
454
+ vectara_summary_num_results=vectara_summary_num_results,
455
+ vectara_summarizer=vectara_summarizer,
456
+ vectara_summary_response_language=vectara_summary_response_language,
457
+ vectara_summary_prompt_text=vectara_summary_prompt_text,
458
+ vectara_max_response_chars=vectara_max_response_chars,
459
+ vectara_max_tokens=vectara_max_tokens,
460
+ vectara_temperature=vectara_temperature,
461
+ vectara_frequency_penalty=vectara_frequency_penalty,
462
+ vectara_presence_penalty=vectara_presence_penalty,
463
+ vectara_save_history=vectara_save_history,
859
464
  return_direct=return_direct,
860
465
  )
861
466
 
862
- assistant_instructions = f"""
863
- - You are a helpful {assistant_specialty} assistant.
864
- - You can answer questions about {data_description}.
865
- - Never discuss politics, and always respond politely.
866
- """
867
-
868
467
  return cls(
869
- tools=[vectara_tool],
870
- topic=assistant_specialty,
871
- custom_instructions=assistant_instructions,
872
- general_instructions=general_instructions,
873
- verbose=verbose,
468
+ chat_history=chat_history,
874
469
  agent_progress_callback=agent_progress_callback,
875
470
  query_logging_callback=query_logging_callback,
876
- agent_config=agent_config,
877
- fallback_agent_config=fallback_agent_config,
878
- chat_history=chat_history,
879
- vectara_api_key=vectara_api_key,
471
+ session_id=session_id,
472
+ **config,
880
473
  )
881
474
 
882
475
  def _switch_agent_config(self) -> None:
883
- """ "
476
+ """
884
477
  Switch the configuration type of the agent.
885
478
  This function is called automatically to switch the agent configuration if the current configuration fails.
479
+ Ensures memory consistency by clearing agent instances so they are recreated with current memory.
886
480
  """
887
481
  if self.agent_config_type == AgentConfigType.DEFAULT:
888
482
  self.agent_config_type = AgentConfigType.FALLBACK
483
+ # Clear the fallback agent so it gets recreated with current memory
484
+ self._fallback_agent = None
889
485
  else:
890
486
  self.agent_config_type = AgentConfigType.DEFAULT
487
+ # Clear the main agent so it gets recreated with current memory
488
+ self._agent = None
891
489
 
892
490
  def report(self, detailed: bool = False) -> None:
893
491
  """
@@ -899,45 +497,25 @@ class Agent:
899
497
  Returns:
900
498
  str: The report from the agent.
901
499
  """
902
- print("Vectara agentic Report:")
903
- print(f"Agent Type = {self.agent_config.agent_type}")
904
- print(f"Topic = {self._topic}")
905
- print("Tools:")
500
+ logger.info("Vectara agentic Report:")
501
+ logger.info(f"Agent Type = {self.agent_config.agent_type}")
502
+ logger.info(f"Topic = {self._topic}")
503
+ logger.info("Tools:")
906
504
  for tool in self.tools:
907
505
  if hasattr(tool, "metadata"):
908
506
  if detailed:
909
- print(f"- {tool.metadata.description}")
507
+ logger.info(f"- {tool.metadata.description}")
910
508
  else:
911
- print(f"- {tool.metadata.name}")
509
+ logger.info(f"- {tool.metadata.name}")
912
510
  else:
913
- print("- tool without metadata")
914
- print(
511
+ logger.info("- tool without metadata")
512
+ logger.info(
915
513
  f"Agent LLM = {get_llm(LLMRole.MAIN, config=self.agent_config).metadata.model_name}"
916
514
  )
917
- print(
515
+ logger.info(
918
516
  f"Tool LLM = {get_llm(LLMRole.TOOL, config=self.agent_config).metadata.model_name}"
919
517
  )
920
518
 
921
- def token_counts(self) -> dict:
922
- """
923
- Get the token counts for the agent and tools.
924
-
925
- Returns:
926
- dict: The token counts for the agent and tools.
927
- """
928
- return {
929
- "main token count": (
930
- self.main_token_counter.total_llm_token_count
931
- if self.main_token_counter
932
- else -1
933
- ),
934
- "tool token count": (
935
- self.tool_token_counter.total_llm_token_count
936
- if self.tool_token_counter
937
- else -1
938
- ),
939
- }
940
-
941
519
  def _get_current_agent(self):
942
520
  return (
943
521
  self.agent
@@ -949,23 +527,11 @@ class Agent:
949
527
  return (
950
528
  self.agent_config.agent_type
951
529
  if self.agent_config_type == AgentConfigType.DEFAULT
530
+ or not self.fallback_agent_config
952
531
  else self.fallback_agent_config.agent_type
953
532
  )
954
533
 
955
- async def _aformat_for_lats(self, prompt, agent_response):
956
- llm_prompt = f"""
957
- Given the question '{prompt}', and agent response '{agent_response.response}',
958
- Please provide a well formatted final response to the query.
959
- final response:
960
- """
961
- agent_type = self._get_current_agent_type()
962
- if agent_type != AgentType.LATS:
963
- return
964
-
965
- agent = self._get_current_agent()
966
- agent_response.response = str(agent.llm.acomplete(llm_prompt))
967
-
968
- def chat(self, prompt: str) -> AgentResponse: # type: ignore
534
+ def chat(self, prompt: str) -> AgentResponse:
969
535
  """
970
536
  Interact with the agent using a chat prompt.
971
537
 
@@ -975,48 +541,15 @@ class Agent:
975
541
  Returns:
976
542
  AgentResponse: The response from the agent.
977
543
  """
978
- return asyncio.run(self.achat(prompt))
979
-
980
- def _calc_fcs(self, agent_response: AgentResponse) -> None:
981
- """
982
- Calculate the Factual consistency score for the agent response.
983
- """
984
- if not self.vectara_api_key:
985
- logging.debug("FCS calculation skipped: 'vectara_api_key' is missing.")
986
- return # can't calculate FCS without Vectara API key
987
-
988
- chat_history = self.memory.get()
989
- context = []
990
- for msg in chat_history:
991
- if msg.role == MessageRole.TOOL:
992
- content = msg.content
993
- if _is_human_readable_output(content):
994
- try:
995
- content = content.to_human_readable()
996
- except Exception as e:
997
- logging.debug(
998
- f"Failed to get human-readable format for FCS calculation: {e}"
999
- )
1000
- # Fall back to string representation of the object
1001
- content = str(content)
1002
-
1003
- context.append(content)
1004
- elif msg.role in [MessageRole.USER, MessageRole.ASSISTANT] and msg.content:
1005
- context.append(msg.content)
1006
-
1007
- if not context:
1008
- return
1009
-
1010
- context_str = "\n".join(context)
1011
544
  try:
1012
- score = HHEM(self.vectara_api_key).compute(
1013
- context_str, agent_response.response
1014
- )
1015
- if agent_response.metadata is None:
1016
- agent_response.metadata = {}
1017
- agent_response.metadata["fcs"] = score
1018
- except Exception as e:
1019
- logging.error(f"Failed to calculate FCS: {e}")
545
+ _ = asyncio.get_running_loop()
546
+ except RuntimeError:
547
+ return asyncio.run(self.achat(prompt))
548
+
549
+ # We are inside a running loop (Jupyter, uvicorn, etc.)
550
+ raise RuntimeError(
551
+ "Use `await agent.achat(...)` inside an event loop (e.g. Jupyter)."
552
+ )
1020
553
 
1021
554
  async def achat(self, prompt: str) -> AgentResponse: # type: ignore
1022
555
  """
@@ -1028,6 +561,9 @@ class Agent:
1028
561
  Returns:
1029
562
  AgentResponse: The response from the agent.
1030
563
  """
564
+ if not prompt:
565
+ return AgentResponse(response="")
566
+
1031
567
  max_attempts = 4 if self.fallback_agent_config else 2
1032
568
  attempt = 0
1033
569
  orig_llm = self.llm.metadata.model_name
@@ -1035,36 +571,205 @@ class Agent:
1035
571
  while attempt < max_attempts:
1036
572
  try:
1037
573
  current_agent = self._get_current_agent()
1038
- agent_response = await current_agent.achat(prompt)
1039
- self._calc_fcs(agent_response)
1040
- await self._aformat_for_lats(prompt, agent_response)
1041
- if self.observability_enabled:
1042
- eval_fcs()
1043
- if self.query_logging_callback:
1044
- self.query_logging_callback(prompt, agent_response.response)
574
+
575
+ # Deal with workflow-based agent types (Function Calling and ReAct)
576
+ if self._get_current_agent_type() in [
577
+ AgentType.FUNCTION_CALLING,
578
+ AgentType.REACT,
579
+ ]:
580
+ from llama_index.core.workflow import Context
581
+
582
+ # Create context and pass memory to the workflow agent
583
+ # According to LlamaIndex docs, we should let the workflow manage memory internally
584
+ ctx = Context(current_agent)
585
+
586
+ handler = current_agent.run(
587
+ user_msg=prompt, memory=self.memory, ctx=ctx
588
+ )
589
+
590
+ # Listen to workflow events if progress callback is set
591
+ if self.agent_progress_callback:
592
+ # Create event tracker for consistent event ID generation
593
+ from .agent_core.streaming import ToolEventTracker
594
+
595
+ event_tracker = ToolEventTracker()
596
+
597
+ async for event in handler.stream_events():
598
+ # Use consistent event ID tracking to ensure tool calls and outputs are paired
599
+ event_id = event_tracker.get_event_id(event)
600
+
601
+ # Handle different types of workflow events using same logic as FunctionCallingStreamHandler
602
+ from llama_index.core.agent.workflow import (
603
+ ToolCall,
604
+ ToolCallResult,
605
+ AgentInput,
606
+ AgentOutput,
607
+ )
608
+
609
+ if isinstance(event, ToolCall):
610
+ self.agent_progress_callback(
611
+ status_type=AgentStatusType.TOOL_CALL,
612
+ msg={
613
+ "tool_name": event.tool_name,
614
+ "arguments": json.dumps(event.tool_kwargs),
615
+ },
616
+ event_id=event_id,
617
+ )
618
+ elif isinstance(event, ToolCallResult):
619
+ self.agent_progress_callback(
620
+ status_type=AgentStatusType.TOOL_OUTPUT,
621
+ msg={
622
+ "tool_name": event.tool_name,
623
+ "content": str(event.tool_output),
624
+ },
625
+ event_id=event_id,
626
+ )
627
+ elif isinstance(event, AgentInput):
628
+ self.agent_progress_callback(
629
+ status_type=AgentStatusType.AGENT_UPDATE,
630
+ msg={"content": f"Agent input: {event.input}"},
631
+ event_id=event_id,
632
+ )
633
+ elif isinstance(event, AgentOutput):
634
+ self.agent_progress_callback(
635
+ status_type=AgentStatusType.AGENT_UPDATE,
636
+ msg={"content": f"Agent output: {event.response}"},
637
+ event_id=event_id,
638
+ )
639
+
640
+ result = await handler
641
+
642
+ # Ensure we have an AgentResponse object with a string response
643
+ if hasattr(result, "response"):
644
+ response_text = result.response
645
+ else:
646
+ response_text = str(result)
647
+
648
+ # Handle case where response is a ChatMessage object
649
+ if hasattr(response_text, "content"):
650
+ response_text = response_text.content
651
+ elif not isinstance(response_text, str):
652
+ response_text = str(response_text)
653
+
654
+ if response_text is None or response_text == "None":
655
+ # Try to find tool outputs in the result object
656
+ response_text = None
657
+
658
+ # Check various possible locations for tool outputs
659
+ if hasattr(result, "tool_outputs") and result.tool_outputs:
660
+ # Get the latest tool output
661
+ latest_output = (
662
+ result.tool_outputs[-1]
663
+ if isinstance(result.tool_outputs, list)
664
+ else result.tool_outputs
665
+ )
666
+ response_text = str(latest_output)
667
+
668
+ # Check if there are tool_calls with results
669
+ elif hasattr(result, "tool_calls") and result.tool_calls:
670
+ # Tool calls might contain the outputs - let's try to extract them
671
+ for tool_call in result.tool_calls:
672
+ if (
673
+ hasattr(tool_call, "tool_output")
674
+ and tool_call.tool_output is not None
675
+ ):
676
+ response_text = str(tool_call.tool_output)
677
+ break
678
+
679
+ elif hasattr(result, "sources") or hasattr(
680
+ result, "source_nodes"
681
+ ):
682
+ sources = getattr(
683
+ result, "sources", getattr(result, "source_nodes", [])
684
+ )
685
+ if (
686
+ sources
687
+ and len(sources) > 0
688
+ and hasattr(sources[0], "text")
689
+ ):
690
+ response_text = sources[0].text
691
+
692
+ # Check for workflow context or chat history that might contain tool results
693
+ elif hasattr(result, "chat_history"):
694
+ # Look for the most recent assistant message that might contain tool results
695
+ chat_history = result.chat_history
696
+ if chat_history and len(chat_history) > 0:
697
+ for msg in reversed(chat_history):
698
+ if (
699
+ msg.role == MessageRole.TOOL
700
+ and msg.content
701
+ and str(msg.content).strip()
702
+ ):
703
+ response_text = msg.content
704
+ break
705
+ if (
706
+ hasattr(msg, "content")
707
+ and msg.content
708
+ and str(msg.content).strip()
709
+ ):
710
+ response_text = msg.content
711
+ break
712
+
713
+ # If we still don't have a response, provide a fallback
714
+ if response_text is None or response_text == "None":
715
+ response_text = "Response completed."
716
+
717
+ agent_response = AgentResponse(
718
+ response=response_text, metadata=getattr(result, "metadata", {})
719
+ )
720
+
721
+ # Retrieve updated memory from workflow context
722
+ # According to LlamaIndex docs, workflow agents manage memory internally
723
+ # and we can access it via ctx.store.get("memory")
724
+ try:
725
+ workflow_memory = await ctx.store.get("memory")
726
+ if workflow_memory:
727
+ # Update our external memory with the workflow's memory
728
+ self.memory = workflow_memory
729
+ except Exception as e:
730
+ # If we can't retrieve workflow memory, fall back to manual management
731
+ warning_msg = (
732
+ f"Could not retrieve workflow memory, falling back to "
733
+ f"manual management: {e}"
734
+ )
735
+ logger.warning(warning_msg)
736
+ user_msg = ChatMessage.from_str(prompt, role=MessageRole.USER)
737
+ assistant_msg = ChatMessage.from_str(
738
+ response_text, role=MessageRole.ASSISTANT
739
+ )
740
+ self.memory.put_messages([user_msg, assistant_msg])
741
+
742
+ # Standard chat interaction for other agent types
743
+ else:
744
+ agent_response = await current_agent.achat(prompt)
745
+
746
+ # Post processing after response is generated
747
+ agent_response.metadata = agent_response.metadata or {}
748
+ user_metadata = agent_response.metadata
749
+ agent_response = await execute_post_stream_processing(
750
+ agent_response, prompt, self, user_metadata
751
+ )
1045
752
  return agent_response
1046
753
 
1047
754
  except Exception as e:
1048
755
  last_error = e
1049
756
  if self.verbose:
1050
- print(f"LLM call failed on attempt {attempt}. " f"Error: {e}.")
1051
- if attempt >= 2:
1052
- if self.verbose:
1053
- print(
1054
- f"LLM call failed on attempt {attempt}. Switching agent configuration."
1055
- )
757
+ logger.warning(
758
+ f"LLM call failed on attempt {attempt}. " f"Error: {e}."
759
+ )
760
+ if attempt >= 2 and self.fallback_agent_config:
1056
761
  self._switch_agent_config()
1057
- time.sleep(1)
762
+ await asyncio.sleep(1)
1058
763
  attempt += 1
1059
764
 
1060
765
  return AgentResponse(
1061
766
  response=(
1062
767
  f"For {orig_llm} LLM - failure can't be resolved after "
1063
- f"{max_attempts} attempts ({last_error}."
768
+ f"{max_attempts} attempts ({last_error})."
1064
769
  )
1065
770
  )
1066
771
 
1067
- def stream_chat(self, prompt: str) -> AgentStreamingResponse: # type: ignore
772
+ def stream_chat(self, prompt: str) -> AgentStreamingResponse:
1068
773
  """
1069
774
  Interact with the agent using a chat prompt with streaming.
1070
775
  Args:
@@ -1072,7 +777,13 @@ class Agent:
1072
777
  Returns:
1073
778
  AgentStreamingResponse: The streaming response from the agent.
1074
779
  """
1075
- return asyncio.run(self.astream_chat(prompt))
780
+ try:
781
+ _ = asyncio.get_running_loop()
782
+ except RuntimeError:
783
+ return asyncio.run(self.astream_chat(prompt))
784
+ raise RuntimeError(
785
+ "Use `await agent.astream_chat(...)` inside an event loop (e.g. Jupyter)."
786
+ )
1076
787
 
1077
788
  async def astream_chat(self, prompt: str) -> AgentStreamingResponse: # type: ignore
1078
789
  """
@@ -1082,50 +793,199 @@ class Agent:
1082
793
  Returns:
1083
794
  AgentStreamingResponse: The streaming response from the agent.
1084
795
  """
796
+ # Store query for VHC processing and clear previous tool outputs
797
+ self._last_query = prompt
798
+ self._clear_tool_outputs()
1085
799
  max_attempts = 4 if self.fallback_agent_config else 2
1086
800
  attempt = 0
1087
801
  orig_llm = self.llm.metadata.model_name
802
+ last_error = None
1088
803
  while attempt < max_attempts:
1089
804
  try:
1090
805
  current_agent = self._get_current_agent()
1091
- agent_response = await current_agent.astream_chat(prompt)
1092
- original_async_response_gen = agent_response.async_response_gen
806
+ user_meta: Dict[str, Any] = {}
807
+
808
+ # Deal with Function Calling agent type
809
+ if self._get_current_agent_type() == AgentType.FUNCTION_CALLING:
810
+ from llama_index.core.workflow import Context
811
+
812
+ # Create context and pass memory to the workflow agent
813
+ # According to LlamaIndex docs, we should let the workflow manage memory internally
814
+ ctx = Context(current_agent)
815
+
816
+ handler = current_agent.run(
817
+ user_msg=prompt, memory=self.memory, ctx=ctx
818
+ )
819
+
820
+ # Use the dedicated FunctionCallingStreamHandler
821
+ stream_handler = FunctionCallingStreamHandler(self, handler, prompt)
822
+ streaming_adapter = stream_handler.create_streaming_response(
823
+ user_meta
824
+ )
825
+
826
+ return AgentStreamingResponse(
827
+ base=streaming_adapter, metadata=user_meta
828
+ )
829
+
830
+ #
831
+ # For other agent types, use the standard async chat method
832
+ #
833
+ li_stream = await current_agent.astream_chat(prompt)
834
+ orig_async = li_stream.async_response_gen
1093
835
 
1094
836
  # Define a wrapper to preserve streaming behavior while executing post-stream logic.
1095
837
  async def _stream_response_wrapper():
1096
- async for token in original_async_response_gen():
1097
- yield token # Yield tokens as they are generated
1098
- # Post-streaming additional logic:
1099
- await self._aformat_for_lats(prompt, agent_response)
1100
- if self.query_logging_callback:
1101
- self.query_logging_callback(prompt, agent_response.response)
1102
- if self.observability_enabled:
1103
- eval_fcs()
1104
- self._calc_fcs(agent_response)
1105
-
1106
- agent_response.async_response_gen = (
1107
- _stream_response_wrapper # Override the generator
1108
- )
1109
- return agent_response
838
+ async for tok in orig_async():
839
+ yield tok
840
+
841
+ # Use shared post-processing function
842
+ await execute_post_stream_processing(
843
+ li_stream, prompt, self, user_meta
844
+ )
845
+
846
+ li_stream.async_response_gen = _stream_response_wrapper
847
+ return AgentStreamingResponse(base=li_stream, metadata=user_meta)
1110
848
 
1111
849
  except Exception as e:
1112
850
  last_error = e
1113
- if attempt >= 2:
1114
- if self.verbose:
1115
- print(
1116
- f"LLM call failed on attempt {attempt}. Switching agent configuration."
1117
- )
851
+ if attempt >= 2 and self.fallback_agent_config:
1118
852
  self._switch_agent_config()
1119
- time.sleep(1)
853
+ await asyncio.sleep(1)
1120
854
  attempt += 1
1121
855
 
1122
- return AgentStreamingResponse(
1123
- response=(
1124
- f"For {orig_llm} LLM - failure can't be resolved after "
1125
- f"{max_attempts} attempts ({last_error})."
1126
- )
856
+ return AgentStreamingResponse.from_error(
857
+ f"For {orig_llm} LLM - failure can't be resolved after "
858
+ f"{max_attempts} attempts ({last_error})."
1127
859
  )
1128
860
 
861
+ def _clear_tool_outputs(self):
862
+ """Clear stored tool outputs at the start of a new query."""
863
+ self._current_tool_outputs.clear()
864
+ logging.info("🔧 [TOOL_STORAGE] Cleared stored tool outputs for new query")
865
+
866
+ def _add_tool_output(self, tool_name: str, content: str):
867
+ """Add a tool output to the current collection for VHC."""
868
+ tool_output = {
869
+ 'status_type': 'TOOL_OUTPUT',
870
+ 'content': content,
871
+ 'tool_name': tool_name
872
+ }
873
+ self._current_tool_outputs.append(tool_output)
874
+ logging.info(f"🔧 [TOOL_STORAGE] Added tool output from '{tool_name}': {len(content)} chars")
875
+
876
+ def _get_stored_tool_outputs(self) -> List[dict]:
877
+ """Get the stored tool outputs from the current query."""
878
+ logging.info(f"🔧 [TOOL_STORAGE] Retrieved {len(self._current_tool_outputs)} stored tool outputs")
879
+ return self._current_tool_outputs.copy()
880
+
881
+ async def acompute_vhc(self) -> Dict[str, Any]:
882
+ """
883
+ Compute VHC for the last query/response pair (async version).
884
+ Results are cached for subsequent calls. Tool outputs are automatically
885
+ collected during streaming and used internally.
886
+
887
+ Returns:
888
+ Dict[str, Any]: Dictionary containing 'corrected_text' and 'corrections'
889
+ """
890
+ logging.info(
891
+ f"🔍🔍🔍 [VHC_AGENT_ENTRY] UNIQUE_DEBUG_MESSAGE acompute_vhc method called - "
892
+ f"stored_tool_outputs_count={len(self._current_tool_outputs)}"
893
+ )
894
+ logging.info(
895
+ f"🔍🔍🔍 [VHC_AGENT_ENTRY] _last_query: {'set' if self._last_query else 'None'}"
896
+ )
897
+
898
+ if not self._last_query:
899
+ logging.info("🔍 [VHC_AGENT] Returning early - no _last_query")
900
+ return {"corrected_text": None, "corrections": []}
901
+
902
+ # For VHC to work, we need the response text from memory
903
+ # Get the latest assistant response from memory
904
+ messages = self.memory.get()
905
+ logging.info(
906
+ f"🔍 [VHC_AGENT] memory.get() returned {len(messages) if messages else 0} messages"
907
+ )
908
+
909
+ if not messages:
910
+ logging.info("🔍 [VHC_AGENT] Returning early - no messages in memory")
911
+ return {"corrected_text": None, "corrections": []}
912
+
913
+ # Find the last assistant message
914
+ last_response = None
915
+ for msg in reversed(messages):
916
+ if msg.role == MessageRole.ASSISTANT:
917
+ last_response = msg.content
918
+ break
919
+
920
+ logging.info(
921
+ f"🔍 [VHC_AGENT] Found last_response: {'set' if last_response else 'None'}"
922
+ )
923
+
924
+ if not last_response:
925
+ logging.info("🔍 [VHC_AGENT] Returning early - no last assistant response found")
926
+ return {"corrected_text": None, "corrections": []}
927
+
928
+ # Update stored response for caching
929
+ self._last_response = last_response
930
+
931
+ # Create cache key from query + response
932
+ cache_key = hash(f"{self._last_query}:{self._last_response}")
933
+
934
+ # Return cached results if available
935
+ if cache_key in self._vhc_cache:
936
+ return self._vhc_cache[cache_key]
937
+
938
+ # Check if we have VHC API key
939
+ logging.info(
940
+ f"🔍 [VHC_AGENT] acompute_vhc called with vectara_api_key={'set' if self.vectara_api_key else 'None'}"
941
+ )
942
+ if not self.vectara_api_key:
943
+ logging.info("🔍 [VHC_AGENT] No vectara_api_key - returning early with None")
944
+ return {"corrected_text": None, "corrections": []}
945
+
946
+ # Compute VHC using existing library function
947
+ from .agent_core.utils.hallucination import analyze_hallucinations
948
+
949
+ try:
950
+ # Use stored tool outputs from current query
951
+ stored_tool_outputs = self._get_stored_tool_outputs()
952
+ logging.info(f"🔧 [VHC_AGENT] Using {len(stored_tool_outputs)} stored tool outputs for VHC")
953
+
954
+ corrected_text, corrections = analyze_hallucinations(
955
+ query=self._last_query,
956
+ chat_history=self.memory.get(),
957
+ agent_response=self._last_response,
958
+ tools=self.tools,
959
+ vectara_api_key=self.vectara_api_key,
960
+ tool_outputs=stored_tool_outputs,
961
+ )
962
+
963
+ # Cache results
964
+ results = {"corrected_text": corrected_text, "corrections": corrections}
965
+ self._vhc_cache[cache_key] = results
966
+
967
+ return results
968
+
969
+ except Exception as e:
970
+ logger.error(f"VHC computation failed: {e}")
971
+ return {"corrected_text": None, "corrections": []}
972
+
973
+ def compute_vhc(self) -> Dict[str, Any]:
974
+ """
975
+ Compute VHC for the last query/response pair (sync version).
976
+ Results are cached for subsequent calls. Tool outputs are automatically
977
+ collected during streaming and used internally.
978
+
979
+ Returns:
980
+ Dict[str, Any]: Dictionary containing 'corrected_text' and 'corrections'
981
+ """
982
+ try:
983
+ loop = asyncio.get_event_loop()
984
+ return loop.run_until_complete(self.acompute_vhc())
985
+ except RuntimeError:
986
+ # No event loop running, create a new one
987
+ return asyncio.run(self.acompute_vhc())
988
+
1129
989
  #
1130
990
  # run() method for running a workflow
1131
991
  # workflow will always get these arguments in the StartEvent: agent, tools, llm, verbose
@@ -1168,6 +1028,8 @@ class Agent:
1168
1028
  f"Fields without default values: {fields_without_default}"
1169
1029
  )
1170
1030
 
1031
+ from llama_index.core.workflow import Context
1032
+
1171
1033
  workflow_context = Context(workflow=workflow)
1172
1034
  try:
1173
1035
  # run workflow
@@ -1197,7 +1059,9 @@ class Agent:
1197
1059
  input_dict[key] = value
1198
1060
  output = outputs_model_on_fail_cls.model_validate(input_dict)
1199
1061
  else:
1200
- print(f"Vectara Agentic: Workflow failed with unexpected error: {e}")
1062
+ logger.warning(
1063
+ f"Vectara Agentic: Workflow failed with unexpected error: {e}"
1064
+ )
1201
1065
  raise type(e)(str(e)).with_traceback(e.__traceback__)
1202
1066
 
1203
1067
  return output
@@ -1225,57 +1089,7 @@ class Agent:
1225
1089
 
1226
1090
  def to_dict(self) -> Dict[str, Any]:
1227
1091
  """Serialize the Agent instance to a dictionary."""
1228
- tool_info = []
1229
- for tool in self.tools:
1230
- if hasattr(tool.metadata, "fn_schema"):
1231
- fn_schema_cls = tool.metadata.fn_schema
1232
- fn_schema_serialized = {
1233
- "schema": (
1234
- fn_schema_cls.model_json_schema()
1235
- if fn_schema_cls and hasattr(fn_schema_cls, "model_json_schema")
1236
- else None
1237
- ),
1238
- "metadata": {
1239
- "module": fn_schema_cls.__module__ if fn_schema_cls else None,
1240
- "class": fn_schema_cls.__name__ if fn_schema_cls else None,
1241
- },
1242
- }
1243
- else:
1244
- fn_schema_serialized = None
1245
-
1246
- tool_dict = {
1247
- "tool_type": tool.metadata.tool_type.value,
1248
- "name": tool.metadata.name,
1249
- "description": tool.metadata.description,
1250
- "fn": (
1251
- pickle.dumps(getattr(tool, "fn", None)).decode("latin-1")
1252
- if getattr(tool, "fn", None)
1253
- else None
1254
- ),
1255
- "async_fn": (
1256
- pickle.dumps(getattr(tool, "async_fn", None)).decode("latin-1")
1257
- if getattr(tool, "async_fn", None)
1258
- else None
1259
- ),
1260
- "fn_schema": fn_schema_serialized,
1261
- }
1262
- tool_info.append(tool_dict)
1263
-
1264
- return {
1265
- "agent_type": self.agent_config.agent_type.value,
1266
- "memory": pickle.dumps(self.agent.memory).decode("latin-1"),
1267
- "tools": tool_info,
1268
- "topic": self._topic,
1269
- "custom_instructions": self._custom_instructions,
1270
- "verbose": self.verbose,
1271
- "agent_config": self.agent_config.to_dict(),
1272
- "fallback_agent": (
1273
- self.fallback_agent_config.to_dict()
1274
- if self.fallback_agent_config
1275
- else None
1276
- ),
1277
- "workflow_cls": self.workflow_cls if self.workflow_cls else None,
1278
- }
1092
+ return serialize_agent_to_dict(self)
1279
1093
 
1280
1094
  @classmethod
1281
1095
  def from_dict(
@@ -1285,114 +1099,6 @@ class Agent:
1285
1099
  query_logging_callback: Optional[Callable] = None,
1286
1100
  ) -> "Agent":
1287
1101
  """Create an Agent instance from a dictionary."""
1288
- agent_config = AgentConfig.from_dict(data["agent_config"])
1289
- fallback_agent_config = (
1290
- AgentConfig.from_dict(data["fallback_agent_config"])
1291
- if data.get("fallback_agent_config")
1292
- else None
1293
- )
1294
- tools: list[FunctionTool] = []
1295
-
1296
- for tool_data in data["tools"]:
1297
- query_args_model = None
1298
- if tool_data.get("fn_schema"):
1299
- schema_info = tool_data["fn_schema"]
1300
- try:
1301
- module_name = schema_info["metadata"]["module"]
1302
- class_name = schema_info["metadata"]["class"]
1303
- mod = importlib.import_module(module_name)
1304
- candidate_cls = getattr(mod, class_name)
1305
- if inspect.isclass(candidate_cls) and issubclass(
1306
- candidate_cls, BaseModel
1307
- ):
1308
- query_args_model = candidate_cls
1309
- else:
1310
- # It's not the Pydantic model class we expected (e.g., it's the function itself)
1311
- # Force fallback to JSON schema reconstruction by raising an error.
1312
- raise ImportError(
1313
- f"Retrieved '{class_name}' from '{module_name}' is not a Pydantic BaseModel class. "
1314
- "Falling back to JSON schema reconstruction."
1315
- )
1316
- except Exception:
1317
- # Fallback: rebuild using the JSON schema
1318
- field_definitions = {}
1319
- json_schema_to_rebuild = schema_info.get("schema")
1320
- if json_schema_to_rebuild and isinstance(
1321
- json_schema_to_rebuild, dict
1322
- ):
1323
- for field, values in json_schema_to_rebuild.get(
1324
- "properties", {}
1325
- ).items():
1326
- field_type = get_field_type(values)
1327
- field_description = values.get(
1328
- "description"
1329
- ) # Defaults to None
1330
- if "default" in values:
1331
- field_definitions[field] = (
1332
- field_type,
1333
- Field(
1334
- description=field_description,
1335
- default=values["default"],
1336
- ),
1337
- )
1338
- else:
1339
- field_definitions[field] = (
1340
- field_type,
1341
- Field(description=field_description),
1342
- )
1343
- query_args_model = create_model(
1344
- json_schema_to_rebuild.get(
1345
- "title", f"{tool_data['name']}_QueryArgs"
1346
- ),
1347
- **field_definitions,
1348
- )
1349
- else: # If schema part is missing or not a dict, create a default empty model
1350
- query_args_model = create_model(
1351
- f"{tool_data['name']}_QueryArgs"
1352
- )
1353
-
1354
- # If fn_schema was not in tool_data or reconstruction failed badly, default to empty pydantic model
1355
- if query_args_model is None:
1356
- query_args_model = create_model(f"{tool_data['name']}_QueryArgs")
1357
-
1358
- fn = (
1359
- pickle.loads(tool_data["fn"].encode("latin-1"))
1360
- if tool_data["fn"]
1361
- else None
1362
- )
1363
- async_fn = (
1364
- pickle.loads(tool_data["async_fn"].encode("latin-1"))
1365
- if tool_data["async_fn"]
1366
- else None
1367
- )
1368
-
1369
- tool = VectaraTool.from_defaults(
1370
- name=tool_data["name"],
1371
- description=tool_data["description"],
1372
- fn=fn,
1373
- async_fn=async_fn,
1374
- fn_schema=query_args_model, # Re-assign the recreated dynamic model
1375
- tool_type=ToolType(tool_data["tool_type"]),
1376
- )
1377
- tools.append(tool)
1378
-
1379
- agent = cls(
1380
- tools=tools,
1381
- agent_config=agent_config,
1382
- topic=data["topic"],
1383
- custom_instructions=data["custom_instructions"],
1384
- verbose=data["verbose"],
1385
- fallback_agent_config=fallback_agent_config,
1386
- workflow_cls=data["workflow_cls"],
1387
- agent_progress_callback=agent_progress_callback,
1388
- query_logging_callback=query_logging_callback,
1389
- )
1390
- memory = (
1391
- pickle.loads(data["memory"].encode("latin-1"))
1392
- if data.get("memory")
1393
- else None
1102
+ return deserialize_agent_from_dict(
1103
+ cls, data, agent_progress_callback, query_logging_callback
1394
1104
  )
1395
- if memory:
1396
- agent.agent.memory = memory
1397
- agent.memory = memory
1398
- return agent