Phase 04: Single Model Q&A - 2 plans created - 5 total tasks defined - Ready for execution Plans: - 04-01: AI client abstraction (openai dep, config, AIClient class) - 04-02: /ask handler and bot integration (M3 milestone) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
141 lines
5 KiB
Markdown
141 lines
5 KiB
Markdown
---
|
|
phase: 04-single-model-qa
|
|
plan: 01
|
|
type: execute
|
|
---
|
|
|
|
<objective>
|
|
Create AI client abstraction layer supporting Requesty and OpenRouter as model routers.
|
|
|
|
Purpose: Establish the foundation for all AI model interactions - single queries, multi-model discussions, and consensus generation all flow through this client.
|
|
Output: Working ai_client.py that can send prompts to any model via Requesty or OpenRouter.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
~/.claude/get-shit-done/workflows/execute-phase.md
|
|
~/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/03-project-crud/03-03-SUMMARY.md
|
|
|
|
# Key files:
|
|
@src/moai/bot/config.py
|
|
@src/moai/core/models.py
|
|
|
|
# From discovery (no DISCOVERY.md needed - Level 1):
|
|
# Both Requesty and OpenRouter are OpenAI SDK compatible:
|
|
# - Requesty: base_url="https://router.requesty.ai/v1", model format "provider/model-name"
|
|
# - OpenRouter: base_url="https://openrouter.ai/api/v1", needs HTTP-Referer header
|
|
# Can use `openai` package with different base_url/headers
|
|
|
|
**Tech available:**
|
|
- python-telegram-bot, sqlalchemy, httpx, aiosqlite
|
|
- pytest, pytest-asyncio
|
|
|
|
**Established patterns:**
|
|
- Service layer in core/services/
|
|
- Config loading from environment in bot/config.py
|
|
- Async functions throughout
|
|
|
|
**Constraining decisions:**
|
|
- AI client as abstraction layer (PROJECT.md)
|
|
- httpx for API calls (SPEC.md)
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add openai dependency and extend config</name>
|
|
<files>pyproject.toml, src/moai/bot/config.py</files>
|
|
<action>
|
|
1. Add `openai` to dependencies in pyproject.toml (unpinned per project standards)
|
|
2. Extend Config class in bot/config.py with:
|
|
- AI_ROUTER: str (env var, default "requesty") - which router to use
|
|
- AI_API_KEY: str (env var) - API key for the router
|
|
- AI_REFERER: str | None (env var, optional) - for OpenRouter's HTTP-Referer requirement
|
|
|
|
Note: Use existing pattern of loading from env with os.getenv(). No need for pydantic or complex validation - keep it simple like existing Config class.
|
|
</action>
|
|
<verify>python -c "from moai.bot.config import Config; c = Config(); print(c.AI_ROUTER)"</verify>
|
|
<done>Config has AI_ROUTER, AI_API_KEY, AI_REFERER attributes; openai in dependencies</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Create AI client abstraction</name>
|
|
<files>src/moai/core/ai_client.py</files>
|
|
<action>
|
|
Create ai_client.py with:
|
|
|
|
1. AIClient class that wraps OpenAI AsyncOpenAI client:
|
|
```python
|
|
class AIClient:
|
|
def __init__(self, router: str, api_key: str, referer: str | None = None):
|
|
# Set base_url based on router ("requesty" or "openrouter")
|
|
# Store referer for OpenRouter
|
|
# Create AsyncOpenAI client with base_url and api_key
|
|
```
|
|
|
|
2. Async method for single completion:
|
|
```python
|
|
async def complete(self, model: str, messages: list[dict], system_prompt: str | None = None) -> str:
|
|
# Build messages list with optional system prompt
|
|
# Call client.chat.completions.create()
|
|
# Add extra_headers with HTTP-Referer if OpenRouter and referer set
|
|
# Return response.choices[0].message.content
|
|
```
|
|
|
|
3. Model name normalization:
|
|
- For Requesty: model names need provider prefix (e.g., "claude" -> "anthropic/claude-sonnet-4-20250514")
|
|
- For OpenRouter: similar format
|
|
- Create MODEL_MAP dict with our short names -> full model identifiers
|
|
- MODEL_MAP = {"claude": "anthropic/claude-sonnet-4-20250514", "gpt": "openai/gpt-4o", "gemini": "google/gemini-2.0-flash"}
|
|
|
|
4. Module-level convenience function:
|
|
```python
|
|
_client: AIClient | None = None
|
|
|
|
def init_ai_client(config: Config) -> AIClient:
|
|
global _client
|
|
_client = AIClient(config.AI_ROUTER, config.AI_API_KEY, config.AI_REFERER)
|
|
return _client
|
|
|
|
def get_ai_client() -> AIClient:
|
|
if _client is None:
|
|
raise RuntimeError("AI client not initialized")
|
|
return _client
|
|
```
|
|
|
|
Keep it minimal - no retry logic, no streaming (yet), no complex error handling. This is the foundation; complexity comes later as needed.
|
|
</action>
|
|
<verify>python -c "from moai.core.ai_client import AIClient, MODEL_MAP; print(MODEL_MAP)"</verify>
|
|
<done>AIClient class exists with complete() method, MODEL_MAP has claude/gpt/gemini mappings</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
Before declaring plan complete:
|
|
- [ ] `uv sync` installs openai package
|
|
- [ ] Config loads AI settings from environment
|
|
- [ ] AIClient can be instantiated with router/key
|
|
- [ ] MODEL_MAP contains claude, gpt, gemini mappings
|
|
- [ ] `ruff check src` passes
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
|
|
- openai package in dependencies
|
|
- Config extended with AI_ROUTER, AI_API_KEY, AI_REFERER
|
|
- AIClient class with complete() method
|
|
- MODEL_MAP with short name -> full model mappings
|
|
- Module-level init_ai_client/get_ai_client functions
|
|
- All code follows project conventions (type hints, docstrings)
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/04-single-model-qa/04-01-SUMMARY.md`
|
|
</output>
|