vectara-agentic 0.2.0__tar.gz → 0.2.1__tar.gz

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 (30) hide show
  1. {vectara_agentic-0.2.0/vectara_agentic.egg-info → vectara_agentic-0.2.1}/PKG-INFO +31 -4
  2. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/README.md +29 -2
  3. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/requirements.txt +1 -1
  4. vectara_agentic-0.2.1/tests/endpoint.py +42 -0
  5. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/tests/test_agent.py +25 -0
  6. vectara_agentic-0.2.1/tests/test_private_llm.py +67 -0
  7. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/tests/test_tools.py +32 -5
  8. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/vectara_agentic/_prompts.py +3 -1
  9. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/vectara_agentic/_version.py +1 -1
  10. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/vectara_agentic/agent.py +93 -32
  11. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/vectara_agentic/agent_config.py +9 -0
  12. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/vectara_agentic/tools.py +10 -4
  13. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/vectara_agentic/tools_catalog.py +1 -1
  14. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/vectara_agentic/types.py +1 -0
  15. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/vectara_agentic/utils.py +4 -0
  16. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1/vectara_agentic.egg-info}/PKG-INFO +31 -4
  17. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/vectara_agentic.egg-info/SOURCES.txt +2 -0
  18. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/vectara_agentic.egg-info/requires.txt +1 -1
  19. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/LICENSE +0 -0
  20. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/MANIFEST.in +0 -0
  21. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/setup.cfg +0 -0
  22. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/setup.py +0 -0
  23. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/tests/__init__.py +0 -0
  24. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/vectara_agentic/__init__.py +0 -0
  25. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/vectara_agentic/_callback.py +0 -0
  26. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/vectara_agentic/_observability.py +0 -0
  27. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/vectara_agentic/agent_endpoint.py +0 -0
  28. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/vectara_agentic/db_tools.py +0 -0
  29. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/vectara_agentic.egg-info/dependency_links.txt +0 -0
  30. {vectara_agentic-0.2.0 → vectara_agentic-0.2.1}/vectara_agentic.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: vectara_agentic
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: A Python package for creating AI Assistants and AI Agents with Vectara
5
5
  Home-page: https://github.com/vectara/py-vectara-agentic
6
6
  Author: Ofer Mendelevitch
@@ -51,7 +51,7 @@ Requires-Dist: pydantic==2.10.3
51
51
  Requires-Dist: retrying==1.3.4
52
52
  Requires-Dist: python-dotenv==1.0.1
53
53
  Requires-Dist: tiktoken==0.8.0
54
- Requires-Dist: dill>=0.3.7
54
+ Requires-Dist: cloudpickle>=3.1.1
55
55
  Requires-Dist: httpx==0.27.2
56
56
  Dynamic: author
57
57
  Dynamic: author-email
@@ -135,7 +135,7 @@ from vectara_agentic.tools import VectaraToolFactory
135
135
  vec_factory = VectaraToolFactory(
136
136
  vectara_api_key=os.environ['VECTARA_API_KEY'],
137
137
  vectara_customer_id=os.environ['VECTARA_CUSTOMER_ID'],
138
- vectara_corpus_id=os.environ['VECTARA_CORPUS_ID']
138
+ vectara_corpus_key=os.environ['VECTARA_CORPUS_KEY']
139
139
  )
140
140
  ```
141
141
 
@@ -315,6 +315,10 @@ def mult_func(x, y):
315
315
  mult_tool = ToolsFactory().create_tool(mult_func)
316
316
  ```
317
317
 
318
+ Note: When you define your own Python functions as tools, implement them at the top module level,
319
+ and not as nested functions. Nested functions are not supported if you use serialization
320
+ (dumps/loads or from_dict/to_dict).
321
+
318
322
  ## 🛠️ Configuration
319
323
 
320
324
  ## Configuring Vectara-agentic
@@ -352,10 +356,31 @@ If any of these are not provided, `AgentConfig` first tries to read the values f
352
356
 
353
357
  When creating a `VectaraToolFactory`, you can pass in a `vectara_api_key`, `vectara_customer_id`, and `vectara_corpus_id` to the factory.
354
358
 
355
- If not passed in, it will be taken from the environment variables (`VECTARA_API_KEY`, `VECTARA_CUSTOMER_ID` and `VECTARA_CORPUS_ID`). Note that `VECTARA_CORPUS_ID` can be a single ID or a comma-separated list of IDs (if you want to query multiple corpora).
359
+ If not passed in, it will be taken from the environment variables (`VECTARA_API_KEY` and `VECTARA_CORPUS_KEY`). Note that `VECTARA_CORPUS_KEY` can be a single KEY or a comma-separated list of KEYs (if you want to query multiple corpora).
356
360
 
357
361
  These values will be used as credentials when creating Vectara tools - in `create_rag_tool()` and `create_search_tool()`.
358
362
 
363
+ ## Setting up a privately hosted LLM
364
+
365
+ If you want to setup vectara-agentic to use your own self-hosted LLM endpoint, follow the example below
366
+
367
+ ```python
368
+ config = AgentConfig(
369
+ agent_type=AgentType.REACT,
370
+ main_llm_provider=ModelProvider.PRIVATE,
371
+ main_llm_model_name="meta-llama/Meta-Llama-3.1-8B-Instruct",
372
+ private_llm_api_base="http://vllm-server.company.com/v1",
373
+ private_llm_api_key="TEST_API_KEY",
374
+ )
375
+ agent = Agent(agent_config=config, tools=tools, topic=topic,
376
+ custom_instructions=custom_instructions)
377
+ ```
378
+
379
+ In this case we specify the Main LLM provider to be privately hosted with Llama-3.1-8B as the model.
380
+ - The `ModelProvider.PRIVATE` specifies a privately hosted LLM.
381
+ - The `private_llm_api_base` specifies the api endpoint to use, and the `private_llm_api_key`
382
+ specifies the private API key requires to use this service.
383
+
359
384
  ## ℹ️ Additional Information
