fabiantoh98 commited on
Commit
327d382
·
1 Parent(s): 30db61e

Add Modal deployment with vLLM support

Browse files
Files changed (3) hide show
  1. modal_app.py +101 -0
  2. modal_vllm.py +105 -0
  3. tools/llama_agent.py +198 -42
modal_app.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Modal deployment for Spice Bae AI Advisor.
2
+
3
+ Deploy with: modal deploy modal_app.py
4
+ Test locally: modal serve modal_app.py
5
+ """
6
+
7
+ import modal
8
+
9
+ app = modal.App("spice-bae")
10
+
11
+ image = (
12
+ modal.Image.debian_slim(python_version="3.11")
13
+ .pip_install(
14
+ "gradio[mcp]==5.50.0",
15
+ "fastapi[standard]",
16
+ "neo4j",
17
+ "python-dotenv",
18
+ "requests",
19
+ "beautifulsoup4>=4.12.0",
20
+ )
21
+ .add_local_file("app.py", "/root/app.py")
22
+ .add_local_dir("tools", "/root/tools")
23
+ .add_local_dir("data", "/root/data")
24
+ )
25
+
26
+
27
+ @app.function(
28
+ image=image,
29
+ secrets=[modal.Secret.from_name("spice-bae-secrets")],
30
+ max_containers=1,
31
+ timeout=600,
32
+ )
33
+ @modal.web_server(port=7860, startup_timeout=120)
34
+ def serve():
35
+ """Serve the Spice Bae Gradio app."""
36
+ import sys
37
+ import os
38
+ import subprocess
39
+
40
+ # Add mounted code directory to Python path
41
+ sys.path.insert(0, "/root")
42
+ os.chdir("/root")
43
+
44
+ # Launch Gradio app directly with its built-in server
45
+ subprocess.Popen(
46
+ [
47
+ sys.executable,
48
+ "-c",
49
+ """
50
+ import sys
51
+ sys.path.insert(0, "/root")
52
+ import os
53
+ os.chdir("/root")
54
+ from app import demo
55
+ demo.launch(
56
+ server_name="0.0.0.0",
57
+ server_port=7860,
58
+ mcp_server=True,
59
+ share=False,
60
+ ssr_mode=False
61
+ )
62
+ """
63
+ ],
64
+ env={**os.environ}
65
+ )
66
+
67
+
68
+ # Instructions for deployment:
69
+ #
70
+ # 1. Install Modal CLI:
71
+ # pip install modal
72
+ # modal setup
73
+ #
74
+ # 2. Deploy vLLM first (for open-source LLM):
75
+ # modal deploy modal_vllm.py
76
+ # Note the URL: https://YOUR_USERNAME--spice-bae-llm-serve.modal.run
77
+ #
78
+ # 3. Create secrets (using vLLM endpoint):
79
+ # modal secret create spice-bae-secrets \
80
+ # NEO4J_URI="neo4j+s://xxx.databases.neo4j.io" \
81
+ # NEO4J_USERNAME="neo4j" \
82
+ # NEO4J_PASSWORD="your_password" \
83
+ # OPENAI_API_BASE="https://YOUR_USERNAME--spice-bae-llm-serve.modal.run/v1" \
84
+ # OPENAI_API_KEY="not-needed" \
85
+ # OPENAI_MODEL="Qwen/Qwen2.5-7B-Instruct"
86
+ #
87
+ # OR use Anthropic instead:
88
+ # modal secret create spice-bae-secrets \
89
+ # NEO4J_URI="neo4j+s://xxx.databases.neo4j.io" \
90
+ # NEO4J_USERNAME="neo4j" \
91
+ # NEO4J_PASSWORD="your_password" \
92
+ # ANTHROPIC_API_KEY="sk-ant-xxx"
93
+ #
94
+ # 4. Deploy:
95
+ # modal deploy modal_app.py
96
+ #
97
+ # 5. Your app will be available at:
98
+ # https://your-username--spice-bae-serve.modal.run
99
+ #
100
+ # MCP Endpoint:
101
+ # https://your-username--spice-bae-serve.modal.run/gradio_api/mcp/sse
modal_vllm.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Modal vLLM deployment for Spice Bae LLM inference.
2
+
3
+ This deploys an open-source LLM (Llama 3.1 8B) using vLLM on Modal,
4
+ providing an OpenAI-compatible API endpoint that Spice Bae can use
5
+ instead of Claude API.
6
+
7
+ Deploy with: modal deploy modal_vllm.py
8
+ Test locally: modal serve modal_vllm.py
9
+
10
+ Uses Modal's $30/month free credits instead of paid API keys.
11
+ """
12
+
13
+ import modal
14
+
15
+ MODELS_DIR = "/llm-models"
16
+ MODEL_NAME = "Qwen/Qwen2.5-7B-Instruct"
17
+
18
+ def download_model_to_image(model_dir: str, model_name: str):
19
+ """Download model during image build."""
20
+ from huggingface_hub import snapshot_download
21
+
22
+ snapshot_download(
23
+ model_name,
24
+ local_dir=model_dir,
25
+ ignore_patterns=["*.pt", "*.bin"],
26
+ )
27
+
28
+
29
+ image = (
30
+ modal.Image.debian_slim(python_version="3.11")
31
+ .pip_install(
32
+ "vllm==0.6.4.post1",
33
+ "huggingface_hub",
34
+ "hf_transfer",
35
+ )
36
+ .env({"HF_HUB_ENABLE_HF_TRANSFER": "1"})
37
+ .run_function(
38
+ download_model_to_image,
39
+ kwargs={"model_dir": MODELS_DIR, "model_name": MODEL_NAME},
40
+ secrets=[modal.Secret.from_name("huggingface-token")],
41
+ timeout=60 * 20,
42
+ )
43
+ )
44
+
45
+ app = modal.App("spice-bae-llm")
46
+
47
+ N_GPU = 1
48
+ MINUTES = 60
49
+
50
+
51
+ @app.function(
52
+ image=image,
53
+ gpu="A10G",
54
+ scaledown_window=5 * MINUTES,
55
+ timeout=20 * MINUTES,
56
+ max_containers=1,
57
+ )
58
+ @modal.web_server(port=8000, startup_timeout=300)
59
+ def serve():
60
+ """Serve vLLM OpenAI-compatible API using built-in server."""
61
+ import subprocess
62
+
63
+ cmd = [
64
+ "python", "-m", "vllm.entrypoints.openai.api_server",
65
+ "--model", MODELS_DIR,
66
+ "--served-model-name", MODEL_NAME,
67
+ "--host", "0.0.0.0",
68
+ "--port", "8000",
69
+ "--gpu-memory-utilization", "0.90",
70
+ "--max-model-len", "4096",
71
+ ]
72
+
73
+ subprocess.Popen(cmd)
74
+
75
+
76
+ # =============================================================================
77
+ # DEPLOYMENT INSTRUCTIONS
78
+ # =============================================================================
79
+ #
80
+ # 1. Install Modal CLI:
81
+ # pip install modal
82
+ # modal setup
83
+ #
84
+ # 2. Create HuggingFace token secret (for gated models like Llama):
85
+ # - Get token from https://huggingface.co/settings/tokens
86
+ # - Accept Llama license at https://huggingface.co/meta-llama/Llama-3.1-8B-Instruct
87
+ # modal secret create huggingface-token HF_TOKEN=hf_xxx
88
+ #
89
+ # 3. Deploy:
90
+ # modal deploy modal_vllm.py
91
+ #
92
+ # 4. Your API will be at:
93
+ # https://YOUR_USERNAME--spice-bae-llm-serve.modal.run
94
+ #
95
+ # 5. Test it:
96
+ # curl https://YOUR_USERNAME--spice-bae-llm-serve.modal.run/v1/chat/completions \
97
+ # -H "Content-Type: application/json" \
98
+ # -d '{"model": "meta-llama/Llama-3.1-8B-Instruct", "messages": [{"role": "user", "content": "Hello!"}]}'
99
+ #
100
+ # 6. Set environment variable for Spice Bae:
101
+ # OPENAI_API_BASE=https://YOUR_USERNAME--spice-bae-llm-serve.modal.run/v1
102
+ # OPENAI_API_KEY=not-needed
103
+ # USE_OPENAI_COMPATIBLE=true
104
+ #
105
+ # =============================================================================
tools/llama_agent.py CHANGED
@@ -1,7 +1,11 @@
1
  """LlamaIndex Agent for Spice Bae conversational interface.
2
 
3
  This module wraps the existing spice database tools as LlamaIndex FunctionTools,
4
- enabling a conversational AI interface powered by Claude.
 
 
 
 
5
 
6
  Architecture:
7
  User Question -> Guardrails -> LlamaIndex AgentWorkflow -> Tool Selection -> Neo4j Query -> Guardrails -> Response
@@ -9,10 +13,10 @@ Architecture:
9
 
10
  import asyncio
11
  import os
12
- from typing import Optional
13
- from llama_index.core.tools import FunctionTool
14
- from llama_index.core.agent.workflow import AgentWorkflow
15
- from llama_index.llms.anthropic import Anthropic
16
 
17
  from tools.neo4j_queries import SpiceDatabase
18
  from tools.guardrails import (
@@ -27,14 +31,14 @@ class SpiceAgent:
27
  """Conversational agent for medicinal spice queries.
