Commit fc5fdd39 authored by Miguel Matos's avatar Miguel Matos
Browse files

feat: add provider-neutral LLM factory with an OpenAI-compatible provider

Select the chat model with LLM_PROVIDER (groq by default, or openai for any
OpenAI-compatible endpoint such as Open WebUI). Agent creation is now
provider-neutral; create_groq_agent remains as an alias.
parent cc51cbf3
Loading
Loading
Loading
Loading

ai_agent/env.py

0 → 100644
+8 −0
Changes for ai_agent/env.py: 8 added lines, 0 removed lines.
Original line number Diff line number Diff line
import os


def require_env(name: str) -> str:
    value = os.getenv(name)
    if value is None or not value.strip():
        raise OSError(f"{name} environment variable is not set")
    return value.strip()

ai_agent/llm.py

0 → 100644
+59 −0
Changes for ai_agent/llm.py: 59 added lines, 0 removed lines.
Original line number Diff line number Diff line
import os

from dotenv import load_dotenv
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_groq import ChatGroq
from langchain_openai import ChatOpenAI
from pydantic import SecretStr

from ai_agent.env import require_env

load_dotenv()

DEFAULT_PROVIDER = "groq"
DEFAULT_GROQ_MODEL = "openai/gpt-oss-20b"
DEFAULT_OPENAI_TIMEOUT = 120.0


def _optional_env(name: str, default: str) -> str:
    return (os.getenv(name) or "").strip() or default


def _create_groq_llm() -> BaseChatModel:
    return ChatGroq(
        model=_optional_env("GROQ_MODEL_NAME", DEFAULT_GROQ_MODEL),
        temperature=0,
        max_tokens=None,
        reasoning_format="parsed",
        timeout=15,
        max_retries=3,
        api_key=SecretStr(require_env("GROQ_API_KEY")),
    )


def _create_openai_llm() -> BaseChatModel:
    """Any OpenAI-compatible endpoint, e.g. Open WebUI (https://host/api) or vLLM."""
    raw_timeout = _optional_env("OPENAI_TIMEOUT", str(DEFAULT_OPENAI_TIMEOUT))
    try:
        timeout = float(raw_timeout)
    except ValueError as e:
        raise OSError(f"OPENAI_TIMEOUT must be a number, got {raw_timeout!r}") from e

    return ChatOpenAI(
        model=require_env("OPENAI_MODEL_NAME"),
        base_url=require_env("OPENAI_BASE_URL"),
        api_key=SecretStr(require_env("OPENAI_API_KEY")),
        temperature=0,
        timeout=timeout,
        max_retries=2,
    )


def create_llm() -> BaseChatModel:
    """Create the chat model selected by LLM_PROVIDER (groq by default)."""
    provider = _optional_env("LLM_PROVIDER", DEFAULT_PROVIDER).lower()
    if provider == "groq":
        return _create_groq_llm()
    if provider == "openai":
        return _create_openai_llm()
    raise OSError(f"LLM_PROVIDER must be 'groq' or 'openai', got {provider!r}")
+1 −0
Changes for ai_agent/requirements.txt: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -6,3 +6,4 @@ python-dotenv==1.2.1
fastapi==0.129.0
uvicorn==0.40.0
redis==5.2.1
langchain-openai==1.7.3
+3 −3
Changes for ai_agent/routes/groq.py: 3 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -6,7 +6,7 @@ from fastapi.responses import JSONResponse, StreamingResponse

from ai_agent.memory import RedisShortTermMemory
from ai_agent.security import get_authorization
from ai_agent.sub_agents.groq_agent import create_groq_agent
from ai_agent.sub_agents.agent import create_agent
from ai_agent.utils import stream_agent_response

logger = logging.getLogger(__name__)
@@ -26,7 +26,7 @@ async def groq_query(
    if not query:
        return JSONResponse(status_code=400, content={"error": "No query provided"})

    agent = await create_groq_agent(authorization)
    agent = await create_agent(authorization)
    try:
        effective_query = await memory_store.augment_query(query, session_id)
        result = await agent.run(effective_query)
@@ -51,7 +51,7 @@ async def groq_stream(
    if not query:
        return JSONResponse(status_code=400, content={"error": "No query provided"})

    agent = await create_groq_agent(authorization)
    agent = await create_agent(authorization)

    try:
        effective_query = await memory_store.augment_query(query, session_id)
+48 −0
Changes for ai_agent/sub_agents/agent.py: 48 added lines, 0 removed lines.
Original line number Diff line number Diff line
import logging

from dotenv import load_dotenv
from mcp_use import MCPAgent, MCPClient  # type: ignore[import-untyped]

from ai_agent.env import require_env
from ai_agent.llm import create_llm
from ai_agent.prompts import CONVERSATION_MEMORY_PROMPT

load_dotenv()
logging.getLogger("mcp_use").setLevel(logging.ERROR)

logger = logging.getLogger(__name__)


async def create_agent(authorization: str | None):
    """Create an MCPAgent backed by the configured LLM provider."""
    try:
        mcp_server_url = require_env("MCP_SERVER_URL")
        llm = create_llm()
    except OSError as e:
        logger.error("Agent configuration error: %s", e)
        raise

    client = MCPClient(
        {
            "mcpServers": {
                "http": {
                    "url": mcp_server_url,
                    "headers": (
                        {"Authorization": authorization} if authorization else {}
                    ),
                    # mcp_use's default ({}) runs OAuth discovery on every connect.
                    "auth": None,
                }
            },
        }
    )
    await client.create_session("http")

    agent = MCPAgent(
        client=client,
        max_steps=20,
        system_prompt=CONVERSATION_MEMORY_PROMPT,
        llm=llm,
        verbose=True,
    )
    return agent
Loading