vectara-agentic 0.2.14__py3-none-any.whl → 0.2.16__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

vectara_agentic/agent.py CHANGED
@@ -14,14 +14,11 @@ import importlib
14
14
  from collections import Counter
15
15
  import inspect
16
16
  from inspect import Signature, Parameter, ismethod
17
-
17
+ from pydantic import Field, create_model, ValidationError, BaseModel
18
18
  import cloudpickle as pickle
19
19
 
20
20
  from dotenv import load_dotenv
21
21
 
22
- from pydantic import Field, create_model, ValidationError
23
-
24
-
25
22
  from llama_index.core.memory import ChatMemoryBuffer
26
23
  from llama_index.core.llms import ChatMessage, MessageRole
27
24
  from llama_index.core.tools import FunctionTool
@@ -145,21 +142,53 @@ def get_field_type(field_schema: dict) -> Any:
145
142
  "array": list,
146
143
  "object": dict,
147
144
  "number": float,
145
+ "null": type(None),
148
146
  }
147
+ if not field_schema: # Handles empty schema {}
148
+ return Any
149
+
149
150
  if "anyOf" in field_schema:
150
151
  types = []
151
- for option in field_schema["anyOf"]:
152
- # If the option has a type, convert it; otherwise, use Any.
153
- if "type" in option:
154
- types.append(json_type_to_python.get(option["type"], Any))
155
- else:
156
- types.append(Any)
157
- # Return a Union of the types. For example, Union[str, int]
152
+ for option_schema in field_schema["anyOf"]:
153
+ types.append(get_field_type(option_schema)) # Recursive call
154
+ if not types:
155
+ return Any
158
156
  return Union[tuple(types)]
159
- elif "type" in field_schema:
160
- return json_type_to_python.get(field_schema["type"], Any)
161
- else:
162
- return Any
157
+
158
+ if "type" in field_schema and isinstance(field_schema["type"], list):
159
+ types = []
160
+ for type_name in field_schema["type"]:
161
+ if type_name == "array":
162
+ item_schema = field_schema.get("items", {})
163
+ types.append(List[get_field_type(item_schema)])
164
+ elif type_name in json_type_to_python:
165
+ types.append(json_type_to_python[type_name])
166
+ else:
167
+ types.append(Any) # Fallback for unknown types in the list
168
+ if not types:
169
+ return Any
170
+ return Union[tuple(types)] # type: ignore
171
+
172
+ if "type" in field_schema:
173
+ schema_type_name = field_schema["type"]
174
+ if schema_type_name == "array":
175
+ item_schema = field_schema.get(
176
+ "items", {}
177
+ ) # Default to Any if "items" is missing
178
+ return List[get_field_type(item_schema)]
179
+
180
+ return json_type_to_python.get(schema_type_name, Any)
181
+
182
+ # If only "items" is present (implies array by some conventions, but less standard)
183
+ # Or if it's a schema with other keywords like 'properties' (implying object)
184
+ # For simplicity, if no "type" or "anyOf" at this point, default to Any or add more specific handling.
185
+ # If 'properties' in field_schema or 'additionalProperties' in field_schema, it's likely an object.
186
+ if "properties" in field_schema or "additionalProperties" in field_schema:
187
+ # This path might need to reconstruct a nested Pydantic model if you encounter such schemas.
188
+ # For now, treating as 'dict' or 'Any' might be a simpler placeholder.
189
+ return dict # Or Any, or more sophisticated object reconstruction.
190
+
191
+ return Any
163
192
 
164
193
 
165
194
  class Agent:
@@ -248,19 +277,27 @@ class Agent:
248
277
 
249
278
  if validate_tools:
