vectara-agentic 0.3.2__py3-none-any.whl → 0.4.0__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 (53) hide show
  1. tests/__init__.py +7 -0
  2. tests/conftest.py +312 -0
  3. tests/endpoint.py +54 -17
  4. tests/run_tests.py +111 -0
  5. tests/test_agent.py +10 -5
  6. tests/test_agent_type.py +82 -143
  7. tests/test_api_endpoint.py +4 -0
  8. tests/test_bedrock.py +4 -0
  9. tests/test_fallback.py +4 -0
  10. tests/test_gemini.py +28 -45
  11. tests/test_groq.py +4 -0
  12. tests/test_private_llm.py +11 -2
  13. tests/test_return_direct.py +6 -2
  14. tests/test_serialization.py +4 -0
  15. tests/test_streaming.py +88 -0
  16. tests/test_tools.py +10 -82
  17. tests/test_vectara_llms.py +4 -0
  18. tests/test_vhc.py +66 -0
  19. tests/test_workflow.py +4 -0
  20. vectara_agentic/__init__.py +27 -4
  21. vectara_agentic/_callback.py +65 -67
  22. vectara_agentic/_observability.py +30 -30
  23. vectara_agentic/_version.py +1 -1
  24. vectara_agentic/agent.py +375 -848
  25. vectara_agentic/agent_config.py +15 -14
  26. vectara_agentic/agent_core/__init__.py +22 -0
  27. vectara_agentic/agent_core/factory.py +501 -0
  28. vectara_agentic/{_prompts.py → agent_core/prompts.py} +3 -35
  29. vectara_agentic/agent_core/serialization.py +345 -0
  30. vectara_agentic/agent_core/streaming.py +495 -0
  31. vectara_agentic/agent_core/utils/__init__.py +34 -0
  32. vectara_agentic/agent_core/utils/hallucination.py +202 -0
  33. vectara_agentic/agent_core/utils/logging.py +52 -0
  34. vectara_agentic/agent_core/utils/prompt_formatting.py +56 -0
  35. vectara_agentic/agent_core/utils/schemas.py +87 -0
  36. vectara_agentic/agent_core/utils/tools.py +125 -0
  37. vectara_agentic/agent_endpoint.py +4 -6
  38. vectara_agentic/db_tools.py +37 -12
  39. vectara_agentic/llm_utils.py +41 -42
  40. vectara_agentic/sub_query_workflow.py +9 -14
  41. vectara_agentic/tool_utils.py +138 -83
  42. vectara_agentic/tools.py +43 -21
  43. vectara_agentic/tools_catalog.py +16 -16
  44. vectara_agentic/types.py +98 -6
  45. {vectara_agentic-0.3.2.dist-info → vectara_agentic-0.4.0.dist-info}/METADATA +69 -30
  46. vectara_agentic-0.4.0.dist-info/RECORD +50 -0
  47. tests/test_agent_planning.py +0 -64
  48. tests/test_hhem.py +0 -100
  49. vectara_agentic/hhem.py +0 -82
  50. vectara_agentic-0.3.2.dist-info/RECORD +0 -39
  51. {vectara_agentic-0.3.2.dist-info → vectara_agentic-0.4.0.dist-info}/WHEEL +0 -0
  52. {vectara_agentic-0.3.2.dist-info → vectara_agentic-0.4.0.dist-info}/licenses/LICENSE +0 -0
  53. {vectara_agentic-0.3.2.dist-info → vectara_agentic-0.4.0.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
+ warnings.simplefilter("ignore", DeprecationWarning)
7
+
8
+ # pylint: disable=wrong-import-position
9
+ from typing import List, Callable, Optional, Dict, Any, Union, Tuple, TYPE_CHECKING
6
10
  import os
7
- import re
8
11
  from datetime import date
9
- import time
10
12
  import json
11
13
  import logging
12
14
  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
15
 
20
- import cloudpickle as pickle
16
+ from pydantic import ValidationError
17
+ from pydantic_core import PydanticUndefined
21
18
 
22
19
  from dotenv import load_dotenv
23
20
 
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
21
+ # Runtime imports for components used at module level
22
+ from llama_index.core.llms import MessageRole
23
+ from llama_index.core.callbacks import CallbackManager
24
+ from llama_index.core.memory import Memory
25
+
26
+ # Heavy llama_index imports moved to TYPE_CHECKING for lazy loading
27
+ if TYPE_CHECKING:
28
+ from llama_index.core.tools import FunctionTool
29
+ from llama_index.core.workflow import Workflow
30
+ from llama_index.core.agent.runner.base import AgentRunner
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,7 +93,7 @@ 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,
223
99
  ) -> None:
