Skip to content

Add Stack Overflow Omi integration app#7414

Open
absalonCRC wants to merge 2 commits into
BasedHardware:mainfrom
absalonCRC:omi-stack-overflow-app
Open

Add Stack Overflow Omi integration app#7414
absalonCRC wants to merge 2 commits into
BasedHardware:mainfrom
absalonCRC:omi-stack-overflow-app

Conversation

@absalonCRC
Copy link
Copy Markdown

Summary\n- add a standalone Stack Overflow / Stack Exchange Omi app under plugins/omi-stack-overflow-app\n- expose no-auth chat tools for question search, question details, and top answers\n- include deployment files and setup docs for Railway/Heroku-style hosting\n\n## Validation\n- python -m py_compile plugins/omi-stack-overflow-app/main.py\n- git diff --check\n- pip install -r plugins/omi-stack-overflow-app/requirements.txt in Python 3.11 venv\n- FastAPI TestClient manifest checks\n- helper checks for limit, tag, and site sanitization\n- live search_questions smoke check against Stack Exchange API\n- live get_top_answers smoke check against Stack Exchange API\n- gitleaks detect --source plugins/omi-stack-overflow-app --no-git --redact --verbose

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 20, 2026

Greptile Summary

This PR adds a standalone Omi plugin that proxies the public Stack Exchange API, exposing three no-auth chat tools (search_questions, get_question, get_top_answers) as FastAPI POST endpoints with input sanitization and HTML-to-text cleaning. The deployment surface is minimal (Procfile, railway.toml, runtime.txt) and no credentials are required.

  • The Stack Exchange backoff field is never inspected after API responses; per the API's terms of service, ignoring it can cause Stack Exchange to throttle or block the service's IP, breaking all three tools.
  • _question_url generates incorrect browser links for several major Stack Exchange network sites (serverfault.com, superuser.com, askubuntu.com, etc.) by appending .stackexchange.com unconditionally; in get_top_answers this URL is always emitted rather than sourced from the API response.
  • A new httpx.AsyncClient is constructed and torn down on every outbound request, foregoing TCP/TLS connection reuse across calls.

Confidence Score: 3/5

The app is straightforward with no secrets or auth surface, but two logic bugs in main.py produce incorrect behavior on the primary code paths.

The missing backoff check means the service can be throttled or blocked by Stack Exchange under normal usage, silently degrading all three tools. The _question_url bug produces broken links for non-StackOverflow SE sites on every get_top_answers call. Both issues affect the main code path and are visible to end users immediately.

plugins/omi-stack-overflow-app/main.py — specifically _request_json (backoff handling) and _question_url (domain mapping)

Important Files Changed

Filename Overview
plugins/omi-stack-overflow-app/main.py Core FastAPI app with three chat-tool endpoints; has two logic bugs: the Stack Exchange backoff field is never checked (can trigger throttling), and _question_url generates broken links for sites like serverfault.com/superuser.com. Connection pooling is also absent.
plugins/omi-stack-overflow-app/requirements.txt Pins four dependencies at concrete versions compatible with Python 3.11; no issues found.
plugins/omi-stack-overflow-app/Procfile Standard Heroku/Railway Procfile launching uvicorn on $PORT; no issues.
plugins/omi-stack-overflow-app/railway.toml Railway deployment config with NIXPACKS builder, matching start command, and health check; no issues.
plugins/omi-stack-overflow-app/runtime.txt Pins Python 3.11.9 for Heroku/Railway runtime; no issues.
plugins/omi-stack-overflow-app/README.md README covering local dev, tool manifest, example curl calls, and deployment notes; no issues.

Sequence Diagram

sequenceDiagram
    participant Omi as Omi Platform
    participant App as omi-stack-overflow-app (FastAPI)
    participant SE as Stack Exchange API

    Omi->>App: GET /.well-known/omi-tools.json
    App-->>Omi: tool manifest (3 tools)

    Omi->>App: POST /tools/search_questions
    App->>App: _safe_site() / _safe_tags() / _safe_limit()
    App->>SE: "GET /search/advanced?site=...&q=..."
    SE-->>App: "{items: [...], backoff?: N}"
    Note over App: backoff field currently ignored
    App->>App: _format_question() x N
    App-->>Omi: "{result: formatted list}"

    Omi->>App: POST /tools/get_question
    App->>SE: "GET /questions/{id}?filter=withbody"
    SE-->>App: "{items: [...]}"
    App->>App: _clean_text(body)
    App-->>Omi: "{result: title + metadata + excerpt}"

    Omi->>App: POST /tools/get_top_answers
    App->>SE: "GET /questions/{id}/answers?sort=votes"
    SE-->>App: "{items: [...]}"
    App->>App: _format_answer() x N
    Note over App: URL via _question_url() wrong for serverfault/superuser/etc
    App-->>Omi: "{result: URL + ranked answers}"
Loading

Reviews (1): Last reviewed commit: "Add Stack Overflow Omi integration app" | Re-trigger Greptile