360
385
 
361
386
  ### About Custom Instructions for your Agent
@@ -376,6 +401,8 @@ The `Agent` class defines a few helpful methods to help you understand the inter
376
401
 
377
402
  The `Agent` class supports serialization. Use the `dumps()` to serialize and `loads()` to read back from a serialized stream.
378
403
 
404
+ Note: due to cloudpickle limitations, if a tool contains Python `weakref` objects, serialization won't work and an exception will be raised.
405
+
379
406
  ### Observability
380
407
 
381
408
  vectara-agentic supports observability via the existing integration of LlamaIndex and Arize Phoenix.
@@ -68,7 +68,7 @@ from vectara_agentic.tools import VectaraToolFactory
68
68
  vec_factory = VectaraToolFactory(
69
69
  vectara_api_key=os.environ['VECTARA_API_KEY'],
70
70
  vectara_customer_id=os.environ['VECTARA_CUSTOMER_ID'],
71
- vectara_corpus_id=os.environ['VECTARA_CORPUS_ID']
71
+ vectara_corpus_key=os.environ['VECTARA_CORPUS_KEY']
72
72
  )
73
73
  ```
74
74
 
@@ -248,6 +248,10 @@ def mult_func(x, y):
248
248
  mult_tool = ToolsFactory().create_tool(mult_func)
249
249
  ```
250
250
 
251
+ Note: When you define your own Python functions as tools, implement them at the top module level,
252
+ and not as nested functions. Nested functions are not supported if you use serialization
253
+ (dumps/loads or from_dict/to_dict).
254
+
251
255
  ## 🛠️ Configuration
252
256
 
253
257
  ## Configuring Vectara-agentic
@@ -285,10 +289,31 @@ If any of these are not provided, `AgentConfig` first tries to read the values f
285
289
 
286
290
  When creating a `VectaraToolFactory`, you can pass in a `vectara_api_key`, `vectara_customer_id`, and `vectara_corpus_id` to the factory.
287
291
 
288
- If not passed in, it will be taken from the environment variables (`VECTARA_API_KEY`, `VECTARA_CUSTOMER_ID` and `VECTARA_CORPUS_ID`). Note that `VECTARA_CORPUS_ID` can be a single ID or a comma-separated list of IDs (if you want to query multiple corpora).
292
+ If not passed in, it will be taken from the environment variables (`VECTARA_API_KEY` and `VECTARA_CORPUS_KEY`). Note that `VECTARA_CORPUS_KEY` can be a single KEY or a comma-separated list of KEYs (if you want to query multiple corpora).
289
293
 
290
294
  These values will be used as credentials when creating Vectara tools - in `create_rag_tool()` and `create_search_tool()`.
291
295
 
296
+ ## Setting up a privately hosted LLM
297
+
298
+ If you want to setup vectara-agentic to use your own self-hosted LLM endpoint, follow the example below
299
+
300
+ ```python
301
+ config = AgentConfig(
302
+ agent_type=AgentType.REACT,
303
+ main_llm_provider=ModelProvider.PRIVATE,
304
+ main_llm_model_name="meta-llama/Meta-Llama-3.1-8B-Instruct",
305
+ private_llm_api_base="http://vllm-server.company.com/v1",
306
+ private_llm_api_key="TEST_API_KEY",
307
+ )
308
+ agent = Agent(agent_config=config, tools=tools, topic=topic,
309
+ custom_instructions=custom_instructions)
310
+ ```
311
+
312
+ In this case we specify the Main LLM provider to be privately hosted with Llama-3.1-8B as the model.
313
+ - The `ModelProvider.PRIVATE` specifies a privately hosted LLM.
314
+ - The `private_llm_api_base` specifies the api endpoint to use, and the `private_llm_api_key`
315
+ specifies the private API key requires to use this service.
316
+
292
317
  ## ℹ️ Additional Information
293
318
 
294
319
  ### About Custom Instructions for your Agent
@@ -309,6 +334,8 @@ The `Agent` class defines a few helpful methods to help you understand the inter
309
334
 
310
335
  The `Agent` class supports serialization. Use the `dumps()` to serialize and `loads()` to read back from a serialized stream.
311
336
 
337
+ Note: due to cloudpickle limitations, if a tool contains Python `weakref` objects, serialization won't work and an exception will be raised.
338
+
312
339
  ### Observability
313
340
 
314
341
  vectara-agentic supports observability via the existing integration of LlamaIndex and Arize Phoenix.
@@ -33,5 +33,5 @@ pydantic==2.10.3
33
33
  retrying==1.3.4
34
34
  python-dotenv==1.0.1
35
35
  tiktoken==0.8.0
36
- dill>=0.3.7
36
+ cloudpickle>=3.1.1
37
37
  httpx==0.27.2
