Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions ai-assistant/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
OPENAI_API_KEY=sk-your-openai-api-key
OPENAI_MODEL=gpt-4o-mini
OPENAI_BASE_URL=
CORS_ORIGINS=http://localhost:3100,http://127.0.0.1:3100
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
12 changes: 12 additions & 0 deletions ai-assistant/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.env
.env.local
.venv/

**/__pycache__/
*.py[cod]

frontend/.env*.local
frontend/.next/
frontend/next-env.d.ts
frontend/node_modules/
frontend/out/
93 changes: 93 additions & 0 deletions ai-assistant/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# AI Assistant

Standalone full-stack AI chatbot example built with:

- Next.js App Router frontend on `http://localhost:3100`
- FastAPI backend on `http://localhost:8000`
- OpenAI Chat Completions API configured through environment variables

This directory is intentionally isolated from the SmartPerfetto application
entry points. It can run next to the main project without using port 3000.

## Project layout

```text
ai-assistant/
.env.example
install.sh
requirements.txt
backend/
main.py
frontend/
app/
globals.css
layout.tsx
page.tsx
package.json
scripts/
test-build.sh
```

## Environment

Copy the example file and set your API key:

```bash
cd ai-assistant
cp .env.example .env
```

Required:

- `OPENAI_API_KEY`: API key used by the FastAPI backend.

Optional:

- `OPENAI_MODEL`: defaults to `gpt-4o-mini`.
- `OPENAI_BASE_URL`: use when targeting an OpenAI-compatible endpoint.
- `CORS_ORIGINS`: comma-separated allowed browser origins.
- `NEXT_PUBLIC_API_BASE_URL`: frontend API URL. Defaults to
`http://localhost:8000` in the Next.js app.

## Install

```bash
cd ai-assistant
./install.sh
```

The installer creates `.venv`, installs Python dependencies from
`requirements.txt`, installs the Next.js app dependencies, and creates `.env`
from `.env.example` if needed.

## Run

Start the backend:

```bash
cd ai-assistant
source .venv/bin/activate
uvicorn backend.main:app --reload --host 0.0.0.0 --port 8000
```

Start the frontend in another terminal:

```bash
cd ai-assistant/frontend
npm run dev
```

Open `http://localhost:3100`.

## Verification

Run the standalone build smoke:

```bash
cd ai-assistant
./scripts/test-build.sh
```

The check compiles the FastAPI backend with Python `compileall`, installs the
frontend dependencies using `npm ci` when a lockfile exists, and runs
`next build`.
1 change: 1 addition & 0 deletions ai-assistant/backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
124 changes: 124 additions & 0 deletions ai-assistant/backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# SPDX-License-Identifier: AGPL-3.0-or-later

from pathlib import Path
from typing import Literal
from uuid import uuid4

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from openai import AsyncOpenAI, OpenAIError
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict


ENV_FILE = Path(__file__).resolve().parents[1] / ".env"


class Settings(BaseSettings):
openai_api_key: str | None = None
openai_model: str = "gpt-4o-mini"
openai_base_url: str | None = None
cors_origins: str = "http://localhost:3100,http://127.0.0.1:3100"

model_config = SettingsConfigDict(env_file=str(ENV_FILE), extra="ignore")

@property
def allowed_origins(self) -> list[str]:
return [
origin.strip()
for origin in self.cors_origins.split(",")
if origin.strip()
]


settings = Settings()

app = FastAPI(
title="AI Assistant API",
description="FastAPI backend for the standalone Next.js AI chatbot.",
version="0.1.0",
)

app.add_middleware(
CORSMiddleware,
allow_origins=settings.allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)


class ChatMessage(BaseModel):
role: Literal["system", "user", "assistant"]
content: str = Field(..., min_length=1)


class ChatRequest(BaseModel):
conversation_id: str | None = None
messages: list[ChatMessage] = Field(..., min_length=1)


class AssistantMessage(BaseModel):
id: str
role: Literal["assistant"]
content: str


class ChatResponse(BaseModel):
conversation_id: str
message: AssistantMessage
model: str


@app.get("/health")
async def health() -> dict[str, str | bool]:
return {
"status": "ok",
"openai_configured": bool(settings.openai_api_key),
"model": settings.openai_model,
}


@app.post("/api/chat", response_model=ChatResponse)
async def chat(request: ChatRequest) -> ChatResponse:
if not settings.openai_api_key:
raise HTTPException(
status_code=500,
detail="OPENAI_API_KEY is not configured. Copy .env.example to .env and set a key.",
)

client = AsyncOpenAI(
api_key=settings.openai_api_key,
base_url=settings.openai_base_url or None,
)

try:
completion = await client.chat.completions.create(
model=settings.openai_model,
messages=[
{"role": message.role, "content": message.content}
for message in request.messages
],
)
except OpenAIError as exc:
raise HTTPException(
status_code=502,
detail=f"OpenAI request failed: {exc}",
) from exc

content = completion.choices[0].message.content
if not content:
raise HTTPException(
status_code=502,
detail="OpenAI returned an empty assistant response.",
)

return ChatResponse(
conversation_id=request.conversation_id or f"conv_{uuid4().hex}",
message=AssistantMessage(
id=f"msg_{uuid4().hex}",
role="assistant",
content=content,
),
model=settings.openai_model,
)
Loading