-
Notifications
You must be signed in to change notification settings - Fork 2k
Add Wikipedia Omi integration app #7413
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| web: uvicorn main:app --host 0.0.0.0 --port $PORT |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| # Wikipedia Omi App | ||
|
|
||
| Search and read Wikipedia from Omi chat tools. This app is useful for quick background research, definitions, people/place lookups, and random topic discovery without leaving a conversation. | ||
|
|
||
| ## Features | ||
|
|
||
| - Search Wikipedia articles by keyword | ||
| - Fetch concise article summaries by title | ||
| - Discover a random article | ||
| - Optional language-code support, defaulting to English | ||
| - No OAuth, accounts, or API keys required | ||
|
|
||
| ## Chat Tools | ||
|
|
||
| ### `search_articles` | ||
|
|
||
| Searches Wikipedia using the MediaWiki API. | ||
|
|
||
| Parameters: | ||
|
|
||
| - `query` (required): topic, person, place, event, or concept to search | ||
| - `language` (optional): Wikipedia language code, defaults to `en` | ||
| - `limit` (optional): maximum results, defaults to 5 and caps at 10 | ||
|
|
||
| ### `get_article_summary` | ||
|
|
||
| Fetches a concise summary for a Wikipedia page using the REST summary endpoint. | ||
|
|
||
| Parameters: | ||
|
|
||
| - `title` (required): exact or near-exact article title | ||
| - `language` (optional): Wikipedia language code, defaults to `en` | ||
|
|
||
| ### `get_random_article` | ||
|
|
||
| Returns a random article summary. | ||
|
|
||
| Parameters: | ||
|
|
||
| - `language` (optional): Wikipedia language code, defaults to `en` | ||
|
|
||
| ## Local Development | ||
|
|
||
| ```bash | ||
| python -m venv .venv | ||
| source .venv/bin/activate | ||
| pip install -r requirements.txt | ||
| uvicorn main:app --reload --port 8080 | ||
| ``` | ||
|
|
||
| Open: | ||
|
|
||
| - `http://localhost:8080/health` | ||
| - `http://localhost:8080/.well-known/omi-tools.json` | ||
|
|
||
| ## Deployment | ||
|
|
||
| Deploy this folder as a standalone FastAPI service. The app does not require environment variables. | ||
|
|
||
| Railway can use the included `Procfile`: | ||
|
|
||
| ```bash | ||
| web: uvicorn main:app --host 0.0.0.0 --port $PORT | ||
| ``` | ||
|
|
||
| ## Notes | ||
|
|
||
| Wikimedia asks API clients to send an identifying user agent. This app sets a static Omi integration user agent for all outbound requests. |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,284 @@ | ||||||||||||
| """ | ||||||||||||
| Wikipedia Integration App for Omi. | ||||||||||||
|
|
||||||||||||
| Provides chat tools for searching Wikipedia, reading concise article summaries, | ||||||||||||
| and finding a random article for exploration. | ||||||||||||
| """ | ||||||||||||
|
|
||||||||||||
| from html import unescape | ||||||||||||
| import re | ||||||||||||
| from typing import Any, Optional | ||||||||||||
| from urllib.parse import quote | ||||||||||||
|
|
||||||||||||
| import httpx | ||||||||||||
| from fastapi import FastAPI | ||||||||||||
| from fastapi.responses import HTMLResponse | ||||||||||||
| from pydantic import BaseModel | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| REQUEST_TIMEOUT_SECONDS = 10 | ||||||||||||
| MAX_LIMIT = 10 | ||||||||||||
| DEFAULT_LANGUAGE = "en" | ||||||||||||
| USER_AGENT = "omi-wikipedia-app/1.0 (https://omi.me)" | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| app = FastAPI( | ||||||||||||
| title="Omi Wikipedia Integration", | ||||||||||||
| description="Search and read Wikipedia from Omi chat tools", | ||||||||||||
| version="1.0.0", | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| class ChatToolResponse(BaseModel): | ||||||||||||
| """Response model for Omi chat tool endpoints.""" | ||||||||||||
|
|
||||||||||||
| result: Optional[str] = None | ||||||||||||
| error: Optional[str] = None | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _safe_limit(limit: Any) -> int: | ||||||||||||
| if limit is None or limit == "": | ||||||||||||
| return 5 | ||||||||||||
| try: | ||||||||||||
| limit = int(limit) | ||||||||||||
| except (TypeError, ValueError): | ||||||||||||
| return 5 | ||||||||||||
| return max(1, min(limit, MAX_LIMIT)) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _safe_language(language: Optional[str]) -> str: | ||||||||||||
| lang = (language or DEFAULT_LANGUAGE).strip().lower() | ||||||||||||
| if not lang.replace("-", "").isalpha() or len(lang) > 12: | ||||||||||||
| return DEFAULT_LANGUAGE | ||||||||||||
| return lang | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| async def _request_json(url: 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(url, params=params) | ||||||||||||
| response.raise_for_status() | ||||||||||||
| return response.json() | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _article_url(language: str, title: str) -> str: | ||||||||||||
| return f"https://{language}.wikipedia.org/wiki/{quote(title.replace(' ', '_'))}" | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _clean_snippet(value: Optional[str]) -> str: | ||||||||||||
| if not value: | ||||||||||||
| return "" | ||||||||||||
|
|
||||||||||||
| text = unescape(value) | ||||||||||||
| text = re.sub(r"<[^>]+>", "", text) | ||||||||||||
| text = re.sub(r"\s+", " ", text) | ||||||||||||
| return text.strip() | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _format_summary(data: dict[str, Any], language: str) -> str: | ||||||||||||
| title = data.get("title") or "Untitled" | ||||||||||||
| extract = data.get("extract") or "No summary was returned for this article." | ||||||||||||
| description = data.get("description") | ||||||||||||
| page_url = data.get("content_urls", {}).get("desktop", {}).get("page") or _article_url(language, title) | ||||||||||||
|
|
||||||||||||
| lines = [title] | ||||||||||||
| if description: | ||||||||||||
| lines.append(description) | ||||||||||||
| lines.extend(["", extract, "", page_url]) | ||||||||||||
| return "\n".join(lines) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @app.get("/") | ||||||||||||
| async def root(): | ||||||||||||
| return HTMLResponse( | ||||||||||||
| """ | ||||||||||||
| <html> | ||||||||||||
| <head><title>Wikipedia x Omi</title></head> | ||||||||||||
| <body style="font-family: sans-serif; max-width: 640px; margin: 48px auto; line-height: 1.5;"> | ||||||||||||
| <h1>Wikipedia x Omi</h1> | ||||||||||||
| <p>Search Wikipedia, fetch article summaries, and discover random articles from Omi.</p> | ||||||||||||
| <p>No sign-in is required.</p> | ||||||||||||
| </body> | ||||||||||||
| </html> | ||||||||||||
| """ | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @app.get("/health") | ||||||||||||
| async def health(): | ||||||||||||
| return {"status": "ok"} | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @app.get("/.well-known/omi-tools.json") | ||||||||||||
| async def get_omi_tools_manifest(): | ||||||||||||
| return { | ||||||||||||
| "tools": [ | ||||||||||||
| { | ||||||||||||
| "name": "search_articles", | ||||||||||||
| "description": "Search Wikipedia articles by keyword. Use this when the user asks about a topic, person, place, event, concept, or wants matching encyclopedia articles.", | ||||||||||||
| "endpoint": "/tools/search_articles", | ||||||||||||
| "method": "POST", | ||||||||||||
| "parameters": { | ||||||||||||
| "type": "object", | ||||||||||||
| "properties": { | ||||||||||||
| "query": { | ||||||||||||
| "type": "string", | ||||||||||||
| "description": "Search query, such as a topic, person, place, event, or concept.", | ||||||||||||
| }, | ||||||||||||
| "language": { | ||||||||||||
| "type": "string", | ||||||||||||
| "description": "Wikipedia language code. Defaults to en.", | ||||||||||||
| }, | ||||||||||||
| "limit": { | ||||||||||||
| "type": "integer", | ||||||||||||
| "description": "Maximum results to return. Defaults to 5, maximum 10.", | ||||||||||||
| }, | ||||||||||||
| }, | ||||||||||||
| "required": ["query"], | ||||||||||||
| }, | ||||||||||||
| "auth_required": False, | ||||||||||||
| "status_message": "Searching Wikipedia...", | ||||||||||||
| }, | ||||||||||||
| { | ||||||||||||
| "name": "get_article_summary", | ||||||||||||
| "description": "Get a concise Wikipedia summary for an exact article title. Use this when the user asks for an overview, definition, background, or key facts about a known topic.", | ||||||||||||
| "endpoint": "/tools/get_article_summary", | ||||||||||||
| "method": "POST", | ||||||||||||
| "parameters": { | ||||||||||||
| "type": "object", | ||||||||||||
| "properties": { | ||||||||||||
| "title": { | ||||||||||||
| "type": "string", | ||||||||||||
| "description": "Exact or near-exact Wikipedia article title.", | ||||||||||||
| }, | ||||||||||||
| "language": { | ||||||||||||
| "type": "string", | ||||||||||||
| "description": "Wikipedia language code. Defaults to en.", | ||||||||||||
| }, | ||||||||||||
| }, | ||||||||||||
| "required": ["title"], | ||||||||||||
| }, | ||||||||||||
| "auth_required": False, | ||||||||||||
| "status_message": "Fetching Wikipedia article...", | ||||||||||||
| }, | ||||||||||||
| { | ||||||||||||
| "name": "get_random_article", | ||||||||||||
| "description": "Get a random Wikipedia article summary. Use this when the user wants to learn something random, discover a topic, or start an exploratory conversation.", | ||||||||||||
| "endpoint": "/tools/get_random_article", | ||||||||||||
| "method": "POST", | ||||||||||||
| "parameters": { | ||||||||||||
| "type": "object", | ||||||||||||
| "properties": { | ||||||||||||
| "language": { | ||||||||||||
| "type": "string", | ||||||||||||
| "description": "Wikipedia language code. Defaults to en.", | ||||||||||||
| } | ||||||||||||
| }, | ||||||||||||
| "required": [], | ||||||||||||
| }, | ||||||||||||
| "auth_required": False, | ||||||||||||
| "status_message": "Finding a random Wikipedia article...", | ||||||||||||
| }, | ||||||||||||
| ] | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @app.post("/tools/search_articles", tags=["chat_tools"], response_model=ChatToolResponse) | ||||||||||||
| async def search_articles(payload: dict[str, Any]): | ||||||||||||
| query = (payload.get("query") or "").strip() | ||||||||||||
| if not query: | ||||||||||||
| return ChatToolResponse(error="Missing required field: query") | ||||||||||||
|
|
||||||||||||
| language = _safe_language(payload.get("language")) | ||||||||||||
| limit = _safe_limit(payload.get("limit")) | ||||||||||||
| url = f"https://{language}.wikipedia.org/w/api.php" | ||||||||||||
|
|
||||||||||||
| try: | ||||||||||||
| data = await _request_json( | ||||||||||||
| url, | ||||||||||||
| { | ||||||||||||
| "action": "query", | ||||||||||||
| "list": "search", | ||||||||||||
| "srsearch": query, | ||||||||||||
| "srlimit": limit, | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| "format": "json", | ||||||||||||
| "utf8": "1", | ||||||||||||
| }, | ||||||||||||
| ) | ||||||||||||
| results = data.get("query", {}).get("search", [])[:limit] | ||||||||||||
| if not results: | ||||||||||||
| return ChatToolResponse(result=f"No Wikipedia articles found for '{query}'.") | ||||||||||||
|
|
||||||||||||
| lines = [f"Wikipedia search results for '{query}':"] | ||||||||||||
| for index, item in enumerate(results, start=1): | ||||||||||||
| title = item.get("title") or "Untitled" | ||||||||||||
| snippet = _clean_snippet(item.get("snippet")) | ||||||||||||
| lines.append(f"\n{index}. {title}") | ||||||||||||
| if snippet: | ||||||||||||
| lines.append(f" {snippet}") | ||||||||||||
| lines.append(f" {_article_url(language, title)}") | ||||||||||||
|
|
||||||||||||
| return ChatToolResponse(result="\n".join(lines)) | ||||||||||||
| except httpx.HTTPStatusError as exc: | ||||||||||||
| return ChatToolResponse(error=f"Wikipedia search failed with status {exc.response.status_code}.") | ||||||||||||
| except httpx.HTTPError as exc: | ||||||||||||
| return ChatToolResponse(error=f"Wikipedia search failed: {exc}") | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @app.post("/tools/get_article_summary", tags=["chat_tools"], response_model=ChatToolResponse) | ||||||||||||
| async def get_article_summary(payload: dict[str, Any]): | ||||||||||||
| title = (payload.get("title") or "").strip() | ||||||||||||
| if not title: | ||||||||||||
| return ChatToolResponse(error="Missing required field: title") | ||||||||||||
|
|
||||||||||||
| language = _safe_language(payload.get("language")) | ||||||||||||
| url = f"https://{language}.wikipedia.org/api/rest_v1/page/summary/{quote(title.replace(' ', '_'))}" | ||||||||||||
|
|
||||||||||||
| try: | ||||||||||||
| data = await _request_json(url) | ||||||||||||
| if data.get("type") == "disambiguation": | ||||||||||||
| return ChatToolResponse( | ||||||||||||
| result=_format_summary(data, language) | ||||||||||||
| + "\n\nThis is a disambiguation page. Use search_articles for more specific matches." | ||||||||||||
| ) | ||||||||||||
| return ChatToolResponse(result=_format_summary(data, language)) | ||||||||||||
| except httpx.HTTPStatusError as exc: | ||||||||||||
| if exc.response.status_code == 404: | ||||||||||||
| return ChatToolResponse(error=f"No Wikipedia article found for '{title}'. Try search_articles first.") | ||||||||||||
| return ChatToolResponse(error=f"Wikipedia article request failed with status {exc.response.status_code}.") | ||||||||||||
| except httpx.HTTPError as exc: | ||||||||||||
| return ChatToolResponse(error=f"Wikipedia article request failed: {exc}") | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @app.post("/tools/get_random_article", tags=["chat_tools"], response_model=ChatToolResponse) | ||||||||||||
| async def get_random_article(payload: dict[str, Any]): | ||||||||||||
| language = _safe_language(payload.get("language")) | ||||||||||||
| url = f"https://{language}.wikipedia.org/w/api.php" | ||||||||||||
|
|
||||||||||||
| try: | ||||||||||||
| data = await _request_json( | ||||||||||||
| url, | ||||||||||||
| { | ||||||||||||
| "action": "query", | ||||||||||||
| "list": "random", | ||||||||||||
| "rnnamespace": "0", | ||||||||||||
| "rnlimit": "1", | ||||||||||||
| "format": "json", | ||||||||||||
| "utf8": "1", | ||||||||||||
| }, | ||||||||||||
| ) | ||||||||||||
| random_items = data.get("query", {}).get("random", []) | ||||||||||||
| if not random_items: | ||||||||||||
| return ChatToolResponse(result="No random Wikipedia article was returned.") | ||||||||||||
|
|
||||||||||||
| title = random_items[0].get("title") | ||||||||||||
| if not title: | ||||||||||||
| return ChatToolResponse(result="Wikipedia returned a random article without a title.") | ||||||||||||
|
|
||||||||||||
| summary_url = f"https://{language}.wikipedia.org/api/rest_v1/page/summary/{quote(title.replace(' ', '_'))}" | ||||||||||||
| summary = await _request_json(summary_url) | ||||||||||||
| return ChatToolResponse(result="Random Wikipedia article:\n\n" + _format_summary(summary, language)) | ||||||||||||
| except httpx.HTTPStatusError as exc: | ||||||||||||
| return ChatToolResponse(error=f"Wikipedia random article request failed with status {exc.response.status_code}.") | ||||||||||||
| except httpx.HTTPError as exc: | ||||||||||||
| return ChatToolResponse(error=f"Wikipedia random article request failed: {exc}") | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| [build] | ||
| builder = "NIXPACKS" | ||
|
|
||
| [deploy] | ||
| startCommand = "uvicorn main:app --host 0.0.0.0 --port $PORT" | ||
| restartPolicyType = "ON_FAILURE" | ||
| restartPolicyMaxRetries = 10 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| fastapi==0.115.6 | ||
| uvicorn[standard]==0.34.0 | ||
| pydantic==2.10.4 | ||
| httpx==0.28.1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| python-3.11.9 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parametersobjects in the manifest are missing the"type": "object"field required by JSON Schema. Without it, strict parsers may reject the schema, and the Omi platform or toolchain may fail to validate or display the tool parameters correctly.