Comment on lines +111 to +115
response.raise_for_status()
data = response.json()
if data.get("error_id"):
raise ValueError(data.get("error_message") or "Stack Exchange API returned an error")
return data
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The Stack Exchange API's backoff field is not checked. When this field is present in a response, the API's terms require the client to wait that many seconds before making another request to the same endpoint. Ignoring it can cause Stack Exchange to begin returning 429s or temporarily blocking the service's IP entirely, breaking all three tool endpoints for every user.

Suggested change
response.raise_for_status()
data = response.json()
if data.get("error_id"):
raise ValueError(data.get("error_message") or "Stack Exchange API returned an error")
return data
response.raise_for_status()
data = response.json()
if data.get("error_id"):
raise ValueError(data.get("error_message") or "Stack Exchange API returned an error")
if data.get("backoff"):
raise ValueError(
f"Stack Exchange API requested a backoff of {data['backoff']} seconds. "
"Retry the request after that delay."
)
return data

Comment on lines +118 to +120
def _question_url(site: str, question_id: Any) -> str:
host = "stackoverflow.com" if site == DEFAULT_SITE else f"{site}.stackexchange.com"
return f"https://{host}/questions/{question_id}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 _question_url unconditionally constructs {site}.stackexchange.com for every site except stackoverflow. Several major Stack Exchange network sites have their own top-level domains (serverfault.com, superuser.com, askubuntu.com, meta.stackoverflow.com, meta.stackexchange.com). In get_top_answers this URL is always emitted in the result (line 378), so users querying those sites will receive broken links pointing to a non-existent host like serverfault.stackexchange.com.

Suggested change
def _question_url(site: str, question_id: Any) -> str:
host = "stackoverflow.com" if site == DEFAULT_SITE else f"{site}.stackexchange.com"
return f"https://{host}/questions/{question_id}"
_KNOWN_DOMAINS: dict[str, str] = {
"stackoverflow": "stackoverflow.com",
"serverfault": "serverfault.com",
"superuser": "superuser.com",
"askubuntu": "askubuntu.com",
"meta.stackoverflow": "meta.stackoverflow.com",
"meta.stackexchange": "meta.stackexchange.com",
"mathoverflow.net": "mathoverflow.net",
}
def _question_url(site: str, question_id: Any) -> str:
host = _KNOWN_DOMAINS.get(site, f"{site}.stackexchange.com")
return f"https://{host}/questions/{question_id}"

Comment on lines +107 to +111
async def _request_json(path: str, params: Optional[dict[str, Any]] = None) -> dict[str, Any]:
headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT_SECONDS, headers=headers) as client:
response = await client.get(f"{STACK_API_BASE_URL}{path}", params=params)
response.raise_for_status()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 A new httpx.AsyncClient is constructed and torn down on every request. This forgoes TCP connection reuse and means a full TLS handshake is performed on each call to the Stack Exchange API. Using a module-level (or lifespan-scoped) client gives connection pooling at no cost.

Suggested change
async def _request_json(path: str, params: Optional[dict[str, Any]] = None) -> dict[str, Any]:
headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT_SECONDS, headers=headers) as client:
response = await client.get(f"{STACK_API_BASE_URL}{path}", params=params)
response.raise_for_status()
_http_client: httpx.AsyncClient | None = None
@app.on_event("startup")
async def _startup() -> None:
global _http_client
_http_client = httpx.AsyncClient(
timeout=REQUEST_TIMEOUT_SECONDS,
headers={"User-Agent": USER_AGENT, "Accept": "application/json"},
)
@app.on_event("shutdown")
async def _shutdown() -> None:
if _http_client:
await _http_client.aclose()
async def _request_json(path: str, params: Optional[dict[str, Any]] = None) -> dict[str, Any]:
client = _http_client or httpx.AsyncClient(
timeout=REQUEST_TIMEOUT_SECONDS,
headers={"User-Agent": USER_AGENT, "Accept": "application/json"},
)
response = await client.get(f"{STACK_API_BASE_URL}{path}", params=params)
response.raise_for_status()

@absalonCRC
Copy link
Copy Markdown
Author

Addressed the Greptile feedback in commit f43e339:\n\n- surface Stack Exchange API backoff responses as graceful tool errors instead of ignoring them\n- map major Stack Exchange network site slugs to their real browser hosts, including serverfault.com, superuser.com, askubuntu.com, mathoverflow.net, and localized Stack Overflow sites\n- reuse a shared httpx.AsyncClient through FastAPI lifespan for connection pooling\n\nValidation rerun:\n- python -m py_compile plugins/omi-stack-overflow-app/main.py\n- git diff --check\n- FastAPI TestClient manifest/lifespan checks\n- explicit URL mapping checks for Stack Overflow, Server Fault, Super User, Ask Ubuntu, MathOverflow, localized Stack Overflow, and default stackexchange.com sites\n- mocked Stack Exchange backoff handling check\n- live search_questions smoke check against the Stack Exchange API

Copy link
Copy Markdown
Collaborator

@kodjima33 kodjima33 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for adding the Stack Overflow app

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants