Skip to content

Commit 644833e

Browse files
feat(server): FastAPI SSE endpoint + Gemini LLM support
1 parent f5bb844 commit 644833e

6 files changed

Lines changed: 412 additions & 31 deletions

File tree

mini_agent_harness/cli.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,19 @@ def init(agent_name: Optional[str] = typer.Argument("quickstart", help="Name of
3131
@app.command()
3232
def serve(model: str = typer.Option("gpt-3.5-turbo", "--model", help="Model name or path to serve with")):
3333
"""Serve the agent via FastAPI (placeholder implementation)."""
34-
typer.echo(
35-
"🚧 The serve command is not implemented yet. Run `mini-agent init` and explore tests in the meantime."
36-
)
34+
try:
35+
import uvicorn # type: ignore
36+
except ModuleNotFoundError as exc: # pragma: no cover
37+
typer.echo("Error: 'uvicorn' not installed. Run `poetry add uvicorn fastapi --group main`.")
38+
raise typer.Exit(1) from exc
39+
40+
from importlib import import_module
41+
42+
# Import here to avoid FastAPI requirement unless serve is run
43+
server_mod = import_module("mini_agent_harness.server")
44+
45+
typer.echo(f"🚀 Serving on http://127.0.0.1:8000 (model={model})")
46+
uvicorn.run(server_mod.app, host="0.0.0.0", port=8000, reload=False)
3747

3848

3949
@app.command()

mini_agent_harness/core/llm.py

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,24 +49,61 @@ def generate(self, prompt: str) -> str: # pragma: no cover
4949
return completion.choices[0].message.content
5050

5151

52+
# ---------------- Google Gemini -----------------
53+
54+
55+
class GeminiLLM: # pragma: no cover
56+
"""Wrapper around Google Generative AI Gemini models."""
57+
58+
def __init__(self, model: str = "gemini-pro") -> None:
59+
try:
60+
import google.generativeai as genai # type: ignore
61+
except ModuleNotFoundError: # pragma: no cover
62+
raise ImportError(
63+
"google-generativeai not installed. Run `poetry add google-generativeai`."
64+
) from None
65+
66+
api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
67+
if not api_key:
68+
raise RuntimeError("GEMINI_API_KEY environment variable not set.")
69+
70+
genai.configure(api_key=api_key)
71+
self._model = genai.GenerativeModel(model)
72+
73+
def generate(self, prompt: str) -> str: # noqa: D401
74+
response = self._model.generate_content(prompt)
75+
# Newer client returns text in .text
76+
return response.text # type: ignore[attr-defined]
77+
78+
5279
_DEF_PROVIDER = "echo" # default provider if nothing specified
5380

81+
_PROVIDERS: dict[str, type[LLM]] = {
82+
"openai": OpenAILLM,
83+
"gemini": GeminiLLM,
84+
"echo": EchoLLM,
85+
}
86+
5487

5588
def get_default_llm() -> LLM:
5689
"""Return an LLM instance based on env vars.
5790
5891
Priority order:
5992
1. `MINI_AGENT_LLM` env var ("openai" or "echo").
6093
2. If `OPENAI_API_KEY` is set → "openai".
61-
3. Fallback to "echo".
94+
3. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set → "gemini".
95+
4. Fallback to "echo".
6296
"""
6397

64-
provider = os.getenv("MINI_AGENT_LLM") or (
65-
"openai" if os.getenv("OPENAI_API_KEY") else _DEF_PROVIDER
66-
)
98+
provider = os.getenv("MINI_AGENT_LLM")
6799

68-
if provider == "openai":
69-
return OpenAILLM()
100+
if not provider:
101+
if os.getenv("OPENAI_API_KEY"):
102+
provider = "openai"
103+
elif os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY"):
104+
provider = "gemini"
105+
else:
106+
provider = _DEF_PROVIDER
70107

71-
# Default to echo
72-
return EchoLLM()
108+
cls = _PROVIDERS.get(provider, EchoLLM)
109+
return cls()

mini_agent_harness/server.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""FastAPI server exposing chat endpoint with simple streaming.
2+
3+
Run via `mini-agent serve` (see CLI). This is an early skeleton: it
4+
instantiates an Agent for each request using the quickstart manifest and
5+
streams back text tokens separated by spaces.
6+
"""
7+
from __future__ import annotations
8+
9+
from pathlib import Path
10+
from typing import AsyncGenerator, Iterator
11+
12+
import yaml # type: ignore
13+
from fastapi import FastAPI, Form # type: ignore
14+
from fastapi.responses import StreamingResponse # type: ignore
15+
16+
from .core import Agent
17+
18+
app = FastAPI(title="MiniAgentHarness")
19+
20+
_MANIFEST_PATH = Path("agents/quickstart.yaml")
21+
22+
23+
def _get_agent() -> Agent:
24+
manifest = yaml.safe_load(_MANIFEST_PATH.read_text())
25+
return Agent(manifest)
26+
27+
28+
def _stream_text(text: str) -> Iterator[bytes]:
29+
for token in text.split():
30+
yield f"data: {token}\n\n".encode()
31+
32+
33+
@app.get("/")
34+
async def root() -> dict[str, str]:
35+
return {"status": "ok"}
36+
37+
38+
@app.post("/chat")
39+
async def chat(message: str = Form(...)): # noqa: D401
40+
agent = _get_agent()
41+
result = agent.run(message)
42+
return StreamingResponse(
43+
_stream_text(result.response_text),
44+
media_type="text/event-stream",
45+
headers={"Cache-Control": "no-cache"},
46+
)

mini_agent_harness/tools/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from pathlib import Path
2020
from typing import Callable, Dict, Iterable, List
2121

22-
import yaml
22+
import yaml # type: ignore
2323

2424

2525
@dataclass

0 commit comments

Comments
 (0)