250
279
  prompt = f"""
251
- Given the following instructions, and a list of tool names,
252
- Please identify tools mentioned in the instructions that do not exist in the list.
253
- Instructions:
280
+ You are provided these tools:
281
+ <tools>{','.join(tool_names)}</tools>
282
+ And these instructions:
283
+ <instructions>
254
284
  {self._custom_instructions}
255
- Tool names: {', '.join(tool_names)}
256
- Your response should include a comma separated list of tool names that do not exist in the list.
257
- Your response should be an empty string if all tools mentioned in the instructions are in the list.
285
+ </instructions>
286
+ Your task is to identify invalid tools.
287
+ A tool is invalid if it is mentioned in the instructions but not in the tools list.
288
+ A tool's name must have at least two characters.
289
+ Your response should be a comma-separated list of the invalid tools.
290
+ If not invalid tools exist, respond with "<OKAY>".
258
291
  """
259
292
  llm = get_llm(LLMRole.MAIN, config=self.agent_config)
260
- bad_tools = llm.complete(prompt).text.split(", ")
261
- if bad_tools:
293
+ bad_tools_str = llm.complete(prompt).text
294
+ if bad_tools_str and bad_tools_str != "<OKAY>":
295
+ bad_tools = [tool.strip() for tool in bad_tools_str.split(",")]
296
+ numbered = ", ".join(
297
+ f"({i}) {tool}" for i, tool in enumerate(bad_tools, 1)
298
+ )
262
299
  raise ValueError(
263
- f"The Agent custom instructions mention these invalid tools: {', '.join(bad_tools)}"
300
+ f"The Agent custom instructions mention these invalid tools: {numbered}"
264
301
  )
265
302
 
266
303
  # Create token counters for the main and tool LLMs
@@ -689,6 +726,7 @@ class Agent:
689
726
  vectara_frequency_penalty: Optional[float] = None,
690
727
  vectara_presence_penalty: Optional[float] = None,
691
728
  vectara_save_history: bool = True,
729
+ return_direct: bool = False,
692
730
  ) -> "Agent":
693
731
  """
694
732
  Create an agent from a single Vectara corpus
@@ -738,6 +776,7 @@ class Agent:
738
776
  vectara_presence_penalty (float, optional): How much to penalize repeating tokens in the response,
739
777
  higher values increasing the diversity of topics.
740
778
  vectara_save_history (bool, optional): Whether to save the query in history.
779
+ return_direct (bool, optional): Whether the agent should return the tool's response directly.
741
780
 
742
781
  Returns:
743
782
  Agent: An instance of the Agent class.
@@ -791,6 +830,7 @@ class Agent:
791
830
  save_history=vectara_save_history,
792
831
  include_citations=True,
793
832
  verbose=verbose,
833
+ return_direct=return_direct,
794
834
  )
795
835
 
796
836
  assistant_instructions = f"""
@@ -1134,41 +1174,66 @@ class Agent:
1134
1174
  tools = []
1135
1175
 
1136
1176
  for tool_data in data["tools"]:
1137
- # Recreate the dynamic model using the schema info
1177
+ query_args_model = None
1138
1178
  if tool_data.get("fn_schema"):
1139
1179
  schema_info = tool_data["fn_schema"]
1140
1180
  try:
1141
1181
  module_name = schema_info["metadata"]["module"]
1142
1182
  class_name = schema_info["metadata"]["class"]
1143
1183
  mod = importlib.import_module(module_name)
1144
- fn_schema_cls = getattr(mod, class_name)
1145
- query_args_model = fn_schema_cls
1184
+ candidate_cls = getattr(mod, class_name)
1185
+ if inspect.isclass(candidate_cls) and issubclass(
1186
+ candidate_cls, BaseModel
1187
+ ):
1188
+ query_args_model = candidate_cls
1189
+ else:
1190
+ # It's not the Pydantic model class we expected (e.g., it's the function itself)
1191
+ # Force fallback to JSON schema reconstruction by raising an error.
1192
+ raise ImportError(
1193
+ f"Retrieved '{class_name}' from '{module_name}' is not a Pydantic BaseModel class. "
1194
+ "Falling back to JSON schema reconstruction."
1195
+ )
1146
1196
  except Exception:
1147
1197
  # Fallback: rebuild using the JSON schema
1148
1198
  field_definitions = {}
