toolproxy 0.3.0__tar.gz → 0.4.0__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.
Files changed (55) hide show
  1. {toolproxy-0.3.0 → toolproxy-0.4.0}/PKG-INFO +5 -1
  2. toolproxy-0.4.0/examples/caching_demo.py +96 -0
  3. toolproxy-0.4.0/examples/fastapi_agent.py +139 -0
  4. toolproxy-0.4.0/examples/persistent_memory.py +91 -0
  5. {toolproxy-0.3.0 → toolproxy-0.4.0}/pyproject.toml +6 -1
  6. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/__init__.py +30 -1
  7. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/agent.py +155 -45
  8. toolproxy-0.4.0/src/toolproxy/cache.py +285 -0
  9. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/executor.py +40 -2
  10. toolproxy-0.4.0/src/toolproxy/integrations/__init__.py +1 -0
  11. toolproxy-0.4.0/src/toolproxy/integrations/fastapi.py +267 -0
  12. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/memory.py +107 -1
  13. toolproxy-0.4.0/src/toolproxy/retry.py +215 -0
  14. toolproxy-0.4.0/tests/test_agent_extensions.py +309 -0
  15. toolproxy-0.4.0/tests/test_cache.py +312 -0
  16. toolproxy-0.4.0/tests/test_fastapi.py +237 -0
  17. toolproxy-0.4.0/tests/test_file_memory.py +213 -0
  18. toolproxy-0.4.0/tests/test_retry.py +211 -0
  19. {toolproxy-0.3.0 → toolproxy-0.4.0}/.gitignore +0 -0
  20. {toolproxy-0.3.0 → toolproxy-0.4.0}/LICENSE +0 -0
  21. {toolproxy-0.3.0 → toolproxy-0.4.0}/README.md +0 -0
  22. {toolproxy-0.3.0 → toolproxy-0.4.0}/examples/basic_chat.py +0 -0
  23. {toolproxy-0.3.0 → toolproxy-0.4.0}/examples/local_ollama.py +0 -0
  24. {toolproxy-0.3.0 → toolproxy-0.4.0}/examples/openrouter_tools.py +0 -0
  25. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/adapters/__init__.py +0 -0
  26. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/adapters/anthropic.py +0 -0
  27. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/adapters/azure.py +0 -0
  28. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/adapters/cohere.py +0 -0
  29. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/adapters/gemini.py +0 -0
  30. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/adapters/groq.py +0 -0
  31. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/config.py +0 -0
  32. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/exceptions.py +0 -0
  33. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/llm_client.py +0 -0
  34. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/loop.py +0 -0
  35. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/mcp.py +0 -0
  36. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/observability/__init__.py +0 -0
  37. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/observability/base.py +0 -0
  38. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/observability/file.py +0 -0
  39. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/observability/otel.py +0 -0
  40. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/observability/print_tracer.py +0 -0
  41. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/planner.py +0 -0
  42. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/py.typed +0 -0
  43. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/schemas.py +0 -0
  44. {toolproxy-0.3.0 → toolproxy-0.4.0}/src/toolproxy/tools.py +0 -0
  45. {toolproxy-0.3.0 → toolproxy-0.4.0}/tests/conftest.py +0 -0
  46. {toolproxy-0.3.0 → toolproxy-0.4.0}/tests/test_adapters.py +0 -0
  47. {toolproxy-0.3.0 → toolproxy-0.4.0}/tests/test_agent_basic.py +0 -0
  48. {toolproxy-0.3.0 → toolproxy-0.4.0}/tests/test_async_agent.py +0 -0
  49. {toolproxy-0.3.0 → toolproxy-0.4.0}/tests/test_emulated_mode.py +0 -0
  50. {toolproxy-0.3.0 → toolproxy-0.4.0}/tests/test_error_handling.py +0 -0
  51. {toolproxy-0.3.0 → toolproxy-0.4.0}/tests/test_mcp.py +0 -0
  52. {toolproxy-0.3.0 → toolproxy-0.4.0}/tests/test_memory.py +0 -0
  53. {toolproxy-0.3.0 → toolproxy-0.4.0}/tests/test_native_mode.py +0 -0
  54. {toolproxy-0.3.0 → toolproxy-0.4.0}/tests/test_observability.py +0 -0
  55. {toolproxy-0.3.0 → toolproxy-0.4.0}/tests/test_tool_registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: toolproxy
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Universal tool-calling wrapper for non-tool-native LLMs — emulates function calling via structured JSON planning
5
5
  Project-URL: Homepage, https://github.com/yourusername/toolproxy
