66
77from pydantic_ai import Agent , RunContext
88from pydantic_ai .usage import UsageLimits
9- from pydantic_ai .models .openai import OpenAIResponsesModel
9+ from pydantic_ai .models .openai import OpenAIResponsesModel , OpenAIChatModel
1010from pydantic_ai .providers .openai import OpenAIProvider
1111from pydantic_ai .messages import BinaryContent
1212
1313from 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
1515from ai_agent .utils .config import get_config
1616from .models import AgentToolSelection , ToolRunLog
1717from .tools .repo_info_tool import tool_repo_summary , RepoSummaryInput
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+ )
4751else :
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" , []):
0 commit comments