1149
- for field, values in (
1150
- schema_info.get("schema", {}).get("properties", {}).items()
1199
+ json_schema_to_rebuild = schema_info.get("schema")
1200
+ if json_schema_to_rebuild and isinstance(
1201
+ json_schema_to_rebuild, dict
1151
1202
  ):
1152
- field_type = get_field_type(values)
1153
- if "default" in values:
1154
- field_definitions[field] = (
1155
- field_type,
1156
- Field(
1157
- description=values.get("description", ""),
1158
- default=values["default"],
1159
- ),
1160
- )
1161
- else:
1162
- field_definitions[field] = (
1163
- field_type,
1164
- Field(description=values.get("description", "")),
1165
- )
1166
- query_args_model = create_model(
1167
- schema_info.get("schema", {}).get("title", "QueryArgs"),
1168
- **field_definitions,
1169
- )
1170
- else:
1171
- query_args_model = create_model("QueryArgs")
1203
+ for field, values in json_schema_to_rebuild.get(
1204
+ "properties", {}
1205
+ ).items():
1206
+ field_type = get_field_type(values)
1207
+ field_description = values.get(
1208
+ "description"
1209
+ ) # Defaults to None
1210
+ if "default" in values:
1211
+ field_definitions[field] = (
1212
+ field_type,
1213
+ Field(
1214
+ description=field_description,
1215
+ default=values["default"],
1216
+ ),
1217
+ )
1218
+ else:
1219
+ field_definitions[field] = (
1220
+ field_type,
1221
+ Field(description=field_description),
1222
+ )
1223
+ query_args_model = create_model(
1224
+ json_schema_to_rebuild.get(
1225
+ "title", f"{tool_data['name']}_QueryArgs"
1226
+ ),
1227
+ **field_definitions,
1228
+ )
1229
+ else: # If schema part is missing or not a dict, create a default empty model
1230
+ query_args_model = create_model(
1231
+ f"{tool_data['name']}_QueryArgs"
1232
+ )
1233
+
1234
+ # If fn_schema was not in tool_data or reconstruction failed badly, default to empty pydantic model
1235
+ if query_args_model is None:
1236
+ query_args_model = create_model(f"{tool_data['name']}_QueryArgs")
1172
1237
 
1173
1238
  fn = (
1174
1239
  pickle.loads(tool_data["fn"].encode("latin-1"))
@@ -112,7 +112,7 @@ class DatabaseTools:
112
112
  List[str]: a list of Document objects from the database.
113
113
  """
114
114
  if sql_query is None:
115
- raise ValueError("A query parameter is necessary to filter the data")
115
+ raise ValueError("A query parameter is necessary to filter the data.")
116
116
 
117
117
  count_query = f"SELECT COUNT(*) FROM ({sql_query})"
118
118
  try:
@@ -123,7 +123,7 @@ class DatabaseTools:
123
123
  if num_rows > self.max_rows:
124
124
  return [
125
125
  f"The query is expected to return more than {self.max_rows} rows. "
126
- "Please refactor your query to make it return less rows. "
126
+ "Please refactor your query to make it return less rows and try again. "
127
127
  ]
128
128
  try:
129
129
  res = self._load_data(sql_query)
@@ -69,12 +69,16 @@ def get_tokenizer_for_model(
69
69
  """
70
70
  Get the tokenizer for the specified model, as determined by the role & config.
71
71
  """
72
- model_provider, model_name = _get_llm_params_for_role(role, config)
73
- if model_provider == ModelProvider.OPENAI:
74
- # This might raise an exception if the model_name is unknown to tiktoken
75
- return tiktoken.encoding_for_model(model_name).encode
76
- if model_provider == ModelProvider.ANTHROPIC:
77
- return Anthropic().tokenizer
72
+ try:
73
+ model_provider, model_name = _get_llm_params_for_role(role, config)
74
+ if model_provider == ModelProvider.OPENAI:
75
+ # This might raise an exception if the model_name is unknown to tiktoken
76
+ return tiktoken.encoding_for_model(model_name).encode
77
+ if model_provider == ModelProvider.ANTHROPIC:
78
+ return Anthropic().tokenizer
79
+ except Exception:
80
+ print(f"Error getting tokenizer for model {model_name}, ignoring")
81
+ return None
78
82
  return None
79
83
 
80
84