@@ -0,0 +1,42 @@
1
+ from openai import OpenAI
2
+ from flask import Flask, request, jsonify
3
+ from functools import wraps
4
+
5
+ app = Flask(__name__)
6
+
7
+ # Set your OpenAI API key (ensure you've set this in your environment)
8
+
9
+ EXPECTED_API_KEY = "TEST_API_KEY"
10
+
11
+ def require_api_key(f):
12
+ @wraps(f)
13
+ def decorated_function(*args, **kwargs):
14
+ api_key = request.headers.get("Authorization").split("Bearer ")[-1]
15
+ if not api_key or api_key != EXPECTED_API_KEY:
16
+ return jsonify({"error": "Unauthorized"}), 401
17
+ return f(*args, **kwargs)
18
+ return decorated_function
19
+
20
+ @app.before_request
21
+ def log_request_info():
22
+ app.logger.info("Request received: %s %s", request.method, request.path)
23
+
24
+ @app.route("/v1/chat/completions", methods=["POST"])
25
+ @require_api_key
26
+ def chat_completions():
27
+ app.logger.info("Received request on /v1/chat/completions")
28
+ data = request.get_json()
29
+ if not data:
30
+ return jsonify({"error": "Invalid JSON payload"}), 400
31
+
32
+ client = OpenAI()
33
+ try:
34
+ completion = client.chat.completions.create(**data)
35
+ return jsonify(completion.model_dump()), 200
36
+ except Exception as e:
37
+ return jsonify({"error": str(e)}), 400
38
+
39
+
40
+ if __name__ == "__main__":
41
+ # Run on port 5000 by default; adjust as needed.
42
+ app.run(debug=True, port=5000)
@@ -77,6 +77,24 @@ class TestAgentPackage(unittest.TestCase):
77
77
  "50",
78
78
  )
79
79
 
80
+ def test_multiturn(self):
81
+ def mult(x, y):
82
+ return x * y
83
+
84
+ tools = [ToolsFactory().create_tool(mult)]
85
+ topic = "AI topic"
86
+ instructions = "Always do as your father tells you, if your mother agrees!"
87
+ agent = Agent(
88
+ tools=tools,
89
+ topic=topic,
90
+ custom_instructions=instructions,
91
+ )
92
+
93
+ agent.chat("What is 5 times 10. Only give the answer, nothing else")
94
+ agent.chat("what is 3 times 7. Only give the answer, nothing else")
95
+ res = agent.chat("multiply the results of the last two questions. Output only the answer.")
96
+ self.assertEqual(res.response, "1050")
97
+
80
98
  def test_from_corpus(self):
81
99
  agent = Agent.from_corpus(
82
100
  tool_name="RAG Tool",
@@ -99,8 +117,15 @@ class TestAgentPackage(unittest.TestCase):
99
117
  )
100
118
 
101
119
  agent_reloaded = agent.loads(agent.dumps())
120
+ agent_reloaded_again = agent_reloaded.loads(agent_reloaded.dumps())
121
+
102
122
  self.assertIsInstance(agent_reloaded, Agent)
103
123
  self.assertEqual(agent, agent_reloaded)
124
+ self.assertEqual(agent.agent_type, agent_reloaded.agent_type)
125
+
126
+ self.assertIsInstance(agent_reloaded, Agent)
127
+ self.assertEqual(agent, agent_reloaded_again)
128
+ self.assertEqual(agent.agent_type, agent_reloaded_again.agent_type)
104
129
 
105
130
 
106
131
  if __name__ == "__main__":
@@ -0,0 +1,67 @@
1
+ import os
2
+ import unittest
3
+ import subprocess
4
+ import time
5
+ import requests
6
+ import signal
7
+
8
+ from vectara_agentic.agent import Agent, AgentType
9
+ from vectara_agentic.agent_config import AgentConfig
10
+ from vectara_agentic.types import ModelProvider
11
+ from vectara_agentic.tools import ToolsFactory
12
+
13
+ class TestPrivateLLM(unittest.TestCase):
14
+
15
+ @classmethod
16
+ def setUp(cls):
17
+ # Start the Flask server as a subprocess
18
+ cls.flask_process = subprocess.Popen(
19
+ ['flask', 'run', '--port=5000'],
20
+ env={**os.environ, 'FLASK_APP': 'tests.endpoint:app', 'FLASK_ENV': 'development'},
21
+ stdout=None, stderr=None,
22
+ )
23
+ # Wait for the server to start
24
+ timeout = 10
25
+ url = 'http://127.0.0.1:5000/'
26
+ for _ in range(timeout):
27
+ try:
28
+ requests.get(url)
29
+ return
30
+ except requests.ConnectionError:
31
+ time.sleep(1)
32
+ raise RuntimeError(f"Failed to start Flask server at {url}")
33
+
34
+ @classmethod
35
+ def tearDown(cls):
36
+ # Terminate the Flask server
37
+ cls.flask_process.send_signal(signal.SIGINT)
38
+ cls.flask_process.wait()
39
+
40
+ def test_endpoint(self):
41
+ def mult(x, y):
42
+ return x * y
43
+
44
+ tools = [ToolsFactory().create_tool(mult)]
45
+ topic = "calculator"
46
+ custom_instructions = "you are an agent specializing in math, assisting a user."
47
+ config = AgentConfig(
48
+ agent_type=AgentType.REACT,
49
+ main_llm_provider=ModelProvider.PRIVATE,
50
+ main_llm_model_name="gpt-4o",
51
+ private_llm_api_base="http://127.0.0.1:5000/v1",
52
+ private_llm_api_key="TEST_API_KEY",
53
+ )
54
+ agent = Agent(agent_config=config, tools=tools, topic=topic,
55
+ custom_instructions=custom_instructions)
56
+
57
+ # To run this test, you must have OPENAI_API_KEY in your environment
58
+ self.assertEqual(
59
+ agent.chat(
60
+ "What is 5 times 10. Only give the answer, nothing else"
61
+ ).response.replace("$", "\\$"),
62
+ "50",
63
+ )
64
+
65
+
66
+ if __name__ == "__main__":
67
+ unittest.main()
@@ -1,8 +1,11 @@
1
1
  import unittest