@@ -232,11 +108,8 @@ class Agent:
232
108
  general_instructions (str, optional): General instructions for the agent.
233
109
  The Agent has a default set of instructions that are crafted to help it operate effectively.
234
110
  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.
111
+ verbose (bool, optional): Whether the agent should print its steps. Defaults to False.
238
112
  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
113
  query_logging_callback (Callable): A callback function the code calls upon completion of a query
241
114
  agent_config (AgentConfig, optional): The configuration of the agent.
242
115
  Defaults to AgentConfig(), which reads from environment variables.
@@ -253,97 +126,51 @@ class Agent:
253
126
  self.agent_config_type = AgentConfigType.DEFAULT
254
127
  self.tools = tools
255
128
  if not any(tool.metadata.name == "get_current_date" for tool in self.tools):
256
- self.tools += [ToolsFactory().create_tool(get_current_date)]
129
+ self.tools += [
130
+ ToolsFactory().create_tool(get_current_date, vhc_eligible=False)
131
+ ]
257
132
  self.agent_type = self.agent_config.agent_type
258
- self.use_structured_planning = use_structured_planning
259
133
  self._llm = None # Lazy loading
260
134
  self._custom_instructions = custom_instructions
261
135
  self._general_instructions = general_instructions
262
136
  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
137
+ self.agent_progress_callback = agent_progress_callback
267
138
 
139
+ self.query_logging_callback = query_logging_callback
268
140
  self.workflow_cls = workflow_cls
269
141
  self.workflow_timeout = workflow_timeout
270
142
  self.vectara_api_key = vectara_api_key or os.environ.get("VECTARA_API_KEY", "")
271
143
 
272
144
  # Sanitize tools for Gemini if needed
273
145
  if self.agent_config.main_llm_provider == ModelProvider.GEMINI:
274
- self.tools = self._sanitize_tools_for_gemini(self.tools)
146
+ self.tools = sanitize_tools_for_gemini(self.tools)
275
147
 
276
148
  # 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
149
  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
- )
150
+ validate_tool_consistency(self.tools, self._custom_instructions, self.agent_config)
319
151
 
320
152
  # Setup callback manager
321
153
  callbacks: list[BaseCallbackHandler] = [
322
154
  AgentCallbackHandler(self.agent_progress_callback)
323
155
  ]
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
156
  self.callback_manager = CallbackManager(callbacks) # type: ignore
329
157
  self.verbose = verbose
330
158
 
159
+ self.session_id = (
160
+ getattr(self, "session_id", None) or f"{topic}:{date.today().isoformat()}"
161
+ )
162
+
163
+ self.memory = Memory.from_defaults(
164
+ session_id=self.session_id, token_limit=65536
165
+ )
331
166
  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)
167
+ from llama_index.core.llms import ChatMessage
168
+
169
+ msgs = []
170
+ for u, a in chat_history:
171
+ msgs.append(ChatMessage.from_str(u, role=MessageRole.USER))
172
+ msgs.append(ChatMessage.from_str(a, role=MessageRole.ASSISTANT))
173
+ self.memory.put_messages(msgs)
347
174
 
348
175
  # Set up main agent and fallback agent
349
176
  self._agent = None # Lazy loading
@@ -354,7 +181,7 @@ class Agent:
354
181
  try:
355
182
  self.observability_enabled = setup_observer(self.agent_config, self.verbose)
356
183
  except Exception as e:
357
- print(f"Failed to set up observer ({e}), ignoring")
184
+ logger.warning(f"Failed to set up observer ({e}), ignoring")
358
185
  self.observability_enabled = False