6
6
  Project-URL: Repository, https://github.com/yourusername/toolproxy
@@ -33,8 +33,12 @@ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
33
33
  Requires-Dist: pytest-mock>=3.0; extra == 'dev'
34
34
  Requires-Dist: pytest>=7.0; extra == 'dev'
35
35
  Requires-Dist: twine>=5.0; extra == 'dev'
36
+ Provides-Extra: fastapi
37
+ Requires-Dist: fastapi>=0.110; extra == 'fastapi'
38
+ Requires-Dist: httpx>=0.25; extra == 'fastapi'
36
39
  Provides-Extra: full
37
40
  Requires-Dist: anthropic>=0.25; extra == 'full'
41
+ Requires-Dist: fastapi>=0.110; extra == 'full'
38
42
  Requires-Dist: opentelemetry-api>=1.20; extra == 'full'
39
43
  Requires-Dist: opentelemetry-sdk>=1.20; extra == 'full'
40
44
  Provides-Extra: gemini
@@ -0,0 +1,96 @@
1
+ """
2
+ Caching Demo — toolproxy v0.4
3
+
4
+ Shows how InMemoryToolCache prevents repeated tool invocations when
5
+ the same query is made multiple times during an agent conversation.
6
+
7
+ Run with:
8
+ python examples/caching_demo.py
9
+
10
+ No API key needed (uses MockClient).
11
+ """
12
+ import time
13
+
14
+ from toolproxy import UniversalAgent, tool
15
+ from toolproxy.cache import InMemoryToolCache
16
+ from toolproxy.llm_client import MockClient, ModelResponse
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Simulated "expensive" tool
21
+ # ---------------------------------------------------------------------------
22
+
23
+ call_count = 0
24
+
25
+
26
+ @tool
27
+ def search_web(query: str) -> str:
28
+ """Search the web for information about a topic."""
29
+ global call_count
30
+ call_count += 1
31
+ # Simulate a slow API call
32
+ time.sleep(0.05)
33
+ print(f" [Tool call #{call_count}] search_web(query={query!r})")
34
+ return f"Results for '{query}': Python was created by Guido van Rossum in 1991."
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Mock responses that ask for the same tool twice
39
+ # ---------------------------------------------------------------------------
40
+
41
+ responses = [
42
+ # First run: call search_web
43
+ ModelResponse(
44
+ raw_text='{"type": "tool_call", "tool": {"tool_name": "search_web", "arguments": {"query": "Python history"}}}'
45
+ ),
46
+ ModelResponse(
47
+ raw_text='{"type": "final", "content": "Python was created in 1991."}'
48
+ ),
49
+ # Second run: ask for same query again (should hit cache)
50
+ ModelResponse(
51
+ raw_text='{"type": "tool_call", "tool": {"tool_name": "search_web", "arguments": {"query": "Python history"}}}'
52
+ ),
53
+ ModelResponse(
54
+ raw_text='{"type": "final", "content": "As I mentioned, Python was created in 1991."}'
55
+ ),
56
+ ]
57
+
58
+
59
+ def main():
60
+ cache = InMemoryToolCache(ttl=300) # 5-minute TTL
61
+
62
+ agent = UniversalAgent(
63
+ model="mock/test",
64
+ client=MockClient(responses=responses),
65
+ tools=[search_web],
66
+ cache=cache,
67
+ )
68
+
69
+ print("=" * 60)
70
+ print("toolproxy v0.4 — Tool Result Caching Demo")
71
+ print("=" * 60)
72
+ print()
73
+
74
+ print("Run 1: First query (cache miss — tool will be called)")
75
+ t0 = time.perf_counter()
76
+ result = agent.run("Tell me about Python's history")
77
+ t1 = time.perf_counter()
78
+ print(f" Answer: {result.content}")
79
+ print(f" Time: {(t1 - t0)*1000:.1f}ms")
80
+ print()
81
+
82
+ print("Run 2: Same query (cache HIT — tool skipped)")
83
+ t0 = time.perf_counter()
84
+ result = agent.run("Tell me about Python's history again")
85
+ t1 = time.perf_counter()
86
+ print(f" Answer: {result.content}")
87
+ print(f" Time: {(t1 - t0)*1000:.1f}ms")
88
+ print()
89
+
90
+ stats = cache.stats()
91
+ print(f"Cache stats: {stats}")
92
+ print(f"Total tool invocations: {call_count} (expected 1 — second was cached)")
93
+
94
+
95
+ if __name__ == "__main__":
96
+ main()
@@ -0,0 +1,139 @@
1
+ """
2
+ FastAPI Agent Server — toolproxy v0.4
3
+
4
+ Shows how to expose a UniversalAgent as an HTTP API using
5
+ the built-in FastAPI integration.
6
+
7
+ Install dependencies:
8
+ pip install toolproxy[fastapi] uvicorn
9
+
10
+ Run:
11
+ python examples/fastapi_agent.py
12
+
13
+ Then test with curl or a browser:
14
+
15
+ # Single-shot run
16
+ curl -X POST http://localhost:8000/agent/run \\
17
+ -H "Content-Type: application/json" \\
18
+ -d '{"prompt": "What is the weather in Chennai?"}'
19
+
20
+ # List tools
21
+ curl http://localhost:8000/agent/tools
22
+
23
+ # Agent info
24
+ curl http://localhost:8000/agent/info
25
+
26
+ # Stream (SSE)
27
+ curl -N -X POST http://localhost:8000/agent/stream \\
28
+ -H "Content-Type: application/json" \\
29
+ -d '{"prompt": "Tell me something interesting"}'
30
+
31
+ # Reset memory
32
+ curl -X POST http://localhost:8000/agent/reset
33
+ """
34
+ from __future__ import annotations
35
+
36
+ try:
37
+ import uvicorn
38
+ from fastapi import FastAPI
39
+ except ImportError:
40
+ print("Please install FastAPI: pip install toolproxy[fastapi] uvicorn")
41
+ raise SystemExit(1)
42
+
43
+ from toolproxy import UniversalAgent, tool
44
+ from toolproxy.cache import InMemoryToolCache
45
+ from toolproxy.integrations.fastapi import create_agent_router
46
+ from toolproxy.llm_client import MockClient, ModelResponse
47
+ from toolproxy.memory import ConversationBuffer
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Tools
52
+ # ---------------------------------------------------------------------------
53
+
54
+ @tool
55
+ def get_weather(city: str) -> str:
56
+ """Get the current weather for a city."""
57
+ weather_data = {
58
+ "chennai": "Sunny, 34°C",
59
+ "london": "Cloudy, 15°C",
60
+ "new york": "Partly cloudy, 22°C",
61
+ }
62
+ return weather_data.get(city.lower(), f"Weather data unavailable for {city}")
63
+
64
+
65
+ @tool
66
+ def calculate(expression: str) -> str:
67
+ """Evaluate a simple mathematical expression safely."""
68
+ try:
69
+ # Only allow safe mathematical operations
70
+ allowed = set("0123456789+-*/()., ")
71
+ if not all(c in allowed for c in expression):
72
+ return "Error: Only basic arithmetic is supported."
73
+ result = eval(expression, {"__builtins__": {}}) # noqa: S307
74
+ return str(result)
75
+ except Exception as exc:
76
+ return f"Calculation error: {exc}"
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Mock LLM for demo (replace with a real model string for production)
81
+ # ---------------------------------------------------------------------------
82
+
83
+ def _make_mock_client():
84
+ """Returns a MockClient with demo responses."""
85
+ return MockClient(
86
+ responses=[
87
+ ModelResponse(
88
+ raw_text='{"type": "tool_call", "tool": {"tool_name": "get_weather", "arguments": {"city": "Chennai"}}}'
89
+ ),
90
+ ModelResponse(
91
+ raw_text='{"type": "final", "content": "The weather in Chennai is Sunny, 34°C."}'
92
+ ),
93
+ ]
94
+ * 10 # repeat for multiple requests
95
+ )
96
+
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # Build the app
100
+ # ---------------------------------------------------------------------------
101
+
102
+ def create_app() -> FastAPI:
103
+ agent = UniversalAgent(
104
+ # Replace with a real model for production:
105
+ # model="openrouter/mistralai/mistral-7b-instruct",
106
+ model="mock/test",
107
+ client=_make_mock_client(),
108
+ tools=[get_weather, calculate],
109
+ memory=ConversationBuffer(),
110
+ cache=InMemoryToolCache(ttl=300),
111
+ )
112
+
113
+ app = FastAPI(
114
+ title="toolproxy Agent API",
115
+ description="Universal tool-calling agent exposed as an HTTP service.",
116
+ version="0.4.0",
117
+ )
118
+
119
+ # Mount the agent router at /agent
120
+ app.include_router(
121
+ create_agent_router(agent),
122
+ prefix="/agent",
123
+ tags=["Agent"],
124
+ )
125
+
126
+ @app.get("/", tags=["Health"])
127
+ async def health():
128
+ return {"status": "ok", "version": "0.4.0"}
129
+
130
+ return app
131
+
132
+
133
+ app = create_app()
134
+
135
+
136
+ if __name__ == "__main__":
137
+ print("Starting toolproxy FastAPI demo on http://localhost:8000")
138
+ print("API docs: http://localhost:8000/docs")
139
+ uvicorn.run(app, host="0.0.0.0", port=8000)
@@ -0,0 +1,91 @@
1
+ """
2
+ Persistent Memory Demo — toolproxy v0.4
3
+
4
+ Shows JsonFileMemory that keeps conversation history across
5
+ multiple script runs (survives process restarts).
6
+
7
+ Run this script several times to see the agent remember prior context:
8
+ python examples/persistent_memory.py
9
+
10
+ No API key needed (uses MockClient that echoes memory-aware responses).
11
+ """
12
+ import os
13
+ from pathlib import Path
14
+
15
+ from toolproxy import UniversalAgent, tool
16
+ from toolproxy.llm_client import MockClient, ModelResponse
17
+ from toolproxy.memory import JsonFileMemory
18
+
19
+
20
+ HISTORY_FILE = Path("chat_history.json")
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Tool
25
+ # ---------------------------------------------------------------------------
26
+
27
+ @tool
28
+ def get_time() -> str:
29
+ """Get the current time."""
30
+ from datetime import datetime
31
+ return datetime.now().strftime("%H:%M:%S")
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Mock responses
36
+ # ---------------------------------------------------------------------------
37
+
38
+ def _responses_for_run(run_number: int):
39
+ if run_number == 1:
40
+ return [
41
+ ModelResponse(raw_text='{"type": "final", "content": "Nice to meet you! I\'ll remember your name."}'),
42
+ ]
43
+ elif run_number == 2:
44
+ return [
45
+ ModelResponse(raw_text='{"type": "final", "content": "Hello again! I remember you from our last conversation."}'),
46
+ ]
47
+ else:
48
+ return [
49
+ ModelResponse(raw_text='{"type": "final", "content": "Welcome back! I still remember all our previous conversations."}'),
50
+ ]
51
+
52
+
53
+ def main():
54
+ memory = JsonFileMemory(HISTORY_FILE, max_messages=20)
55
+ history = memory.load()
56
+ run_number = max(1, len([m for m in history if m.role == "user"]) + 1)
57
+
58
+ print("=" * 60)
59
+ print("toolproxy v0.4 — Persistent Memory Demo")
60
+ print("=" * 60)
61
+ print(f"Run #{run_number} | History file: {HISTORY_FILE}")
62
+ print(f"Messages in memory: {len(history)}")
63
+ print()
64
+
65
+ agent = UniversalAgent(
66
+ model="mock/test",
67
+ client=MockClient(responses=_responses_for_run(run_number)),
68
+ tools=[get_time],
69
+ memory=memory,
70
+ )
71
+
72
+ prompts = {
73
+ 1: "Hi! My name is Alice.",
74
+ 2: "Do you remember who I am?",
75
+ 3: "How many times have we spoken?",
76
+ }
77
+ prompt = prompts.get(run_number, "Tell me something you remember.")
78
+
79
+ print(f"User: {prompt}")
80
+ result = agent.run(prompt)
81
+ print(f"Agent: {result.content}")
82
+ print()
83
+
84
+ updated = memory.load()
85
+ print(f"History now has {len(updated)} message(s).")
86
+ print(f"Run this script again to continue the conversation!")
87
+ print(f"(Delete '{HISTORY_FILE}' to start fresh)")
88
+
89
+
90
+ if __name__ == "__main__":
91
+ main()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "toolproxy"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "Universal tool-calling wrapper for non-tool-native LLMs — emulates function calling via structured JSON planning"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -58,12 +58,17 @@ groq = [
58
58
  cohere = [
59
59
  # No extra package needed — uses Cohere's OpenAI-compat endpoint
60
60
  ]
61
+ fastapi = [
62
+ "fastapi>=0.110",
63
+ "httpx>=0.25",
64
+ ]
61
65
  otel = [
62
66
  "opentelemetry-sdk>=1.20",
63
67
  "opentelemetry-api>=1.20",
64
68
  ]
65
69
  full = [
66
70
  "anthropic>=0.25",
71
+ "fastapi>=0.110",
67
72
  "opentelemetry-sdk>=1.20",
68
73
  "opentelemetry-api>=1.20",
69
74
  ]
@@ -16,6 +16,9 @@ Public API:
16
16
  Memory (v0.3):
17
17
  BaseMemory, ConversationBuffer, SlidingWindowMemory, TokenWindowMemory
18
18
 
19
+ Memory persistent (v0.4):
20
+ JsonFileMemory
21
+
19
22
  Observability (v0.3):
20
23
  BaseTracer, FileTracer, PrintTracer, OpenTelemetryTracer
21
24
 
@@ -25,6 +28,12 @@ Adapters (v0.3):
25
28
  MCP (v0.3):
26
29
  MCPToolset, MCPClient, MCPToolInfo
27
30
 
31
+ Caching (v0.4):
32
+ ToolCache, InMemoryToolCache, FileToolCache
33
+
34
+ Retry (v0.4):
35
+ RetryConfig, with_retry, awith_retry, HTTP_RETRY_CONFIG
36
+
28
37
  Utilities:
29
38
  probe_tool_support — (v0.2) async helper to detect native tool support
30
39
 
@@ -87,6 +96,15 @@ from .adapters import (
87
96
  # v0.3 — MCP
88
97
  from .mcp import MCPClient, MCPToolInfo, MCPToolset
89
98
 
99
+ # v0.4 — Caching
100
+ from .cache import FileToolCache, InMemoryToolCache, ToolCache
101
+
102
+ # v0.4 — Retry
103
+ from .retry import HTTP_RETRY_CONFIG, RetryConfig, awith_retry, with_retry
104
+
105
+ # v0.4 — Persistent memory (JsonFileMemory added to memory module)
106
+ from .memory import JsonFileMemory
107
+
90
108
  __all__ = [
91
109
  # Main API
92
110
  "UniversalAgent",
@@ -134,6 +152,17 @@ __all__ = [
134
152
  "MCPClient",
135
153
  "MCPToolInfo",
136
154
  "MCPToolset",
155
+ # v0.4 Caching
156
+ "ToolCache",
157
+ "InMemoryToolCache",
158
+ "FileToolCache",
159
+ # v0.4 Retry
160
+ "RetryConfig",
161
+ "with_retry",
162
+ "awith_retry",
163
+ "HTTP_RETRY_CONFIG",
164
+ # v0.4 Persistent Memory
165
+ "JsonFileMemory",
137
166
  # Exceptions
138
167
  "UniversalAgentError",
139
168
  "ToolNotFoundError",
@@ -144,4 +173,4 @@ __all__ = [
144
173
  "ExecutionPolicyError",
145
174
  ]
146
175
 
147
- __version__ = "0.3.0"
176
+ __version__ = "0.4.0"