2
2
 
3
+ from pydantic import Field, BaseModel
4
+
3
5
  from vectara_agentic.tools import VectaraTool, VectaraToolFactory, ToolsFactory, ToolType
4
6
  from vectara_agentic.agent import Agent
5
- from pydantic import Field, BaseModel
7
+ from vectara_agentic.agent_config import AgentConfig
8
+
6
9
  from llama_index.core.tools import FunctionTool
7
10
 
8
11
 
@@ -60,9 +63,6 @@ class TestToolsPackage(unittest.TestCase):
60
63
  vectara_corpus_key = "vectara-docs_1"
61
64
  vectara_api_key = "zqt_UXrBcnI2UXINZkrv4g1tQPhzj02vfdtqYJIDiA"
62
65
 
63
- class QueryToolArgs(BaseModel):
64
- query: str = Field(description="The user query")
65
-
66
66
  agent = Agent.from_corpus(
67
67
  vectara_corpus_key=vectara_corpus_key,
68
68
  vectara_api_key=vectara_api_key,
@@ -72,7 +72,34 @@ class TestToolsPackage(unittest.TestCase):
72
72
  vectara_summarizer="mockingbird-1.0-2024-07-16"
73
73
  )
74
74
 
75
- self.assertIn("Vectara is an end-to-end platform", agent.chat("What is Vectara?"))
75
+ self.assertIn("Vectara is an end-to-end platform", str(agent.chat("What is Vectara?")))
76
+
77
+ def test_class_method_as_tool(self):
78
+ class TestClass:
79
+ def __init__(self):
80
+ pass
81
+
82
+ def mult(self, x, y):
83
+ return x * y
84
+
85
+ test_class = TestClass()
86
+ tools = [ToolsFactory().create_tool(test_class.mult)]
87
+ topic = "AI topic"
88
+ instructions = "Always do as your father tells you, if your mother agrees!"
89
+ config = AgentConfig()
90
+ agent = Agent(
91
+ tools=tools,
92
+ topic=topic,
93
+ custom_instructions=instructions,
94
+ agent_config=config
95
+ )
96
+
97
+ self.assertEqual(
98
+ agent.chat(
99
+ "What is 5 times 10. Only give the answer, nothing else"
100
+ ).response.replace("$", "\\$"),
101
+ "50",
102
+ )
76
103
 
77
104
 
78
105
  if __name__ == "__main__":
@@ -5,7 +5,9 @@ This file contains the prompt templates for the different types of agents.
5
5
  # General (shared) instructions
6
6
  GENERAL_INSTRUCTIONS = """
7
7
  - Use tools as your main source of information, do not respond without using a tool. Do not respond based on pre-trained knowledge.
8
- - Always call the 'get_current_date' tool to ensure you know the exact date when a user asks a question.
8
+ - Before responding to a user query that requires knowledge of the current date, call the 'get_current_date' tool to get the current date.
9
+ Never rely on previous knowledge of the current date.
10
+ Example queries that require the current date: "What is the revenue of Apple last october?" or "What was the stock price 5 days ago?".
9
11
  - When using a tool with arguments, simplify the query as much as possible if you use the tool with arguments.
10
12
  For example, if the original query is "revenue for apple in 2021", you can use the tool with a query "revenue" with arguments year=2021 and company=apple.
11
13
  - If a tool responds with "I do not have enough information", try one of the following:
@@ -1,4 +1,4 @@
1
1
  """
2
2
  Define the version of the package.
3
3
  """
4
- __version__ = "0.2.0"
4
+ __version__ = "0.2.1"
@@ -1,8 +1,9 @@
1
1
  """
2
2
  This module contains the Agent class for handling different types of agents and their interactions.