28
 
29
  Wraps the SpiceDatabase functions as LlamaIndex tools and uses
30
- Claude for natural language understanding. Includes comprehensive
31
- guardrails for safety, cost control, and compliance.
32
 
33
  Attributes:
34
  db: SpiceDatabase instance for Neo4j queries
35
- llm: Anthropic Claude LLM for processing queries
36
- workflow: LlamaIndex AgentWorkflow for tool orchestration
37
  guardrails: GuardrailManager for safety checks
 
38
  """
39
 
40
  DEFAULT_MODEL = "claude-sonnet-4-20250514"
@@ -50,17 +54,32 @@ class SpiceAgent:
50
  """Initialize the spice agent.
51
 
52
  Args:
53
- api_key: Anthropic API key. If None, reads from ANTHROPIC_API_KEY env var.
54
- model: Model name to use. Defaults to claude-sonnet-4-20250514.
55
  enable_guardrails: Whether to enable safety guardrails.
56
  daily_cost_limit: Maximum daily spend in USD (default: $1/day).
57
  strict_topic_filter: Whether to strictly block off-topic queries.
58
  """
59
  self.db = SpiceDatabase()
60
- self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
61
- self.model = model or self.DEFAULT_MODEL
62
  self.llm = None
63
  self.workflow = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
  if enable_guardrails:
66
  self.guardrails = create_default_guardrails(
@@ -73,14 +92,20 @@ class SpiceAgent:
73
  self.guardrails = None
74
  print("[GUARDRAILS] Disabled")
75
 
76
- if self.api_key:
77
- self._initialize_agent()
 
 
 
 
 
 
 
 
78
 
79
- def _initialize_agent(self) -> None:
80
- """Initialize LLM and agent workflow with tools."""
81
  self.llm = Anthropic(
82
  model=self.model,
83
- api_key=self.api_key,
84
  )
85
 
86
  tools = self._create_tools()
@@ -88,7 +113,12 @@ class SpiceAgent:
88
  self.workflow = AgentWorkflow.from_tools_or_functions(
89
  tools_or_functions=tools,
90
  llm=self.llm,
91
- system_prompt="""You are a helpful medicinal cuisine advisor that helps users learn about spices, their nutritional content, and health benefits.
 
 
 
 
 
