Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"dockerComposeFile": [
"../compose-dev.yaml"
],
"service": "backend",
"service": "api",
"workspaceFolder": "/app",
"customizations": {
"vscode": {
Expand Down Expand Up @@ -39,6 +39,7 @@
"editor.formatOnSave": true,
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"python.venvPath": "/app/.venv/bin/python",
"[python]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
Expand Down
6 changes: 0 additions & 6 deletions backend/Dockerfile

This file was deleted.

19 changes: 19 additions & 0 deletions backend/api/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: FastAPI",
"type": "debugpy",
"request": "launch",
"module": "fastapi",
"args": [
"run",
"api/main.py",
"--reload"
]
}
]
}
11 changes: 11 additions & 0 deletions backend/api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM ghcr.io/astral-sh/uv:python3.13-bookworm

WORKDIR /app
COPY ./backend/api/uv.lock ./backend/api/pyproject.toml ./
RUN uv sync --frozen
RUN apt-get update && apt-get install -y --no-install-recommends curl
COPY ./envs/backend.env /opt/.env
COPY ./backend/api /app/api
COPY ./backend/shared_mcp /app/shared_mcp
ENV PYTHONPATH /app:$PYTHONPATH
ENTRYPOINT ["uv", "run", "fastapi", "run", "api/main.py"]
20 changes: 20 additions & 0 deletions backend/api/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file="/opt/.env",
env_ignore_empty=True,
extra="ignore",
)

model: str = "gpt-4o-mini-2024-07-18"
openai_api_key: str = ""
mcp_server_port: int = 8050

pg_url: str = "postgres://postgres"
pg_user: str = "postgres"
pg_pass: str = "postgres"


settings = Settings()
23 changes: 23 additions & 0 deletions backend/api/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import Annotated, Iterable

from config import settings
from fastapi import Depends
from langchain_openai import ChatOpenAI


def llm_factory() -> ChatOpenAI:
llm = ChatOpenAI(
streaming=True,
model=settings.model,
temperature=0,
api_key=settings.openai_api_key,
stream_usage=True,
)
return llm


def get_llm_session() -> Iterable[ChatOpenAI]:
yield llm_factory()


LLMDep = Annotated[ChatOpenAI, Depends(get_llm_session)]
7 changes: 7 additions & 0 deletions backend/api/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from fastapi import FastAPI

from api.routers import llms, mcps

app = FastAPI(swagger_ui_parameters={"tryItOutEnabled": True})
app.include_router(llms.router, prefix="/v1")
app.include_router(mcps.router, prefix="/v1")
3 changes: 2 additions & 1 deletion backend/pyproject.toml → backend/api/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[project]
name = "app"
name = "api"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
Expand All @@ -14,6 +14,7 @@ dependencies = [
"langchain-postgres==0.0.12",
"langfuse==2.60.2",
"langgraph==0.2.39",
"mcp[cli]>=1.6.0",
"prometheus-client==0.21.1",
"psycopg[binary]==3.2.3",
"pydantic-settings==2.6.0",
Expand Down
22 changes: 22 additions & 0 deletions backend/api/routers/llms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from typing import AsyncGenerator

from fastapi import APIRouter
from sse_starlette.sse import EventSourceResponse
from starlette.responses import Response

from api.dependencies import LLMDep

router = APIRouter(tags=["chat"])


async def stream(
query: str, llm: LLMDep
) -> AsyncGenerator[dict[str, str], None]:
async for chunk in llm.astream_events(query):
yield dict(data=chunk)


@router.get("/chat/completions")
async def completions(query: str, llm: LLMDep) -> Response:
"""Stream completions via Server Sent Events"""
return EventSourceResponse(stream(query, llm))
51 changes: 51 additions & 0 deletions backend/api/routers/mcps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from contextlib import asynccontextmanager
from typing import Iterable

from config import settings
from fastapi import APIRouter
from mcp import ClientSession, types
from mcp.client.sse import sse_client

from shared_mcp.models import ToolRequest

router = APIRouter(prefix="/mcps", tags=["mcps"])


@asynccontextmanager
async def mcp_sse_client():
async with sse_client(f"http://mcp:{settings.mcp_server_port}/sse") as (
read_stream,
write_stream,
):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
yield session


@router.get("/list-tools")
async def list_tools() -> Iterable[types.Tool]:
"""
Lists tools available from MCP server

This endpoint establishes a Server-Sent Events connection with the client
and forwards communication to the Model Context Protocol server.
"""
async with mcp_sse_client() as session:
response = await session.list_tools()
return response.tools


@router.post("/call-tool")
async def call_tool(request: ToolRequest) -> str:
"""
Calls tool available from MCP server

This endpoint establishes a Server-Sent Events connection with the client
and forwards communication to the Model Context Protocol server.
"""
async with mcp_sse_client() as session:
response = await session.call_tool(
request.tool_name,
arguments=request.model_dump(exclude=["tool_name"]),
)
return response.content[0].text
Loading