3
3
  """
4
- from typing import List, Callable, Optional, Dict, Any
4
+ from typing import List, Callable, Optional, Dict, Any, Union, Tuple
5
5
  import os
6
+ import re
6
7
  from datetime import date
7
8
  import time
8
9
  import json
@@ -10,12 +11,15 @@ import logging
10
11
  import traceback
11
12
  import asyncio
12
13
 
13
- import dill
14
+ import cloudpickle as pickle
15
+
14
16
  from dotenv import load_dotenv
15
17
 
16
18
  from retrying import retry
17
19
  from pydantic import Field, create_model
18
20
 
21
+ from llama_index.core.memory import ChatMemoryBuffer
22
+ from llama_index.core.llms import ChatMessage, MessageRole
19
23
  from llama_index.core.tools import FunctionTool
20
24
  from llama_index.core.agent import ReActAgent
21
25
  from llama_index.core.agent.react.formatter import ReActChatFormatter
@@ -24,7 +28,7 @@ from llama_index.agent.lats import LATSAgentWorker
24
28
  from llama_index.core.callbacks import CallbackManager, TokenCountingHandler
25
29
  from llama_index.core.callbacks.base_handler import BaseCallbackHandler
26
30
  from llama_index.agent.openai import OpenAIAgent
27
- from llama_index.core.memory import ChatMemoryBuffer
31
+
28
32
 
29
33
  from .types import AgentType, AgentStatusType, LLMRole, ToolType, AgentResponse, AgentStreamingResponse
30
34
  from .utils import get_llm, get_tokenizer_for_model
@@ -35,6 +39,21 @@ from .tools import VectaraToolFactory, VectaraTool, ToolsFactory
35
39
  from .tools_catalog import get_current_date
36
40
  from .agent_config import AgentConfig
37
41
 
42
+ class IgnoreUnpickleableAttributeFilter(logging.Filter):
43
+ '''
44
+ Filter to ignore log messages that contain certain strings
45
+ '''
46
+ def filter(self, record):
47
+ msgs_to_ignore = [
48
+ "Removing unpickleable private attribute _chunking_tokenizer_fn",
49
+ "Removing unpickleable private attribute _split_fns",
50
+ "Removing unpickleable private attribute _sub_sentence_split_fns",
51
+ ]
52
+ return all(msg not in record.getMessage() for msg in msgs_to_ignore)
53
+
54
+
55
+ logging.getLogger().addFilter(IgnoreUnpickleableAttributeFilter())
56
+
38
57
  logger = logging.getLogger("opentelemetry.exporter.otlp.proto.http.trace_exporter")
39
58
  logger.setLevel(logging.CRITICAL)
40
59
 
@@ -81,6 +100,34 @@ def _retry_if_exception(exception):
81
100
  return isinstance(exception, (TimeoutError))
82
101
 
83
102
 
103
+ def get_field_type(field_schema: dict) -> Any:
104
+ """
105
+ Convert a JSON schema field definition to a Python type.
106
+ Handles 'type' and 'anyOf' cases.
107
+ """
108
+ json_type_to_python = {
109
+ "string": str,
110
+ "integer": int,
111
+ "boolean": bool,
112
+ "array": list,
113
+ "object": dict,
114
+ "number": float,
115
+ }
116
+ if "anyOf" in field_schema:
117
+ types = []
118
+ for option in field_schema["anyOf"]:
119
+ # If the option has a type, convert it; otherwise, use Any.
120
+ if "type" in option:
121
+ types.append(json_type_to_python.get(option["type"], Any))
122
+ else:
123
+ types.append(Any)
124
+ # Return a Union of the types. For example, Union[str, int]
125
+ return Union[tuple(types)]
126
+ elif "type" in field_schema:
127
+ return json_type_to_python.get(field_schema["type"], Any)
128
+ else:
129
+ return Any
130
+
84
131
  class Agent:
85
132
  """
86
133
  Agent class for handling different types of agents and their interactions.
@@ -96,6 +143,7 @@ class Agent:
96
143
  agent_progress_callback: Optional[Callable[[AgentStatusType, str], None]] = None,
97
144
  query_logging_callback: Optional[Callable[[str, str], None]] = None,
98
145
  agent_config: Optional[AgentConfig] = None,
146
+ chat_history: Optional[list[Tuple[str, str]]] = None,
99
147
  ) -> None:
100
148
  """
101
149
  Initialize the agent with the specified type, tools, topic, and system message.
@@ -111,10 +159,13 @@ class Agent:
111
159
  query_logging_callback (Callable): A callback function the code calls upon completion of a query
112
160
  agent_config (AgentConfig, optional): The configuration of the agent.
113
161
  Defaults to AgentConfig(), which reads from environment variables.
162
+ chat_history (Tuple[str, str], optional): A list of user/agent chat pairs to initialize the agent memory.
114
163
  """
115
164
  self.agent_config = agent_config or AgentConfig()
116
165
  self.agent_type = self.agent_config.agent_type
117
- self.tools = tools + [ToolsFactory().create_tool(get_current_date)]
166
+ self.tools = tools
167
+ if not any(tool.metadata.name == 'get_current_date' for tool in self.tools):
168
+ self.tools += [ToolsFactory().create_tool(get_current_date)]
118
169
  self.llm = get_llm(LLMRole.MAIN, config=self.agent_config)
119
170
  self._custom_instructions = custom_instructions
120
171
  self._topic = topic
@@ -135,7 +186,14 @@ class Agent:
135
186
  self.llm.callback_manager = callback_manager
136
187
  self.verbose = verbose
137
188
 
138
- self.memory = ChatMemoryBuffer.from_defaults(token_limit=128000)
189
+ if chat_history:
190
+ msg_history = []
191
+ for inx, text in enumerate(chat_history):
192
+ role = MessageRole.USER if inx % 2 == 0 else MessageRole.ASSISTANT
193
+ msg_history.append(ChatMessage.from_str(content=text, role=role))
194
+ self.memory = ChatMemoryBuffer.from_defaults(token_limit=128000, chat_history=msg_history)
195
+ else:
196
+ self.memory = ChatMemoryBuffer.from_defaults(token_limit=128000)
139
197
  if self.agent_type == AgentType.REACT:
140
198
  prompt = _get_prompt(REACT_PROMPT_TEMPLATE, topic, custom_instructions)
141
199
  self.agent = ReActAgent.from_tools(
@@ -219,7 +277,10 @@ class Agent:
219
277
 
220
278
  # Compare tools
221
279
  if self.tools != other.tools:
222
- print(f"Comparison failed: tools differ. (self.tools: {self.tools}, other.tools: {other.tools})")
280
+ print(
281
+ "Comparison failed: tools differ."
282
+ f"(self.tools: {[t.metadata.name for t in self.tools]}, "
283
+ f"other.tools: {[t.metadata.name for t in other.tools]})")
223
284
  return False
224
285
 
225
286
  # Compare topic
@@ -263,6 +324,7 @@ class Agent:
263
324
  agent_progress_callback: Optional[Callable[[AgentStatusType, str], None]] = None,
264
325
  query_logging_callback: Optional[Callable[[str, str], None]] = None,
265
326
  agent_config: AgentConfig = AgentConfig(),
327
+ chat_history: Optional[list[Tuple[str, str]]] = None,
266
328
  ) -> "Agent":
267
329
  """
268
330
  Create an agent from tools, agent type, and language model.
@@ -277,6 +339,7 @@ class Agent:
277
339
  update_func (Callable): old name for agent_progress_callback. Will be deprecated in future.
278
340
  query_logging_callback (Callable): A callback function the code calls upon completion of a query
279
341
  agent_config (AgentConfig, optional): The configuration of the agent.
342
+ chat_history (Tuple[str, str], optional): A list of user/agent chat pairs to initialize the agent memory.
280
343
 
281
344
  Returns:
282
345
  Agent: An instance of the Agent class.
@@ -285,7 +348,8 @@ class Agent:
285
348
  tools=tools, topic=topic, custom_instructions=custom_instructions,
286
349
  verbose=verbose, agent_progress_callback=agent_progress_callback,
287
350
  query_logging_callback=query_logging_callback,
288
- update_func=update_func, agent_config=agent_config
351
+ update_func=update_func, agent_config=agent_config,
352
+ chat_history=chat_history,
289
353
  )
290
354
 
291
355
  @classmethod
@@ -322,7 +386,7 @@ class Agent:
322
386
  vectara_temperature: Optional[float] = None,
323
387
  vectara_frequency_penalty: Optional[float] = None,
324
388
  vectara_presence_penalty: Optional[float] = None,
325
- vectara_save_history: bool = False,
389
+ vectara_save_history: bool = True,
326
390
  ) -> "Agent":
327
391
  """
328
392
  Create an agent from a single Vectara corpus
@@ -383,6 +447,10 @@ class Agent:
383
447
  ) # type: ignore
384
448
  query_args = create_model("QueryArgs", **field_definitions) # type: ignore
385
449
 
450
+ # tool name must be valid Python function name
451
+ if tool_name:
452
+ tool_name = re.sub(r"[^A-Za-z0-9_]", "_", tool_name)
453
+
386
454
  vectara_tool = vec_factory.create_rag_tool(
387
455
  tool_name=tool_name or f"vectara_{vectara_corpus_key}",
388
456
  tool_description=f"""
@@ -414,6 +482,7 @@ class Agent:
414
482
  presence_penalty=vectara_presence_penalty,
415
483
  save_history=vectara_save_history,
416
484
  include_citations=True,
485
+ verbose=verbose,
417
486
  )
418
487
 
419
488
  assistant_instructions = f"""
@@ -583,12 +652,13 @@ class Agent:
583
652
 
584
653
  for tool in self.tools:
585
654
  # Serialize each tool's metadata, function, and dynamic model schema (QueryArgs)
655
+ # TODO: deal with tools that have weakref (e.g. db_tools); for now those cannot be serialized.
586
656
  tool_dict = {
587
657
  "tool_type": tool.metadata.tool_type.value,
588
658
  "name": tool.metadata.name,
589
659
  "description": tool.metadata.description,
590
- "fn": dill.dumps(tool.fn).decode("latin-1") if tool.fn else None, # Serialize fn
591
- "async_fn": dill.dumps(tool.async_fn).decode("latin-1")
660
+ "fn": pickle.dumps(tool.fn).decode("latin-1") if tool.fn else None, # Serialize fn
661
+ "async_fn": pickle.dumps(tool.async_fn).decode("latin-1")
592
662
  if tool.async_fn
593
663
  else None, # Serialize async_fn
594
664
  "fn_schema": tool.metadata.fn_schema.model_json_schema()
@@ -599,7 +669,7 @@ class Agent:
599
669
 
600
670
  return {
601
671
  "agent_type": self.agent_type.value,
602
- "memory": dill.dumps(self.agent.memory).decode("latin-1"),
672
+ "memory": pickle.dumps(self.agent.memory).decode("latin-1"),
603
673
  "tools": tool_info,
604
674
  "topic": self._topic,
605
675
  "custom_instructions": self._custom_instructions,
@@ -613,39 +683,30 @@ class Agent:
613
683
  agent_config = AgentConfig.from_dict(data["agent_config"])
614
684
  tools = []
615
685
 
616
- json_type_to_python = {
617
- "string": str,
618
- "integer": int,
619
- "boolean": bool,
620
- "array": list,
621
- "object": dict,
622
- "number": float,
623
- }
624
-
625
686
  for tool_data in data["tools"]:
626
687
  # Recreate the dynamic model using the schema info
627
688
  if tool_data.get("fn_schema"):
628
689
  field_definitions = {}
629
690
  for field, values in tool_data["fn_schema"]["properties"].items():
691
+ # Instead of checking for 'type', use the helper:
692
+ field_type = get_field_type(values)
693
+ # If there's a default value, include it.
630
694
  if "default" in values:
631
695
  field_definitions[field] = (
632
- json_type_to_python.get(values["type"], values["type"]),
633
- Field(
634
- description=values["description"],
635
- default=values["default"],
636
- ),
637
- ) # type: ignore
696
+ field_type,
697
+ Field(description=values.get("description", ""), default=values["default"]),
698
+ )
638
699
  else:
639
700
  field_definitions[field] = (
640
- json_type_to_python.get(values["type"], values["type"]),
641
- Field(description=values["description"]),
642
- ) # type: ignore
701
+ field_type,
702
+ Field(description=values.get("description", "")),
703
+ )
643
704
  query_args_model = create_model("QueryArgs", **field_definitions) # type: ignore
644
705
  else:
645
706
  query_args_model = create_model("QueryArgs")
646
707
 
647
- fn = dill.loads(tool_data["fn"].encode("latin-1")) if tool_data["fn"] else None
648
- async_fn = dill.loads(tool_data["async_fn"].encode("latin-1")) if tool_data["async_fn"] else None
708
+ fn = pickle.loads(tool_data["fn"].encode("latin-1")) if tool_data["fn"] else None
709
+ async_fn = pickle.loads(tool_data["async_fn"].encode("latin-1")) if tool_data["async_fn"] else None
649
710
 
650
711
  tool = VectaraTool.from_defaults(
651
712
  name=tool_data["name"],
@@ -664,7 +725,7 @@ class Agent:
664
725
  custom_instructions=data["custom_instructions"],
665
726
  verbose=data["verbose"],
666
727
  )
667
- memory = dill.loads(data["memory"].encode("latin-1")) if data.get("memory") else None
728
+ memory = pickle.loads(data["memory"].encode("latin-1")) if data.get("memory") else None
668
729
  if memory:
669
730
  agent.agent.memory = memory
670
731
  return agent
@@ -44,6 +44,15 @@ class AgentConfig:
44
44
  default_factory=lambda: os.getenv("VECTARA_AGENTIC_TOOL_MODEL_NAME", "")
45
45
  )
