Skip to content

Commit 4db1f38

Browse files
committed
fixed config usage and logo
1 parent 4f83d28 commit 4db1f38

5 files changed

Lines changed: 184 additions & 90 deletions

File tree

config.yaml

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,42 @@
11
# AI Agent Model Configuration
22

3-
# Default config
4-
# agent_model:
5-
# name: "gpt-5.1" # "gpt-4o" # Model name
6-
# base_url: null # null for default OpenAI endpoint
7-
# api_key_env: "OPENAI_API_KEY" # Environment variable containing API key
8-
9-
# Using EPFL's inference server
3+
# Default/fallback model (used for CLI and initial startup)
104
agent_model:
11-
name: "openai/gpt-oss-120b"
12-
base_url: "https://inference.rcp.epfl.ch/v1"
13-
api_key_env: "EPFL_API_KEY" # Set EPFL_API_KEY in .env
5+
name: "gpt-5.1"
6+
base_url: null # null for default OpenAI endpoint
7+
api_key_env: "OPENAI_API_KEY" # Environment variable containing API key
8+
9+
# Default model for UI dropdown (display_name from available_models)
10+
default_ui_model: "gpt-5.1"
11+
12+
# Available models for UI dropdown
13+
available_models:
14+
- display_name: "gpt-4o-mini"
15+
name: "gpt-4o-mini"
16+
base_url: null
17+
provider: "OpenAI"
18+
api_key_env: "OPENAI_API_KEY"
19+
20+
- display_name: "gpt-4o"
21+
name: "gpt-4o"
22+
base_url: null
23+
provider: "OpenAI"
24+
api_key_env: "OPENAI_API_KEY"
25+
26+
- display_name: "gpt-5-mini"
27+
name: "gpt-5-mini"
28+
base_url: null
29+
provider: "OpenAI"
30+
api_key_env: "OPENAI_API_KEY"
31+
32+
- display_name: "gpt-5.1"
33+
name: "gpt-5.1"
34+
base_url: null
35+
provider: "OpenAI"
36+
api_key_env: "OPENAI_API_KEY"
37+
38+
- display_name: "GPT-OSS-120B [EPFL]"
39+
name: "openai/gpt-oss-120b"
40+
base_url: "https://inference-rcp.epfl.ch/v1"
41+
provider: "EPFL"
42+
api_key_env: "EPFL_API_KEY"

src/ai_agent/agent/agent.py

Lines changed: 86 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66

77
from pydantic_ai import Agent, RunContext
88
from pydantic_ai.usage import UsageLimits
9-
from pydantic_ai.models.openai import OpenAIResponsesModel
9+
from pydantic_ai.models.openai import OpenAIResponsesModel, OpenAIChatModel
1010
from pydantic_ai.providers.openai import OpenAIProvider
1111
from pydantic_ai.messages import BinaryContent
1212

1313
from ai_agent.generator.prompts import get_agent_system_prompt
14-
from ai_agent.generator.schema import ToolSelection
14+
from ai_agent.generator.schema import ToolSelection, Conversation, ConversationStatus
1515
from ai_agent.utils.config import get_config
1616
from .models import AgentToolSelection, ToolRunLog
1717
from .tools.repo_info_tool import tool_repo_summary, RepoSummaryInput
@@ -44,6 +44,10 @@
4444
base_url=agent_model_config.base_url,
4545
api_key=api_key,
4646
)
47+
openai_model = OpenAIChatModel(
48+
model_name=agent_model_config.name,
49+
provider=provider,
50+
)
4751
else:
4852
provider = OpenAIProvider(api_key=api_key)
4953