359
186
 
360
187
  @property
@@ -380,231 +207,57 @@ class Agent:
380
207
  )
381
208
  return self._fallback_agent
382
209
 
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
210
  def _create_agent(
441
- self, config: AgentConfig, llm_callback_manager: CallbackManager
442
- ) -> Union[BaseAgent, AgentRunner]:
211
+ self, config: AgentConfig, llm_callback_manager: "CallbackManager"
212
+ ) -> Union["BaseAgent", "AgentRunner"]:
443
213
  """
444
214
  Creates the agent based on the configuration object.
445
215
 
446
216
  Args:
447
-
448
217
  config: The configuration of the agent.
449
218
  llm_callback_manager: The callback manager for the agent's llm.
450
219
 
451
220
  Returns:
452
221
  Union[BaseAgent, AgentRunner]: The configured agent object.
453
222
  """
454
- agent_type = config.agent_type
455
223
  # Use the same LLM instance for consistency
456
- llm = self.llm if config == self.agent_config else get_llm(LLMRole.MAIN, config=config)
224
+ llm = (
225
+ self.llm
226
+ if config == self.agent_config
227
+ else get_llm(LLMRole.MAIN, config=config)
228
+ )
457
229
  llm.callback_manager = llm_callback_manager
458
230
 
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
231
+ return create_agent_from_config(
232
+ tools=self.tools,
233
+ llm=llm,
234
+ memory=self.memory,
235
+ config=config,
236
+ callback_manager=llm_callback_manager,
237
+ general_instructions=self._general_instructions,
238
+ topic=self._topic,
239
+ custom_instructions=self._custom_instructions,
240
+ verbose=self.verbose,
241
+ )
583
242
 
584
243
  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}")
244
+ """Clear the agent's memory."""
245
+ self.memory.reset()
246
+ if getattr(self, "_agent", None):
247
+ self._agent.memory = self.memory
248
+ if getattr(self, "_fallback_agent", None):
249
+ self._fallback_agent.memory = self.memory
597
250
 
598
251
  def __eq__(self, other):
599
252
  if not isinstance(other, Agent):
600
- print(
253
+ logger.debug(
601
254
  f"Comparison failed: other is not an instance of Agent. (self: {type(self)}, other: {type(other)})"
602
255
  )
603
256
  return False
604
257
 
605
258
  # Compare agent_type
606
259
  if self.agent_config.agent_type != other.agent_config.agent_type:
607
- print(
260
+ logger.debug(
608
261
  f"Comparison failed: agent_type differs. (self.agent_config.agent_type: {self.agent_config.agent_type},"
609
262
  f" other.agent_config.agent_type: {other.agent_config.agent_type})"
610
263
  )
@@ -612,7 +265,7 @@ class Agent:
612
265
 
613
266
  # Compare tools
614
267
  if self.tools != other.tools:
615
- print(
268
+ logger.debug(
616
269
  "Comparison failed: tools differ."
617
270
  f"(self.tools: {[t.metadata.name for t in self.tools]}, "
618
271
  f"other.tools: {[t.metadata.name for t in other.tools]})"
@@ -621,14 +274,14 @@ class Agent:
621
274
 
622
275
  # Compare topic
623
276
  if self._topic != other._topic:
624
- print(
277
+ logger.debug(
625
278
  f"Comparison failed: topic differs. (self.topic: {self._topic}, other.topic: {other._topic})"
626
279
  )
627
280
  return False
628
281
 
629
282
  # Compare custom_instructions
630
283
  if self._custom_instructions != other._custom_instructions:
631
- print(
284
+ logger.debug(
632
285
  "Comparison failed: custom_instructions differ. (self.custom_instructions: "
633
286
  f"{self._custom_instructions}, other.custom_instructions: {other._custom_instructions})"
634
287
  )
@@ -636,31 +289,27 @@ class Agent:
636
289
 
637
290
  # Compare verbose
638
291
  if self.verbose != other.verbose:
639
- print(
292
+ logger.debug(
640
293
  f"Comparison failed: verbose differs. (self.verbose: {self.verbose}, other.verbose: {other.verbose})"
641
294
  )
642
295
  return False
643
296
 
644
297
  # 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
- )
298
+ if self.memory.get() != other.memory.get():
299
+ logger.debug("Comparison failed: agent memory differs.")
650
300
  return False
651
301
 
652
302
  # If all comparisons pass
653
- print("All comparisons passed. Objects are equal.")
303
+ logger.debug("All comparisons passed. Objects are equal.")
654
304
  return True
655
305
 
656
306
  @classmethod
657
307
  def from_tools(
658
308
  cls,
659
- tools: List[FunctionTool],
309
+ tools: List["FunctionTool"],
660
310
  topic: str = "general",
661
311
  custom_instructions: str = "",
662
312
  verbose: bool = True,
663
- update_func: Optional[Callable[[AgentStatusType, dict, str], None]] = None,
664
313
  agent_progress_callback: Optional[
665
314
  Callable[[AgentStatusType, dict, str], None]
666
315
  ] = None,
@@ -669,7 +318,7 @@ class Agent:
669
318
  validate_tools: bool = False,
670
319
  fallback_agent_config: Optional[AgentConfig] = None,
671
320
  chat_history: Optional[list[Tuple[str, str]]] = None,
672
- workflow_cls: Optional[Workflow] = None,
321
+ workflow_cls: Optional["Workflow"] = None,
673
322
  workflow_timeout: int = 120,
674
323
  ) -> "Agent":
675
324
  """
@@ -682,7 +331,6 @@ class Agent:
682
331
  custom_instructions (str, optional): custom instructions for the agent. Defaults to ''.
683
332
  verbose (bool, optional): Whether the agent should print its steps. Defaults to True.
684
333
  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
334
  query_logging_callback (Callable): A callback function the code calls upon completion of a query
687
335
  agent_config (AgentConfig, optional): The configuration of the agent.
688
336
  fallback_agent_config (AgentConfig, optional): The fallback configuration of the agent.
@@ -702,7 +350,6 @@ class Agent:
702
350
  verbose=verbose,
703
351
  agent_progress_callback=agent_progress_callback,
704
352
  query_logging_callback=query_logging_callback,
705
- update_func=update_func,
706
353
  agent_config=agent_config,
707
354
  chat_history=chat_history,
708
355
  validate_tools=validate_tools,
@@ -754,133 +401,54 @@ class Agent:
754
401
  vectara_save_history: bool = True,
755
402
  return_direct: bool = False,
756
403
  ) -> "Agent":
757
- """
758
- Create an agent from a single Vectara corpus
759
-
760
- 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.
809
- """
810
- vec_factory = VectaraToolFactory(
811
- vectara_api_key=vectara_api_key,
404
+ """Create an agent from a single Vectara corpus using the factory function."""
405
+ # Use the factory function to avoid code duplication
406
+ config = create_agent_from_corpus(
407
+ tool_name=tool_name,
408
+ data_description=data_description,
409
+ assistant_specialty=assistant_specialty,
410
+ general_instructions=general_instructions,
812
411
  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,
412
+ vectara_api_key=vectara_api_key,
413
+ agent_config=agent_config,
414
+ fallback_agent_config=fallback_agent_config,
858
415
  verbose=verbose,
416
+ vectara_filter_fields=vectara_filter_fields,
417
+ vectara_offset=vectara_offset,
418
+ vectara_lambda_val=vectara_lambda_val,
419
+ vectara_semantics=vectara_semantics,
420
+ vectara_custom_dimensions=vectara_custom_dimensions,
421
+ vectara_reranker=vectara_reranker,
422
+ vectara_rerank_k=vectara_rerank_k,
423
+ vectara_rerank_limit=vectara_rerank_limit,
424
+ vectara_rerank_cutoff=vectara_rerank_cutoff,
425
+ vectara_diversity_bias=vectara_diversity_bias,
426
+ vectara_udf_expression=vectara_udf_expression,
427
+ vectara_rerank_chain=vectara_rerank_chain,
428
+ vectara_n_sentences_before=vectara_n_sentences_before,
429
+ vectara_n_sentences_after=vectara_n_sentences_after,
430
+ vectara_summary_num_results=vectara_summary_num_results,
431
+ vectara_summarizer=vectara_summarizer,
432
+ vectara_summary_response_language=vectara_summary_response_language,
433
+ vectara_summary_prompt_text=vectara_summary_prompt_text,
434
+ vectara_max_response_chars=vectara_max_response_chars,
435
+ vectara_max_tokens=vectara_max_tokens,
436
+ vectara_temperature=vectara_temperature,
437
+ vectara_frequency_penalty=vectara_frequency_penalty,
438
+ vectara_presence_penalty=vectara_presence_penalty,
439
+ vectara_save_history=vectara_save_history,
859
440
  return_direct=return_direct,
860
441
  )
861
442
 
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
443
  return cls(
869
- tools=[vectara_tool],
870
- topic=assistant_specialty,
871
- custom_instructions=assistant_instructions,
872
- general_instructions=general_instructions,
873
- verbose=verbose,
444
+ chat_history=chat_history,
874
445
  agent_progress_callback=agent_progress_callback,
875
446
  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,
447
+ **config,
880
448
  )
881
449
 
882
450
  def _switch_agent_config(self) -> None:
883
- """ "
451
+ """
884
452
  Switch the configuration type of the agent.
885
453
  This function is called automatically to switch the agent configuration if the current configuration fails.
886
454
  """
@@ -899,45 +467,25 @@ class Agent:
899
467
  Returns:
900
468
  str: The report from the agent.
901
469
  """
902
- print("Vectara agentic Report:")
903
- print(f"Agent Type = {self.agent_config.agent_type}")
904
- print(f"Topic = {self._topic}")
905
- print("Tools:")
470
+ logger.info("Vectara agentic Report:")
471
+ logger.info(f"Agent Type = {self.agent_config.agent_type}")
472
+ logger.info(f"Topic = {self._topic}")
473
+ logger.info("Tools:")
906
474
  for tool in self.tools:
907
475
  if hasattr(tool, "metadata"):
908
476
  if detailed:
909
- print(f"- {tool.metadata.description}")
477
+ logger.info(f"- {tool.metadata.description}")
910
478
  else:
911
- print(f"- {tool.metadata.name}")
479
+ logger.info(f"- {tool.metadata.name}")
912
480
  else:
913
- print("- tool without metadata")
914
- print(
481
+ logger.info("- tool without metadata")
482
+ logger.info(
915
483
  f"Agent LLM = {get_llm(LLMRole.MAIN, config=self.agent_config).metadata.model_name}"
916
484
  )
917
- print(
485
+ logger.info(
918
486
  f"Tool LLM = {get_llm(LLMRole.TOOL, config=self.agent_config).metadata.model_name}"
919
487
  )
920
488
 
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
489
  def _get_current_agent(self):
942
490
  return (
943
491
  self.agent
@@ -949,6 +497,7 @@ class Agent:
949
497
  return (
950
498
  self.agent_config.agent_type
951
499
  if self.agent_config_type == AgentConfigType.DEFAULT
500
+ or not self.fallback_agent_config
952
501
  else self.fallback_agent_config.agent_type
953
502
  )
954
503
 
@@ -963,9 +512,9 @@ class Agent:
963
512
  return
964
513
 
965
514
  agent = self._get_current_agent()
966
- agent_response.response = str(agent.llm.acomplete(llm_prompt))
515
+ agent_response.response = (await agent.llm.acomplete(llm_prompt)).text
967
516
 
968
- def chat(self, prompt: str) -> AgentResponse: # type: ignore
517
+ def chat(self, prompt: str) -> AgentResponse:
969
518
  """
970
519
  Interact with the agent using a chat prompt.
971
520
 
@@ -975,48 +524,15 @@ class Agent:
975
524
  Returns:
976
525
  AgentResponse: The response from the agent.
977
526
  """
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
527
  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}")
528
+ _ = asyncio.get_running_loop()
529
+ except RuntimeError:
530
+ return asyncio.run(self.achat(prompt))
531
+
532
+ # We are inside a running loop (Jupyter, uvicorn, etc.)
533
+ raise RuntimeError(
534
+ "Use `await agent.achat(...)` inside an event loop (e.g. Jupyter)."
535
+ )
1020
536
 
1021
537
  async def achat(self, prompt: str) -> AgentResponse: # type: ignore
1022
538
  """
@@ -1028,6 +544,9 @@ class Agent:
1028
544
  Returns:
1029
545
  AgentResponse: The response from the agent.
1030
546
  """
547
+ if not prompt:
548
+ return AgentResponse(response="")
549
+
1031
550
  max_attempts = 4 if self.fallback_agent_config else 2
1032
551
  attempt = 0
1033
552
  orig_llm = self.llm.metadata.model_name
@@ -1035,36 +554,179 @@ class Agent:
1035
554
  while attempt < max_attempts:
1036
555
  try:
1037
556
  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)
557
+
558
+ # Deal with workflow-based agent types (Function Calling and ReAct)
559
+ if self._get_current_agent_type() in [
560
+ AgentType.FUNCTION_CALLING,
561
+ AgentType.REACT,
562
+ ]:
563
+ from llama_index.core.workflow import Context
564
+
565
+ ctx = Context(current_agent)
566
+ handler = current_agent.run(
567
+ user_msg=prompt, ctx=ctx, memory=self.memory
568
+ )
569
+
570
+ # Listen to workflow events if progress callback is set
571
+ if self.agent_progress_callback:
572
+ # Create event tracker for consistent event ID generation
573
+ from .agent_core.streaming import ToolEventTracker
574
+
575
+ event_tracker = ToolEventTracker()
576
+
577
+ async for event in handler.stream_events():
578
+ # Use consistent event ID tracking to ensure tool calls and outputs are paired
579
+ event_id = event_tracker.get_event_id(event)
580
+
581
+ # Handle different types of workflow events using same logic as FunctionCallingStreamHandler
582
+ from llama_index.core.agent.workflow import (
583
+ ToolCall,
584
+ ToolCallResult,
585
+ AgentInput,
586
+ AgentOutput,
587
+ )
588
+
589
+ if isinstance(event, ToolCall):
590
+ self.agent_progress_callback(
591
+ status_type=AgentStatusType.TOOL_CALL,
592
+ msg={
593
+ "tool_name": event.tool_name,
594
+ "arguments": json.dumps(event.tool_kwargs),
595
+ },
596
+ event_id=event_id,
597
+ )
598
+ elif isinstance(event, ToolCallResult):
599
+ self.agent_progress_callback(
600
+ status_type=AgentStatusType.TOOL_OUTPUT,
601
+ msg={
602
+ "tool_name": event.tool_name,
603
+ "content": str(event.tool_output),
604
+ },
605
+ event_id=event_id,
606
+ )
607
+ elif isinstance(event, AgentInput):
608
+ self.agent_progress_callback(
609
+ status_type=AgentStatusType.AGENT_UPDATE,
610
+ msg={"content": f"Agent input: {event.input}"},
611
+ event_id=event_id,
612
+ )
613
+ elif isinstance(event, AgentOutput):
614
+ self.agent_progress_callback(
615
+ status_type=AgentStatusType.AGENT_UPDATE,
616
+ msg={"content": f"Agent output: {event.response}"},
617
+ event_id=event_id,
618
+ )
619
+
620
+ result = await handler
621
+
622
+ # Ensure we have an AgentResponse object with a string response
623
+ if hasattr(result, "response"):
624
+ response_text = result.response
625
+ else:
626
+ response_text = str(result)
627
+
628
+ # Handle case where response is a ChatMessage object
629
+ if hasattr(response_text, "content"):
630
+ response_text = response_text.content
631
+ elif not isinstance(response_text, str):
632
+ response_text = str(response_text)
633
+
634
+ if response_text is None or response_text == "None":
635
+ # Try to find tool outputs in the result object
636
+ response_text = None
637
+
638
+ # Check various possible locations for tool outputs
639
+ if hasattr(result, "tool_outputs") and result.tool_outputs:
640
+ # Get the latest tool output
641
+ latest_output = (
642
+ result.tool_outputs[-1]
643
+ if isinstance(result.tool_outputs, list)
644
+ else result.tool_outputs
645
+ )
646
+ response_text = str(latest_output)
647
+
648
+ # Check if there are tool_calls with results
649
+ elif hasattr(result, "tool_calls") and result.tool_calls:
650
+ # Tool calls might contain the outputs - let's try to extract them
651
+ for tool_call in result.tool_calls:
652
+ if (
653
+ hasattr(tool_call, "tool_output")
654
+ and tool_call.tool_output is not None
655
+ ):
656
+ response_text = str(tool_call.tool_output)
657
+ break
658
+
659
+ elif hasattr(result, "sources") or hasattr(
660
+ result, "source_nodes"
661
+ ):
662
+ sources = getattr(
663
+ result, "sources", getattr(result, "source_nodes", [])
664
+ )
665
+ if (
666
+ sources
667
+ and len(sources) > 0
668
+ and hasattr(sources[0], "text")
669
+ ):
670
+ response_text = sources[0].text
671
+
672
+ # Check for workflow context or chat history that might contain tool results
673
+ elif hasattr(result, "chat_history"):
674
+ # Look for the most recent assistant message that might contain tool results
675
+ chat_history = result.chat_history
676
+ if chat_history and len(chat_history) > 0:
677
+ for msg in reversed(chat_history):
678
+ if (
679
+ msg.role == MessageRole.TOOL
680
+ and msg.content
681
+ and str(msg.content).strip()
682
+ ):
683
+ response_text = msg.content
684
+ break
685
+ if (
686
+ hasattr(msg, "content")
687
+ and msg.content
688
+ and str(msg.content).strip()
689
+ ):
690
+ response_text = msg.content
691
+ break
692
+
693
+ # If we still don't have a response, provide a fallback
694
+ if response_text is None or response_text == "None":
695
+ response_text = "Response completed."
696
+
697
+ agent_response = AgentResponse(
698
+ response=response_text, metadata=getattr(result, "metadata", {})
699
+ )
700
+
701
+ # Standard chat interaction for other agent types
702
+ else:
703
+ agent_response = await current_agent.achat(prompt)
704
+
705
+ # Post processing after response is generated
706
+ agent_response.metadata = agent_response.metadata or {}
707
+ user_metadata = agent_response.metadata
708
+ agent_response = await execute_post_stream_processing(
709
+ agent_response, prompt, self, user_metadata
710
+ )
1045
711
  return agent_response
1046
712
 
1047
713
  except Exception as e:
1048
714
  last_error = e
1049
715
  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
- )
716
+ logger.warning(f"LLM call failed on attempt {attempt}. " f"Error: {e}.")
717
+ if attempt >= 2 and self.fallback_agent_config:
1056
718
  self._switch_agent_config()
1057
- time.sleep(1)
719
+ await asyncio.sleep(1)
1058
720
  attempt += 1
1059
721
 
1060
722
  return AgentResponse(
1061
723
  response=(
1062
724
  f"For {orig_llm} LLM - failure can't be resolved after "
1063
- f"{max_attempts} attempts ({last_error}."
725
+ f"{max_attempts} attempts ({last_error})."
1064
726
  )
1065
727
  )
1066
728
 
1067
- def stream_chat(self, prompt: str) -> AgentStreamingResponse: # type: ignore
729
+ def stream_chat(self, prompt: str) -> AgentStreamingResponse:
1068
730
  """
1069
731
  Interact with the agent using a chat prompt with streaming.
1070
732
  Args:
@@ -1072,7 +734,13 @@ class Agent:
1072
734
  Returns:
1073
735
  AgentStreamingResponse: The streaming response from the agent.
1074
736
  """
1075
- return asyncio.run(self.astream_chat(prompt))
737
+ try:
738
+ _ = asyncio.get_running_loop()
739
+ except RuntimeError:
740
+ return asyncio.run(self.astream_chat(prompt))
741
+ raise RuntimeError(
742
+ "Use `await agent.astream_chat(...)` inside an event loop (e.g. Jupyter)."
743
+ )
1076
744
 
1077
745
  async def astream_chat(self, prompt: str) -> AgentStreamingResponse: # type: ignore
1078
746
  """
@@ -1085,45 +753,60 @@ class Agent:
1085
753
  max_attempts = 4 if self.fallback_agent_config else 2
1086
754
  attempt = 0
1087
755
  orig_llm = self.llm.metadata.model_name
756
+ last_error = None
1088
757
  while attempt < max_attempts:
1089
758
  try:
1090
759
  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
760
+ user_meta: Dict[str, Any] = {}
761
+
762
+ # Deal with Function Calling agent type
763
+ if self._get_current_agent_type() == AgentType.FUNCTION_CALLING:
764
+ from llama_index.core.workflow import Context
765
+
766
+ ctx = Context(current_agent)
767
+ handler = current_agent.run(
768
+ user_msg=prompt, ctx=ctx, memory=self.memory
769
+ )
770
+
771
+ # Use the dedicated FunctionCallingStreamHandler
772
+ stream_handler = FunctionCallingStreamHandler(self, handler, prompt)
773
+ streaming_adapter = stream_handler.create_streaming_response(
774
+ user_meta
775
+ )
776
+
777
+ return AgentStreamingResponse(
778
+ base=streaming_adapter, metadata=user_meta
779
+ )
780
+
781
+ #
782
+ # For other agent types, use the standard async chat method
783
+ #
784
+ li_stream = await current_agent.astream_chat(prompt)
785
+ orig_async = li_stream.async_response_gen
1093
786
 
1094
787
  # Define a wrapper to preserve streaming behavior while executing post-stream logic.
1095
788
  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
789
+ async for tok in orig_async():
790
+ yield tok
791
+
792
+ # Use shared post-processing function
793
+ await execute_post_stream_processing(
794
+ li_stream, prompt, self, user_meta
795
+ )
796
+
797
+ li_stream.async_response_gen = _stream_response_wrapper
798
+ return AgentStreamingResponse(base=li_stream, metadata=user_meta)
1110
799
 
1111
800
  except Exception as e:
1112
801
  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
- )
802
+ if attempt >= 2 and self.fallback_agent_config:
1118
803
  self._switch_agent_config()
1119
- time.sleep(1)
804
+ await asyncio.sleep(1)
1120
805
  attempt += 1
1121
806
 
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
- )
807
+ return AgentStreamingResponse.from_error(
808
+ f"For {orig_llm} LLM - failure can't be resolved after "
809
+ f"{max_attempts} attempts ({last_error})."
1127
810
  )
1128
811
 
1129
812
  #
@@ -1168,6 +851,8 @@ class Agent:
1168
851
  f"Fields without default values: {fields_without_default}"
1169
852
  )
1170
853
 
854
+ from llama_index.core.workflow import Context
855
+
1171
856
  workflow_context = Context(workflow=workflow)
1172
857
  try:
1173
858
  # run workflow
@@ -1197,7 +882,7 @@ class Agent:
1197
882
  input_dict[key] = value
1198
883
  output = outputs_model_on_fail_cls.model_validate(input_dict)
1199
884
  else:
1200
- print(f"Vectara Agentic: Workflow failed with unexpected error: {e}")
885
+ logger.warning(f"Vectara Agentic: Workflow failed with unexpected error: {e}")
1201
886
  raise type(e)(str(e)).with_traceback(e.__traceback__)
1202
887
 
1203
888
  return output
@@ -1225,57 +910,7 @@ class Agent:
1225
910
 
1226
911
  def to_dict(self) -> Dict[str, Any]:
1227
912
  """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
- }
913
+ return serialize_agent_to_dict(self)
1279
914
 
1280
915
  @classmethod
1281
916
  def from_dict(
@@ -1285,114 +920,6 @@ class Agent:
1285
920
  query_logging_callback: Optional[Callable] = None,
1286
921
  ) -> "Agent":
1287
922
  """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
923
+ return deserialize_agent_from_dict(
924
+ cls, data, agent_progress_callback, query_logging_callback
1394
925
  )
1395
- if memory:
1396
- agent.agent.memory = memory
1397
- agent.memory = memory
1398
- return agent