46
46
 
47
+ # Params for Private LLM endpoint if used
48
+ private_llm_api_base: str = field(
49
+ default_factory=lambda: os.getenv("VECTARA_AGENTIC_PRIVATE_LLM_API_BASE",
50
+ "http://private-endpoint.company.com:5000/v1")
51
+ )
52
+ private_llm_api_key: str = field(
53
+ default_factory=lambda: os.getenv("VECTARA_AGENTIC_PRIVATE_LLM_API_KEY", "<private-api-key>")
54
+ )
55
+
47
56
  # Observer
48
57
  observer: ObserverType = field(
49
58
  default_factory=lambda: ObserverType(
@@ -110,6 +110,9 @@ class VectaraTool(FunctionTool):
110
110
  if self.metadata.tool_type != other.metadata.tool_type:
111
111
  return False
112
112
 
113
+ if self.metadata.name != other.metadata.name or self.metadata.description != other.metadata.description:
114
+ return False
115
+
113
116
  # Check if fn_schema is an instance of a BaseModel or a class itself (metaclass)
114
117
  self_schema_dict = self.metadata.fn_schema.model_fields
115
118
  other_schema_dict = other.metadata.fn_schema.model_fields
@@ -252,7 +255,10 @@ def _build_filter_string(kwargs: Dict[str, Any], tool_args_type: Dict[str, dict]
252
255
  filter_parts.append(f"{prefix}.{key}='{val_str}'")
253
256
 
254
257
  filter_str = " AND ".join(filter_parts)
255
- return f"({fixed_filter}) AND ({filter_str})" if fixed_filter else filter_str
258
+ if fixed_filter and filter_str:
259
+ return f"({fixed_filter}) AND ({filter_str})"
260
+ else:
261
+ return fixed_filter or filter_str
256
262
 
257
263
  class VectaraToolFactory:
258
264
  """
@@ -294,7 +300,7 @@ class VectaraToolFactory:
294
300
  mmr_diversity_bias: float = 0.2,
295
301
  udf_expression: str = None,
296
302
  rerank_chain: List[Dict] = None,
297
- save_history: bool = False,
303
+ save_history: bool = True,
298
304
  verbose: bool = False,
299
305
  ) -> VectaraTool:
300
306
  """
@@ -426,7 +432,7 @@ class VectaraToolFactory:
426
432
 
427
433
  # Create the tool function signature string
428
434
  fields = []
429
- for name, field in tool_args_schema.__fields__.items():
435
+ for name, field in tool_args_schema.model_fields.items():
430
436
  annotation = field.annotation
431
437
  type_name = annotation.__name__ if hasattr(annotation, '__name__') else str(annotation)
432
438
  fields.append(f"{name}: {type_name}")
@@ -677,7 +683,7 @@ class VectaraToolFactory:
677
683
 
678
684
  # Create the tool function signature string
679
685
  fields = []
680
- for name, field in tool_args_schema.__fields__.items():
686
+ for name, field in tool_args_schema.model_fields.items():
681
687
  annotation = field.annotation
682
688
  type_name = annotation.__name__ if hasattr(annotation, '__name__') else str(annotation)
683
689
  fields.append(f"{name}: {type_name}")
@@ -25,7 +25,7 @@ get_headers = {
25
25
 
26
26
  def get_current_date() -> str:
27
27
  """
28
- Returns: the current date.
28
+ Returns: the current date (when called) as a string.
29
29
  """
30
30
  return date.today().strftime("%A, %B %d, %Y")
31
31
 
@@ -33,6 +33,7 @@ class ModelProvider(Enum):
33
33
  COHERE = "COHERE"
34
34
  GEMINI = "GEMINI"
35
35
  BEDROCK = "BEDROCK"
36
+ PRIVATE = "PRIVATE"
36
37
 
37
38
 
38
39
  class AgentStatusType(Enum):
@@ -112,6 +112,10 @@ def get_llm(
112
112
  elif model_provider == ModelProvider.COHERE:
113
113
  from llama_index.llms.cohere import Cohere
114
114
  llm = Cohere(model=model_name, temperature=0)
115
+ elif model_provider == ModelProvider.PRIVATE:
116
+ from llama_index.llms.openai_like import OpenAILike
117
+ llm = OpenAILike(model=model_name, temperature=0, is_function_calling_model=True,is_chat_model=True,
118
+ api_base=config.private_llm_api_base, api_key=config.private_llm_api_key)
115
119
  else:
116
120
  raise ValueError(f"Unknown LLM provider: {model_provider}")
117
121
  return llm
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: vectara_agentic
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: A Python package for creating AI Assistants and AI Agents with Vectara
5
5
  Home-page: https://github.com/vectara/py-vectara-agentic
6
6
  Author: Ofer Mendelevitch
@@ -51,7 +51,7 @@ Requires-Dist: pydantic==2.10.3
51
51
  Requires-Dist: retrying==1.3.4
52
52
  Requires-Dist: python-dotenv==1.0.1
53
53
  Requires-Dist: tiktoken==0.8.0
54
- Requires-Dist: dill>=0.3.7
54
+ Requires-Dist: cloudpickle>=3.1.1
55
55
  Requires-Dist: httpx==0.27.2
56
56
  Dynamic: author
57
57
  Dynamic: author-email
@@ -135,7 +135,7 @@ from vectara_agentic.tools import VectaraToolFactory
135
135
  vec_factory = VectaraToolFactory(
136
136
  vectara_api_key=os.environ['VECTARA_API_KEY'],
137
137
  vectara_customer_id=os.environ['VECTARA_CUSTOMER_ID'],
138
- vectara_corpus_id=os.environ['VECTARA_CORPUS_ID']
138
+ vectara_corpus_key=os.environ['VECTARA_CORPUS_KEY']
139
139
  )
140
140
  ```
141
141
 
@@ -315,6 +315,10 @@ def mult_func(x, y):
315
315
  mult_tool = ToolsFactory().create_tool(mult_func)
316
316
  ```
317
317
 
318
+ Note: When you define your own Python functions as tools, implement them at the top module level,
319
+ and not as nested functions. Nested functions are not supported if you use serialization
320
+ (dumps/loads or from_dict/to_dict).
321
+
318
322
  ## 🛠️ Configuration
319
323
 
320
324
  ## Configuring Vectara-agentic
@@ -352,10 +356,31 @@ If any of these are not provided, `AgentConfig` first tries to read the values f
352
356
 
353
357
  When creating a `VectaraToolFactory`, you can pass in a `vectara_api_key`, `vectara_customer_id`, and `vectara_corpus_id` to the factory.
354
358
 
355
- If not passed in, it will be taken from the environment variables (`VECTARA_API_KEY`, `VECTARA_CUSTOMER_ID` and `VECTARA_CORPUS_ID`). Note that `VECTARA_CORPUS_ID` can be a single ID or a comma-separated list of IDs (if you want to query multiple corpora).
359
+ If not passed in, it will be taken from the environment variables (`VECTARA_API_KEY` and `VECTARA_CORPUS_KEY`). Note that `VECTARA_CORPUS_KEY` can be a single KEY or a comma-separated list of KEYs (if you want to query multiple corpora).
356
360
 
357
361
  These values will be used as credentials when creating Vectara tools - in `create_rag_tool()` and `create_search_tool()`.
358
362
 
363
+ ## Setting up a privately hosted LLM
364
+
365
+ If you want to setup vectara-agentic to use your own self-hosted LLM endpoint, follow the example below
366
+
367
+ ```python
368
+ config = AgentConfig(
369
+ agent_type=AgentType.REACT,
370
+ main_llm_provider=ModelProvider.PRIVATE,
371
+ main_llm_model_name="meta-llama/Meta-Llama-3.1-8B-Instruct",
372
+ private_llm_api_base="http://vllm-server.company.com/v1",
373
+ private_llm_api_key="TEST_API_KEY",
374
+ )
375
+ agent = Agent(agent_config=config, tools=tools, topic=topic,
376
+ custom_instructions=custom_instructions)
377
+ ```
378
+
379
+ In this case we specify the Main LLM provider to be privately hosted with Llama-3.1-8B as the model.
380
+ - The `ModelProvider.PRIVATE` specifies a privately hosted LLM.
381
+ - The `private_llm_api_base` specifies the api endpoint to use, and the `private_llm_api_key`
382
+ specifies the private API key requires to use this service.
383
+
359
384
  ## ℹ️ Additional Information
360
385
 
361
386
  ### About Custom Instructions for your Agent
@@ -376,6 +401,8 @@ The `Agent` class defines a few helpful methods to help you understand the inter
376
401
 
377
402
  The `Agent` class supports serialization. Use the `dumps()` to serialize and `loads()` to read back from a serialized stream.
378
403
 
404
+ Note: due to cloudpickle limitations, if a tool contains Python `weakref` objects, serialization won't work and an exception will be raised.
405
+
379
406
  ### Observability
380
407
 
381
408
  vectara-agentic supports observability via the existing integration of LlamaIndex and Arize Phoenix.
@@ -4,7 +4,9 @@ README.md
4
4
  requirements.txt
5
5
  setup.py
6
6
  tests/__init__.py
7
+ tests/endpoint.py
7
8
  tests/test_agent.py
9
+ tests/test_private_llm.py
8
10
  tests/test_tools.py
9
11
  vectara_agentic/__init__.py
10
12
  vectara_agentic/_callback.py
@@ -33,5 +33,5 @@ pydantic==2.10.3
33
33
  retrying==1.3.4
34
34
  python-dotenv==1.0.1
35
35
  tiktoken==0.8.0
36
- dill>=0.3.7
36
+ cloudpickle>=3.1.1
37
37
  httpx==0.27.2
File without changes