@@ -160,38 +164,51 @@ async def search_alternative(
160164

161165
@agent.tool(retries=2, prepare=cap_prepare)
162166
@limit_tool_calls("repo_info", cap=12)
163-
async def repo_info(ctx: RunContext[AgentState], url: str) -> dict:
167+
async def repo_info(ctx: RunContext[AgentState], url: str, tool_name: str = None) -> dict:
164168
"""
165169
Fetch a short summary of a GitHub repository.
166170
167171
Non-GitHub URLs are ignored; the tool returns a small dict noting
168-
that it was skipped.
172+
that it was skipped. If a tool_name is provided and the URL is not
173+
a GitHub URL, the tool will attempt to look up the GitHub URL from
174+
the catalog.
175+
176+
Args:
177+
url: Repository URL or GitHub owner/repo format
178+
tool_name: Optional tool name to look up in catalog if URL is not GitHub
169179
"""
170180
norm_url = coerce_github_url_or_none(url)
171-
if not norm_url:
181+
182+
# If URL is not a GitHub URL and tool_name is provided, try catalog lookup
183+
if not norm_url and tool_name:
184+
log.info(f"Non-GitHub URL provided, tool_name={tool_name}, attempting catalog lookup")
185+
# The tool_repo_summary will handle the catalog lookup
186+
norm_url = url # Pass through, tool_repo_summary will handle it
187+
elif not norm_url:
172188
payload = {
173189
"tool": "repo_info",
174190
"url": url,
175191
"skipped": True,
176192
"reason": "NON_GITHUB_URL",
177-
"hint": "Pass a GitHub repo URL or 'owner/repo' to repo_info(url).",
193+
"hint": "Pass a GitHub repo URL or 'owner/repo' to repo_info(url). Optionally provide tool_name for catalog lookup.",
178194
"timestamp": datetime.now().isoformat()
179195
}
180196
ctx.deps.tool_calls.append(payload)
181197
return {k: v for k, v in payload.items() if k != "tool"}
182198

183199
try:
184-
out = await tool_repo_summary(RepoSummaryInput(url=norm_url))
200+
out = await tool_repo_summary(RepoSummaryInput(url=norm_url, tool_name=tool_name))
185201
except Exception as e:
186202
ctx.deps.tool_calls.append(
187-
{"tool": "repo_info", "url": norm_url, "error": str(e), "timestamp": datetime.now().isoformat()}
203+
{"tool": "repo_info", "url": norm_url, "tool_name": tool_name, "error": str(e), "timestamp": datetime.now().isoformat()}
188204
)
189205
raise
190206

191207
ctx.deps.tool_calls.append(
192208
{
193209
"tool": "repo_info",
194210
"url": norm_url,
211+
"tool_name": tool_name,
195212
"truncated": getattr(out, "truncated", False),
196213
"timestamp": datetime.now().isoformat()
197214
}
@@ -244,6 +261,7 @@ def run_agent(
244261
image_bytes: bytes | None = None,
245262
model: str | None = None,
246263
base_url: str | None = None,
264+
api_key_env: str | None = None,
247265
top_k: int | None = None,
248266
num_choices: int | None = None,
249267
image_metadata: str | None = None,
@@ -315,30 +333,19 @@ def run_agent(
315333

316334
# When model is provided from UI, base_url comes with it (can be None for OpenAI)
317335
if model:
318-
if base_url and "inference.rcp.epfl.ch" in base_url:
319-
runtime_api_key = os.getenv("EPFL_API_KEY")
320-
if not runtime_api_key:
321-
raise ValueError("EPFL_API_KEY not found. Cannot use EPFL models without VPN and API key.")
322-
effective_base_url = base_url
323-
log.info("✓ Using EPFL_API_KEY for EPFL inference server")
324-
else:
325-
runtime_api_key = os.getenv("OPENAI_API_KEY")
326-
if not runtime_api_key:
327-
raise ValueError("OPENAI_API_KEY not found. Cannot use OpenAI models.")
328-
effective_base_url = base_url # None for OpenAI
329-
log.info("✓ Using OPENAI_API_KEY for OpenAI endpoint")
336+
# Use api_key_env from config if provided, otherwise default to OPENAI_API_KEY
337+
key_env_name = api_key_env if api_key_env else "OPENAI_API_KEY"
338+
runtime_api_key = os.getenv(key_env_name)
339+
if not runtime_api_key:
340+
raise ValueError(f"{key_env_name} not found in environment. Cannot use this model.")
341+
effective_base_url = base_url # Can be None for OpenAI
342+
log.info(f"✓ Using {key_env_name} for model {effective_model}")
343+
log.debug(f"{key_env_name} starts with: {runtime_api_key[:10] if runtime_api_key else 'NONE'}... (len={len(runtime_api_key) if runtime_api_key else 0})")
330344
else:
345+
# No model override - use config defaults
331346
effective_base_url = agent_model_config.base_url
332-
if effective_base_url and "inference.rcp.epfl.ch" in effective_base_url:
333-
runtime_api_key = os.getenv("EPFL_API_KEY")
334-
if not runtime_api_key:
335-
raise ValueError("EPFL_API_KEY not found")
336-
log.info("✓ Using EPFL_API_KEY from config")
337-
else:
338-
runtime_api_key = os.getenv("OPENAI_API_KEY")
339-
if not runtime_api_key:
340-
raise ValueError("OPENAI_API_KEY not found")
341-
log.info("✓ Using OPENAI_API_KEY from config")
347+
runtime_api_key = api_key # Already loaded from config at startup
348+
log.info(f"✓ Using API key from config for model {effective_model}")
342349

343350
# Log runtime configuration
344351
endpoint_display = effective_base_url if effective_base_url else "api.openai.com"
@@ -362,7 +369,13 @@ def run_agent(
362369
base_url=effective_base_url,
363370
api_key=runtime_api_key,
364371
)
365-
runtime_model = OpenAIResponsesModel(model_name=effective_model, provider=runtime_provider)
372+
373+
# Use OpenAIModel (chat/completions) for custom endpoints, OpenAIResponsesModel for default OpenAI
374+
if effective_base_url:
375+
log.info("Using OpenAIChatModel (chat/completions API) for custom endpoint")
376+
runtime_model = OpenAIChatModel(model_name=effective_model, provider=runtime_provider)
377+
else:
378+
runtime_model = OpenAIResponsesModel(model_name=effective_model, provider=runtime_provider)
366379

367380
agent_instance = Agent(
368381
model=runtime_model,
@@ -416,27 +429,51 @@ def run_agent(
416429
user_prompt = prompt
417430

418431
# ---- 6) Run the agent --------------------------------------------------
419-
run_result = agent_instance.run_sync(
420-
user_prompt,
421-
deps=deps,
422-
output_type=ToolSelection,
423-
usage_limits=UsageLimits(tool_calls_limit=20),
424-
)
425-
result = run_result.output
432+
try:
433+
run_result = agent_instance.run_sync(
434+
user_prompt,
435+
deps=deps,
436+
output_type=ToolSelection,
437+
usage_limits=UsageLimits(tool_calls_limit=20),
438+
)
439+
result = run_result.output
426440

427-
log.info(f"✅ Agent execution complete - choices returned: {len(result.choices)}")
441+
log.info(f"✅ Agent execution complete - choices returned: {len(result.choices)}")
428442

429-
# Log usage (helpful, but may not explicitly expose image-specific counters)
430-
if run_result.usage:
431-
usage = run_result.usage()
432-
log.info(
433-
f"📊 Usage: total_tokens={usage.total_tokens}, "
434-
f"request_tokens={usage.request_tokens}, response_tokens={usage.response_tokens}"
435-
)
443+
# Log usage (helpful, but may not explicitly expose image-specific counters)
444+
if run_result.usage:
445+
usage = run_result.usage()
446+
log.info(
447+
f"📊 Usage: total_tokens={usage.total_tokens}, "
448+
f"input_tokens={usage.input_tokens}, output_tokens={usage.output_tokens}"
449+
)
450+
451+
# Warn if using non-OpenAI endpoint with images
452+
if image_bytes and effective_base_url:
453+
log.warning("⚠️ Using custom endpoint - confirm the selected model supports vision.")
436454

437-
if image_bytes and ("inference.rcp.epfl.ch" in endpoint_display):
438-
log.warning("⚠️ Using EPFL inference server - confirm the selected model supports vision on that endpoint.")
439-
log.warning(" OpenAI billing/dashboard may not reflect image usage when using a non-OpenAI endpoint.")
455+
except Exception as e:
456+
# Handle global tool quota limit (UsageLimitExceeded) and other errors gracefully
457+
error_msg = str(e)
458+
log.warning(f"⚠️ Agent execution encountered an error: {error_msg}")
459+
460+
# Check if this is a usage limit error (global tool quota)
461+
if "UsageLimitExceeded" in str(type(e).__name__) or "tool_calls_limit" in error_msg.lower():
462+
log.warning("Global tool call quota reached - continuing with partial results")
463+
464+
result = ToolSelection(
465+
conversation=Conversation(
466+
status=ConversationStatus.COMPLETE,
467+
context="The agent reached the maximum number of tool calls allowed. Please try a more specific query or break down your request into smaller parts.",
468+
question=None,
469+
options=None
470+
),
471+
choices=[],
472+
explanation="Tool call limit reached during execution. Try refining your query.",
473+
reason=None
474+
)
475+
else:
476+
raise
440477

441478
# ---- 7) Convert raw tool call records into ToolRunLog objects ----------
442479
for tc in getattr(deps, "tool_calls", []):

src/ai_agent/ui/components.py

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,10 @@
1010

1111
from .handlers import respond
1212
from .visualizations import create_tool_usage_chart, create_tool_timeline, create_disabled_tools_display
13+
from .utils import get_available_models, get_default_ui_model
1314

1415
log = logging.getLogger("chat_components")
1516

16-
# Model configurations with their inference servers
17-
MODEL_CONFIGS = {
18-
# OpenAI models (default endpoint)
19-
"gpt-4o-mini": {"name": "gpt-4o-mini", "base_url": None, "provider": "OpenAI"},
20-
"gpt-4o": {"name": "gpt-4o", "base_url": None, "provider": "OpenAI"},
21-
"gpt-4-turbo": {"name": "gpt-4-turbo", "base_url": None, "provider": "OpenAI"},
22-
23-
# EPFL inference server models
24-
"openai/gpt-oss-120b [EPFL]": {
25-
"name": "openai/gpt-oss-120b",
26-
"base_url": "https://inference-rcp.epfl.ch/v1",
27-
"provider": "EPFL"
28-
},
29-
"mistralai/Mistral-Small-3.2-24B-Instruct-2506 [EPFL]": {
30-
"name": "mistralai/Mistral-Small-3.2-24B-Instruct-2506",
31-
"base_url": "https://inference.rcp.epfl.ch/v1",
32-
"provider": "EPFL"
33-
},
34-
}
35-
36-
def get_model_config(model_display_name: str) -> Dict[str, str]:
37-
"""Get model configuration from display name."""
38-
return MODEL_CONFIGS.get(model_display_name, {"name": model_display_name, "base_url": None, "provider": "Unknown"})
39-
4017

4118
def create_chat_interface(doc_index: Dict[str, SoftwareDoc]):
4219
"""
@@ -125,7 +102,7 @@ def create_chat_interface(doc_index: Dict[str, SoftwareDoc]):
125102
with gr.Row(elem_classes="main-header"):
126103
gr.HTML("""
127104
<div class="logo-container">
128-
<img src="https://imaging-plaza.epfl.ch/logos/imaging_plaza_white.svg"
105+
<img src="https://imaging-plaza.epfl.ch/logos/imaging_plaza.svg"
129106
alt="Imaging Plaza Logo"
130107
style="height: 48px; width: auto;" />
131108
<div>
@@ -138,9 +115,14 @@ def create_chat_interface(doc_index: Dict[str, SoftwareDoc]):
138115
# Settings section (collapsed by default)
139116
with gr.Accordion("⚙️ Settings", open=False):
140117
with gr.Row():
118+
# Load models and default from config
119+
available_models = get_available_models()
120+
model_choices = [m["display_name"] for m in available_models]
121+
default_model = get_default_ui_model()
122+
141123
model_dropdown = gr.Dropdown(
142-
choices=list(MODEL_CONFIGS.keys()),
143-
value="gpt-4o-mini",
124+
choices=model_choices,
125+
value=default_model,
144126
label="Model",
145127
info="Select AI model and inference server",
146128
)

src/ai_agent/ui/handlers.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from .state import ChatState, ChatMessage
1717
from .formatters import format_tool_card
18+
from .utils import get_model_config
1819

1920
log = logging.getLogger("chat_handlers")
2021

@@ -201,13 +202,13 @@ def respond(
201202
# Parse model configuration if provided
202203
model_name = None
203204
base_url_override = None # Use different variable name
205+
api_key_env = None
204206
if model:
205-
# Import here to avoid circular dependency
206-
from ai_agent.ui.components import get_model_config
207207
model_config = get_model_config(model)
208208
model_name = model_config.get("name")
209209
base_url_override = model_config.get("base_url") # Can be None for OpenAI
210-
log.info(f"Model config: {model} -> name={model_name}, base_url={base_url_override}")
210+
api_key_env = model_config.get("api_key_env", "OPENAI_API_KEY")
211+
log.info(f"Model config: {model} -> name={model_name}, base_url={base_url_override}, api_key_env={api_key_env}")
211212

212213
effective_paths = file_paths or (state.last_files or [])
213214

@@ -228,7 +229,7 @@ def respond(
228229
conversation_history=state.conversation_history,
229230
model=model_name,
230231
base_url=base_url_override if model else None, # Only override if model selected
231-
top_k=top_k,
232+
api_key_env=api_key_env,
232233
num_choices=num_choices,
233234
)
234235
except ValueError as e:

0 commit comments

Comments
 (0)