92
 
93
  You have access to a database of 88+ spices with:
94
  - Nutritional data from USDA FoodData Central
@@ -102,8 +132,44 @@ When answering questions:
102
  2. Provide clear, helpful responses
103
  3. Include source attribution (USDA or NCCIH)
104
  4. Mention relevant safety information when discussing health benefits
105
- """,
106
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  def _create_tools(self) -> list:
109
  """Create LlamaIndex FunctionTools from database methods.
@@ -111,6 +177,8 @@ When answering questions:
111
  Returns:
112
  List of FunctionTool objects for the agent.
113
  """
 
 
114
  tools = [
115
  FunctionTool.from_defaults(
116
  fn=self._get_spice_info,
@@ -205,8 +273,12 @@ When answering questions:
205
  Returns:
206
  Agent's response string.
207
  """
208
- if not self.workflow:
209
- return "Error: Agent not initialized. Please ensure ANTHROPIC_API_KEY is set."
 
 
 
 
210
 
211
  if self.guardrails:
212
  should_proceed, block_message, context = self.guardrails.check_input(
@@ -216,26 +288,106 @@ When answering questions:
216
  return block_message
217
 
218
  try:
219
- loop = asyncio.new_event_loop()
220
- asyncio.set_event_loop(loop)
221
- try:
222
- result = loop.run_until_complete(self._async_chat(message))
223
-
224
- if self.guardrails:
225
- result = self.guardrails.check_output(result)
226
-
227
- usage_tracker = self.guardrails.get_guardrail("usage_tracking")
228
- if usage_tracker and isinstance(usage_tracker, UsageTrackingGuardrail):
229
- input_tokens = len(message) // 4
230
- output_tokens = len(result) // 4
231
- usage_tracker.record_usage(input_tokens, output_tokens, session_id)
232
-
233
- return result
234
- finally:
235
- loop.close()
236
  except Exception as e:
237
  return f"Error processing request: {str(e)}"
238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  async def _async_chat(self, message: str) -> str:
240
  """Async chat handler for the workflow.
241
 
@@ -276,9 +428,13 @@ When answering questions:
276
  """Check if agent is ready to process queries.
277
 
278
  Returns:
279
- True if workflow is initialized, False otherwise.
280
  """
281
- return self.workflow is not None
 
 
 
 
282
 
283
 
284
  def create_agent(api_key: Optional[str] = None) -> SpiceAgent:
 
1
  """LlamaIndex Agent for Spice Bae conversational interface.
2
 
3
  This module wraps the existing spice database tools as LlamaIndex FunctionTools,
4
+ enabling a conversational AI interface powered by Claude or OpenAI-compatible endpoints.
5
+
6
+ Supports:
7
+ - Anthropic Claude (via ANTHROPIC_API_KEY) - uses function calling
8
+ - OpenAI-compatible APIs like vLLM (via OPENAI_API_BASE) - uses ReAct prompting
9
 
10
  Architecture:
11
  User Question -> Guardrails -> LlamaIndex AgentWorkflow -> Tool Selection -> Neo4j Query -> Guardrails -> Response
 
13
 
14
  import asyncio
15
  import os
16
+ import re
17
+ import json
18
+ import requests
19
+ from typing import Optional, Tuple, Any, Dict
20
 
21
  from tools.neo4j_queries import SpiceDatabase
22
  from tools.guardrails import (
 
31
  """Conversational agent for medicinal spice queries.
32
 
33
  Wraps the SpiceDatabase functions as LlamaIndex tools and uses
34
+ Claude or OpenAI-compatible LLMs for natural language understanding.
 
35
 
36
  Attributes:
37
  db: SpiceDatabase instance for Neo4j queries
38
+ llm: LLM instance for processing queries
39
+ workflow: LlamaIndex AgentWorkflow for tool orchestration (Anthropic only)
40
  guardrails: GuardrailManager for safety checks
41
+ provider: LLM provider type ('anthropic' or 'openai')
42
  """
43
 
44
  DEFAULT_MODEL = "claude-sonnet-4-20250514"
 
54
  """Initialize the spice agent.
55
 
56
  Args:
57
+ api_key: API key. If None, reads from env vars.
58
+ model: Model name to use.
59
  enable_guardrails: Whether to enable safety guardrails.
60
  daily_cost_limit: Maximum daily spend in USD (default: $1/day).
61
  strict_topic_filter: Whether to strictly block off-topic queries.
62
  """
63
  self.db = SpiceDatabase()
 
 
64
  self.llm = None
65
  self.workflow = None
66
+ self.provider = None
67
+ self.openai_base = os.getenv("OPENAI_API_BASE")
68
+ self.openai_key = os.getenv("OPENAI_API_KEY", "not-needed")
69
+ self.anthropic_key = api_key or os.getenv("ANTHROPIC_API_KEY")
70
+
71
+ # Determine provider and model
72
+ if self.openai_base:
73
+ self.provider = "openai"
74
+ self.model = model or os.getenv("OPENAI_MODEL", "Qwen/Qwen2.5-7B-Instruct")
75
+ print(f"[LLM] Using OpenAI-compatible endpoint: {self.openai_base}")
76
+ print(f"[LLM] Model: {self.model}")
77
+ elif self.anthropic_key:
78
+ self.provider = "anthropic"
79
+ self.model = model or self.DEFAULT_MODEL
80
+ print(f"[LLM] Using Anthropic Claude: {self.model}")
81
+ else:
82
+ print("[LLM] No API key configured")
83
 
84
  if enable_guardrails:
85
  self.guardrails = create_default_guardrails(
 
92
  self.guardrails = None
93
  print("[GUARDRAILS] Disabled")
94
 
95
+ if self.provider == "anthropic" and self.anthropic_key:
96
+ self._initialize_anthropic_agent()
97
+ elif self.provider == "openai" and self.openai_base:
98
+ self._tools = self._build_tool_registry()
99
+
100
+ def _initialize_anthropic_agent(self) -> None:
101
+ """Initialize Anthropic LLM and agent workflow with tools."""
102
+ from llama_index.core.tools import FunctionTool
103
+ from llama_index.core.agent.workflow import AgentWorkflow
104
+ from llama_index.llms.anthropic import Anthropic
105
 
 
 
106
  self.llm = Anthropic(
107
  model=self.model,
108
+ api_key=self.anthropic_key,
109
  )
110
 
111
  tools = self._create_tools()
 
113
  self.workflow = AgentWorkflow.from_tools_or_functions(
114
  tools_or_functions=tools,
115
  llm=self.llm,
116
+ system_prompt=self._get_system_prompt(),
117
+ )
118
+
119
+ def _get_system_prompt(self) -> str:
120
+ """Get the system prompt for the agent."""
121
+ return """You are a helpful medicinal cuisine advisor that helps users learn about spices, their nutritional content, and health benefits.
122
 
123
  You have access to a database of 88+ spices with:
124
  - Nutritional data from USDA FoodData Central
 
132
  2. Provide clear, helpful responses
133
  3. Include source attribution (USDA or NCCIH)
134
  4. Mention relevant safety information when discussing health benefits
135
+ """
136
+
137
+ def _build_tool_registry(self) -> Dict[str, callable]:
138
+ """Build a registry of available tools for ReAct agent."""
139
+ return {
140
+ "get_spice_information": self._get_spice_info,
141
+ "list_available_spices": self._list_spices,
142
+ "get_nutrient_content": self._get_nutrient,
143
+ "find_spice_substitutes": self._find_substitutes,
144
+ "get_health_benefits": self._get_health_benefits,
145
+ "find_spices_for_benefit": self._find_by_benefit,
146
+ "get_safety_information": self._get_safety_info,
147
+ "find_medicinal_substitutes": self._find_medicinal_substitutes,
148
+ }
149
+
150
+ def _get_react_system_prompt(self) -> str:
151
+ """Get ReAct-style system prompt for OpenAI-compatible endpoints."""
152
+ return """You are a helpful medicinal cuisine advisor. You help users learn about spices, their nutritional content, and health benefits.
153
+
154
+ You have access to a database of 88+ spices. To answer questions, you MUST use the available tools.
155
+
156
+ AVAILABLE TOOLS:
157
+ - get_spice_information(spice_name): Get comprehensive information about a spice
158
+ - list_available_spices(): List all spices in the database
159
+ - get_nutrient_content(spice_name, nutrient_name): Get specific nutrient content
160
+ - find_spice_substitutes(spice_name): Find substitute spices
161
+ - get_health_benefits(spice_name): Get health benefits of a spice
162
+ - find_spices_for_benefit(benefit_keyword): Find spices for a health condition
163
+ - get_safety_information(spice_name): Get safety info and cautions
164
+ - find_medicinal_substitutes(spice_name): Find substitutes with similar health benefits
165
+
166
+ TO USE A TOOL, respond with EXACTLY this format:
167
+ TOOL: tool_name
168
+ ARGS: {"param1": "value1", "param2": "value2"}
169
+
170
+ After receiving tool results, provide a helpful response to the user.
171
+
172
+ IMPORTANT: This information is for educational purposes only, not medical advice."""
173
 
174
  def _create_tools(self) -> list:
175
  """Create LlamaIndex FunctionTools from database methods.
 
177
  Returns:
178
  List of FunctionTool objects for the agent.
179
  """
180
+ from llama_index.core.tools import FunctionTool
181
+
182
  tools = [
183
  FunctionTool.from_defaults(
184
  fn=self._get_spice_info,
 
273
  Returns:
274
  Agent's response string.
275
  """
276
+ if not self.is_ready():
277
+ return (
278
+ "Error: Agent not initialized. Please set either:\n"
279
+ "- ANTHROPIC_API_KEY for Claude, or\n"
280
+ "- OPENAI_API_BASE for OpenAI-compatible endpoints"
281
+ )
282
 
283
  if self.guardrails:
284
  should_proceed, block_message, context = self.guardrails.check_input(
 
288
  return block_message
289
 
290
  try:
291
+ if self.provider == "anthropic":
292
+ result = self._chat_anthropic(message)
293
+ else:
294
+ result = self._chat_openai(message)
295
+
296
+ if self.guardrails:
297
+ result = self.guardrails.check_output(result)
298
+
299
+ usage_tracker = self.guardrails.get_guardrail("usage_tracking")
300
+ if usage_tracker and isinstance(usage_tracker, UsageTrackingGuardrail):
301
+ input_tokens = len(message) // 4
302
+ output_tokens = len(result) // 4
303
+ usage_tracker.record_usage(input_tokens, output_tokens, session_id)
304
+
305
+ return result
 
 
306
  except Exception as e:
307
  return f"Error processing request: {str(e)}"
308
 
309
+ def _chat_anthropic(self, message: str) -> str:
310
+ """Process chat using Anthropic/LlamaIndex workflow."""
311
+ loop = asyncio.new_event_loop()
312
+ asyncio.set_event_loop(loop)
313
+ try:
314
+ return loop.run_until_complete(self._async_chat(message))
315
+ finally:
316
+ loop.close()
317
+
318
+ def _chat_openai(self, message: str) -> str:
319
+ """Process chat using OpenAI-compatible endpoint with ReAct prompting."""
320
+ messages = [
321
+ {"role": "system", "content": self._get_react_system_prompt()},
322
+ {"role": "user", "content": message},
323
+ ]
324
+
325
+ max_iterations = 5
326
+ for _ in range(max_iterations):
327
+ response = self._call_openai_api(messages)
328
+
329
+ if not response:
330
+ return "Error: Failed to get response from LLM"
331
+
332
+ content = response.get("choices", [{}])[0].get("message", {}).get("content", "")
333
+
334
+ tool_match = re.search(r"TOOL:\s*(\w+)\s*\nARGS:\s*(\{.*?\})", content, re.DOTALL)
335
+
336
+ if tool_match:
337
+ tool_name = tool_match.group(1)
338
+ try:
339
+ args = json.loads(tool_match.group(2))
340
+ except json.JSONDecodeError:
341
+ args = {}
342
+
343
+ tool_result = self._execute_tool(tool_name, args)
344
+
345
+ messages.append({"role": "assistant", "content": content})
346
+ messages.append({"role": "user", "content": f"TOOL RESULT:\n{tool_result}"})
347
+ else:
348
+ return content
349
+
350
+ return content
351
+
352
+ def _call_openai_api(self, messages: list) -> Optional[dict]:
353
+ """Make a request to OpenAI-compatible API."""
354
+ url = f"{self.openai_base.rstrip('/')}/chat/completions"
355
+
356
+ headers = {
357
+ "Content-Type": "application/json",
358
+ "Authorization": f"Bearer {self.openai_key}",
359
+ }
360
+
361
+ data = {
362
+ "model": self.model,
363
+ "messages": messages,
364
+ "temperature": 0.7,
365
+ "max_tokens": 2048,
366
+ }
367
+
368
+ try:
369
+ response = requests.post(url, headers=headers, json=data, timeout=60)
370
+ response.raise_for_status()
371
+ return response.json()
372
+ except Exception as e:
373
+ print(f"[API] Error calling OpenAI API: {e}")
374
+ return None
375
+
376
+ def _execute_tool(self, tool_name: str, args: dict) -> str:
377
+ """Execute a tool by name with given arguments."""
378
+ if not hasattr(self, "_tools"):
379
+ return f"Error: Tool registry not initialized"
380
+
381
+ tool_fn = self._tools.get(tool_name)
382
+ if not tool_fn:
383
+ return f"Error: Unknown tool '{tool_name}'"
384
+
385
+ try:
386
+ print(f"[TOOL] {tool_name} called with: {args}")
387
+ return tool_fn(**args)
388
+ except Exception as e:
389
+ return f"Error executing {tool_name}: {str(e)}"
390
+
391
  async def _async_chat(self, message: str) -> str:
392
  """Async chat handler for the workflow.
393
 
 
428
  """Check if agent is ready to process queries.
429
 
430
  Returns:
431
+ True if agent is properly initialized, False otherwise.
432
  """
433
+ if self.provider == "anthropic":
434
+ return self.workflow is not None
435
+ elif self.provider == "openai":
436
+ return hasattr(self, "_tools") and self._tools is not None
437
+ return False
438
 
439
 
440
  def create_agent(api_key: Optional[str] = None) -> SpiceAgent: