Compare commits
59 commits
1b55df508c
...
bd1db003b7
| Author | SHA1 | Date | |
|---|---|---|---|
| bd1db003b7 | |||
| 2a86d3903a | |||
| 3296874408 | |||
| 5934d21256 | |||
| a59321cc3b | |||
| 3ae08e9317 | |||
| 104eceb246 | |||
| 9133d4eebf | |||
| bed0fbcb3e | |||
| 7f461700d8 | |||
| cef1898352 | |||
| 81b5bfff35 | |||
| b2610cd90a | |||
| baf02bb11f | |||
| 3258c3a596 | |||
| 5afd8b6213 | |||
| 71eec42baf | |||
| 7078379a9a | |||
| 32983c9301 | |||
| 821b419271 | |||
| f1a001f923 | |||
| e04ce4eeeb | |||
| 3740691dac | |||
| 4ea13efe8f | |||
| 6ec3d069d8 | |||
| afab4f84e2 | |||
| bb3eab7bb4 | |||
| e2e10d9b2e | |||
| 8791398b02 | |||
| 9922c333cb | |||
| 70dd517e6a | |||
| 4d8c66ee07 | |||
| 3f3b5ce28f | |||
| 718dcea7dc | |||
| e3d72dab60 | |||
| dfdadc62f0 | |||
| 1908f5f61d | |||
| 15307d7c85 | |||
| 2a563efce0 | |||
| cb185e139c | |||
| 98b71829cc | |||
| 712024eb10 | |||
| 0a818551a5 | |||
| c3a849b2b3 | |||
| 4381e12609 | |||
| 4d6768e55c | |||
| ad396eca0e | |||
| fb81feaa3e | |||
| bb932e68d3 | |||
| 4bc59d796c | |||
| a0de94141b | |||
| 61da27c7d5 | |||
| 3e90f9cf21 | |||
| 44e23226b1 | |||
| 5856e6b2aa | |||
| 39b1781f19 | |||
| c48eb1cea7 | |||
| 048278e46b | |||
| 1686f90467 |
54 changed files with 5934 additions and 0 deletions
21
.env.example
Normal file
21
.env.example
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# MoAI Bot Configuration
|
||||||
|
# Copy this file to .env and fill in your values
|
||||||
|
|
||||||
|
# Telegram Bot Token (required)
|
||||||
|
# Get this from @BotFather on Telegram
|
||||||
|
BOT_TOKEN=
|
||||||
|
|
||||||
|
# Allowed Users (optional)
|
||||||
|
# Comma-separated Telegram user IDs that can use the bot
|
||||||
|
# Leave empty to allow all users
|
||||||
|
# To find your user ID, message @userinfobot on Telegram
|
||||||
|
ALLOWED_USERS=
|
||||||
|
|
||||||
|
# Database URL (optional)
|
||||||
|
# Defaults to SQLite: sqlite+aiosqlite:///./moai.db
|
||||||
|
# DATABASE_URL=sqlite+aiosqlite:///./moai.db
|
||||||
|
|
||||||
|
# Log Level (optional)
|
||||||
|
# Options: DEBUG, INFO, WARNING, ERROR
|
||||||
|
# Defaults to INFO
|
||||||
|
# LOG_LEVEL=INFO
|
||||||
73
.planning/PROJECT.md
Normal file
73
.planning/PROJECT.md
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
# MoAI - Master of AIs
|
||||||
|
|
||||||
|
## What This Is
|
||||||
|
|
||||||
|
A multi-AI collaborative brainstorming platform where multiple AI models (Claude, GPT, Gemini) discuss topics together, see each other's responses, and work toward consensus. Phase 1 is a Telegram bot for personal use; Phase 2 adds a web UI; future phases enable lightweight SaaS with multi-user collaboration.
|
||||||
|
|
||||||
|
## Core Value
|
||||||
|
|
||||||
|
Get richer, more diverse AI insights through structured multi-model discussions—ask a team of AIs instead of just one.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Validated
|
||||||
|
|
||||||
|
(None yet — ship to validate)
|
||||||
|
|
||||||
|
### Active
|
||||||
|
|
||||||
|
- [ ] Project scaffolding (pyproject.toml, ruff, pre-commit, src layout)
|
||||||
|
- [ ] M1: Bot responds to /help, /status
|
||||||
|
- [ ] M2: Project CRUD (/projects, /project new, select, delete, models, info)
|
||||||
|
- [ ] M3: Single model Q&A working
|
||||||
|
- [ ] M4: Open mode (parallel) with multiple models
|
||||||
|
- [ ] M5: Discuss mode (sequential rounds)
|
||||||
|
- [ ] M6: Consensus generation (/consensus)
|
||||||
|
- [ ] M7: Export to markdown (/export)
|
||||||
|
- [ ] M8: @mention direct messages
|
||||||
|
|
||||||
|
### Out of Scope
|
||||||
|
|
||||||
|
- Web UI — Phase 2, after Telegram POC is validated
|
||||||
|
- Multi-user collaboration — Phase 3 future
|
||||||
|
- Personas (optimist/critic/pragmatist modes) — future enhancement
|
||||||
|
- Voting/tallying — future enhancement
|
||||||
|
- Cross-project memory — future enhancement
|
||||||
|
- Automated triggers/webhooks — future enhancement
|
||||||
|
- Voice memo transcription — future enhancement
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
**SPEC.md contains:**
|
||||||
|
- Full architecture diagram (Telegram → Python backend → Requesty/OpenRouter → AI APIs)
|
||||||
|
- Complete data model (Project, Discussion, Round, Message, Consensus)
|
||||||
|
- All Telegram commands with syntax
|
||||||
|
- System prompts for models and consensus detection
|
||||||
|
- Export markdown format
|
||||||
|
- File structure specification
|
||||||
|
|
||||||
|
**Current state:** Greenfield. Only documentation exists (SPEC.md, README.md, CLAUDE.md).
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- **Python version**: 3.11+ — required for modern async patterns
|
||||||
|
- **Bot framework**: python-telegram-bot (async) — spec requirement
|
||||||
|
- **Database**: SQLAlchemy + SQLite — upgrades to PostgreSQL in Phase 2
|
||||||
|
- **AI routing**: Modular abstraction layer — Requesty first, support OpenRouter and others
|
||||||
|
- **Linting**: ruff (line length 100) — enforced via pre-commit
|
||||||
|
- **Testing**: pytest, 80%+ coverage on core logic
|
||||||
|
- **Type hints**: Required on all public functions
|
||||||
|
- **Docstrings**: Required on modules and classes
|
||||||
|
- **Logging**: logging module only, no print()
|
||||||
|
- **Dependencies**: Unpinned unless security requires it
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
| Decision | Rationale | Outcome |
|
||||||
|
|----------|-----------|---------|
|
||||||
|
| AI client as abstraction layer | Support Requesty, OpenRouter, direct APIs without changing core code | — Pending |
|
||||||
|
| Full project scaffolding first | Consistent tooling from day one; prevents tech debt | — Pending |
|
||||||
|
| User allowlist auth (Phase 1) | Simple for single-user POC, each user brings own AI credentials later | — Pending |
|
||||||
|
|
||||||
|
---
|
||||||
|
*Last updated: 2026-01-16 after initialization*
|
||||||
81
.planning/ROADMAP.md
Normal file
81
.planning/ROADMAP.md
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
# Roadmap: MoAI
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Build a Telegram bot where multiple AI models (Claude, GPT, Gemini) collaborate on discussions. Start with project scaffolding and tooling, add bot infrastructure, then layer in project management, single-model queries, multi-model discussions, and finally consensus/export features.
|
||||||
|
|
||||||
|
## Domain Expertise
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
## Phases
|
||||||
|
|
||||||
|
**Phase Numbering:**
|
||||||
|
- Integer phases (1, 2, 3): Planned milestone work
|
||||||
|
- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED)
|
||||||
|
|
||||||
|
- [x] **Phase 1: Foundation** - Project scaffolding, tooling, database models
|
||||||
|
- [x] **Phase 2: Bot Core** - Telegram bot setup, /help, /status (M1)
|
||||||
|
- [x] **Phase 3: Project CRUD** - Project management commands (M2)
|
||||||
|
- [x] **Phase 4: Single Model Q&A** - AI client abstraction, basic queries (M3)
|
||||||
|
- [x] **Phase 5: Multi-Model Discussions** - Open mode, discuss mode, @mentions (M4, M5, M8)
|
||||||
|
- [ ] **Phase 6: Consensus & Export** - Consensus generation, markdown export (M6, M7)
|
||||||
|
|
||||||
|
## Phase Details
|
||||||
|
|
||||||
|
### Phase 1: Foundation ✓
|
||||||
|
**Goal**: Complete project scaffolding with pyproject.toml, ruff, pre-commit, src layout, and SQLAlchemy models
|
||||||
|
**Depends on**: Nothing (first phase)
|
||||||
|
**Research**: Unlikely (established patterns)
|
||||||
|
**Plans**: 3 (01-01 scaffolding, 01-02 models, 01-03 database & tests)
|
||||||
|
**Completed**: 2026-01-16
|
||||||
|
|
||||||
|
### Phase 2: Bot Core ✓
|
||||||
|
**Goal**: Working Telegram bot responding to /help and /status commands
|
||||||
|
**Depends on**: Phase 1
|
||||||
|
**Research**: Likely (python-telegram-bot async patterns)
|
||||||
|
**Research topics**: python-telegram-bot v20+ async API, Application builder, handler registration
|
||||||
|
**Plans**: 2 (02-01 infrastructure, 02-02 help/status commands)
|
||||||
|
**Completed**: 2026-01-16
|
||||||
|
|
||||||
|
### Phase 3: Project CRUD ✓
|
||||||
|
**Goal**: Full project management via Telegram (/projects, /project new/select/delete/models/info)
|
||||||
|
**Depends on**: Phase 2
|
||||||
|
**Research**: Unlikely (standard CRUD with established patterns)
|
||||||
|
**Plans**: 3 (03-01 service & list/create, 03-02 select/info, 03-03 delete/models)
|
||||||
|
**Completed**: 2026-01-16
|
||||||
|
|
||||||
|
### Phase 4: Single Model Q&A ✓
|
||||||
|
**Goal**: Query a single AI model through the bot with abstracted AI client layer
|
||||||
|
**Depends on**: Phase 3
|
||||||
|
**Research**: Likely (external AI API integration)
|
||||||
|
**Research topics**: Requesty API documentation, OpenRouter API, async HTTP patterns with httpx/aiohttp
|
||||||
|
**Plans**: 2 (04-01 AI client, 04-02 /ask command)
|
||||||
|
**Completed**: 2026-01-16
|
||||||
|
|
||||||
|
### Phase 5: Multi-Model Discussions ✓
|
||||||
|
**Goal**: Open mode (parallel), discuss mode (sequential rounds), and @mention direct messages
|
||||||
|
**Depends on**: Phase 4
|
||||||
|
**Research**: Unlikely (builds on Phase 4 AI client patterns)
|
||||||
|
**Plans**: 4 (05-01 discussion service, 05-02 open mode, 05-03 discuss mode, 05-04 mentions)
|
||||||
|
**Completed**: 2026-01-16
|
||||||
|
|
||||||
|
### Phase 6: Consensus & Export
|
||||||
|
**Goal**: Consensus generation from discussions and markdown export
|
||||||
|
**Depends on**: Phase 5
|
||||||
|
**Research**: Unlikely (internal patterns, markdown generation)
|
||||||
|
**Plans**: TBD
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
**Execution Order:**
|
||||||
|
Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6
|
||||||
|
|
||||||
|
| Phase | Plans Complete | Status | Completed |
|
||||||
|
|-------|----------------|--------|-----------|
|
||||||
|
| 1. Foundation | 3/3 | Complete | 2026-01-16 |
|
||||||
|
| 2. Bot Core | 2/2 | Complete | 2026-01-16 |
|
||||||
|
| 3. Project CRUD | 3/3 | Complete | 2026-01-16 |
|
||||||
|
| 4. Single Model Q&A | 2/2 | Complete | 2026-01-16 |
|
||||||
|
| 5. Multi-Model Discussions | 4/4 | Complete | 2026-01-16 |
|
||||||
|
| 6. Consensus & Export | 0/TBD | Not started | - |
|
||||||
87
.planning/STATE.md
Normal file
87
.planning/STATE.md
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
# Project State
|
||||||
|
|
||||||
|
## Project Reference
|
||||||
|
|
||||||
|
See: .planning/PROJECT.md (updated 2026-01-16)
|
||||||
|
|
||||||
|
**Core value:** Get richer, more diverse AI insights through structured multi-model discussions—ask a team of AIs instead of just one.
|
||||||
|
**Current focus:** Phase 6 — Consensus & Export (next)
|
||||||
|
|
||||||
|
## Current Position
|
||||||
|
|
||||||
|
Phase: 5 of 6 (Multi-Model Discussions)
|
||||||
|
Plan: 4 of 4 in current phase
|
||||||
|
Status: Phase complete
|
||||||
|
Last activity: 2026-01-16 — Completed 05-04-PLAN.md (mention mode)
|
||||||
|
|
||||||
|
Progress: █████████░ ~85%
|
||||||
|
|
||||||
|
## Performance Metrics
|
||||||
|
|
||||||
|
**Velocity:**
|
||||||
|
- Total plans completed: 14
|
||||||
|
- Average duration: 4 min
|
||||||
|
- Total execution time: 0.95 hours
|
||||||
|
|
||||||
|
**By Phase:**
|
||||||
|
|
||||||
|
| Phase | Plans | Total | Avg/Plan |
|
||||||
|
|-------|-------|-------|----------|
|
||||||
|
| 01-foundation | 3 | 15 min | 5 min |
|
||||||
|
| 02-bot-core | 2 | 4 min | 2 min |
|
||||||
|
| 03-project-crud | 3 | 11 min | 4 min |
|
||||||
|
| 04-single-model-qa | 2 | 10 min | 5 min |
|
||||||
|
| 05-multi-model | 4 | 18 min | 5 min |
|
||||||
|
|
||||||
|
**Recent Trend:**
|
||||||
|
- Last 5 plans: 05-01 (2 min), 05-02 (3 min), 05-03 (5 min), 05-04 (8 min)
|
||||||
|
- Trend: Fast
|
||||||
|
|
||||||
|
## Accumulated Context
|
||||||
|
|
||||||
|
### Decisions
|
||||||
|
|
||||||
|
Decisions are logged in PROJECT.md Key Decisions table.
|
||||||
|
Recent decisions affecting current work:
|
||||||
|
|
||||||
|
- **01-01:** hatchling as build backend with explicit src layout config
|
||||||
|
- **01-01:** ruff-pre-commit v0.14.13 with --fix for auto-corrections
|
||||||
|
- **01-02:** String(36) for UUID storage (SQLite compatibility)
|
||||||
|
- **01-02:** JSON type for list/dict fields (no ARRAY for SQLite)
|
||||||
|
- **01-03:** expire_on_commit=False for async session usability
|
||||||
|
- **01-03:** Module-level globals for engine/session factory (simple singleton)
|
||||||
|
- **02-01:** Module-level config reference for post_init callback access
|
||||||
|
- **02-01:** Config stored in bot_data for handler access
|
||||||
|
- **02-02:** Markdown parse_mode for formatted help text
|
||||||
|
- **02-02:** Placeholder status until project CRUD in Phase 3
|
||||||
|
- **03-01:** Service layer pattern (core/services/) for database operations
|
||||||
|
- **03-01:** Single /project handler with subcommand parsing
|
||||||
|
- **03-02:** Case-insensitive name matching with ilike
|
||||||
|
- **03-02:** user_data dict for storing selected_project_id
|
||||||
|
- **03-03:** Explicit project ID required for delete (safety)
|
||||||
|
- **03-03:** Comma-separated model list parsing
|
||||||
|
- **04-01:** OpenAI SDK for router abstraction (Requesty/OpenRouter compatible)
|
||||||
|
- **04-01:** Module-level singleton for AI client (matches database pattern)
|
||||||
|
- **04-02:** AI client initialized in post_init alongside database
|
||||||
|
- **04-02:** Typing indicator shown while waiting for AI response
|
||||||
|
- **05-02:** asyncio.gather for parallel model queries with graceful per-model error handling
|
||||||
|
- **05-02:** SYSTEM_PROMPT includes participant list and topic for roundtable context
|
||||||
|
- **05-03:** Sequential model execution with for-loop so each model sees prior responses
|
||||||
|
- **05-03:** Context stored in user_data["discussion_state"] for multi-command flows
|
||||||
|
- **05-04:** Direct messages prefix with "[Direct to you]:" for model awareness
|
||||||
|
- **05-04:** MessageHandler registered AFTER CommandHandlers for correct priority
|
||||||
|
- **05-04:** @mentions persist with is_direct=True in current round
|
||||||
|
|
||||||
|
### Deferred Issues
|
||||||
|
|
||||||
|
None yet.
|
||||||
|
|
||||||
|
### Blockers/Concerns
|
||||||
|
|
||||||
|
None yet.
|
||||||
|
|
||||||
|
## Session Continuity
|
||||||
|
|
||||||
|
Last session: 2026-01-16T19:58:00Z
|
||||||
|
Stopped at: Completed 05-04-PLAN.md (mention mode) - Phase 5 complete
|
||||||
|
Resume file: None
|
||||||
18
.planning/config.json
Normal file
18
.planning/config.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"mode": "yolo",
|
||||||
|
"depth": "standard",
|
||||||
|
"gates": {
|
||||||
|
"confirm_project": false,
|
||||||
|
"confirm_phases": false,
|
||||||
|
"confirm_roadmap": false,
|
||||||
|
"confirm_breakdown": false,
|
||||||
|
"confirm_plan": false,
|
||||||
|
"execute_next_plan": false,
|
||||||
|
"issues_review": false,
|
||||||
|
"confirm_transition": false
|
||||||
|
},
|
||||||
|
"safety": {
|
||||||
|
"always_confirm_destructive": true,
|
||||||
|
"always_confirm_external_services": true
|
||||||
|
}
|
||||||
|
}
|
||||||
132
.planning/phases/01-foundation/01-01-PLAN.md
Normal file
132
.planning/phases/01-foundation/01-01-PLAN.md
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
---
|
||||||
|
phase: 01-foundation
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Set up project scaffolding with pyproject.toml, ruff, pre-commit, and src layout.
|
||||||
|
|
||||||
|
Purpose: Establish consistent tooling from day one—linting, formatting, testing infrastructure.
|
||||||
|
Output: Working Python project structure with `uv sync` installing all deps, ruff/pre-commit configured.
|
||||||
|
</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
|
||||||
|
@SPEC.md
|
||||||
|
@CLAUDE.md
|
||||||
|
|
||||||
|
**Constraints from PROJECT.md:**
|
||||||
|
- Python 3.11+
|
||||||
|
- ruff (line length 100)
|
||||||
|
- pytest, 80%+ coverage on core logic
|
||||||
|
- Type hints required on public functions
|
||||||
|
- Docstrings required on modules and classes
|
||||||
|
- Dependencies unpinned unless security required
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create pyproject.toml with dependencies and tool config</name>
|
||||||
|
<files>pyproject.toml</files>
|
||||||
|
<action>
|
||||||
|
Create pyproject.toml with:
|
||||||
|
|
||||||
|
**[project] section:**
|
||||||
|
- name = "moai"
|
||||||
|
- version = "0.1.0"
|
||||||
|
- description = "Multi-AI collaborative brainstorming platform"
|
||||||
|
- requires-python = ">=3.11"
|
||||||
|
- dependencies: python-telegram-bot, sqlalchemy, httpx, aiosqlite (for async SQLite)
|
||||||
|
|
||||||
|
**[project.optional-dependencies]:**
|
||||||
|
- dev: pytest, pytest-cov, pytest-asyncio, ruff, pre-commit
|
||||||
|
|
||||||
|
**[tool.ruff] section:**
|
||||||
|
- line-length = 100
|
||||||
|
- target-version = "py311"
|
||||||
|
|
||||||
|
**[tool.ruff.lint]:**
|
||||||
|
- select = ["E", "F", "I", "N", "W", "UP"]
|
||||||
|
|
||||||
|
**[tool.pytest.ini_options]:**
|
||||||
|
- testpaths = ["tests"]
|
||||||
|
- asyncio_mode = "auto"
|
||||||
|
|
||||||
|
**[build-system]:**
|
||||||
|
- requires = ["hatchling"]
|
||||||
|
- build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
Use hatchling as build backend (modern, works well with uv).
|
||||||
|
Do NOT pin dependency versions.
|
||||||
|
</action>
|
||||||
|
<verify>uv sync completes without errors</verify>
|
||||||
|
<done>pyproject.toml valid, all dependencies installable</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Create pre-commit configuration</name>
|
||||||
|
<files>.pre-commit-config.yaml</files>
|
||||||
|
<action>
|
||||||
|
Create .pre-commit-config.yaml with:
|
||||||
|
|
||||||
|
**Hooks:**
|
||||||
|
1. ruff (linting): repo = https://github.com/astral-sh/ruff-pre-commit, hooks = [ruff, ruff-format]
|
||||||
|
2. Standard pre-commit hooks: trailing-whitespace, end-of-file-fixer, check-yaml
|
||||||
|
|
||||||
|
Use latest rev for ruff-pre-commit (check GitHub for current version, approximately v0.11.x).
|
||||||
|
|
||||||
|
Do NOT add pytest hook—running tests on every commit is too slow. Tests run manually or in CI.
|
||||||
|
</action>
|
||||||
|
<verify>pre-commit install && pre-commit run --all-files passes</verify>
|
||||||
|
<done>Pre-commit hooks installed and passing</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Create src layout and package structure</name>
|
||||||
|
<files>src/moai/__init__.py, src/moai/bot/__init__.py, src/moai/bot/handlers/__init__.py, src/moai/core/__init__.py, tests/__init__.py</files>
|
||||||
|
<action>
|
||||||
|
Create directory structure per SPEC.md:
|
||||||
|
|
||||||
|
src/moai/__init__.py - Package marker with __version__ = "0.1.0"
|
||||||
|
src/moai/bot/__init__.py - Bot subpackage marker (docstring: "Telegram bot handlers and entry point")
|
||||||
|
src/moai/bot/handlers/__init__.py - Handlers subpackage marker
|
||||||
|
src/moai/core/__init__.py - Core subpackage marker (docstring: "Core business logic, models, and services")
|
||||||
|
tests/__init__.py - Test package marker
|
||||||
|
|
||||||
|
Each __init__.py should have a module docstring describing its purpose.
|
||||||
|
Use triple-quoted docstrings at the top of each file.
|
||||||
|
</action>
|
||||||
|
<verify>python -c "import moai; print(moai.__version__)" prints "0.1.0"</verify>
|
||||||
|
<done>Package importable, structure matches SPEC.md</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `uv sync` succeeds without errors
|
||||||
|
- [ ] `pre-commit run --all-files` passes
|
||||||
|
- [ ] `python -c "import moai"` succeeds
|
||||||
|
- [ ] `ruff check src tests` passes
|
||||||
|
- [ ] Directory structure matches SPEC.md file structure
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
|
||||||
|
- All tasks completed
|
||||||
|
- All verification checks pass
|
||||||
|
- No linting errors
|
||||||
|
- Package installable and importable
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
119
.planning/phases/01-foundation/01-01-SUMMARY.md
Normal file
119
.planning/phases/01-foundation/01-01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
---
|
||||||
|
phase: 01-foundation
|
||||||
|
plan: 01
|
||||||
|
subsystem: infra
|
||||||
|
tags: [python, ruff, pre-commit, hatchling, pytest]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires: []
|
||||||
|
provides:
|
||||||
|
- Python project structure with src layout
|
||||||
|
- pyproject.toml with hatchling build backend
|
||||||
|
- ruff linting and formatting configuration
|
||||||
|
- pre-commit hooks for code quality
|
||||||
|
- pytest configuration with async support
|
||||||
|
affects: [02-bot-core, all-future-phases]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: [python-telegram-bot, sqlalchemy, httpx, aiosqlite, pytest, pytest-cov, pytest-asyncio, ruff, pre-commit, hatchling]
|
||||||
|
patterns: [src-layout, editable-install]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- pyproject.toml
|
||||||
|
- .pre-commit-config.yaml
|
||||||
|
- src/moai/__init__.py
|
||||||
|
- src/moai/bot/__init__.py
|
||||||
|
- src/moai/bot/handlers/__init__.py
|
||||||
|
- src/moai/core/__init__.py
|
||||||
|
- tests/__init__.py
|
||||||
|
modified: []
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "hatchling as build backend (modern, works well with uv/pip)"
|
||||||
|
- "src layout with tool.hatch.build.targets.wheel.packages configuration"
|
||||||
|
- "ruff-pre-commit v0.14.13 with --fix flag for auto-corrections"
|
||||||
|
- "pre-commit-hooks v5.0.0 for standard file hygiene"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "src layout: all source code under src/moai/"
|
||||||
|
- "Module docstrings: required on all __init__.py files"
|
||||||
|
- "Version in __init__.py: moai.__version__ for programmatic access"
|
||||||
|
|
||||||
|
issues-created: []
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 8min
|
||||||
|
completed: 2026-01-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 1, Plan 1: Project Scaffolding Summary
|
||||||
|
|
||||||
|
**Python project scaffolding with pyproject.toml (hatchling), ruff linting, pre-commit hooks, and src layout structure**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 8 min
|
||||||
|
- **Started:** 2026-01-16T15:00:00Z
|
||||||
|
- **Completed:** 2026-01-16T15:08:00Z
|
||||||
|
- **Tasks:** 3
|
||||||
|
- **Files created:** 7
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
- Created pyproject.toml with all dependencies and tool configurations
|
||||||
|
- Configured pre-commit with ruff linting/formatting and standard hygiene hooks
|
||||||
|
- Established src layout package structure matching SPEC.md architecture
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Create pyproject.toml** - `39b1781` (build)
|
||||||
|
2. **Task 2: Create pre-commit configuration** - `5856e6b` (chore)
|
||||||
|
3. **Task 3: Create src layout and package structure** - `44e2322` (feat)
|
||||||
|
|
||||||
|
**Plan metadata:** (pending)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
- `pyproject.toml` - Project metadata, dependencies, ruff/pytest config
|
||||||
|
- `.pre-commit-config.yaml` - Pre-commit hooks configuration
|
||||||
|
- `src/moai/__init__.py` - Package root with __version__
|
||||||
|
- `src/moai/bot/__init__.py` - Bot subpackage marker
|
||||||
|
- `src/moai/bot/handlers/__init__.py` - Handlers subpackage marker
|
||||||
|
- `src/moai/core/__init__.py` - Core subpackage marker
|
||||||
|
- `tests/__init__.py` - Test package marker
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
- Used hatchling with explicit `packages = ["src/moai"]` for src layout (required for editable install)
|
||||||
|
- Selected ruff-pre-commit v0.14.13 (latest stable as of 2026-01-16)
|
||||||
|
- Used pre-commit-hooks v5.0.0 for standard hooks
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Rule 3 - Blocking] Added hatch build configuration for src layout**
|
||||||
|
- **Found during:** Task 1 (pyproject.toml creation)
|
||||||
|
- **Issue:** Hatchling couldn't find package without explicit src layout config
|
||||||
|
- **Fix:** Added `[tool.hatch.build.targets.wheel] packages = ["src/moai"]`
|
||||||
|
- **Files modified:** pyproject.toml
|
||||||
|
- **Verification:** pip install -e ".[dev]" succeeds
|
||||||
|
- **Committed in:** 39b1781 (Task 1 commit)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Total deviations:** 1 auto-fixed (blocking issue), 0 deferred
|
||||||
|
**Impact on plan:** Auto-fix was necessary for hatchling to work with src layout. No scope creep.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
None - all tasks completed successfully after the hatchling configuration fix.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
- Project scaffolding complete, ready for Phase 1 Plan 2 (database models)
|
||||||
|
- All tooling in place: ruff, pre-commit, pytest
|
||||||
|
- Package importable and version accessible
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 01-foundation*
|
||||||
|
*Completed: 2026-01-16*
|
||||||
163
.planning/phases/01-foundation/01-02-PLAN.md
Normal file
163
.planning/phases/01-foundation/01-02-PLAN.md
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
---
|
||||||
|
phase: 01-foundation
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create SQLAlchemy models for Project, Discussion, Round, Message, and Consensus.
|
||||||
|
|
||||||
|
Purpose: Define the data model that powers all discussion features.
|
||||||
|
Output: Complete models.py with all relationships, ready for database creation.
|
||||||
|
</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
|
||||||
|
@SPEC.md
|
||||||
|
@CLAUDE.md
|
||||||
|
|
||||||
|
**Data model from SPEC.md:**
|
||||||
|
```
|
||||||
|
Project (has many) -> Discussion (has many) -> Round (has many) -> Message
|
||||||
|
\-> Discussion (has one) -> Consensus
|
||||||
|
```
|
||||||
|
|
||||||
|
**Field specifications:**
|
||||||
|
- Project: id (uuid), name, created_at, updated_at, models (JSON array), settings (JSON)
|
||||||
|
- Discussion: id, project_id (FK), question, type (open|discuss), status (active|completed), created_at
|
||||||
|
- Round: id, discussion_id (FK), round_number, type (parallel|sequential)
|
||||||
|
- Message: id, round_id (FK), model, content, timestamp, is_direct
|
||||||
|
- Consensus: id, discussion_id (FK), agreements (JSON array), disagreements (JSON array), generated_at, generated_by
|
||||||
|
|
||||||
|
**Constraints:**
|
||||||
|
- Use SQLAlchemy 2.0 style (mapped_column, DeclarativeBase)
|
||||||
|
- Type hints required
|
||||||
|
- SQLite compatible (use JSON type, not ARRAY)
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create base model and enums</name>
|
||||||
|
<files>src/moai/core/models.py</files>
|
||||||
|
<action>
|
||||||
|
Create src/moai/core/models.py with:
|
||||||
|
|
||||||
|
**Imports:** SQLAlchemy 2.0 style (DeclarativeBase, Mapped, mapped_column, relationship), uuid, datetime, enum
|
||||||
|
|
||||||
|
**Base class:**
|
||||||
|
- class Base(DeclarativeBase): pass
|
||||||
|
|
||||||
|
**Enums (use Python Enum, store as string):**
|
||||||
|
- DiscussionType: OPEN = "open", DISCUSS = "discuss"
|
||||||
|
- DiscussionStatus: ACTIVE = "active", COMPLETED = "completed"
|
||||||
|
- RoundType: PARALLEL = "parallel", SEQUENTIAL = "sequential"
|
||||||
|
|
||||||
|
**UUID helper:**
|
||||||
|
- Use uuid4() for default IDs
|
||||||
|
- Store as String(36) for SQLite compatibility (NOT native UUID type)
|
||||||
|
|
||||||
|
Add module docstring explaining the data model.
|
||||||
|
</action>
|
||||||
|
<verify>python -c "from moai.core.models import Base, DiscussionType, DiscussionStatus, RoundType"</verify>
|
||||||
|
<done>Base class and enums importable</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Create Project and Discussion models</name>
|
||||||
|
<files>src/moai/core/models.py</files>
|
||||||
|
<action>
|
||||||
|
Add to models.py:
|
||||||
|
|
||||||
|
**Project model:**
|
||||||
|
- id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))
|
||||||
|
- name: Mapped[str] = mapped_column(String(255))
|
||||||
|
- created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
|
- updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
- models: Mapped[list] = mapped_column(JSON, default=list) - stores ["claude", "gpt", "gemini"]
|
||||||
|
- settings: Mapped[dict] = mapped_column(JSON, default=dict) - stores {default_rounds, consensus_threshold, system_prompt_override}
|
||||||
|
- discussions: Mapped[list["Discussion"]] = relationship(back_populates="project", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
**Discussion model:**
|
||||||
|
- id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))
|
||||||
|
- project_id: Mapped[str] = mapped_column(ForeignKey("project.id"))
|
||||||
|
- question: Mapped[str] = mapped_column(Text)
|
||||||
|
- type: Mapped[DiscussionType] = mapped_column(Enum(DiscussionType))
|
||||||
|
- status: Mapped[DiscussionStatus] = mapped_column(Enum(DiscussionStatus), default=DiscussionStatus.ACTIVE)
|
||||||
|
- created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
|
- project: Mapped["Project"] = relationship(back_populates="discussions")
|
||||||
|
- rounds: Mapped[list["Round"]] = relationship(back_populates="discussion", cascade="all, delete-orphan")
|
||||||
|
- consensus: Mapped["Consensus"] = relationship(back_populates="discussion", uselist=False, cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
Use __tablename__ = "project" and "discussion" (singular, lowercase).
|
||||||
|
</action>
|
||||||
|
<verify>python -c "from moai.core.models import Project, Discussion; print(Project.__tablename__, Discussion.__tablename__)"</verify>
|
||||||
|
<done>Project and Discussion models defined with bidirectional relationships</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Create Round, Message, and Consensus models</name>
|
||||||
|
<files>src/moai/core/models.py</files>
|
||||||
|
<action>
|
||||||
|
Add to models.py:
|
||||||
|
|
||||||
|
**Round model:**
|
||||||
|
- id: Mapped[str] (uuid, primary key)
|
||||||
|
- discussion_id: Mapped[str] = mapped_column(ForeignKey("discussion.id"))
|
||||||
|
- round_number: Mapped[int]
|
||||||
|
- type: Mapped[RoundType] = mapped_column(Enum(RoundType))
|
||||||
|
- discussion: Mapped["Discussion"] = relationship(back_populates="rounds")
|
||||||
|
- messages: Mapped[list["Message"]] = relationship(back_populates="round", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
**Message model:**
|
||||||
|
- id: Mapped[str] (uuid, primary key)
|
||||||
|
- round_id: Mapped[str] = mapped_column(ForeignKey("round.id"))
|
||||||
|
- model: Mapped[str] = mapped_column(String(50)) - e.g., "claude", "gpt", "gemini"
|
||||||
|
- content: Mapped[str] = mapped_column(Text)
|
||||||
|
- timestamp: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
|
- is_direct: Mapped[bool] = mapped_column(Boolean, default=False) - true if @mentioned
|
||||||
|
- round: Mapped["Round"] = relationship(back_populates="messages")
|
||||||
|
|
||||||
|
**Consensus model:**
|
||||||
|
- id: Mapped[str] (uuid, primary key)
|
||||||
|
- discussion_id: Mapped[str] = mapped_column(ForeignKey("discussion.id"), unique=True)
|
||||||
|
- agreements: Mapped[list] = mapped_column(JSON, default=list) - bullet point strings
|
||||||
|
- disagreements: Mapped[list] = mapped_column(JSON, default=list) - [{topic, positions: {model: position}}]
|
||||||
|
- generated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
|
- generated_by: Mapped[str] = mapped_column(String(50)) - which model summarized
|
||||||
|
- discussion: Mapped["Discussion"] = relationship(back_populates="consensus")
|
||||||
|
|
||||||
|
Use __tablename__ = "round", "message", "consensus" (singular, lowercase).
|
||||||
|
</action>
|
||||||
|
<verify>python -c "from moai.core.models import Round, Message, Consensus; print('All models imported')"</verify>
|
||||||
|
<done>All 5 models defined with complete relationships</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `python -c "from moai.core.models import Base, Project, Discussion, Round, Message, Consensus"` succeeds
|
||||||
|
- [ ] `ruff check src/moai/core/models.py` passes
|
||||||
|
- [ ] All foreign keys reference correct tables
|
||||||
|
- [ ] All relationships are bidirectional
|
||||||
|
- [ ] All fields have type hints
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
|
||||||
|
- All tasks completed
|
||||||
|
- All 5 models importable
|
||||||
|
- No linting errors
|
||||||
|
- Relationships match SPEC.md data model diagram
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
95
.planning/phases/01-foundation/01-02-SUMMARY.md
Normal file
95
.planning/phases/01-foundation/01-02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
---
|
||||||
|
phase: 01-foundation
|
||||||
|
plan: 02
|
||||||
|
subsystem: database
|
||||||
|
tags: [sqlalchemy, sqlite, orm, models]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 01-foundation/01-01
|
||||||
|
provides: Python project structure with src layout
|
||||||
|
provides:
|
||||||
|
- SQLAlchemy 2.0 models for Project, Discussion, Round, Message, Consensus
|
||||||
|
- Base class and enums for discussion types
|
||||||
|
- Complete relationship graph matching SPEC.md
|
||||||
|
affects: [03-project-crud, 04-single-model, 05-multi-model, 06-consensus-export]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [sqlalchemy-2.0-declarative, uuid-as-string-36, json-for-arrays]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- src/moai/core/models.py
|
||||||
|
modified: []
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Use String(36) for UUID storage (SQLite compatibility)"
|
||||||
|
- "Store enums as strings via str-based Enum classes"
|
||||||
|
- "Use JSON type for models list and settings dict (no ARRAY for SQLite)"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "SQLAlchemy 2.0 style: Mapped, mapped_column, DeclarativeBase"
|
||||||
|
- "UUID generation via helper function _uuid()"
|
||||||
|
- "Cascade delete-orphan on all parent-child relationships"
|
||||||
|
|
||||||
|
issues-created: []
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 3min
|
||||||
|
completed: 2026-01-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 1, Plan 2: Database Models Summary
|
||||||
|
|
||||||
|
**SQLAlchemy 2.0 models with Project, Discussion, Round, Message, Consensus and bidirectional relationships**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 3 min
|
||||||
|
- **Started:** 2026-01-16T15:06:35Z
|
||||||
|
- **Completed:** 2026-01-16T15:09:42Z
|
||||||
|
- **Tasks:** 3
|
||||||
|
- **Files created:** 1
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
- Created complete SQLAlchemy 2.0 data model matching SPEC.md hierarchy
|
||||||
|
- Defined three enums: DiscussionType (open/discuss), DiscussionStatus (active/completed), RoundType (parallel/sequential)
|
||||||
|
- Established all bidirectional relationships with cascade delete-orphan
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Create base model and enums** - `61da27c` (feat)
|
||||||
|
2. **Task 2+3: Create all entity models** - `a0de941` (feat)
|
||||||
|
|
||||||
|
*Note: Tasks 2 and 3 were committed together because forward references (Round, Consensus) required all models to exist for ruff linting to pass.*
|
||||||
|
|
||||||
|
**Plan metadata:** (pending)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
- `src/moai/core/models.py` - All SQLAlchemy models, enums, and Base class
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
- Used String(36) for UUID storage instead of native UUID type (SQLite compatibility)
|
||||||
|
- Stored enums as strings by inheriting from both str and Enum
|
||||||
|
- Used JSON type for list/dict fields (models, settings, agreements, disagreements)
|
||||||
|
- Made Consensus.discussion_id unique (one consensus per discussion)
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
None - all tasks completed successfully.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
- Database models complete and importable
|
||||||
|
- Ready for Phase 1 Plan 3 (database.py session management) or Phase 2 (Bot Core)
|
||||||
|
- All relationships verified bidirectional
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 01-foundation*
|
||||||
|
*Completed: 2026-01-16*
|
||||||
184
.planning/phases/01-foundation/01-03-PLAN.md
Normal file
184
.planning/phases/01-foundation/01-03-PLAN.md
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
---
|
||||||
|
phase: 01-foundation
|
||||||
|
plan: 03
|
||||||
|
type: execute
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create database module with async session management and write model tests.
|
||||||
|
|
||||||
|
Purpose: Enable database operations and validate models work correctly.
|
||||||
|
Output: Working database.py with async session factory, passing model tests.
|
||||||
|
</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
|
||||||
|
@SPEC.md
|
||||||
|
@CLAUDE.md
|
||||||
|
@src/moai/core/models.py
|
||||||
|
|
||||||
|
**Tech choices:**
|
||||||
|
- SQLAlchemy 2.0 async (create_async_engine, AsyncSession)
|
||||||
|
- aiosqlite for async SQLite
|
||||||
|
- pytest-asyncio for async tests
|
||||||
|
|
||||||
|
**From CLAUDE.md:**
|
||||||
|
- Testing: pytest, target 80%+ coverage on core logic
|
||||||
|
- Database: SQLAlchemy + SQLite, upgrades to PostgreSQL in Phase 2
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create database module with async session management</name>
|
||||||
|
<files>src/moai/core/database.py</files>
|
||||||
|
<action>
|
||||||
|
Create src/moai/core/database.py with:
|
||||||
|
|
||||||
|
**Imports:** sqlalchemy.ext.asyncio (create_async_engine, AsyncSession, async_sessionmaker), contextlib
|
||||||
|
|
||||||
|
**Module-level:**
|
||||||
|
- DATABASE_URL: str = "sqlite+aiosqlite:///./moai.db" (default, can be overridden)
|
||||||
|
- engine: AsyncEngine = None (initialized lazily)
|
||||||
|
- async_session_factory: async_sessionmaker = None
|
||||||
|
|
||||||
|
**Functions:**
|
||||||
|
1. init_db(url: str | None = None) -> None:
|
||||||
|
- Creates engine with echo=False
|
||||||
|
- Creates async_session_factory
|
||||||
|
- Stores in module globals
|
||||||
|
- Use: `create_async_engine(url, echo=False)`
|
||||||
|
|
||||||
|
2. async create_tables() -> None:
|
||||||
|
- Imports Base from models
|
||||||
|
- Runs `async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all)`
|
||||||
|
|
||||||
|
3. @contextlib.asynccontextmanager
|
||||||
|
async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
- Yields session from factory
|
||||||
|
- Handles commit on success, rollback on exception
|
||||||
|
- Pattern: `async with async_session_factory() as session: yield session; await session.commit()`
|
||||||
|
|
||||||
|
4. async def close_db() -> None:
|
||||||
|
- Disposes engine: `await engine.dispose()`
|
||||||
|
|
||||||
|
Add module docstring explaining session management pattern.
|
||||||
|
</action>
|
||||||
|
<verify>python -c "from moai.core.database import init_db, create_tables, get_session, close_db"</verify>
|
||||||
|
<done>Database module importable with all functions</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Create model tests with in-memory database</name>
|
||||||
|
<files>tests/test_models.py</files>
|
||||||
|
<action>
|
||||||
|
Create tests/test_models.py with:
|
||||||
|
|
||||||
|
**Fixtures:**
|
||||||
|
- @pytest.fixture
|
||||||
|
async def db_session():
|
||||||
|
- init_db("sqlite+aiosqlite:///:memory:")
|
||||||
|
- await create_tables()
|
||||||
|
- async with get_session() as session: yield session
|
||||||
|
- await close_db()
|
||||||
|
|
||||||
|
**Test cases:**
|
||||||
|
|
||||||
|
1. test_create_project:
|
||||||
|
- Create Project(name="Test Project", models=["claude", "gpt"])
|
||||||
|
- Add to session, commit
|
||||||
|
- Assert id is set (UUID string), name correct, models correct
|
||||||
|
|
||||||
|
2. test_create_discussion_with_project:
|
||||||
|
- Create Project, add Discussion linked to it
|
||||||
|
- Assert discussion.project_id matches project.id
|
||||||
|
- Assert project.discussions contains the discussion
|
||||||
|
|
||||||
|
3. test_create_full_discussion_chain:
|
||||||
|
- Create Project -> Discussion -> Round -> Message
|
||||||
|
- Verify all relationships work
|
||||||
|
- Verify cascade (all linked when navigating relationships)
|
||||||
|
|
||||||
|
4. test_create_consensus:
|
||||||
|
- Create Discussion with Consensus
|
||||||
|
- Assert discussion.consensus is set
|
||||||
|
- Assert consensus.discussion links back
|
||||||
|
|
||||||
|
5. test_project_cascade_delete:
|
||||||
|
- Create Project with Discussion with Round with Message
|
||||||
|
- Delete Project
|
||||||
|
- Assert all children deleted (cascade)
|
||||||
|
|
||||||
|
Use pytest.mark.asyncio on all async tests.
|
||||||
|
Import all models and database functions.
|
||||||
|
</action>
|
||||||
|
<verify>pytest tests/test_models.py -v passes all tests</verify>
|
||||||
|
<done>5 model tests passing, cascade behavior verified</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Add .gitignore entries and verify full test suite</name>
|
||||||
|
<files>.gitignore</files>
|
||||||
|
<action>
|
||||||
|
Update .gitignore to add:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run full test suite with coverage to verify everything works together.
|
||||||
|
</action>
|
||||||
|
<verify>pytest --cov=moai --cov-report=term-missing shows coverage, all tests pass</verify>
|
||||||
|
<done>.gitignore updated, tests pass with coverage report</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `pytest tests/test_models.py -v` passes all 5 tests
|
||||||
|
- [ ] `pytest --cov=moai --cov-report=term-missing` runs successfully
|
||||||
|
- [ ] `ruff check src tests` passes
|
||||||
|
- [ ] Database file (moai.db) is gitignored
|
||||||
|
- [ ] Phase 1 complete: scaffolding, models, database all working
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
|
||||||
|
- All tasks completed
|
||||||
|
- All tests pass
|
||||||
|
- No linting errors
|
||||||
|
- Phase 1: Foundation complete
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md` with:
|
||||||
|
- Summary of all 3 plans in Phase 1
|
||||||
|
- Final verification that foundation is complete
|
||||||
|
- Ready for Phase 2: Bot Core
|
||||||
|
</output>
|
||||||
106
.planning/phases/01-foundation/01-03-SUMMARY.md
Normal file
106
.planning/phases/01-foundation/01-03-SUMMARY.md
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
---
|
||||||
|
phase: 01-foundation
|
||||||
|
plan: 03
|
||||||
|
subsystem: database
|
||||||
|
tags: [sqlalchemy, async, aiosqlite, pytest, testing]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 01-foundation/01-02
|
||||||
|
provides: SQLAlchemy models (Project, Discussion, Round, Message, Consensus)
|
||||||
|
provides:
|
||||||
|
- Async database session management (init_db, create_tables, get_session, close_db)
|
||||||
|
- In-memory database testing pattern
|
||||||
|
- Model test coverage (5 tests)
|
||||||
|
affects: [02-bot-core, 03-discussion-engine]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: [sqlalchemy.ext.asyncio, aiosqlite]
|
||||||
|
patterns: [async context manager for sessions, in-memory SQLite for tests]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- src/moai/core/database.py
|
||||||
|
- tests/test_models.py
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Session auto-commits on context exit, rollback on exception"
|
||||||
|
- "Module-level globals for engine/session factory (simple singleton pattern)"
|
||||||
|
- "expire_on_commit=False for async session usability"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Database fixture pattern: init_db with in-memory URL, create_tables, yield session, close_db"
|
||||||
|
- "Relationship testing via refresh() for lazy-loaded collections"
|
||||||
|
|
||||||
|
issues-created: []
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 4min
|
||||||
|
completed: 2026-01-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 01-03: Database & Tests Summary
|
||||||
|
|
||||||
|
**Async SQLAlchemy session management with in-memory test fixture and 5 model tests at 95% coverage**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 4 min
|
||||||
|
- **Started:** 2026-01-16T15:13:19Z
|
||||||
|
- **Completed:** 2026-01-16T15:17:06Z
|
||||||
|
- **Tasks:** 3 (2 with commits, 1 verification-only)
|
||||||
|
- **Files modified:** 2
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
- Created database.py with async session factory and context manager
|
||||||
|
- Implemented 5 comprehensive model tests covering creation, relationships, and cascades
|
||||||
|
- Achieved 95% code coverage on core module
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Create database module** - `bb932e6` (feat)
|
||||||
|
2. **Task 2: Create model tests** - `fb81fea` (test)
|
||||||
|
3. **Task 3: Verify gitignore and test suite** - No commit needed (entries already present)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
- `src/moai/core/database.py` - Async session management with init_db, create_tables, get_session, close_db
|
||||||
|
- `tests/test_models.py` - 5 tests: create_project, create_discussion_with_project, create_full_discussion_chain, create_consensus, project_cascade_delete
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
- Used `expire_on_commit=False` in session factory to allow attribute access after commit without re-query
|
||||||
|
- Session context manager handles commit/rollback automatically
|
||||||
|
- Module-level globals for engine/factory simplifies dependency injection for this Phase 1 scope
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
**Note:** Task 3 required no commit because `.gitignore` already contained all specified entries from a previous setup phase.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
None
|
||||||
|
|
||||||
|
## Test Results
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/test_models.py::test_create_project PASSED
|
||||||
|
tests/test_models.py::test_create_discussion_with_project PASSED
|
||||||
|
tests/test_models.py::test_create_full_discussion_chain PASSED
|
||||||
|
tests/test_models.py::test_create_consensus PASSED
|
||||||
|
tests/test_models.py::test_project_cascade_delete PASSED
|
||||||
|
|
||||||
|
Coverage: 95% (101 statements, 5 missing)
|
||||||
|
Missing: error handling paths in database.py (lines 62, 85, 91-93)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
- Foundation phase complete: scaffolding, models, database all working
|
||||||
|
- Ready for Phase 2: Bot Core (handlers, middleware, bot setup)
|
||||||
|
- All core infrastructure tested and operational
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 01-foundation*
|
||||||
|
*Completed: 2026-01-16*
|
||||||
167
.planning/phases/02-bot-core/02-01-PLAN.md
Normal file
167
.planning/phases/02-bot-core/02-01-PLAN.md
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
---
|
||||||
|
phase: 02-bot-core
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Set up Telegram bot infrastructure with Application builder, config loading, and handler registration pattern.
|
||||||
|
|
||||||
|
Purpose: Establish the bot entry point and configuration loading so handlers can be added incrementally.
|
||||||
|
Output: Working bot main.py that starts, loads config, and registers handlers (empty initially).
|
||||||
|
</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/phases/01-foundation/01-03-SUMMARY.md
|
||||||
|
@src/moai/core/database.py
|
||||||
|
@pyproject.toml
|
||||||
|
|
||||||
|
**Tech stack available:**
|
||||||
|
- SQLAlchemy async with aiosqlite (from Phase 1)
|
||||||
|
- python-telegram-bot (in dependencies, not yet used)
|
||||||
|
|
||||||
|
**Established patterns:**
|
||||||
|
- Async context manager for sessions
|
||||||
|
- Module-level globals for engine/session factory
|
||||||
|
|
||||||
|
**Constraining decisions:**
|
||||||
|
- Phase 1: Module-level globals for database (simple singleton)
|
||||||
|
- Phase 1: expire_on_commit=False for async sessions
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create bot configuration module</name>
|
||||||
|
<files>src/moai/bot/config.py</files>
|
||||||
|
<action>
|
||||||
|
Create config.py that loads bot configuration from environment variables:
|
||||||
|
- BOT_TOKEN (required): Telegram bot token
|
||||||
|
- ALLOWED_USERS (optional): Comma-separated list of Telegram user IDs for allowlist auth
|
||||||
|
- DATABASE_URL (optional): Database URL, defaults to sqlite+aiosqlite:///./moai.db
|
||||||
|
- LOG_LEVEL (optional): Logging level, defaults to INFO
|
||||||
|
|
||||||
|
Use pydantic-settings or simple os.environ with dataclass. Keep it simple - use dataclass with classmethod from_env().
|
||||||
|
Raise ValueError if BOT_TOKEN is missing.
|
||||||
|
|
||||||
|
Do NOT use pydantic-settings - it adds a dependency. Use stdlib dataclass + os.environ.
|
||||||
|
</action>
|
||||||
|
<verify>python -c "from moai.bot.config import BotConfig; print('Config module loads')"</verify>
|
||||||
|
<done>BotConfig dataclass exists with from_env() classmethod, raises on missing BOT_TOKEN</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Create bot main.py with Application setup</name>
|
||||||
|
<files>src/moai/bot/main.py</files>
|
||||||
|
<action>
|
||||||
|
Create main.py as the bot entry point using python-telegram-bot v21+ patterns:
|
||||||
|
|
||||||
|
1. Import ApplicationBuilder from telegram.ext
|
||||||
|
2. Load config via BotConfig.from_env()
|
||||||
|
3. Create Application with ApplicationBuilder().token(config.bot_token).build()
|
||||||
|
4. Add post_init callback to initialize database (init_db, create_tables)
|
||||||
|
5. Add post_shutdown callback to close database (close_db)
|
||||||
|
6. Import and register handlers from handlers/ (empty for now, will add in 02-02)
|
||||||
|
7. Call app.run_polling()
|
||||||
|
|
||||||
|
Structure:
|
||||||
|
```python
|
||||||
|
import logging
|
||||||
|
from telegram.ext import ApplicationBuilder
|
||||||
|
from moai.bot.config import BotConfig
|
||||||
|
from moai.core.database import init_db, create_tables, close_db
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def post_init(application):
|
||||||
|
init_db(config.database_url)
|
||||||
|
await create_tables()
|
||||||
|
logger.info("Database initialized")
|
||||||
|
|
||||||
|
async def post_shutdown(application):
|
||||||
|
await close_db()
|
||||||
|
logger.info("Database closed")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
config = BotConfig.from_env()
|
||||||
|
app = (
|
||||||
|
ApplicationBuilder()
|
||||||
|
.token(config.bot_token)
|
||||||
|
.post_init(post_init)
|
||||||
|
.post_shutdown(post_shutdown)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
# Handlers will be registered here in 02-02
|
||||||
|
logger.info("Starting bot...")
|
||||||
|
app.run_polling()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: post_init receives the application as argument. Store config at module level or pass via application.bot_data.
|
||||||
|
</action>
|
||||||
|
<verify>python -c "from moai.bot.main import main; print('Main module loads')" (will fail at runtime without BOT_TOKEN, but import should work)</verify>
|
||||||
|
<done>main.py exists with ApplicationBuilder setup, post_init/post_shutdown hooks for database lifecycle</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Create handlers package structure</name>
|
||||||
|
<files>src/moai/bot/handlers/__init__.py</files>
|
||||||
|
<action>
|
||||||
|
Create handlers/__init__.py with a register_handlers function that takes an Application and registers all handlers.
|
||||||
|
|
||||||
|
For now, it's empty (no handlers yet), but the structure allows 02-02 to add handlers cleanly:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Telegram command handlers for MoAI bot."""
|
||||||
|
|
||||||
|
from telegram.ext import Application
|
||||||
|
|
||||||
|
def register_handlers(app: Application) -> None:
|
||||||
|
"""Register all command handlers with the application.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: The telegram Application instance.
|
||||||
|
"""
|
||||||
|
# Handlers will be imported and registered here
|
||||||
|
# from moai.bot.handlers import commands
|
||||||
|
# app.add_handler(CommandHandler("help", commands.help_command))
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
Update main.py to call register_handlers(app) before run_polling().
|
||||||
|
</action>
|
||||||
|
<verify>python -c "from moai.bot.handlers import register_handlers; print('Handlers package loads')"</verify>
|
||||||
|
<done>handlers/__init__.py exists with register_handlers function, main.py calls it</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `python -c "from moai.bot.config import BotConfig"` succeeds
|
||||||
|
- [ ] `python -c "from moai.bot.main import main"` succeeds
|
||||||
|
- [ ] `python -c "from moai.bot.handlers import register_handlers"` succeeds
|
||||||
|
- [ ] `ruff check src/moai/bot/` passes
|
||||||
|
- [ ] All new files have docstrings
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- All tasks completed
|
||||||
|
- All verification checks pass
|
||||||
|
- Bot infrastructure ready for handler registration
|
||||||
|
- No TypeScript errors or ruff violations
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/02-bot-core/02-01-SUMMARY.md` using summary template.
|
||||||
|
</output>
|
||||||
94
.planning/phases/02-bot-core/02-01-SUMMARY.md
Normal file
94
.planning/phases/02-bot-core/02-01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
---
|
||||||
|
phase: 02-bot-core
|
||||||
|
plan: 01
|
||||||
|
subsystem: bot
|
||||||
|
tags: [telegram, python-telegram-bot, async, configuration]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 01-foundation/01-03
|
||||||
|
provides: Async database session management (init_db, create_tables, close_db)
|
||||||
|
provides:
|
||||||
|
- Bot entry point with Application lifecycle
|
||||||
|
- Configuration loading from environment
|
||||||
|
- Handler registration pattern
|
||||||
|
affects: [02-02-handlers, 03-project-crud]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: [python-telegram-bot]
|
||||||
|
patterns: [ApplicationBuilder with lifecycle hooks, module-level config singleton]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- src/moai/bot/config.py
|
||||||
|
- src/moai/bot/main.py
|
||||||
|
modified:
|
||||||
|
- src/moai/bot/handlers/__init__.py
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Module-level config reference for post_init callback access"
|
||||||
|
- "Config stored in bot_data for handler access"
|
||||||
|
- "Empty register_handlers as extension point for future handlers"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Bot lifecycle: post_init for DB setup, post_shutdown for cleanup"
|
||||||
|
- "Environment config with BotConfig.from_env() pattern"
|
||||||
|
|
||||||
|
issues-created: []
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 2min
|
||||||
|
completed: 2026-01-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 02-01: Bot Infrastructure Summary
|
||||||
|
|
||||||
|
**Telegram bot entry point with ApplicationBuilder, config loading, and handler registration pattern**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 2 min
|
||||||
|
- **Started:** 2026-01-16T15:34:55Z
|
||||||
|
- **Completed:** 2026-01-16T15:37:27Z
|
||||||
|
- **Tasks:** 3
|
||||||
|
- **Files modified:** 3
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
- BotConfig dataclass loading configuration from environment variables
|
||||||
|
- Bot main.py with ApplicationBuilder and database lifecycle hooks
|
||||||
|
- Handler registration pattern ready for incremental handler addition
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Create bot configuration module** - `4381e12` (feat)
|
||||||
|
2. **Task 2: Create bot main.py with Application setup** - `c3a849b` (feat)
|
||||||
|
3. **Task 3: Create handlers package structure** - `0a81855` (feat)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
- `src/moai/bot/config.py` - BotConfig dataclass with from_env() loading BOT_TOKEN, ALLOWED_USERS, DATABASE_URL, LOG_LEVEL
|
||||||
|
- `src/moai/bot/main.py` - Bot entry point with ApplicationBuilder, post_init/post_shutdown hooks, register_handlers call
|
||||||
|
- `src/moai/bot/handlers/__init__.py` - register_handlers function placeholder for future handler registration
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
- Used module-level `_config` variable for post_init callback to access config (simpler than passing through Application)
|
||||||
|
- Store config in `app.bot_data["config"]` for handlers to access user settings
|
||||||
|
- Keep register_handlers as empty placeholder rather than removing it - cleaner extension point
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
None
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
- Bot infrastructure complete, ready for handler implementation
|
||||||
|
- Ready for 02-02: /help and /status command handlers
|
||||||
|
- Database lifecycle integrated with bot startup/shutdown
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 02-bot-core*
|
||||||
|
*Completed: 2026-01-16*
|
||||||
192
.planning/phases/02-bot-core/02-02-PLAN.md
Normal file
192
.planning/phases/02-bot-core/02-02-PLAN.md
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
---
|
||||||
|
phase: 02-bot-core
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Implement /help and /status command handlers completing M1 milestone.
|
||||||
|
|
||||||
|
Purpose: Get the bot responding to basic commands, proving the infrastructure works end-to-end.
|
||||||
|
Output: Working /help and /status commands that respond in Telegram.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
~/.claude/get-shit-done/workflows/execute-phase.md
|
||||||
|
~/.claude/get-shit-done/templates/summary.md
|
||||||
|
~/.claude/get-shit-done/references/checkpoints.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/phases/02-bot-core/02-01-SUMMARY.md (created in previous plan)
|
||||||
|
@src/moai/bot/main.py
|
||||||
|
@src/moai/bot/config.py
|
||||||
|
@src/moai/bot/handlers/__init__.py
|
||||||
|
@SPEC.md (for command format reference)
|
||||||
|
|
||||||
|
**From SPEC.md:**
|
||||||
|
- /help - Show commands
|
||||||
|
- /status - Show current project/discussion state
|
||||||
|
|
||||||
|
**Tech stack available:**
|
||||||
|
- python-telegram-bot v21+ with ApplicationBuilder
|
||||||
|
- Async handlers with Update, ContextTypes
|
||||||
|
|
||||||
|
**Constraining decisions:**
|
||||||
|
- Phase 2-01: Handlers registered via register_handlers() in handlers/__init__.py
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create commands.py with /help and /start handlers</name>
|
||||||
|
<files>src/moai/bot/handlers/commands.py</files>
|
||||||
|
<action>
|
||||||
|
Create commands.py with help_command and start_command handlers:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Basic command handlers for MoAI bot."""
|
||||||
|
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import ContextTypes
|
||||||
|
|
||||||
|
HELP_TEXT = """
|
||||||
|
*MoAI - Master of AIs*
|
||||||
|
|
||||||
|
Multi-AI collaborative brainstorming platform.
|
||||||
|
|
||||||
|
*Project Commands:*
|
||||||
|
/projects - List all projects
|
||||||
|
/project new "Name" - Create new project
|
||||||
|
/project select <id|name> - Switch to project
|
||||||
|
/project delete <id> - Delete project
|
||||||
|
/project models claude,gpt - Set models
|
||||||
|
/project info - Show current project
|
||||||
|
|
||||||
|
*Discussion Commands:*
|
||||||
|
/open <question> - Ask all models (parallel)
|
||||||
|
/discuss [rounds] - Start discussion (default: 3)
|
||||||
|
/next - Trigger next round
|
||||||
|
/stop - Stop current discussion
|
||||||
|
|
||||||
|
*Output Commands:*
|
||||||
|
/consensus - Generate consensus summary
|
||||||
|
/export - Export project as markdown
|
||||||
|
|
||||||
|
*Utility:*
|
||||||
|
/status - Show current state
|
||||||
|
/help - Show this message
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Handle /start command - welcome message."""
|
||||||
|
await update.message.reply_text(
|
||||||
|
"Welcome to MoAI! 🤖\n\n"
|
||||||
|
"Use /help to see available commands.\n"
|
||||||
|
"Use /project new \"Name\" to create your first project."
|
||||||
|
)
|
||||||
|
|
||||||
|
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Handle /help command - show available commands."""
|
||||||
|
await update.message.reply_text(HELP_TEXT, parse_mode="Markdown")
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Use Markdown parse_mode for formatting. Keep HELP_TEXT as a module constant for easy updates.
|
||||||
|
</action>
|
||||||
|
<verify>python -c "from moai.bot.handlers.commands import help_command, start_command; print('Commands module loads')"</verify>
|
||||||
|
<done>commands.py has start_command and help_command handlers with proper docstrings</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Create status.py with /status handler</name>
|
||||||
|
<files>src/moai/bot/handlers/status.py</files>
|
||||||
|
<action>
|
||||||
|
Create status.py with status_command handler:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Status command handler for MoAI bot."""
|
||||||
|
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import ContextTypes
|
||||||
|
|
||||||
|
async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Handle /status command - show current project/discussion state.
|
||||||
|
|
||||||
|
For now, shows a placeholder since project management isn't implemented yet.
|
||||||
|
Will be expanded in Phase 3 to show actual project state.
|
||||||
|
"""
|
||||||
|
# TODO: Phase 3 - Query actual project/discussion state from database
|
||||||
|
status_text = (
|
||||||
|
"*MoAI Status*\n\n"
|
||||||
|
"Bot: ✅ Online\n"
|
||||||
|
"Database: ✅ Connected\n\n"
|
||||||
|
"_No project selected. Use /project new \"Name\" to create one._"
|
||||||
|
)
|
||||||
|
await update.message.reply_text(status_text, parse_mode="Markdown")
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a placeholder that will be enhanced in Phase 3 when project CRUD is implemented.
|
||||||
|
</action>
|
||||||
|
<verify>python -c "from moai.bot.handlers.status import status_command; print('Status module loads')"</verify>
|
||||||
|
<done>status.py has status_command handler with placeholder implementation</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Register handlers in __init__.py</name>
|
||||||
|
<files>src/moai/bot/handlers/__init__.py</files>
|
||||||
|
<action>
|
||||||
|
Update handlers/__init__.py to import and register all command handlers:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Telegram command handlers for MoAI bot."""
|
||||||
|
|
||||||
|
from telegram.ext import Application, CommandHandler
|
||||||
|
|
||||||
|
from moai.bot.handlers.commands import help_command, start_command
|
||||||
|
from moai.bot.handlers.status import status_command
|
||||||
|
|
||||||
|
|
||||||
|
def register_handlers(app: Application) -> None:
|
||||||
|
"""Register all command handlers with the application.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: The telegram Application instance.
|
||||||
|
"""
|
||||||
|
# Basic commands
|
||||||
|
app.add_handler(CommandHandler("start", start_command))
|
||||||
|
app.add_handler(CommandHandler("help", help_command))
|
||||||
|
|
||||||
|
# Status
|
||||||
|
app.add_handler(CommandHandler("status", status_command))
|
||||||
|
```
|
||||||
|
</action>
|
||||||
|
<verify>python -c "from moai.bot.handlers import register_handlers; from telegram.ext import ApplicationBuilder; print('Handler registration ready')"</verify>
|
||||||
|
<done>register_handlers imports and registers start, help, and status command handlers</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `python -c "from moai.bot.handlers.commands import help_command, start_command"` succeeds
|
||||||
|
- [ ] `python -c "from moai.bot.handlers.status import status_command"` succeeds
|
||||||
|
- [ ] `python -c "from moai.bot.handlers import register_handlers"` succeeds
|
||||||
|
- [ ] `ruff check src/moai/bot/` passes
|
||||||
|
- [ ] All handler files have module and function docstrings
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- All tasks completed
|
||||||
|
- All verification checks pass
|
||||||
|
- /start, /help, and /status handlers implemented
|
||||||
|
- Handlers registered via register_handlers()
|
||||||
|
- M1 milestone requirements met (bot responds to /help, /status)
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/02-bot-core/02-02-SUMMARY.md` using summary template.
|
||||||
|
|
||||||
|
Note: Phase 2 complete after this plan. Ready for Phase 3 (Project CRUD).
|
||||||
|
</output>
|
||||||
103
.planning/phases/02-bot-core/02-02-SUMMARY.md
Normal file
103
.planning/phases/02-bot-core/02-02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
---
|
||||||
|
phase: 02-bot-core
|
||||||
|
plan: 02
|
||||||
|
subsystem: bot
|
||||||
|
tags: [telegram, python-telegram-bot, handlers, commands]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 02-01
|
||||||
|
provides: Bot infrastructure with register_handlers pattern
|
||||||
|
provides:
|
||||||
|
- /start, /help, /status command handlers
|
||||||
|
- M1 milestone complete (bot responds to basic commands)
|
||||||
|
affects: [03-project-crud]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- Async command handlers with Update/ContextTypes
|
||||||
|
- Module-level HELP_TEXT constant for command documentation
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- src/moai/bot/handlers/commands.py
|
||||||
|
- src/moai/bot/handlers/status.py
|
||||||
|
modified:
|
||||||
|
- src/moai/bot/handlers/__init__.py
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Markdown parse_mode for formatted help text"
|
||||||
|
- "Placeholder status until project CRUD in Phase 3"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Command handler pattern: async def xxx_command(update, context) -> None"
|
||||||
|
- "Help text as module constant for maintainability"
|
||||||
|
|
||||||
|
issues-created: []
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 2min
|
||||||
|
completed: 2026-01-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 2 Plan 02: Help/Status Commands Summary
|
||||||
|
|
||||||
|
**/start, /help, /status command handlers implementing M1 milestone**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 2 min
|
||||||
|
- **Started:** 2026-01-16T18:15:28Z
|
||||||
|
- **Completed:** 2026-01-16T18:17:46Z
|
||||||
|
- **Tasks:** 3
|
||||||
|
- **Files modified:** 3
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Implemented /start command with welcome message
|
||||||
|
- Implemented /help command with full command reference (Markdown formatted)
|
||||||
|
- Implemented /status command with placeholder status display
|
||||||
|
- Completed M1 milestone: Bot responds to /help, /status
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Create commands.py with /help and /start handlers** - `98b7182` (feat)
|
||||||
|
2. **Task 2: Create status.py with /status handler** - `cb185e1` (feat)
|
||||||
|
3. **Task 3: Register handlers in __init__.py** - `2a563ef` (feat)
|
||||||
|
|
||||||
|
**Plan metadata:** `ced668a` (docs: complete plan)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `src/moai/bot/handlers/commands.py` - start_command and help_command with HELP_TEXT constant
|
||||||
|
- `src/moai/bot/handlers/status.py` - status_command with placeholder implementation
|
||||||
|
- `src/moai/bot/handlers/__init__.py` - Updated to import and register all handlers
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- Used Markdown parse_mode for formatted help text display
|
||||||
|
- Status shows placeholder until Phase 3 implements actual project state
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
Note: Subagent removed emojis from welcome/status messages per CLAUDE.md guidelines (no emojis unless explicitly requested). This is adherence to project standards, not a deviation.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- Phase 2 complete - all bot core infrastructure in place
|
||||||
|
- M1 milestone achieved: Bot responds to /help, /status
|
||||||
|
- Ready for Phase 3 (Project CRUD)
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 02-bot-core*
|
||||||
|
*Completed: 2026-01-16*
|
||||||
105
.planning/phases/03-project-crud/03-01-PLAN.md
Normal file
105
.planning/phases/03-project-crud/03-01-PLAN.md
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
---
|
||||||
|
phase: 03-project-crud
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create project service layer and implement /projects and /project new commands.
|
||||||
|
|
||||||
|
Purpose: Enable users to list existing projects and create new ones via Telegram.
|
||||||
|
Output: Working project service with list/create functions and corresponding handlers.
|
||||||
|
</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
|
||||||
|
|
||||||
|
# Prior phase context
|
||||||
|
@.planning/phases/02-bot-core/02-02-SUMMARY.md
|
||||||
|
|
||||||
|
# Relevant source files
|
||||||
|
@src/moai/core/models.py
|
||||||
|
@src/moai/core/database.py
|
||||||
|
@src/moai/bot/handlers/__init__.py
|
||||||
|
@src/moai/bot/handlers/commands.py
|
||||||
|
|
||||||
|
**Established patterns:**
|
||||||
|
- SQLAlchemy 2.0 async with get_session() context manager
|
||||||
|
- Handler pattern: async def xxx_command(update, context) -> None
|
||||||
|
- Markdown parse_mode for formatted output
|
||||||
|
|
||||||
|
**Constraining decisions:**
|
||||||
|
- String(36) for UUID storage
|
||||||
|
- JSON type for models list
|
||||||
|
- expire_on_commit=False for async session usability
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create project service module</name>
|
||||||
|
<files>src/moai/core/services/__init__.py, src/moai/core/services/project.py</files>
|
||||||
|
<action>
|
||||||
|
Create services package and project service module with:
|
||||||
|
- list_projects() -> list[Project]: Query all projects ordered by created_at desc
|
||||||
|
- create_project(name: str, models: list[str] | None = None) -> Project: Create and return new project
|
||||||
|
- get_project(project_id: str) -> Project | None: Get project by ID
|
||||||
|
|
||||||
|
Use get_session() context manager for database operations. Return the refreshed objects after commit.
|
||||||
|
Default models list to ["claude", "gpt", "gemini"] if not provided.
|
||||||
|
</action>
|
||||||
|
<verify>python -c "from moai.core.services.project import list_projects, create_project, get_project"</verify>
|
||||||
|
<done>Service module imports without errors, all three functions defined</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Implement /projects and /project new handlers</name>
|
||||||
|
<files>src/moai/bot/handlers/projects.py, src/moai/bot/handlers/__init__.py</files>
|
||||||
|
<action>
|
||||||
|
Create projects.py with:
|
||||||
|
- projects_command: List all projects with name and ID, or "No projects yet" message
|
||||||
|
- project_new_command: Parse quoted name from args (e.g., /project new "My Project"), create project, confirm creation
|
||||||
|
|
||||||
|
Handle edge cases:
|
||||||
|
- /project new without name: Reply "Usage: /project new \"Project Name\""
|
||||||
|
- Empty project list: Reply "No projects yet. Use /project new \"Name\" to create one."
|
||||||
|
|
||||||
|
Register handlers in __init__.py:
|
||||||
|
- CommandHandler("projects", projects_command)
|
||||||
|
- CommandHandler with MessageHandler for "project" + args pattern (use ConversationHandler or simple args parsing)
|
||||||
|
|
||||||
|
For /project subcommands, use a single handler that parses context.args[0] to determine action (new/select/delete/models/info).
|
||||||
|
</action>
|
||||||
|
<verify>ruff check src/moai/bot/handlers/projects.py && python -c "from moai.bot.handlers.projects import projects_command, project_command"</verify>
|
||||||
|
<done>/projects lists projects, /project new "Name" creates project with confirmation message</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `ruff check src/moai/core/services/ src/moai/bot/handlers/projects.py` passes
|
||||||
|
- [ ] `python -c "from moai.core.services.project import list_projects, create_project"` succeeds
|
||||||
|
- [ ] `python -c "from moai.bot.handlers.projects import projects_command, project_command"` succeeds
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
|
||||||
|
- All tasks completed
|
||||||
|
- All verification checks pass
|
||||||
|
- No TypeScript errors
|
||||||
|
- Project service provides list/create/get functions
|
||||||
|
- /projects command lists all projects
|
||||||
|
- /project new "Name" creates a project and confirms
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/03-project-crud/03-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
100
.planning/phases/03-project-crud/03-01-SUMMARY.md
Normal file
100
.planning/phases/03-project-crud/03-01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
---
|
||||||
|
phase: 03-project-crud
|
||||||
|
plan: 01
|
||||||
|
subsystem: api
|
||||||
|
tags: [telegram, crud, sqlalchemy, services]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 02-bot-core
|
||||||
|
provides: Handler registration pattern, Telegram bot infrastructure
|
||||||
|
provides:
|
||||||
|
- Project service layer with list/create/get operations
|
||||||
|
- /projects and /project new handlers
|
||||||
|
affects: [03-02-project-select, 03-03-project-operations, 04-single-model]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- Service layer pattern: services/ package for business logic
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- src/moai/core/services/__init__.py
|
||||||
|
- src/moai/core/services/project.py
|
||||||
|
- src/moai/bot/handlers/projects.py
|
||||||
|
modified:
|
||||||
|
- src/moai/bot/handlers/__init__.py
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Service layer pattern with get_session() context manager"
|
||||||
|
- "Single /project handler with subcommand parsing"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Service layer: core/services/ package for database operations"
|
||||||
|
- "Subcommand pattern: /project <action> with args parsing"
|
||||||
|
|
||||||
|
issues-created: []
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 3min
|
||||||
|
completed: 2026-01-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 3 Plan 1: Project Service & List/Create Summary
|
||||||
|
|
||||||
|
**Project service layer with list/create functions and /projects, /project new Telegram commands**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 3 min
|
||||||
|
- **Started:** 2026-01-16T18:40:00Z
|
||||||
|
- **Completed:** 2026-01-16T18:43:00Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 4
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Created services package with project service module
|
||||||
|
- Implemented list_projects(), create_project(), get_project() functions
|
||||||
|
- Added /projects command to list all projects with details
|
||||||
|
- Added /project new "Name" command to create projects
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Create project service module** - `718dcea` (feat)
|
||||||
|
2. **Task 2: Implement /projects and /project new handlers** - `3f3b5ce` (feat)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `src/moai/core/services/__init__.py` - Service package init, exports
|
||||||
|
- `src/moai/core/services/project.py` - list_projects, create_project, get_project
|
||||||
|
- `src/moai/bot/handlers/projects.py` - projects_command, project_command handlers
|
||||||
|
- `src/moai/bot/handlers/__init__.py` - Register project handlers
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- Used service layer pattern to keep handlers thin
|
||||||
|
- Single project_command handler parses subcommands (new/select/delete/models/info)
|
||||||
|
- Default models list: ["claude", "gpt", "gemini"]
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- Project service ready for additional operations (select, delete, models, info)
|
||||||
|
- Handler structure supports adding more subcommands
|
||||||
|
- Ready for 03-02-PLAN.md (project select/delete)
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 03-project-crud*
|
||||||
|
*Completed: 2026-01-16*
|
||||||
94
.planning/phases/03-project-crud/03-02-PLAN.md
Normal file
94
.planning/phases/03-project-crud/03-02-PLAN.md
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
---
|
||||||
|
phase: 03-project-crud
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Implement project selection and info display commands.
|
||||||
|
|
||||||
|
Purpose: Enable users to switch between projects and view project details.
|
||||||
|
Output: Working /project select and /project info commands with user state tracking.
|
||||||
|
</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
|
||||||
|
|
||||||
|
# Prior plan context
|
||||||
|
@.planning/phases/03-project-crud/03-01-SUMMARY.md
|
||||||
|
|
||||||
|
# Relevant source files
|
||||||
|
@src/moai/core/services/project.py
|
||||||
|
@src/moai/bot/handlers/projects.py
|
||||||
|
@src/moai/bot/handlers/__init__.py
|
||||||
|
|
||||||
|
**Established patterns:**
|
||||||
|
- user_data dict in context for per-user state (python-telegram-bot pattern)
|
||||||
|
- Handler pattern: async def xxx_command(update, context) -> None
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add get_project_by_name to service</name>
|
||||||
|
<files>src/moai/core/services/project.py</files>
|
||||||
|
<action>
|
||||||
|
Add function to project service:
|
||||||
|
- get_project_by_name(name: str) -> Project | None: Query project by exact name match (case-insensitive using ilike)
|
||||||
|
|
||||||
|
This allows /project select to work with either ID or name.
|
||||||
|
</action>
|
||||||
|
<verify>python -c "from moai.core.services.project import get_project_by_name"</verify>
|
||||||
|
<done>get_project_by_name function exists and is importable</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Implement /project select and /project info handlers</name>
|
||||||
|
<files>src/moai/bot/handlers/projects.py</files>
|
||||||
|
<action>
|
||||||
|
Extend project_command handler to support:
|
||||||
|
|
||||||
|
/project select <id|name>:
|
||||||
|
- Store selected project ID in context.user_data["selected_project_id"]
|
||||||
|
- Reply with confirmation: "Selected project: {name}"
|
||||||
|
- If not found: "Project not found. Use /projects to list available projects."
|
||||||
|
|
||||||
|
/project info:
|
||||||
|
- Get selected project from user_data
|
||||||
|
- If no project selected: "No project selected. Use /project select <name> first."
|
||||||
|
- Display: name, ID, models list, created_at, discussion count
|
||||||
|
|
||||||
|
Helper function get_selected_project(context) -> Project | None to retrieve currently selected project from user_data.
|
||||||
|
</action>
|
||||||
|
<verify>ruff check src/moai/bot/handlers/projects.py</verify>
|
||||||
|
<done>/project select stores selection, /project info displays project details</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `ruff check src/moai/bot/handlers/projects.py` passes
|
||||||
|
- [ ] `python -c "from moai.core.services.project import get_project_by_name"` succeeds
|
||||||
|
- [ ] project_command handles select and info subcommands
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
|
||||||
|
- All tasks completed
|
||||||
|
- All verification checks pass
|
||||||
|
- /project select stores selection in user_data
|
||||||
|
- /project info shows project details
|
||||||
|
- Error messages are user-friendly
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/03-project-crud/03-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
91
.planning/phases/03-project-crud/03-02-SUMMARY.md
Normal file
91
.planning/phases/03-project-crud/03-02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
---
|
||||||
|
phase: 03-project-crud
|
||||||
|
plan: 02
|
||||||
|
subsystem: bot
|
||||||
|
tags: [telegram, user-state, project-selection]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 03-01
|
||||||
|
provides: project service with list/create/get functions
|
||||||
|
provides:
|
||||||
|
- get_project_by_name service function
|
||||||
|
- /project select command with user_data storage
|
||||||
|
- /project info command with project details display
|
||||||
|
- get_selected_project helper for retrieving current selection
|
||||||
|
affects: [discussion-commands, export-commands]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [user_data for per-user state tracking]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: []
|
||||||
|
modified:
|
||||||
|
- src/moai/core/services/project.py
|
||||||
|
- src/moai/bot/handlers/projects.py
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Case-insensitive name matching with ilike"
|
||||||
|
- "user_data dict for storing selected_project_id"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "get_selected_project(context) pattern for retrieving current project"
|
||||||
|
|
||||||
|
issues-created: []
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 3min
|
||||||
|
completed: 2026-01-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 3 Plan 2: Project Selection & Info Summary
|
||||||
|
|
||||||
|
**Project selection via /project select <id|name> with user_data storage, /project info displays full details**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 3 min
|
||||||
|
- **Started:** 2026-01-16T18:41:00Z
|
||||||
|
- **Completed:** 2026-01-16T18:43:43Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 2
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
- Added get_project_by_name function with case-insensitive ilike matching
|
||||||
|
- Implemented /project select command storing selection in context.user_data
|
||||||
|
- Implemented /project info command showing name, ID, models, created_at, discussion count
|
||||||
|
- Created get_selected_project helper for future handler use
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Add get_project_by_name to service** - `70dd517` (feat)
|
||||||
|
2. **Task 2: Implement /project select and /project info handlers** - `9922c33` (feat)
|
||||||
|
|
||||||
|
**Plan metadata:** `298c8d7` (docs: complete plan)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
- `src/moai/core/services/project.py` - Added get_project_by_name function
|
||||||
|
- `src/moai/bot/handlers/projects.py` - Added select/info handlers and get_selected_project helper
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
- Used ilike for case-insensitive name matching (SQLAlchemy pattern)
|
||||||
|
- Stored selected_project_id in context.user_data (python-telegram-bot per-user state pattern)
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
None
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
- Ready for 03-03-PLAN.md (delete/models commands)
|
||||||
|
- Project selection foundation established for discussion commands
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 03-project-crud*
|
||||||
|
*Completed: 2026-01-16*
|
||||||
104
.planning/phases/03-project-crud/03-03-PLAN.md
Normal file
104
.planning/phases/03-project-crud/03-03-PLAN.md
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
---
|
||||||
|
phase: 03-project-crud
|
||||||
|
plan: 03
|
||||||
|
type: execute
|
||||||
|
status: complete
|
||||||
|
completed: 2026-01-16
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Implement project model configuration and deletion commands to complete M2 milestone.
|
||||||
|
|
||||||
|
Purpose: Enable users to configure which AI models a project uses and delete unwanted projects.
|
||||||
|
Output: Working /project models and /project delete commands, completing full Project CRUD.
|
||||||
|
</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
|
||||||
|
|
||||||
|
# Prior plan context
|
||||||
|
@.planning/phases/03-project-crud/03-02-SUMMARY.md
|
||||||
|
|
||||||
|
# Relevant source files
|
||||||
|
@src/moai/core/services/project.py
|
||||||
|
@src/moai/bot/handlers/projects.py
|
||||||
|
|
||||||
|
**Established patterns:**
|
||||||
|
- Service layer handles database operations
|
||||||
|
- Handlers are thin, delegate to services
|
||||||
|
- user_data["selected_project_id"] for current project
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add update_models and delete_project to service</name>
|
||||||
|
<files>src/moai/core/services/project.py</files>
|
||||||
|
<action>
|
||||||
|
Add functions to project service:
|
||||||
|
|
||||||
|
- update_project_models(project_id: str, models: list[str]) -> Project | None:
|
||||||
|
Update project's models list, return updated project or None if not found.
|
||||||
|
|
||||||
|
- delete_project(project_id: str) -> bool:
|
||||||
|
Delete project by ID, return True if deleted, False if not found.
|
||||||
|
Cascade delete will handle discussions/rounds/messages via SQLAlchemy relationship config.
|
||||||
|
</action>
|
||||||
|
<verify>python -c "from moai.core.services.project import update_project_models, delete_project"</verify>
|
||||||
|
<done>Both functions exist and are importable</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Implement /project models and /project delete handlers</name>
|
||||||
|
<files>src/moai/bot/handlers/projects.py</files>
|
||||||
|
<action>
|
||||||
|
Extend project_command handler to support:
|
||||||
|
|
||||||
|
/project models claude,gpt,gemini:
|
||||||
|
- Require project to be selected first
|
||||||
|
- Parse comma-separated model names from args
|
||||||
|
- Update via service, confirm: "Models updated: claude, gpt, gemini"
|
||||||
|
- If no args: show current models list
|
||||||
|
|
||||||
|
/project delete <id>:
|
||||||
|
- Require explicit project ID (not name) for safety
|
||||||
|
- Delete via service, confirm: "Deleted project: {name}"
|
||||||
|
- If deleted project was selected, clear user_data["selected_project_id"]
|
||||||
|
- If not found: "Project not found."
|
||||||
|
|
||||||
|
Update /status handler (if exists) to show selected project info now that project CRUD is complete.
|
||||||
|
</action>
|
||||||
|
<verify>ruff check src/moai/bot/handlers/projects.py</verify>
|
||||||
|
<done>/project models updates models, /project delete removes project, M2 milestone complete</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `ruff check src/moai/core/services/ src/moai/bot/handlers/` passes
|
||||||
|
- [ ] All service functions importable
|
||||||
|
- [ ] Full CRUD cycle works: new → select → info → models → delete
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
|
||||||
|
- All tasks completed
|
||||||
|
- All verification checks pass
|
||||||
|
- /project models configures AI models for project
|
||||||
|
- /project delete removes project with confirmation
|
||||||
|
- M2 milestone complete (full project CRUD via Telegram)
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/03-project-crud/03-03-SUMMARY.md`
|
||||||
|
|
||||||
|
Summary should note M2 milestone completion and readiness for Phase 4 (Single Model Q&A).
|
||||||
|
</output>
|
||||||
94
.planning/phases/03-project-crud/03-03-SUMMARY.md
Normal file
94
.planning/phases/03-project-crud/03-03-SUMMARY.md
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
---
|
||||||
|
phase: 03-project-crud
|
||||||
|
plan: 03
|
||||||
|
subsystem: bot
|
||||||
|
tags: [telegram, project-crud, models-config]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 03-02
|
||||||
|
provides: project select/info commands, get_selected_project helper
|
||||||
|
provides:
|
||||||
|
- update_project_models service function
|
||||||
|
- delete_project service function
|
||||||
|
- /project models command for configuring AI models
|
||||||
|
- /project delete command for project removal
|
||||||
|
affects: [discussion-commands, ai-client]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: []
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: []
|
||||||
|
modified:
|
||||||
|
- src/moai/core/services/project.py
|
||||||
|
- src/moai/bot/handlers/projects.py
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Explicit project ID required for delete (safety)"
|
||||||
|
- "Comma-separated model list parsing"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Service functions return None or bool for not-found cases"
|
||||||
|
|
||||||
|
issues-created: []
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 5min
|
||||||
|
completed: 2026-01-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 3 Plan 3: Project Models & Delete Summary
|
||||||
|
|
||||||
|
**Full Project CRUD complete: /project models configures AI model list, /project delete removes projects with cascade - M2 milestone done**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 5 min
|
||||||
|
- **Started:** 2026-01-16T18:50:00Z
|
||||||
|
- **Completed:** 2026-01-16T18:55:00Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 2
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
- Added update_project_models(project_id, models) to service layer
|
||||||
|
- Added delete_project(project_id) with cascade handling
|
||||||
|
- Implemented /project models command (show/set AI models for current project)
|
||||||
|
- Implemented /project delete command requiring explicit ID for safety
|
||||||
|
- Completed M2 milestone: full project CRUD via Telegram
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Add update_models and delete_project to service** - `e2e10d9` (feat)
|
||||||
|
2. **Task 2: Implement /project models and /project delete handlers** - `bb3eab7` (feat)
|
||||||
|
|
||||||
|
**Plan metadata:** `afab4f8` (docs: complete plan)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
- `src/moai/core/services/project.py` - Added update_project_models and delete_project functions
|
||||||
|
- `src/moai/bot/handlers/projects.py` - Added models/delete handlers, updated usage help
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
- Require explicit project ID for delete (not name) for safety
|
||||||
|
- Comma-separated model list parsing (e.g., "claude,gpt,gemini")
|
||||||
|
- Clear user_data selection when deleting the currently selected project
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
None
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
- M2 milestone complete
|
||||||
|
- Full project CRUD available: new, select, info, models, delete
|
||||||
|
- Ready for Phase 4: Single Model Q&A
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 03-project-crud*
|
||||||
|
*Completed: 2026-01-16*
|
||||||
141
.planning/phases/04-single-model-qa/04-01-PLAN.md
Normal file
141
.planning/phases/04-single-model-qa/04-01-PLAN.md
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
---
|
||||||
|
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>
|
||||||
95
.planning/phases/04-single-model-qa/04-01-SUMMARY.md
Normal file
95
.planning/phases/04-single-model-qa/04-01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
---
|
||||||
|
phase: 04-single-model-qa
|
||||||
|
plan: 01
|
||||||
|
subsystem: api
|
||||||
|
tags: [openai, ai-client, requesty, openrouter, async]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 03-project-crud
|
||||||
|
provides: BotConfig pattern, project context
|
||||||
|
provides:
|
||||||
|
- AIClient class for AI model interactions
|
||||||
|
- MODEL_MAP with claude/gpt/gemini short names
|
||||||
|
- Module-level init_ai_client/get_ai_client functions
|
||||||
|
- Config extended with AI router settings
|
||||||
|
affects: [04-02, 04-03, 05-multi-model, discussion handlers]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: [openai]
|
||||||
|
patterns: [async-client-singleton, model-routing-abstraction]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: [src/moai/core/ai_client.py]
|
||||||
|
modified: [pyproject.toml, src/moai/bot/config.py]
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "OpenAI SDK for router abstraction (both Requesty and OpenRouter are OpenAI-compatible)"
|
||||||
|
- "Module-level singleton pattern for AI client (matches database pattern)"
|
||||||
|
- "Short model names (claude/gpt/gemini) mapped to full identifiers"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "AI client abstraction: all AI calls go through AIClient.complete()"
|
||||||
|
- "Model name resolution: short names in code, full identifiers to routers"
|
||||||
|
|
||||||
|
issues-created: []
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 5min
|
||||||
|
completed: 2026-01-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 04-01: AI Client Abstraction Summary
|
||||||
|
|
||||||
|
**OpenAI SDK-based AI client with Requesty/OpenRouter routing and claude/gpt/gemini model mappings**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 5 min
|
||||||
|
- **Started:** 2026-01-16T19:00:00Z
|
||||||
|
- **Completed:** 2026-01-16T19:05:00Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 3
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
- Created AIClient class wrapping AsyncOpenAI for model routing
|
||||||
|
- Extended BotConfig with ai_router, ai_api_key, ai_referer settings
|
||||||
|
- Added MODEL_MAP with claude, gpt, gemini short name mappings
|
||||||
|
- Implemented module-level singleton pattern with init_ai_client/get_ai_client
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Add openai dependency and extend config** - `3740691` (feat)
|
||||||
|
2. **Task 2: Create AI client abstraction** - `e04ce4e` (feat)
|
||||||
|
|
||||||
|
**Plan metadata:** `f8fa4e7` (docs)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
- `src/moai/core/ai_client.py` - AIClient class, MODEL_MAP, init/get functions
|
||||||
|
- `pyproject.toml` - Added openai dependency
|
||||||
|
- `src/moai/bot/config.py` - Extended BotConfig with AI settings
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
- Used OpenAI SDK instead of raw httpx - both Requesty and OpenRouter are OpenAI-compatible
|
||||||
|
- Default router is "requesty" (can be changed via AI_ROUTER env var)
|
||||||
|
- Model short names (claude/gpt/gemini) resolve to specific model versions
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
- AI client foundation ready for /ask command implementation in 04-02
|
||||||
|
- Model routing abstraction enables easy addition of new models
|
||||||
|
- Singleton pattern allows handlers to access client via get_ai_client()
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 04-single-model-qa*
|
||||||
|
*Completed: 2026-01-16*
|
||||||
189
.planning/phases/04-single-model-qa/04-02-PLAN.md
Normal file
189
.planning/phases/04-single-model-qa/04-02-PLAN.md
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
---
|
||||||
|
phase: 04-single-model-qa
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Implement /ask command for single model Q&A and integrate AI client into bot lifecycle.
|
||||||
|
|
||||||
|
Purpose: Complete M3 milestone - users can ask questions to individual AI models through Telegram.
|
||||||
|
Output: Working /ask command that sends questions to AI models and returns responses.
|
||||||
|
</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/04-single-model-qa/04-01-SUMMARY.md (will exist after 04-01)
|
||||||
|
|
||||||
|
# Key files:
|
||||||
|
@src/moai/bot/main.py
|
||||||
|
@src/moai/bot/config.py
|
||||||
|
@src/moai/bot/handlers/__init__.py
|
||||||
|
@src/moai/bot/handlers/projects.py
|
||||||
|
@src/moai/core/ai_client.py (will exist after 04-01)
|
||||||
|
|
||||||
|
**Tech available:**
|
||||||
|
- python-telegram-bot, sqlalchemy, httpx, aiosqlite, openai (added in 04-01)
|
||||||
|
- AI client abstraction (04-01)
|
||||||
|
|
||||||
|
**Established patterns:**
|
||||||
|
- Handler registration in handlers/__init__.py
|
||||||
|
- get_selected_project() helper in projects.py
|
||||||
|
- Module-level init pattern (database.py, ai_client.py)
|
||||||
|
- Async command handlers
|
||||||
|
|
||||||
|
**Constraining decisions:**
|
||||||
|
- Thin handlers delegating to core (CLAUDE.md)
|
||||||
|
- Service layer for business logic
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Integrate AI client into bot lifecycle</name>
|
||||||
|
<files>src/moai/bot/main.py</files>
|
||||||
|
<action>
|
||||||
|
Modify main.py to initialize AI client during bot startup:
|
||||||
|
|
||||||
|
1. Import init_ai_client from moai.core.ai_client
|
||||||
|
2. In post_init callback (or create one if needed), after database init:
|
||||||
|
- Call init_ai_client(config)
|
||||||
|
- Log "AI client initialized with {config.AI_ROUTER}"
|
||||||
|
|
||||||
|
Follow existing pattern - database is initialized in post_init, AI client goes right after.
|
||||||
|
Keep error handling minimal - if AI_API_KEY is missing, let it fail at first use rather than at startup (user may just want to test bot commands first).
|
||||||
|
</action>
|
||||||
|
<verify>python -c "import moai.bot.main; print('main imports ok')"</verify>
|
||||||
|
<done>main.py imports and initializes AI client alongside database in post_init</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Create /ask handler for single model queries</name>
|
||||||
|
<files>src/moai/bot/handlers/discussion.py, src/moai/bot/handlers/__init__.py</files>
|
||||||
|
<action>
|
||||||
|
Create discussion.py with /ask handler:
|
||||||
|
|
||||||
|
1. Create src/moai/bot/handlers/discussion.py:
|
||||||
|
```python
|
||||||
|
"""Discussion handlers for MoAI bot."""
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import ContextTypes
|
||||||
|
|
||||||
|
from moai.bot.handlers.projects import get_selected_project
|
||||||
|
from moai.core.ai_client import get_ai_client, MODEL_MAP
|
||||||
|
|
||||||
|
async def ask_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Handle /ask <model> <question> command.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
/ask claude What is Python?
|
||||||
|
/ask gpt Explain async/await
|
||||||
|
"""
|
||||||
|
args = context.args or []
|
||||||
|
|
||||||
|
if len(args) < 2:
|
||||||
|
available = ", ".join(MODEL_MAP.keys())
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"Usage: /ask <model> <question>\n"
|
||||||
|
f"Available models: {available}\n\n"
|
||||||
|
f"Example: /ask claude What is Python?"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
model_name = args[0].lower()
|
||||||
|
question = " ".join(args[1:])
|
||||||
|
|
||||||
|
# Validate model
|
||||||
|
if model_name not in MODEL_MAP:
|
||||||
|
available = ", ".join(MODEL_MAP.keys())
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"Unknown model: {model_name}\n"
|
||||||
|
f"Available: {available}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get project context if available (optional for /ask)
|
||||||
|
project = await get_selected_project(context)
|
||||||
|
project_context = f"Project: {project.name}\n" if project else ""
|
||||||
|
|
||||||
|
# Send "typing" indicator while waiting for AI
|
||||||
|
await update.message.chat.send_action("typing")
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = get_ai_client()
|
||||||
|
response = await client.complete(
|
||||||
|
model=model_name,
|
||||||
|
messages=[{"role": "user", "content": question}],
|
||||||
|
system_prompt=f"{project_context}You are a helpful AI assistant."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format response with model name
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"*{model_name.title()}:*\n\n{response}",
|
||||||
|
parse_mode="Markdown"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
await update.message.reply_text(f"Error: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Update handlers/__init__.py:
|
||||||
|
- Import ask_command from discussion
|
||||||
|
- Add CommandHandler("ask", ask_command) to register_handlers
|
||||||
|
</action>
|
||||||
|
<verify>python -c "from moai.bot.handlers.discussion import ask_command; print('handler ok')"</verify>
|
||||||
|
<done>/ask handler registered, validates model name, sends typing indicator, calls AI client</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Update help text and status</name>
|
||||||
|
<files>src/moai/bot/handlers/commands.py, src/moai/bot/handlers/status.py</files>
|
||||||
|
<action>
|
||||||
|
1. Update HELP_TEXT in commands.py to include:
|
||||||
|
```
|
||||||
|
*Questions*
|
||||||
|
/ask <model> <question> - Ask a single model
|
||||||
|
```
|
||||||
|
Add after the Project Management section.
|
||||||
|
|
||||||
|
2. Update status.py to show AI client status:
|
||||||
|
- Import get_ai_client (wrapped in try/except)
|
||||||
|
- In status_command, add a line showing AI router configured
|
||||||
|
- Example: "AI Router: requesty ✓" or "AI Router: not configured"
|
||||||
|
</action>
|
||||||
|
<verify>python -c "from moai.bot.handlers.commands import HELP_TEXT; print('/ask' in HELP_TEXT)"</verify>
|
||||||
|
<done>Help shows /ask command, status shows AI router status</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `ruff check src` passes
|
||||||
|
- [ ] Bot starts without errors (with valid AI_API_KEY in env)
|
||||||
|
- [ ] /help shows /ask command
|
||||||
|
- [ ] /status shows AI router status
|
||||||
|
- [ ] /ask without args shows usage
|
||||||
|
- [ ] /ask with invalid model shows available models
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
|
||||||
|
- AI client initialized in bot lifecycle
|
||||||
|
- /ask command works with model validation
|
||||||
|
- Help text updated with /ask
|
||||||
|
- Status shows AI configuration
|
||||||
|
- M3 milestone: Single model Q&A working
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/04-single-model-qa/04-02-SUMMARY.md`
|
||||||
|
|
||||||
|
Note: This completes Phase 4. Summary should note M3 milestone complete.
|
||||||
|
</output>
|
||||||
114
.planning/phases/04-single-model-qa/04-02-SUMMARY.md
Normal file
114
.planning/phases/04-single-model-qa/04-02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
---
|
||||||
|
phase: 04-single-model-qa
|
||||||
|
plan: 02
|
||||||
|
subsystem: bot
|
||||||
|
tags: [telegram, handler, ai-client, ask-command, m3-milestone]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 04-single-model-qa
|
||||||
|
plan: 01
|
||||||
|
provides: AIClient, MODEL_MAP, init_ai_client/get_ai_client
|
||||||
|
provides:
|
||||||
|
- /ask command handler for single model Q&A
|
||||||
|
- AI client integration in bot lifecycle
|
||||||
|
- AI router status in /status command
|
||||||
|
- Questions section in /help text
|
||||||
|
affects: [05-multi-model, discussion-handlers]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [typing-indicator, command-validation]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: [src/moai/bot/handlers/discussion.py]
|
||||||
|
modified: [src/moai/bot/main.py, src/moai/bot/handlers/__init__.py, src/moai/bot/handlers/commands.py, src/moai/bot/handlers/status.py]
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "AI client initialized in post_init alongside database"
|
||||||
|
- "Typing indicator shown while waiting for AI response"
|
||||||
|
- "Project context optionally included in AI prompts"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Discussion handlers in discussion.py module"
|
||||||
|
- "AI status reporting in /status command"
|
||||||
|
|
||||||
|
issues-created: []
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 5min
|
||||||
|
completed: 2026-01-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 04-02: /ask Command Handler Summary
|
||||||
|
|
||||||
|
**Single model Q&A via /ask command, completing M3 milestone and Phase 4**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 5 min
|
||||||
|
- **Started:** 2026-01-16T19:10:00Z
|
||||||
|
- **Completed:** 2026-01-16T19:15:00Z
|
||||||
|
- **Tasks:** 3
|
||||||
|
- **Files modified:** 5
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
- Integrated AI client initialization into bot lifecycle (post_init)
|
||||||
|
- Created /ask handler with model validation and usage help
|
||||||
|
- Added "Questions" section to help text with /ask command
|
||||||
|
- Updated /status to show AI router configuration
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Integrate AI client into bot lifecycle** - `821b419` (feat)
|
||||||
|
2. **Task 2: Create /ask handler for single model queries** - `32983c9` (feat)
|
||||||
|
3. **Task 3: Update help text and status** - `7078379` (feat)
|
||||||
|
|
||||||
|
**Plan metadata:** (this commit) (docs)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
- `src/moai/bot/handlers/discussion.py` - ask_command handler (new)
|
||||||
|
- `src/moai/bot/main.py` - AI client initialization in post_init
|
||||||
|
- `src/moai/bot/handlers/__init__.py` - Register /ask handler
|
||||||
|
- `src/moai/bot/handlers/commands.py` - Questions section in HELP_TEXT
|
||||||
|
- `src/moai/bot/handlers/status.py` - AI router status display
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
- AI client initialized alongside database in post_init (consistent pattern)
|
||||||
|
- Typing indicator sent while waiting for AI response (UX feedback)
|
||||||
|
- Project context optionally included if a project is selected
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Milestone Completion
|
||||||
|
|
||||||
|
**M3 Milestone: Single Model Q&A - COMPLETE**
|
||||||
|
|
||||||
|
Users can now:
|
||||||
|
- Query individual AI models via `/ask <model> <question>`
|
||||||
|
- See available models (claude, gpt, gemini) in usage help
|
||||||
|
- View AI router status via `/status`
|
||||||
|
|
||||||
|
## Phase 4 Completion
|
||||||
|
|
||||||
|
**Phase 4: Single Model Q&A - COMPLETE**
|
||||||
|
|
||||||
|
Both plans completed:
|
||||||
|
- 04-01: AI client abstraction (AIClient, MODEL_MAP, config)
|
||||||
|
- 04-02: /ask command handler (this plan)
|
||||||
|
|
||||||
|
Note: Original roadmap estimated 3 plans (including 04-03 error handling), but core functionality is complete. Error handling can be enhanced in future phases if needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 04-single-model-qa*
|
||||||
|
*Plan: 02*
|
||||||
|
*Completed: 2026-01-16*
|
||||||
91
.planning/phases/05-multi-model-discussions/05-01-PLAN.md
Normal file
91
.planning/phases/05-multi-model-discussions/05-01-PLAN.md
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
---
|
||||||
|
phase: 05-multi-model-discussions
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create discussion service layer with CRUD operations for Discussion, Round, and Message entities.
|
||||||
|
|
||||||
|
Purpose: Establish the data layer that all multi-model discussion commands depend on.
|
||||||
|
Output: Working discussion service with create/get/list operations for discussions, rounds, and messages.
|
||||||
|
</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
|
||||||
|
|
||||||
|
# Prior phase context:
|
||||||
|
@.planning/phases/04-single-model-qa/04-02-SUMMARY.md
|
||||||
|
|
||||||
|
# Key files:
|
||||||
|
@src/moai/core/models.py
|
||||||
|
@src/moai/core/services/project.py
|
||||||
|
@src/moai/core/database.py
|
||||||
|
|
||||||
|
**Tech stack available:** sqlalchemy, aiosqlite, python-telegram-bot
|
||||||
|
**Established patterns:** Service layer pattern (core/services/), async context manager for sessions, module-level singleton
|
||||||
|
**Constraining decisions:**
|
||||||
|
- 03-01: Service layer pattern for database operations
|
||||||
|
- 01-03: expire_on_commit=False for async session usability
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create discussion service with CRUD operations</name>
|
||||||
|
<files>src/moai/core/services/discussion.py, src/moai/core/services/__init__.py</files>
|
||||||
|
<action>Create discussion.py service following the project.py pattern. Include:
|
||||||
|
- create_discussion(project_id, question, discussion_type) - creates Discussion with DiscussionType enum
|
||||||
|
- get_discussion(discussion_id) - returns Discussion with eager-loaded rounds/messages
|
||||||
|
- get_active_discussion(project_id) - returns active discussion for project (status=ACTIVE), or None
|
||||||
|
- list_discussions(project_id) - returns all discussions for a project
|
||||||
|
- complete_discussion(discussion_id) - sets status to COMPLETED
|
||||||
|
|
||||||
|
Use selectinload for eager loading rounds→messages to avoid N+1 queries. Follow existing async context manager pattern from project.py.</action>
|
||||||
|
<verify>Import service in Python REPL, verify functions exist and type hints correct</verify>
|
||||||
|
<done>discussion.py exists with 5 async functions, proper type hints, uses selectinload for relationships</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Add round and message operations to discussion service</name>
|
||||||
|
<files>src/moai/core/services/discussion.py</files>
|
||||||
|
<action>Add to discussion.py:
|
||||||
|
- create_round(discussion_id, round_number, round_type) - creates Round with RoundType enum
|
||||||
|
- get_current_round(discussion_id) - returns highest round_number Round for discussion
|
||||||
|
- create_message(round_id, model, content, is_direct=False) - creates Message
|
||||||
|
- get_round_messages(round_id) - returns messages for a round ordered by timestamp
|
||||||
|
|
||||||
|
All functions follow same async context manager pattern. Use proper enum imports from models.py.</action>
|
||||||
|
<verify>Import service, verify all 4 new functions exist with correct signatures</verify>
|
||||||
|
<done>discussion.py has 9 total functions (5 discussion + 4 round/message), all async with proper types</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `python -c "from moai.core.services.discussion import *"` succeeds
|
||||||
|
- [ ] All 9 functions have async def signatures
|
||||||
|
- [ ] Type hints include Discussion, Round, Message, DiscussionType, RoundType
|
||||||
|
- [ ] No import errors when running bot
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
|
||||||
|
- Discussion service exists at src/moai/core/services/discussion.py
|
||||||
|
- 9 async functions for discussion/round/message CRUD
|
||||||
|
- Follows established service layer pattern
|
||||||
|
- Eager loading prevents N+1 queries
|
||||||
|
- No TypeScript/import errors
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/05-multi-model-discussions/05-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
96
.planning/phases/05-multi-model-discussions/05-01-SUMMARY.md
Normal file
96
.planning/phases/05-multi-model-discussions/05-01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
---
|
||||||
|
phase: 05-multi-model-discussions
|
||||||
|
plan: 01
|
||||||
|
subsystem: api
|
||||||
|
tags: [sqlalchemy, async, services, crud]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 04-single-model-qa
|
||||||
|
provides: AI client abstraction
|
||||||
|
- phase: 01-foundation
|
||||||
|
provides: SQLAlchemy models
|
||||||
|
provides:
|
||||||
|
- Discussion CRUD operations (create, get, list, complete)
|
||||||
|
- Round management (create, get current)
|
||||||
|
- Message management (create, list)
|
||||||
|
affects: [05-02 open mode, 05-03 discuss mode, 05-04 mentions, 06-consensus]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [selectinload for eager loading, async context manager for sessions]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: [src/moai/core/services/discussion.py]
|
||||||
|
modified: [src/moai/core/services/__init__.py]
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "selectinload for rounds→messages to prevent N+1 queries"
|
||||||
|
- "Eager load consensus relationship in get_discussion"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Discussion service pattern matching project.py"
|
||||||
|
- "get_current_round returns highest round_number"
|
||||||
|
|
||||||
|
issues-created: []
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 2min
|
||||||
|
completed: 2026-01-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 5 Plan 1: Discussion Service Summary
|
||||||
|
|
||||||
|
**Discussion service with 9 async CRUD operations for discussions, rounds, and messages using selectinload eager loading**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 2 min
|
||||||
|
- **Started:** 2026-01-16T19:24:14Z
|
||||||
|
- **Completed:** 2026-01-16T19:26:56Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 2
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Created discussion service following established service layer pattern
|
||||||
|
- Implemented 5 discussion operations: create, get (with eager loading), get_active, list, complete
|
||||||
|
- Added 4 round/message operations: create_round, get_current_round, create_message, get_round_messages
|
||||||
|
- Used selectinload for eager loading rounds→messages to avoid N+1 queries
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Create discussion service with CRUD operations** - `3258c3a` (feat)
|
||||||
|
2. **Task 2: Add round and message operations** - `baf02bb` (feat)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `src/moai/core/services/discussion.py` - Discussion, Round, Message CRUD operations (9 async functions)
|
||||||
|
- `src/moai/core/services/__init__.py` - Updated exports
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- Used selectinload for eager loading rounds→messages→consensus to prevent N+1 queries
|
||||||
|
- get_discussion includes consensus in eager loading for future phase 6
|
||||||
|
- get_current_round orders by round_number desc with limit 1 for efficiency
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- Discussion service ready for /open and /discuss command handlers
|
||||||
|
- Round/message operations available for multi-model discussion flow
|
||||||
|
- Ready for 05-02-PLAN.md (open mode handler)
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 05-multi-model-discussions*
|
||||||
|
*Completed: 2026-01-16*
|
||||||
116
.planning/phases/05-multi-model-discussions/05-02-PLAN.md
Normal file
116
.planning/phases/05-multi-model-discussions/05-02-PLAN.md
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
---
|
||||||
|
phase: 05-multi-model-discussions
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Implement /open command for parallel multi-model queries (M4 milestone).
|
||||||
|
|
||||||
|
Purpose: Allow users to get parallel responses from all project models on a question.
|
||||||
|
Output: Working /open command that queries all models simultaneously and displays responses.
|
||||||
|
</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
|
||||||
|
|
||||||
|
# Prior plan context:
|
||||||
|
@.planning/phases/05-multi-model-discussions/05-01-SUMMARY.md
|
||||||
|
|
||||||
|
# Key files:
|
||||||
|
@src/moai/core/ai_client.py
|
||||||
|
@src/moai/bot/handlers/discussion.py
|
||||||
|
@src/moai/bot/handlers/projects.py
|
||||||
|
@src/moai/core/services/discussion.py
|
||||||
|
|
||||||
|
**Tech stack available:** python-telegram-bot, openai (async), sqlalchemy
|
||||||
|
**Established patterns:** Typing indicator, command validation, service layer, AIClient.complete()
|
||||||
|
**Constraining decisions:**
|
||||||
|
- 04-02: Typing indicator shown while waiting for AI response
|
||||||
|
- 04-01: OpenAI SDK for router abstraction (async calls)
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create orchestrator module with parallel query function</name>
|
||||||
|
<files>src/moai/core/orchestrator.py</files>
|
||||||
|
<action>Create orchestrator.py with:
|
||||||
|
- SYSTEM_PROMPT constant for roundtable discussion (from SPEC.md)
|
||||||
|
- async query_models_parallel(models: list[str], question: str, project_name: str) -> dict[str, str]
|
||||||
|
- Uses asyncio.gather() to call AIClient.complete() for all models simultaneously
|
||||||
|
- Returns dict mapping model name → response
|
||||||
|
- Handles individual model failures gracefully (returns error message for that model)
|
||||||
|
- Builds system prompt with "Other participants: {models}" and "Topic: {project_name}"
|
||||||
|
|
||||||
|
Do NOT build full discussion context yet - that's for discuss mode in 05-03.</action>
|
||||||
|
<verify>Import orchestrator, verify query_models_parallel signature and SYSTEM_PROMPT exists</verify>
|
||||||
|
<done>orchestrator.py exists with SYSTEM_PROMPT and query_models_parallel function using asyncio.gather</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Implement /open command handler with database persistence</name>
|
||||||
|
<files>src/moai/bot/handlers/discussion.py, src/moai/bot/handlers/__init__.py</files>
|
||||||
|
<action>Add to discussion.py:
|
||||||
|
- open_command(update, context) handler for "/open <question>"
|
||||||
|
- Requires selected project (error if none)
|
||||||
|
- Uses project's models list (error if empty)
|
||||||
|
- Creates Discussion(type=OPEN) and Round(type=PARALLEL, round_number=1) via discussion service
|
||||||
|
- Calls query_models_parallel() with project models
|
||||||
|
- Creates Message for each response
|
||||||
|
- Formats output: "**Model:**\n> response" for each model
|
||||||
|
- Shows typing indicator while waiting
|
||||||
|
|
||||||
|
Register /open handler in __init__.py with CommandHandler. Update HELP_TEXT in commands.py with /open usage.</action>
|
||||||
|
<verify>Run bot, use `/open What is Python?` with a selected project that has models configured</verify>
|
||||||
|
<done>/open queries all project models in parallel, persists to DB, displays formatted responses</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Update help text and status for multi-model support</name>
|
||||||
|
<files>src/moai/bot/handlers/commands.py</files>
|
||||||
|
<action>Update HELP_TEXT to add Discussion section:
|
||||||
|
```
|
||||||
|
**Discussion**
|
||||||
|
/open <question> - Ask all models (parallel)
|
||||||
|
/discuss [rounds] - Start discussion (default: 3)
|
||||||
|
/next - Next round manually
|
||||||
|
/stop - End discussion
|
||||||
|
@model <message> - Direct message to model
|
||||||
|
```
|
||||||
|
|
||||||
|
This documents commands for the full phase even though /discuss, /next, /stop, and @mentions are implemented in later plans.</action>
|
||||||
|
<verify>Run bot, /help shows Discussion section with all commands listed</verify>
|
||||||
|
<done>HELP_TEXT includes Discussion section with all multi-model commands</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `python -c "from moai.core.orchestrator import query_models_parallel"` succeeds
|
||||||
|
- [ ] Bot responds to /open with parallel model responses
|
||||||
|
- [ ] Discussion/Round/Messages persisted to database after /open
|
||||||
|
- [ ] /help shows Discussion section
|
||||||
|
- [ ] Error handling for no project selected, no models configured
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
|
||||||
|
- /open command works with parallel AI queries
|
||||||
|
- Responses persisted as Discussion → Round → Messages
|
||||||
|
- Typing indicator shown during queries
|
||||||
|
- Proper error messages for edge cases
|
||||||
|
- M4 milestone (Open mode parallel) complete
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/05-multi-model-discussions/05-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
101
.planning/phases/05-multi-model-discussions/05-02-SUMMARY.md
Normal file
101
.planning/phases/05-multi-model-discussions/05-02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
---
|
||||||
|
phase: 05-multi-model-discussions
|
||||||
|
plan: 02
|
||||||
|
subsystem: api
|
||||||
|
tags: [asyncio, telegram, ai-orchestration, parallel-queries]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 05-multi-model-discussions/01
|
||||||
|
provides: Discussion service CRUD operations
|
||||||
|
- phase: 04-single-model-qa/02
|
||||||
|
provides: AIClient.complete() and typing indicator pattern
|
||||||
|
provides:
|
||||||
|
- orchestrator module with query_models_parallel()
|
||||||
|
- /open command for parallel multi-model queries
|
||||||
|
- Discussion/Round/Message persistence for open mode
|
||||||
|
affects: [05-03-discuss-mode, 05-04-mentions, 06-consensus]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [asyncio.gather for parallel AI calls, per-model error handling]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: [src/moai/core/orchestrator.py]
|
||||||
|
modified: [src/moai/bot/handlers/discussion.py, src/moai/bot/handlers/__init__.py, src/moai/bot/handlers/commands.py]
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "asyncio.gather for parallel model queries with graceful per-model error handling"
|
||||||
|
- "SYSTEM_PROMPT includes participant list and topic for roundtable context"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "query_models_parallel returns dict[str, str] mapping model → response"
|
||||||
|
- "Individual model failures don't block other model responses"
|
||||||
|
|
||||||
|
issues-created: []
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 3min
|
||||||
|
completed: 2026-01-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 5 Plan 2: Open Mode Summary
|
||||||
|
|
||||||
|
**Parallel multi-model queries via /open command with asyncio.gather orchestration and database persistence**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 3 min
|
||||||
|
- **Started:** 2026-01-16T19:34:44Z
|
||||||
|
- **Completed:** 2026-01-16T19:37:57Z
|
||||||
|
- **Tasks:** 3
|
||||||
|
- **Files modified:** 4
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Created orchestrator module with SYSTEM_PROMPT and query_models_parallel() using asyncio.gather
|
||||||
|
- Implemented /open command that queries all project models simultaneously
|
||||||
|
- Persists Discussion/Round/Message records for each open query
|
||||||
|
- Updated HELP_TEXT with full Discussion section (commands for current and future plans)
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Create orchestrator module** - `81b5bff` (feat)
|
||||||
|
2. **Task 2: Implement /open command handler** - `cef1898` (feat)
|
||||||
|
3. **Task 3: Update help text** - `7f46170` (docs)
|
||||||
|
|
||||||
|
**Plan metadata:** (pending)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `src/moai/core/orchestrator.py` - SYSTEM_PROMPT constant and query_models_parallel() function
|
||||||
|
- `src/moai/bot/handlers/discussion.py` - Added open_command handler with DB persistence
|
||||||
|
- `src/moai/bot/handlers/__init__.py` - Registered /open CommandHandler
|
||||||
|
- `src/moai/bot/handlers/commands.py` - Added Discussion section to HELP_TEXT
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- Used asyncio.gather for parallel execution with individual try/except for per-model error handling
|
||||||
|
- SYSTEM_PROMPT provides roundtable context with "Other participants: {models}" and "Topic: {project_name}"
|
||||||
|
- Error responses returned as "[Error: {e}]" strings to keep response dict complete
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- /open command complete (M4 milestone - Open mode parallel)
|
||||||
|
- Ready for 05-03-PLAN.md (discuss mode with sequential rounds)
|
||||||
|
- orchestrator.py ready for discuss mode additions
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 05-multi-model-discussions*
|
||||||
|
*Completed: 2026-01-16*
|
||||||
127
.planning/phases/05-multi-model-discussions/05-03-PLAN.md
Normal file
127
.planning/phases/05-multi-model-discussions/05-03-PLAN.md
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
---
|
||||||
|
phase: 05-multi-model-discussions
|
||||||
|
plan: 03
|
||||||
|
type: execute
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Implement /discuss mode with sequential rounds, context building, and /next, /stop commands (M5 milestone).
|
||||||
|
|
||||||
|
Purpose: Enable structured multi-round discussions where each model sees prior responses.
|
||||||
|
Output: Working /discuss, /next, /stop commands with full conversation context passed to each model.
|
||||||
|
</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
|
||||||
|
|
||||||
|
# Prior plan context:
|
||||||
|
@.planning/phases/05-multi-model-discussions/05-02-SUMMARY.md
|
||||||
|
|
||||||
|
# Key files:
|
||||||
|
@src/moai/core/orchestrator.py
|
||||||
|
@src/moai/core/services/discussion.py
|
||||||
|
@src/moai/bot/handlers/discussion.py
|
||||||
|
@SPEC.md (system prompts and discussion flow)
|
||||||
|
|
||||||
|
**Tech stack available:** python-telegram-bot, openai (async), sqlalchemy
|
||||||
|
**Established patterns:** query_models_parallel, Discussion/Round/Message persistence, typing indicator
|
||||||
|
**Constraining decisions:**
|
||||||
|
- 05-02: Orchestrator pattern established
|
||||||
|
- 04-02: Typing indicator for AI calls
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add context building and sequential round execution to orchestrator</name>
|
||||||
|
<files>src/moai/core/orchestrator.py</files>
|
||||||
|
<action>Add to orchestrator.py:
|
||||||
|
- build_context(discussion: Discussion) -> list[dict]
|
||||||
|
- Converts all rounds/messages to OpenAI message format
|
||||||
|
- Returns list of {"role": "assistant"/"user", "content": "**Model:** response"}
|
||||||
|
- Models see their own responses as assistant, others' as user (simplified: all prior as user context)
|
||||||
|
- Include original question as first user message
|
||||||
|
|
||||||
|
- async run_discussion_round(discussion: Discussion, models: list[str], project_name: str) -> dict[str, str]
|
||||||
|
- Builds context from all prior rounds
|
||||||
|
- Calls each model SEQUENTIALLY (not parallel) so each sees previous in same round
|
||||||
|
- Returns dict mapping model → response
|
||||||
|
- Creates Round(type=SEQUENTIAL) and Messages via discussion service
|
||||||
|
|
||||||
|
Sequential means: Claude responds, then GPT sees Claude's response AND responds, then Gemini sees both.
|
||||||
|
Use asyncio loop, not gather, to ensure sequential execution within round.</action>
|
||||||
|
<verify>Import orchestrator, verify build_context and run_discussion_round exist</verify>
|
||||||
|
<done>orchestrator.py has build_context and run_discussion_round with sequential model calls</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Implement /discuss command with round limit</name>
|
||||||
|
<files>src/moai/bot/handlers/discussion.py, src/moai/bot/handlers/__init__.py</files>
|
||||||
|
<action>Add to discussion.py:
|
||||||
|
- discuss_command(update, context) handler for "/discuss [rounds]"
|
||||||
|
- Requires selected project with models
|
||||||
|
- Requires active discussion (from /open) or starts new one with inline question
|
||||||
|
- Parses optional rounds argument (default: 3 from project settings or hardcoded)
|
||||||
|
- Stores round_limit and current_round in context.user_data["discussion_state"]
|
||||||
|
- Runs first round via run_discussion_round
|
||||||
|
- Displays round results with "**Round N:**" header
|
||||||
|
- Shows "Round 1/N complete. Use /next or /stop"
|
||||||
|
|
||||||
|
Register /discuss handler. Store discussion_id in user_data for /next and /stop to reference.</action>
|
||||||
|
<verify>After /open, run /discuss 3, verify round 1 executes with sequential responses</verify>
|
||||||
|
<done>/discuss starts sequential discussion, stores state for continuation, displays formatted output</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Implement /next and /stop commands</name>
|
||||||
|
<files>src/moai/bot/handlers/discussion.py, src/moai/bot/handlers/__init__.py</files>
|
||||||
|
<action>Add to discussion.py:
|
||||||
|
- next_command(update, context) handler for "/next"
|
||||||
|
- Reads discussion_state from user_data
|
||||||
|
- Error if no active discussion or round_limit reached
|
||||||
|
- Increments round counter, runs run_discussion_round
|
||||||
|
- If final round: auto-complete discussion, show "Discussion complete (N rounds)"
|
||||||
|
- Otherwise: show "Round N/M complete. Use /next or /stop"
|
||||||
|
|
||||||
|
- stop_command(update, context) handler for "/stop"
|
||||||
|
- Reads discussion_state from user_data
|
||||||
|
- Completes discussion early via complete_discussion service
|
||||||
|
- Clears discussion_state from user_data
|
||||||
|
- Shows "Discussion stopped at round N. Use /consensus to summarize."
|
||||||
|
|
||||||
|
Register both handlers in __init__.py.</action>
|
||||||
|
<verify>Run full flow: /open → /discuss 2 → /next → verify round 2 runs → /stop or let it complete</verify>
|
||||||
|
<done>/next advances rounds with context, /stop ends early, both clear state appropriately</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] Full flow works: /open → /discuss 3 → /next → /next → auto-completes
|
||||||
|
- [ ] /stop works mid-discussion
|
||||||
|
- [ ] Each round shows sequential responses (Claude first, then GPT seeing Claude, etc.)
|
||||||
|
- [ ] Round counter displays correctly (Round 1/3, Round 2/3, etc.)
|
||||||
|
- [ ] Discussion marked COMPLETED when finished
|
||||||
|
- [ ] Error messages for: no discussion, round limit reached
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
|
||||||
|
- /discuss starts sequential multi-round discussion
|
||||||
|
- /next advances with full context passed to models
|
||||||
|
- /stop ends discussion early
|
||||||
|
- Models see all prior responses in context
|
||||||
|
- M5 milestone (Discuss mode sequential) complete
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/05-multi-model-discussions/05-03-SUMMARY.md`
|
||||||
|
</output>
|
||||||
104
.planning/phases/05-multi-model-discussions/05-03-SUMMARY.md
Normal file
104
.planning/phases/05-multi-model-discussions/05-03-SUMMARY.md
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
---
|
||||||
|
phase: 05-multi-model-discussions
|
||||||
|
plan: 03
|
||||||
|
subsystem: api
|
||||||
|
tags: [asyncio, telegram, ai-orchestration, sequential-rounds, context-building]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 05-multi-model-discussions/02
|
||||||
|
provides: Orchestrator with query_models_parallel, /open command
|
||||||
|
- phase: 04-single-model-qa/02
|
||||||
|
provides: AIClient.complete() and typing indicator pattern
|
||||||
|
provides:
|
||||||
|
- build_context() for assembling discussion history
|
||||||
|
- run_discussion_round() for sequential model execution
|
||||||
|
- /discuss command for starting multi-round discussions
|
||||||
|
- /next command for round progression
|
||||||
|
- /stop command for early termination
|
||||||
|
affects: [05-04-mentions, 06-consensus, 06-export]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [sequential model execution with context accumulation, user_data state for multi-command flows]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: []
|
||||||
|
modified: [src/moai/core/orchestrator.py, src/moai/bot/handlers/discussion.py, src/moai/bot/handlers/__init__.py]
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Sequential execution uses for-loop (not asyncio.gather) so each model sees prior responses"
|
||||||
|
- "Context stored in user_data['discussion_state'] for /next and /stop access"
|
||||||
|
- "All prior responses formatted as user messages with **Model:** prefix for context"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "run_discussion_round returns dict[str, str] and creates Round+Messages"
|
||||||
|
- "Discussion state in user_data enables multi-command flows"
|
||||||
|
|
||||||
|
issues-created: []
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 5min
|
||||||
|
completed: 2026-01-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 5 Plan 3: Discuss Mode Summary
|
||||||
|
|
||||||
|
**Sequential multi-round discussion with /discuss, /next, /stop commands and full context building**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 5 min
|
||||||
|
- **Started:** 2026-01-16T19:40:00Z
|
||||||
|
- **Completed:** 2026-01-16T19:45:21Z
|
||||||
|
- **Tasks:** 3
|
||||||
|
- **Files modified:** 3
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Added build_context() to convert discussion history to OpenAI message format
|
||||||
|
- Added run_discussion_round() for sequential model execution with context accumulation
|
||||||
|
- Implemented /discuss [rounds] command with configurable round limit
|
||||||
|
- Implemented /next for round progression and /stop for early termination
|
||||||
|
- State stored in user_data for multi-command discussion flow
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Add context building and sequential round execution** - `9133d4e` (feat)
|
||||||
|
2. **Task 2: Implement /discuss command handler** - `104eceb` (feat)
|
||||||
|
3. **Task 3: Implement /next and /stop commands** - `3ae08e9` (feat)
|
||||||
|
|
||||||
|
**Plan metadata:** (pending)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `src/moai/core/orchestrator.py` - Added build_context() and run_discussion_round() functions
|
||||||
|
- `src/moai/bot/handlers/discussion.py` - Added discuss_command, next_command, stop_command handlers
|
||||||
|
- `src/moai/bot/handlers/__init__.py` - Registered /discuss, /next, /stop command handlers
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- Sequential execution uses for-loop instead of asyncio.gather so each model sees responses from earlier models in the same round
|
||||||
|
- Context messages use user role with **Model:** prefix for AI context
|
||||||
|
- Discussion state stored in user_data["discussion_state"] for multi-command flow
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- M5 milestone (Discuss mode sequential) complete
|
||||||
|
- Ready for 05-04-PLAN.md (@mention direct messages)
|
||||||
|
- Discussion infrastructure ready for consensus generation in Phase 6
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 05-multi-model-discussions*
|
||||||
|
*Completed: 2026-01-16*
|
||||||
121
.planning/phases/05-multi-model-discussions/05-04-PLAN.md
Normal file
121
.planning/phases/05-multi-model-discussions/05-04-PLAN.md
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
---
|
||||||
|
phase: 05-multi-model-discussions
|
||||||
|
plan: 04
|
||||||
|
type: execute
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Implement @mention direct messages to specific models (M8 milestone).
|
||||||
|
|
||||||
|
Purpose: Allow users to direct questions/comments to specific models during discussions.
|
||||||
|
Output: Working @claude, @gpt, @gemini message handlers that query specific models with context.
|
||||||
|
</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
|
||||||
|
|
||||||
|
# Prior plan context:
|
||||||
|
@.planning/phases/05-multi-model-discussions/05-03-SUMMARY.md
|
||||||
|
|
||||||
|
# Key files:
|
||||||
|
@src/moai/core/orchestrator.py
|
||||||
|
@src/moai/core/ai_client.py
|
||||||
|
@src/moai/core/services/discussion.py
|
||||||
|
@src/moai/bot/handlers/discussion.py
|
||||||
|
|
||||||
|
**Tech stack available:** python-telegram-bot (MessageHandler with filters), openai (async)
|
||||||
|
**Established patterns:** build_context, AIClient.complete, typing indicator, Message(is_direct=True)
|
||||||
|
**Constraining decisions:**
|
||||||
|
- 05-03: Context building for discussions established
|
||||||
|
- 04-02: Typing indicator pattern
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add direct message function to orchestrator</name>
|
||||||
|
<files>src/moai/core/orchestrator.py</files>
|
||||||
|
<action>Add to orchestrator.py:
|
||||||
|
- async query_model_direct(model: str, message: str, discussion: Discussion | None, project_name: str) -> str
|
||||||
|
- Calls single model via AIClient.complete()
|
||||||
|
- If discussion provided, includes full context via build_context()
|
||||||
|
- System prompt includes "This is a direct message to you specifically"
|
||||||
|
- Returns model response
|
||||||
|
- Handles errors gracefully (returns error message string)
|
||||||
|
|
||||||
|
This is similar to /ask but with optional discussion context.</action>
|
||||||
|
<verify>Import orchestrator, verify query_model_direct signature exists</verify>
|
||||||
|
<done>orchestrator.py has query_model_direct function for single model with optional context</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Implement @mention message handler</name>
|
||||||
|
<files>src/moai/bot/handlers/discussion.py, src/moai/bot/handlers/__init__.py</files>
|
||||||
|
<action>Add to discussion.py:
|
||||||
|
- mention_handler(update, context) for messages starting with @model
|
||||||
|
- Use regex filter: MessageHandler(filters.Regex(r'^@(claude|gpt|gemini)\s'), mention_handler)
|
||||||
|
- Parse model name from first word (strip @)
|
||||||
|
- Rest of message is the content
|
||||||
|
- Get active discussion if exists (for context), otherwise just query with project context
|
||||||
|
- Call query_model_direct with discussion context
|
||||||
|
- If discussion active: create Message(is_direct=True) to persist
|
||||||
|
- Display: "**@Model (direct):**\n> response"
|
||||||
|
- Show typing indicator while waiting
|
||||||
|
|
||||||
|
Register MessageHandler in __init__.py AFTER CommandHandlers (order matters for telegram-bot).</action>
|
||||||
|
<verify>With active discussion, send "@claude What do you think?", verify response with context</verify>
|
||||||
|
<done>@mention messages route to specific model with full discussion context, marked is_direct=True</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Update status to show active discussion info</name>
|
||||||
|
<files>src/moai/bot/handlers/status.py</files>
|
||||||
|
<action>Update status_command to show:
|
||||||
|
- If discussion_state exists in user_data:
|
||||||
|
- "Active discussion: Round N/M"
|
||||||
|
- "Discussion ID: {short_id}"
|
||||||
|
- Show count of messages in current discussion
|
||||||
|
- Use get_active_discussion service if user_data cleared but DB has active
|
||||||
|
|
||||||
|
This helps users know their current discussion state.</action>
|
||||||
|
<verify>/status shows active discussion info during a discussion session</verify>
|
||||||
|
<done>/status displays current discussion state (round progress, message count)</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] @claude, @gpt, @gemini messages work
|
||||||
|
- [ ] Direct messages include discussion context when active
|
||||||
|
- [ ] Messages marked is_direct=True in database
|
||||||
|
- [ ] /status shows active discussion info
|
||||||
|
- [ ] Works without active discussion (just project context)
|
||||||
|
- [ ] M8 milestone (@mention direct messages) complete
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
|
||||||
|
- @mention syntax routes to specific models
|
||||||
|
- Full discussion context passed when available
|
||||||
|
- Direct messages persisted with is_direct flag
|
||||||
|
- /status shows discussion state
|
||||||
|
- M8 milestone complete
|
||||||
|
- Phase 5 complete (M4, M5, M8 all done)
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/05-multi-model-discussions/05-04-SUMMARY.md`
|
||||||
|
|
||||||
|
Note: This is the final plan for Phase 5. Success criteria for Phase 5:
|
||||||
|
- M4: Open mode (parallel) ✓ (05-02)
|
||||||
|
- M5: Discuss mode (sequential rounds) ✓ (05-03)
|
||||||
|
- M8: @mention direct messages ✓ (05-04)
|
||||||
|
</output>
|
||||||
103
.planning/phases/05-multi-model-discussions/05-04-SUMMARY.md
Normal file
103
.planning/phases/05-multi-model-discussions/05-04-SUMMARY.md
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
---
|
||||||
|
phase: 05-multi-model-discussions
|
||||||
|
plan: 04
|
||||||
|
subsystem: api
|
||||||
|
tags: [telegram, ai-orchestration, mention-handler, direct-messages, message-handler]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 05-multi-model-discussions/03
|
||||||
|
provides: build_context() for discussion context, discussion service with is_direct flag
|
||||||
|
- phase: 04-single-model-qa/02
|
||||||
|
provides: AIClient.complete() and typing indicator pattern
|
||||||
|
provides:
|
||||||
|
- query_model_direct() for single model queries with optional context
|
||||||
|
- @mention handler (@claude, @gpt, @gemini) for direct model messages
|
||||||
|
- Enhanced /status showing active discussion state
|
||||||
|
affects: [06-consensus, 06-export]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [MessageHandler with regex filter for @mentions, direct messages with is_direct flag]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: []
|
||||||
|
modified: [src/moai/core/orchestrator.py, src/moai/bot/handlers/discussion.py, src/moai/bot/handlers/__init__.py, src/moai/bot/handlers/status.py]
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Direct messages include '[Direct to you]:' prefix in context for model awareness"
|
||||||
|
- "MessageHandler registered AFTER CommandHandlers (telegram-bot ordering)"
|
||||||
|
- "@mentions persist with is_direct=True in current round if discussion active"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "query_model_direct for single model queries with optional discussion context"
|
||||||
|
- "MessageHandler with Regex filter for @mention syntax"
|
||||||
|
|
||||||
|
issues-created: []
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 8min
|
||||||
|
completed: 2026-01-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 5 Plan 4: @Mention Direct Messages Summary
|
||||||
|
|
||||||
|
**@claude/@gpt/@gemini direct message handlers with discussion context and enhanced /status display**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 8 min
|
||||||
|
- **Started:** 2026-01-16T19:50:00Z
|
||||||
|
- **Completed:** 2026-01-16T19:58:00Z
|
||||||
|
- **Tasks:** 3
|
||||||
|
- **Files modified:** 4
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Added query_model_direct() function for single model queries with optional discussion context
|
||||||
|
- Implemented @mention message handler with regex filter for @claude, @gpt, @gemini
|
||||||
|
- Direct messages persist with is_direct=True flag when discussion is active
|
||||||
|
- Enhanced /status command to show active discussion info (round progress, message count, discussion ID)
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Add query_model_direct() to orchestrator** - `5934d21` (feat)
|
||||||
|
2. **Task 2: Implement @mention message handler** - `3296874` (feat)
|
||||||
|
3. **Task 3: Update /status to show active discussion info** - `2a86d39` (feat)
|
||||||
|
|
||||||
|
**Plan metadata:** (pending)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `src/moai/core/orchestrator.py` - Added query_model_direct() function for direct model queries
|
||||||
|
- `src/moai/bot/handlers/discussion.py` - Added mention_handler for @model messages
|
||||||
|
- `src/moai/bot/handlers/__init__.py` - Registered MessageHandler with regex filter
|
||||||
|
- `src/moai/bot/handlers/status.py` - Enhanced to show project and discussion state
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- Direct messages prefix user input with "[Direct to you]:" so model knows it's specifically addressed
|
||||||
|
- MessageHandler registered after CommandHandlers to ensure commands take priority
|
||||||
|
- Regex filter `^@(claude|gpt|gemini)\s` requires space after model name
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- M8 milestone (@mention direct messages) complete
|
||||||
|
- Phase 5 complete (M4, M5, M8 all done)
|
||||||
|
- Discussion infrastructure ready for consensus generation (Phase 6)
|
||||||
|
- Message context and is_direct flag available for export filtering
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 05-multi-model-discussions*
|
||||||
|
*Completed: 2026-01-16*
|
||||||
14
.pre-commit-config.yaml
Normal file
14
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.14.13
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
args: [--fix]
|
||||||
|
- id: ruff-format
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v5.0.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: check-yaml
|
||||||
40
pyproject.toml
Normal file
40
pyproject.toml
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[project]
|
||||||
|
name = "moai"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Multi-AI collaborative brainstorming platform"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"python-telegram-bot",
|
||||||
|
"python-dotenv",
|
||||||
|
"sqlalchemy",
|
||||||
|
"httpx",
|
||||||
|
"aiosqlite",
|
||||||
|
"openai",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest",
|
||||||
|
"pytest-cov",
|
||||||
|
"pytest-asyncio",
|
||||||
|
"ruff",
|
||||||
|
"pre-commit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py311"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "I", "N", "W", "UP"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/moai"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
7
src/moai/__init__.py
Normal file
7
src/moai/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
"""MoAI - Multi-AI collaborative brainstorming platform.
|
||||||
|
|
||||||
|
A Telegram bot platform enabling multiple AI models (Claude, GPT, Gemini)
|
||||||
|
to discuss topics together in structured rounds, working toward consensus.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
5
src/moai/bot/__init__.py
Normal file
5
src/moai/bot/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
"""Telegram bot handlers and entry point.
|
||||||
|
|
||||||
|
This module contains the Telegram bot setup, command handlers,
|
||||||
|
and middleware for the MoAI platform.
|
||||||
|
"""
|
||||||
81
src/moai/bot/config.py
Normal file
81
src/moai/bot/config.py
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
"""Bot configuration loaded from environment variables.
|
||||||
|
|
||||||
|
Provides BotConfig dataclass with configuration loaded from environment.
|
||||||
|
Required variables will raise ValueError if missing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BotConfig:
|
||||||
|
"""Configuration for the MoAI Telegram bot.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
bot_token: Telegram Bot API token (required).
|
||||||
|
allowed_users: Set of allowed Telegram user IDs. Empty means all users allowed.
|
||||||
|
database_url: SQLAlchemy database URL.
|
||||||
|
log_level: Logging level string (DEBUG, INFO, WARNING, ERROR).
|
||||||
|
ai_router: AI router service ("requesty" or "openrouter").
|
||||||
|
ai_api_key: API key for the AI router service.
|
||||||
|
ai_referer: HTTP-Referer header for OpenRouter (optional).
|
||||||
|
"""
|
||||||
|
|
||||||
|
bot_token: str
|
||||||
|
allowed_users: set[int]
|
||||||
|
database_url: str
|
||||||
|
log_level: str
|
||||||
|
ai_router: str
|
||||||
|
ai_api_key: str
|
||||||
|
ai_referer: str | None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_env(cls) -> "BotConfig":
|
||||||
|
"""Load configuration from environment variables.
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
BOT_TOKEN (required): Telegram bot token from @BotFather.
|
||||||
|
ALLOWED_USERS (optional): Comma-separated Telegram user IDs.
|
||||||
|
DATABASE_URL (optional): Database URL, defaults to SQLite.
|
||||||
|
LOG_LEVEL (optional): Logging level, defaults to INFO.
|
||||||
|
AI_ROUTER (optional): AI router service, defaults to "requesty".
|
||||||
|
AI_API_KEY (optional): API key for the AI router service.
|
||||||
|
AI_REFERER (optional): HTTP-Referer header for OpenRouter.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BotConfig instance populated from environment.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If BOT_TOKEN is not set.
|
||||||
|
"""
|
||||||
|
bot_token = os.environ.get("BOT_TOKEN")
|
||||||
|
if not bot_token:
|
||||||
|
raise ValueError("BOT_TOKEN environment variable is required")
|
||||||
|
|
||||||
|
# Parse allowed users from comma-separated string
|
||||||
|
allowed_users_str = os.environ.get("ALLOWED_USERS", "")
|
||||||
|
allowed_users: set[int] = set()
|
||||||
|
if allowed_users_str.strip():
|
||||||
|
for user_id in allowed_users_str.split(","):
|
||||||
|
user_id = user_id.strip()
|
||||||
|
if user_id:
|
||||||
|
allowed_users.add(int(user_id))
|
||||||
|
|
||||||
|
database_url = os.environ.get("DATABASE_URL", "sqlite+aiosqlite:///./moai.db")
|
||||||
|
log_level = os.environ.get("LOG_LEVEL", "INFO")
|
||||||
|
|
||||||
|
# AI router configuration
|
||||||
|
ai_router = os.environ.get("AI_ROUTER", "requesty")
|
||||||
|
ai_api_key = os.environ.get("AI_API_KEY", "")
|
||||||
|
ai_referer = os.environ.get("AI_REFERER")
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
bot_token=bot_token,
|
||||||
|
allowed_users=allowed_users,
|
||||||
|
database_url=database_url,
|
||||||
|
log_level=log_level,
|
||||||
|
ai_router=ai_router,
|
||||||
|
ai_api_key=ai_api_key,
|
||||||
|
ai_referer=ai_referer,
|
||||||
|
)
|
||||||
48
src/moai/bot/handlers/__init__.py
Normal file
48
src/moai/bot/handlers/__init__.py
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
"""Telegram command handlers for MoAI bot.
|
||||||
|
|
||||||
|
This module contains handlers for Telegram bot commands including
|
||||||
|
project management, discussion commands, and export functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from telegram.ext import Application, CommandHandler, MessageHandler, filters
|
||||||
|
|
||||||
|
from moai.bot.handlers.commands import help_command, start_command
|
||||||
|
from moai.bot.handlers.discussion import (
|
||||||
|
ask_command,
|
||||||
|
discuss_command,
|
||||||
|
mention_handler,
|
||||||
|
next_command,
|
||||||
|
open_command,
|
||||||
|
stop_command,
|
||||||
|
)
|
||||||
|
from moai.bot.handlers.projects import project_command, projects_command
|
||||||
|
from moai.bot.handlers.status import status_command
|
||||||
|
|
||||||
|
|
||||||
|
def register_handlers(app: Application) -> None:
|
||||||
|
"""Register all command handlers with the application.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: The telegram Application instance.
|
||||||
|
"""
|
||||||
|
# Basic commands
|
||||||
|
app.add_handler(CommandHandler("start", start_command))
|
||||||
|
app.add_handler(CommandHandler("help", help_command))
|
||||||
|
|
||||||
|
# Status
|
||||||
|
app.add_handler(CommandHandler("status", status_command))
|
||||||
|
|
||||||
|
# Project management
|
||||||
|
app.add_handler(CommandHandler("projects", projects_command))
|
||||||
|
app.add_handler(CommandHandler("project", project_command))
|
||||||
|
|
||||||
|
# Discussion / Q&A
|
||||||
|
app.add_handler(CommandHandler("ask", ask_command))
|
||||||
|
app.add_handler(CommandHandler("open", open_command))
|
||||||
|
app.add_handler(CommandHandler("discuss", discuss_command))
|
||||||
|
app.add_handler(CommandHandler("next", next_command))
|
||||||
|
app.add_handler(CommandHandler("stop", stop_command))
|
||||||
|
|
||||||
|
# @mention handler - MessageHandler registered AFTER CommandHandlers
|
||||||
|
# Matches messages starting with @claude, @gpt, or @gemini followed by content
|
||||||
|
app.add_handler(MessageHandler(filters.Regex(r"^@(claude|gpt|gemini)\s"), mention_handler))
|
||||||
50
src/moai/bot/handlers/commands.py
Normal file
50
src/moai/bot/handlers/commands.py
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
"""Basic command handlers for MoAI bot."""
|
||||||
|
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import ContextTypes
|
||||||
|
|
||||||
|
HELP_TEXT = """
|
||||||
|
*MoAI - Master of AIs*
|
||||||
|
|
||||||
|
Multi-AI collaborative brainstorming platform.
|
||||||
|
|
||||||
|
*Project Commands:*
|
||||||
|
/projects - List all projects
|
||||||
|
/project new "Name" - Create new project
|
||||||
|
/project select <id|name> - Switch to project
|
||||||
|
/project delete <id> - Delete project
|
||||||
|
/project models claude,gpt - Set models
|
||||||
|
/project info - Show current project
|
||||||
|
|
||||||
|
*Questions:*
|
||||||
|
/ask <model> <question> - Ask a single model
|
||||||
|
|
||||||
|
*Discussion Commands:*
|
||||||
|
/open <question> - Ask all models (parallel)
|
||||||
|
/discuss [rounds] - Start discussion (default: 3)
|
||||||
|
/next - Trigger next round
|
||||||
|
/stop - Stop current discussion
|
||||||
|
@model <message> - Direct message to model
|
||||||
|
|
||||||
|
*Output Commands:*
|
||||||
|
/consensus - Generate consensus summary
|
||||||
|
/export - Export project as markdown
|
||||||
|
|
||||||
|
*Utility:*
|
||||||
|
/status - Show current state
|
||||||
|
/help - Show this message
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
|
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Handle /start command - welcome message."""
|
||||||
|
await update.message.reply_text(
|
||||||
|
"Welcome to MoAI!\n\n"
|
||||||
|
"Use /help to see available commands.\n"
|
||||||
|
'Use /project new "Name" to create your first project.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Handle /help command - show available commands."""
|
||||||
|
await update.message.reply_text(HELP_TEXT, parse_mode="Markdown")
|
||||||
427
src/moai/bot/handlers/discussion.py
Normal file
427
src/moai/bot/handlers/discussion.py
Normal file
|
|
@ -0,0 +1,427 @@
|
||||||
|
"""Discussion handlers for MoAI bot."""
|
||||||
|
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import ContextTypes
|
||||||
|
|
||||||
|
from moai.bot.handlers.projects import get_selected_project
|
||||||
|
from moai.core.ai_client import MODEL_MAP, get_ai_client
|
||||||
|
from moai.core.models import DiscussionType, RoundType
|
||||||
|
from moai.core.orchestrator import query_model_direct, query_models_parallel, run_discussion_round
|
||||||
|
from moai.core.services.discussion import (
|
||||||
|
complete_discussion,
|
||||||
|
create_discussion,
|
||||||
|
create_message,
|
||||||
|
create_round,
|
||||||
|
get_active_discussion,
|
||||||
|
get_current_round,
|
||||||
|
get_discussion,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def ask_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Handle /ask <model> <question> command.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
/ask claude What is Python?
|
||||||
|
/ask gpt Explain async/await
|
||||||
|
"""
|
||||||
|
args = context.args or []
|
||||||
|
|
||||||
|
if len(args) < 2:
|
||||||
|
available = ", ".join(MODEL_MAP.keys())
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"Usage: /ask <model> <question>\n"
|
||||||
|
f"Available models: {available}\n\n"
|
||||||
|
f"Example: /ask claude What is Python?"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
model_name = args[0].lower()
|
||||||
|
question = " ".join(args[1:])
|
||||||
|
|
||||||
|
# Validate model
|
||||||
|
if model_name not in MODEL_MAP:
|
||||||
|
available = ", ".join(MODEL_MAP.keys())
|
||||||
|
await update.message.reply_text(f"Unknown model: {model_name}\nAvailable: {available}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get project context if available (optional for /ask)
|
||||||
|
project = await get_selected_project(context)
|
||||||
|
project_context = f"Project: {project.name}\n" if project else ""
|
||||||
|
|
||||||
|
# Send "typing" indicator while waiting for AI
|
||||||
|
await update.message.chat.send_action("typing")
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = get_ai_client()
|
||||||
|
response = await client.complete(
|
||||||
|
model=model_name,
|
||||||
|
messages=[{"role": "user", "content": question}],
|
||||||
|
system_prompt=f"{project_context}You are a helpful AI assistant.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format response with model name
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"*{model_name.title()}:*\n\n{response}",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
await update.message.reply_text(f"Error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def open_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Handle /open <question> command - ask all project models in parallel.
|
||||||
|
|
||||||
|
Requires a selected project with configured models. Creates a Discussion
|
||||||
|
with OPEN type and a PARALLEL round, then queries all models simultaneously.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
/open What is Python?
|
||||||
|
/open How should we approach this problem?
|
||||||
|
"""
|
||||||
|
args = context.args or []
|
||||||
|
|
||||||
|
if not args:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"Usage: /open <question>\n\nExample: /open What are the pros and cons of microservices?"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
question = " ".join(args)
|
||||||
|
|
||||||
|
# Require a selected project
|
||||||
|
project = await get_selected_project(context)
|
||||||
|
if project is None:
|
||||||
|
await update.message.reply_text("No project selected. Use /project select <name> first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Require configured models
|
||||||
|
if not project.models:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"No models configured for this project.\n"
|
||||||
|
"Use /project models claude,gpt,gemini to set models."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Show typing indicator while waiting for AI
|
||||||
|
await update.message.chat.send_action("typing")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create discussion and round in database
|
||||||
|
discussion = await create_discussion(
|
||||||
|
project_id=project.id,
|
||||||
|
question=question,
|
||||||
|
discussion_type=DiscussionType.OPEN,
|
||||||
|
)
|
||||||
|
round_ = await create_round(
|
||||||
|
discussion_id=discussion.id,
|
||||||
|
round_number=1,
|
||||||
|
round_type=RoundType.PARALLEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Query all models in parallel
|
||||||
|
responses = await query_models_parallel(
|
||||||
|
models=project.models,
|
||||||
|
question=question,
|
||||||
|
project_name=project.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Persist messages and build response text
|
||||||
|
response_lines = [f"*Question:* {question}\n"]
|
||||||
|
|
||||||
|
for model, response in responses.items():
|
||||||
|
await create_message(
|
||||||
|
round_id=round_.id,
|
||||||
|
model=model,
|
||||||
|
content=response,
|
||||||
|
)
|
||||||
|
response_lines.append(f"*{model.title()}:*\n{response}\n")
|
||||||
|
|
||||||
|
# Send combined response
|
||||||
|
await update.message.reply_text(
|
||||||
|
"\n".join(response_lines),
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await update.message.reply_text(f"Error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def discuss_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Handle /discuss [rounds] command - start sequential multi-round discussion.
|
||||||
|
|
||||||
|
Requires a selected project with configured models and an active discussion
|
||||||
|
(created via /open). Starts a sequential discussion where each model sees
|
||||||
|
prior responses.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
/discuss - Start 3-round discussion (default)
|
||||||
|
/discuss 5 - Start 5-round discussion
|
||||||
|
"""
|
||||||
|
args = context.args or []
|
||||||
|
|
||||||
|
# Parse optional round limit (default: 3)
|
||||||
|
round_limit = 3
|
||||||
|
if args:
|
||||||
|
try:
|
||||||
|
round_limit = int(args[0])
|
||||||
|
if round_limit < 1:
|
||||||
|
await update.message.reply_text("Round limit must be at least 1.")
|
||||||
|
return
|
||||||
|
except ValueError:
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"Invalid round limit: {args[0]}\n\nUsage: /discuss [rounds]"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Require a selected project
|
||||||
|
project = await get_selected_project(context)
|
||||||
|
if project is None:
|
||||||
|
await update.message.reply_text("No project selected. Use /project select <name> first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Require configured models
|
||||||
|
if not project.models:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"No models configured for this project.\n"
|
||||||
|
"Use /project models claude,gpt,gemini to set models."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check for active discussion
|
||||||
|
discussion = await get_active_discussion(project.id)
|
||||||
|
if discussion is None:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"No active discussion. Start one with /open <question> first."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Calculate next round number (continue from existing rounds)
|
||||||
|
current_round_num = len(discussion.rounds) + 1
|
||||||
|
|
||||||
|
# Store discussion state for /next and /stop
|
||||||
|
context.user_data["discussion_state"] = {
|
||||||
|
"discussion_id": discussion.id,
|
||||||
|
"project_id": project.id,
|
||||||
|
"project_name": project.name,
|
||||||
|
"models": project.models,
|
||||||
|
"current_round": current_round_num,
|
||||||
|
"round_limit": round_limit,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show typing indicator
|
||||||
|
await update.message.chat.send_action("typing")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Reload discussion with full eager loading for context building
|
||||||
|
discussion = await get_discussion(discussion.id)
|
||||||
|
|
||||||
|
# Run first round
|
||||||
|
responses = await run_discussion_round(
|
||||||
|
discussion=discussion,
|
||||||
|
models=project.models,
|
||||||
|
project_name=project.name,
|
||||||
|
round_number=current_round_num,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build response text
|
||||||
|
response_lines = [f"*Round {current_round_num}/{round_limit}:*\n"]
|
||||||
|
for model, response in responses.items():
|
||||||
|
response_lines.append(f"*{model.title()}:*\n{response}\n")
|
||||||
|
|
||||||
|
if current_round_num >= round_limit:
|
||||||
|
response_lines.append(f"\n_Discussion complete ({round_limit} rounds)._")
|
||||||
|
# Clear state when done
|
||||||
|
del context.user_data["discussion_state"]
|
||||||
|
else:
|
||||||
|
response_lines.append(
|
||||||
|
f"\n_Round {current_round_num}/{round_limit} complete. Use /next or /stop._"
|
||||||
|
)
|
||||||
|
# Update current round for next time
|
||||||
|
context.user_data["discussion_state"]["current_round"] = current_round_num + 1
|
||||||
|
|
||||||
|
await update.message.reply_text(
|
||||||
|
"\n".join(response_lines),
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await update.message.reply_text(f"Error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def next_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Handle /next command - advance to the next discussion round.
|
||||||
|
|
||||||
|
Requires an active discussion started with /discuss. Runs the next
|
||||||
|
sequential round with full context from prior rounds.
|
||||||
|
"""
|
||||||
|
# Check for active discussion state
|
||||||
|
state = context.user_data.get("discussion_state")
|
||||||
|
if state is None:
|
||||||
|
await update.message.reply_text("No active discussion. Start one with /open then /discuss.")
|
||||||
|
return
|
||||||
|
|
||||||
|
current_round = state["current_round"]
|
||||||
|
round_limit = state["round_limit"]
|
||||||
|
|
||||||
|
# Check if already at limit
|
||||||
|
if current_round > round_limit:
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"Round limit ({round_limit}) reached. Start a new discussion with /open."
|
||||||
|
)
|
||||||
|
del context.user_data["discussion_state"]
|
||||||
|
return
|
||||||
|
|
||||||
|
# Show typing indicator
|
||||||
|
await update.message.chat.send_action("typing")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Load discussion with full context
|
||||||
|
discussion = await get_discussion(state["discussion_id"])
|
||||||
|
if discussion is None:
|
||||||
|
await update.message.reply_text("Discussion not found.")
|
||||||
|
del context.user_data["discussion_state"]
|
||||||
|
return
|
||||||
|
|
||||||
|
# Run the next round
|
||||||
|
responses = await run_discussion_round(
|
||||||
|
discussion=discussion,
|
||||||
|
models=state["models"],
|
||||||
|
project_name=state["project_name"],
|
||||||
|
round_number=current_round,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build response text
|
||||||
|
response_lines = [f"*Round {current_round}/{round_limit}:*\n"]
|
||||||
|
for model, response in responses.items():
|
||||||
|
response_lines.append(f"*{model.title()}:*\n{response}\n")
|
||||||
|
|
||||||
|
if current_round >= round_limit:
|
||||||
|
# Final round - complete discussion
|
||||||
|
await complete_discussion(state["discussion_id"])
|
||||||
|
response_lines.append(f"\n_Discussion complete ({round_limit} rounds)._")
|
||||||
|
del context.user_data["discussion_state"]
|
||||||
|
else:
|
||||||
|
response_lines.append(
|
||||||
|
f"\n_Round {current_round}/{round_limit} complete. Use /next or /stop._"
|
||||||
|
)
|
||||||
|
# Update state for next round
|
||||||
|
context.user_data["discussion_state"]["current_round"] = current_round + 1
|
||||||
|
|
||||||
|
await update.message.reply_text(
|
||||||
|
"\n".join(response_lines),
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await update.message.reply_text(f"Error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def stop_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Handle /stop command - stop the current discussion early.
|
||||||
|
|
||||||
|
Completes the discussion at the current round and clears the session state.
|
||||||
|
"""
|
||||||
|
# Check for active discussion state
|
||||||
|
state = context.user_data.get("discussion_state")
|
||||||
|
if state is None:
|
||||||
|
await update.message.reply_text("No active discussion to stop.")
|
||||||
|
return
|
||||||
|
|
||||||
|
current_round = state["current_round"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Complete the discussion in database
|
||||||
|
await complete_discussion(state["discussion_id"])
|
||||||
|
|
||||||
|
# Clear session state
|
||||||
|
del context.user_data["discussion_state"]
|
||||||
|
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"_Discussion stopped at round {current_round - 1}. Use /consensus to summarize._",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await update.message.reply_text(f"Error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def mention_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Handle @model mention messages.
|
||||||
|
|
||||||
|
Messages starting with @claude, @gpt, or @gemini are routed to that
|
||||||
|
specific model. If a discussion is active, includes full context.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
@claude What do you think about this?
|
||||||
|
@gpt Can you elaborate on your previous point?
|
||||||
|
@gemini Do you agree with Claude?
|
||||||
|
"""
|
||||||
|
message_text = update.message.text
|
||||||
|
if not message_text:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Parse model name from first word (e.g., "@claude" -> "claude")
|
||||||
|
parts = message_text.split(maxsplit=1)
|
||||||
|
if not parts:
|
||||||
|
return
|
||||||
|
|
||||||
|
model_tag = parts[0].lower()
|
||||||
|
# Strip the @ prefix
|
||||||
|
model_name = model_tag.lstrip("@")
|
||||||
|
|
||||||
|
# Validate model
|
||||||
|
if model_name not in MODEL_MAP:
|
||||||
|
return # Not a valid model mention, ignore
|
||||||
|
|
||||||
|
# Get the rest of the message as content
|
||||||
|
content = parts[1] if len(parts) > 1 else ""
|
||||||
|
if not content.strip():
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"Usage: @{model_name} <message>\n\nExample: @{model_name} What do you think?"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get project context if available
|
||||||
|
project = await get_selected_project(context)
|
||||||
|
if project is None:
|
||||||
|
await update.message.reply_text("No project selected. Use /project select <name> first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
project_name = project.name
|
||||||
|
|
||||||
|
# Check for active discussion (for context)
|
||||||
|
discussion = await get_active_discussion(project.id)
|
||||||
|
|
||||||
|
# Show typing indicator
|
||||||
|
await update.message.chat.send_action("typing")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Query the model directly with optional discussion context
|
||||||
|
response = await query_model_direct(
|
||||||
|
model=model_name,
|
||||||
|
message=content,
|
||||||
|
discussion=discussion,
|
||||||
|
project_name=project_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# If there's an active discussion, persist the message
|
||||||
|
if discussion is not None:
|
||||||
|
# Get or create a round for this direct message
|
||||||
|
current_round = await get_current_round(discussion.id)
|
||||||
|
if current_round is not None:
|
||||||
|
await create_message(
|
||||||
|
round_id=current_round.id,
|
||||||
|
model=model_name,
|
||||||
|
content=response,
|
||||||
|
is_direct=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format response
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"*@{model_name.title()} (direct):*\n{response}",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await update.message.reply_text(f"Error: {e}")
|
||||||
237
src/moai/bot/handlers/projects.py
Normal file
237
src/moai/bot/handlers/projects.py
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
"""Project management handlers for MoAI bot."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import ContextTypes
|
||||||
|
|
||||||
|
from moai.core.models import Project
|
||||||
|
from moai.core.services.project import (
|
||||||
|
create_project,
|
||||||
|
delete_project,
|
||||||
|
get_project,
|
||||||
|
get_project_by_name,
|
||||||
|
list_projects,
|
||||||
|
update_project_models,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def projects_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Handle /projects command - list all projects."""
|
||||||
|
projects = await list_projects()
|
||||||
|
|
||||||
|
if not projects:
|
||||||
|
await update.message.reply_text('No projects yet. Use /project new "Name" to create one.')
|
||||||
|
return
|
||||||
|
|
||||||
|
lines = ["*Your Projects:*\n"]
|
||||||
|
for p in projects:
|
||||||
|
models_str = ", ".join(p.models) if p.models else "none"
|
||||||
|
lines.append(f"• *{p.name}*\n ID: `{p.id[:8]}...`\n Models: {models_str}")
|
||||||
|
|
||||||
|
await update.message.reply_text("\n".join(lines), parse_mode="Markdown")
|
||||||
|
|
||||||
|
|
||||||
|
async def project_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Handle /project subcommands.
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
new "Name" - Create new project
|
||||||
|
select <id|name> - Switch to project
|
||||||
|
delete <id> - Delete project by ID
|
||||||
|
models <list> - Set models for current project
|
||||||
|
info - Show current project
|
||||||
|
"""
|
||||||
|
args = context.args or []
|
||||||
|
|
||||||
|
if not args:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"Usage:\n"
|
||||||
|
'/project new "Name" - Create project\n'
|
||||||
|
"/project select <id|name> - Switch project\n"
|
||||||
|
"/project info - Show current project\n"
|
||||||
|
"/project models [model1,model2,...] - Set/show models\n"
|
||||||
|
"/project delete <id> - Delete project"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
subcommand = args[0].lower()
|
||||||
|
|
||||||
|
if subcommand == "new":
|
||||||
|
await _handle_project_new(update, context, args[1:])
|
||||||
|
elif subcommand == "select":
|
||||||
|
await _handle_project_select(update, context, args[1:])
|
||||||
|
elif subcommand == "info":
|
||||||
|
await _handle_project_info(update, context)
|
||||||
|
elif subcommand == "models":
|
||||||
|
await _handle_project_models(update, context, args[1:])
|
||||||
|
elif subcommand == "delete":
|
||||||
|
await _handle_project_delete(update, context, args[1:])
|
||||||
|
else:
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"Unknown subcommand: {subcommand}\nAvailable: new, select, info, models, delete"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_project_new(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE, args: list[str]
|
||||||
|
) -> None:
|
||||||
|
"""Handle /project new "Name" command."""
|
||||||
|
if not args:
|
||||||
|
await update.message.reply_text('Usage: /project new "Project Name"')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Join args and extract quoted name
|
||||||
|
text = " ".join(args)
|
||||||
|
match = re.match(r'^"([^"]+)"', text) or re.match(r"^'([^']+)'", text)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
name = match.group(1)
|
||||||
|
else:
|
||||||
|
# No quotes - use the first arg as name
|
||||||
|
name = args[0]
|
||||||
|
|
||||||
|
project = await create_project(name)
|
||||||
|
models_str = ", ".join(project.models)
|
||||||
|
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"*Project Created*\n\n"
|
||||||
|
f"Name: {project.name}\n"
|
||||||
|
f"ID: `{project.id}`\n"
|
||||||
|
f"Models: {models_str}\n\n"
|
||||||
|
f"Use /project select {project.id[:8]} to switch to this project.",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_project_select(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE, args: list[str]
|
||||||
|
) -> None:
|
||||||
|
"""Handle /project select <id|name> command."""
|
||||||
|
if not args:
|
||||||
|
await update.message.reply_text("Usage: /project select <id|name>")
|
||||||
|
return
|
||||||
|
|
||||||
|
identifier = " ".join(args)
|
||||||
|
|
||||||
|
# Try to find by ID first (supports partial ID match)
|
||||||
|
project = await get_project(identifier)
|
||||||
|
|
||||||
|
# If not found by ID, try by name
|
||||||
|
if project is None:
|
||||||
|
project = await get_project_by_name(identifier)
|
||||||
|
|
||||||
|
if project is None:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"Project not found. Use /projects to list available projects."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Store selected project ID in user_data
|
||||||
|
context.user_data["selected_project_id"] = project.id
|
||||||
|
|
||||||
|
await update.message.reply_text(f"Selected project: {project.name}")
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_project_info(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Handle /project info command."""
|
||||||
|
project = await get_selected_project(context)
|
||||||
|
|
||||||
|
if project is None:
|
||||||
|
await update.message.reply_text("No project selected. Use /project select <name> first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
models_str = ", ".join(project.models) if project.models else "none"
|
||||||
|
# Note: discussion_count will be available when discussions are implemented
|
||||||
|
discussion_count = len(project.discussions) if project.discussions else 0
|
||||||
|
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"*Project Info*\n\n"
|
||||||
|
f"Name: {project.name}\n"
|
||||||
|
f"ID: `{project.id}`\n"
|
||||||
|
f"Models: {models_str}\n"
|
||||||
|
f"Created: {project.created_at.strftime('%Y-%m-%d %H:%M')}\n"
|
||||||
|
f"Discussions: {discussion_count}",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_project_models(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE, args: list[str]
|
||||||
|
) -> None:
|
||||||
|
"""Handle /project models [model1,model2,...] command."""
|
||||||
|
project = await get_selected_project(context)
|
||||||
|
|
||||||
|
if project is None:
|
||||||
|
await update.message.reply_text("No project selected. Use /project select <name> first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# No args: show current models
|
||||||
|
if not args:
|
||||||
|
models_str = ", ".join(project.models) if project.models else "none"
|
||||||
|
await update.message.reply_text(f"Current models: {models_str}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Parse comma-separated model names
|
||||||
|
models_input = " ".join(args)
|
||||||
|
models = [m.strip() for m in models_input.split(",") if m.strip()]
|
||||||
|
|
||||||
|
if not models:
|
||||||
|
await update.message.reply_text("Usage: /project models claude,gpt,gemini")
|
||||||
|
return
|
||||||
|
|
||||||
|
updated_project = await update_project_models(project.id, models)
|
||||||
|
|
||||||
|
if updated_project is None:
|
||||||
|
await update.message.reply_text("Project not found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
models_str = ", ".join(updated_project.models)
|
||||||
|
await update.message.reply_text(f"Models updated: {models_str}")
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_project_delete(
|
||||||
|
update: Update, context: ContextTypes.DEFAULT_TYPE, args: list[str]
|
||||||
|
) -> None:
|
||||||
|
"""Handle /project delete <id> command."""
|
||||||
|
if not args:
|
||||||
|
await update.message.reply_text("Usage: /project delete <project-id>")
|
||||||
|
return
|
||||||
|
|
||||||
|
project_id = args[0]
|
||||||
|
|
||||||
|
# Get project first to show name in confirmation
|
||||||
|
project = await get_project(project_id)
|
||||||
|
|
||||||
|
if project is None:
|
||||||
|
await update.message.reply_text("Project not found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
project_name = project.name
|
||||||
|
|
||||||
|
deleted = await delete_project(project_id)
|
||||||
|
|
||||||
|
if not deleted:
|
||||||
|
await update.message.reply_text("Project not found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Clear selection if deleted project was selected
|
||||||
|
if context.user_data.get("selected_project_id") == project_id:
|
||||||
|
context.user_data.pop("selected_project_id", None)
|
||||||
|
|
||||||
|
await update.message.reply_text(f"Deleted project: {project_name}")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_selected_project(context: ContextTypes.DEFAULT_TYPE) -> Project | None:
|
||||||
|
"""Get the currently selected project from user_data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: The telegram context with user_data.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The Project object if one is selected, None otherwise.
|
||||||
|
"""
|
||||||
|
project_id = context.user_data.get("selected_project_id")
|
||||||
|
if project_id is None:
|
||||||
|
return None
|
||||||
|
return await get_project(project_id)
|
||||||
73
src/moai/bot/handlers/status.py
Normal file
73
src/moai/bot/handlers/status.py
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
"""Status command handler for MoAI bot."""
|
||||||
|
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import ContextTypes
|
||||||
|
|
||||||
|
from moai.bot.handlers.projects import get_selected_project
|
||||||
|
from moai.core.ai_client import get_ai_client
|
||||||
|
from moai.core.services.discussion import get_active_discussion
|
||||||
|
|
||||||
|
|
||||||
|
async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Handle /status command - show current project/discussion state.
|
||||||
|
|
||||||
|
Shows:
|
||||||
|
- Bot and AI router status
|
||||||
|
- Selected project (if any)
|
||||||
|
- Active discussion state (if any)
|
||||||
|
"""
|
||||||
|
# Check AI client status
|
||||||
|
try:
|
||||||
|
client = get_ai_client()
|
||||||
|
ai_status = f"AI Router: {client.router}"
|
||||||
|
except RuntimeError:
|
||||||
|
ai_status = "AI Router: not configured"
|
||||||
|
|
||||||
|
# Build status lines
|
||||||
|
status_lines = [
|
||||||
|
"*MoAI Status*\n",
|
||||||
|
"Bot: Online",
|
||||||
|
"Database: Connected",
|
||||||
|
ai_status,
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Check for selected project
|
||||||
|
project = await get_selected_project(context)
|
||||||
|
if project is None:
|
||||||
|
status_lines.append('_No project selected. Use /project new "Name" to create one._')
|
||||||
|
else:
|
||||||
|
status_lines.append(f"*Project:* {project.name}")
|
||||||
|
if project.models:
|
||||||
|
models_str = ", ".join(project.models)
|
||||||
|
status_lines.append(f"Models: {models_str}")
|
||||||
|
else:
|
||||||
|
status_lines.append("Models: none configured")
|
||||||
|
|
||||||
|
# Check for active discussion
|
||||||
|
discussion = await get_active_discussion(project.id)
|
||||||
|
if discussion is not None:
|
||||||
|
# Count messages across all rounds
|
||||||
|
message_count = sum(len(r.messages) for r in discussion.rounds)
|
||||||
|
round_count = len(discussion.rounds)
|
||||||
|
|
||||||
|
# Check for in-progress discussion state
|
||||||
|
disc_state = context.user_data.get("discussion_state")
|
||||||
|
if disc_state and disc_state.get("discussion_id") == discussion.id:
|
||||||
|
current_round = disc_state["current_round"]
|
||||||
|
round_limit = disc_state["round_limit"]
|
||||||
|
status_lines.append("")
|
||||||
|
status_lines.append(f"*Active Discussion:* Round {current_round}/{round_limit}")
|
||||||
|
status_lines.append(f"Discussion ID: {discussion.id[:8]}...")
|
||||||
|
status_lines.append(f"Messages: {message_count}")
|
||||||
|
else:
|
||||||
|
status_lines.append("")
|
||||||
|
status_lines.append(f"*Active Discussion:* {round_count} rounds completed")
|
||||||
|
status_lines.append(f"Discussion ID: {discussion.id[:8]}...")
|
||||||
|
status_lines.append(f"Messages: {message_count}")
|
||||||
|
status_lines.append("_Use /discuss to continue or /stop to end._")
|
||||||
|
else:
|
||||||
|
status_lines.append("")
|
||||||
|
status_lines.append("_No active discussion. Use /open <question> to start._")
|
||||||
|
|
||||||
|
await update.message.reply_text("\n".join(status_lines), parse_mode="Markdown")
|
||||||
88
src/moai/bot/main.py
Normal file
88
src/moai/bot/main.py
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
"""MoAI Telegram bot entry point.
|
||||||
|
|
||||||
|
Sets up and runs the Telegram bot with database lifecycle hooks.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python -m moai.bot.main
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from telegram.ext import Application, ApplicationBuilder
|
||||||
|
|
||||||
|
from moai.bot.config import BotConfig
|
||||||
|
from moai.bot.handlers import register_handlers
|
||||||
|
from moai.core.ai_client import init_ai_client
|
||||||
|
from moai.core.database import close_db, create_tables, init_db
|
||||||
|
|
||||||
|
# Module-level config reference for post_init callback
|
||||||
|
_config: BotConfig | None = None
|
||||||
|
|
||||||
|
|
||||||
|
async def post_init(application: Application) -> None:
|
||||||
|
"""Initialize database after bot application is built.
|
||||||
|
|
||||||
|
Called automatically by python-telegram-bot after Application.build().
|
||||||
|
Sets up the database engine and creates tables if needed.
|
||||||
|
"""
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
if _config is None:
|
||||||
|
raise RuntimeError("Config not initialized before post_init")
|
||||||
|
|
||||||
|
init_db(_config.database_url)
|
||||||
|
await create_tables()
|
||||||
|
logger.info("Database initialized")
|
||||||
|
|
||||||
|
init_ai_client(_config)
|
||||||
|
logger.info("AI client initialized with %s", _config.ai_router)
|
||||||
|
|
||||||
|
|
||||||
|
async def post_shutdown(application: Application) -> None:
|
||||||
|
"""Clean up database on bot shutdown.
|
||||||
|
|
||||||
|
Called automatically by python-telegram-bot during Application shutdown.
|
||||||
|
Disposes database engine and releases connections.
|
||||||
|
"""
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
await close_db()
|
||||||
|
logger.info("Database closed")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Run the MoAI Telegram bot.
|
||||||
|
|
||||||
|
Loads configuration from environment, sets up the Application with
|
||||||
|
database lifecycle hooks, registers handlers, and starts polling.
|
||||||
|
"""
|
||||||
|
global _config
|
||||||
|
|
||||||
|
load_dotenv() # Load .env file
|
||||||
|
_config = BotConfig.from_env()
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=getattr(logging, _config.log_level.upper(), logging.INFO),
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
app = (
|
||||||
|
ApplicationBuilder()
|
||||||
|
.token(_config.bot_token)
|
||||||
|
.post_init(post_init)
|
||||||
|
.post_shutdown(post_shutdown)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store config in bot_data for handler access
|
||||||
|
app.bot_data["config"] = _config
|
||||||
|
|
||||||
|
register_handlers(app)
|
||||||
|
|
||||||
|
logger.info("Starting MoAI bot...")
|
||||||
|
app.run_polling()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
5
src/moai/core/__init__.py
Normal file
5
src/moai/core/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
"""Core business logic, models, and services.
|
||||||
|
|
||||||
|
This module contains the core functionality including database models,
|
||||||
|
the AI orchestrator, AI client abstraction, and export services.
|
||||||
|
"""
|
||||||
125
src/moai/core/ai_client.py
Normal file
125
src/moai/core/ai_client.py
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
"""AI client abstraction for model routing.
|
||||||
|
|
||||||
|
Provides AIClient class that wraps the OpenAI SDK to communicate with
|
||||||
|
AI model routers (Requesty or OpenRouter). Both routers are OpenAI-compatible.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from openai import AsyncOpenAI
|
||||||
|
|
||||||
|
from moai.bot.config import BotConfig
|
||||||
|
|
||||||
|
# Router base URLs
|
||||||
|
ROUTER_URLS = {
|
||||||
|
"requesty": "https://router.requesty.ai/v1",
|
||||||
|
"openrouter": "https://openrouter.ai/api/v1",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Short model names to full model identifiers
|
||||||
|
MODEL_MAP = {
|
||||||
|
"claude": "anthropic/claude-sonnet-4-20250514",
|
||||||
|
"gpt": "openai/gpt-4o",
|
||||||
|
"gemini": "google/gemini-2.0-flash",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AIClient:
|
||||||
|
"""AI client wrapping OpenAI SDK for model routing.
|
||||||
|
|
||||||
|
Supports Requesty and OpenRouter as backend routers. Both use
|
||||||
|
OpenAI-compatible APIs with different base URLs and headers.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
router: The router service name ("requesty" or "openrouter").
|
||||||
|
referer: HTTP-Referer header for OpenRouter (optional).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, router: str, api_key: str, referer: str | None = None) -> None:
|
||||||
|
"""Initialize AI client.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
router: Router service name ("requesty" or "openrouter").
|
||||||
|
api_key: API key for the router service.
|
||||||
|
referer: HTTP-Referer header for OpenRouter (optional).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If router is not supported.
|
||||||
|
"""
|
||||||
|
if router not in ROUTER_URLS:
|
||||||
|
raise ValueError(f"Unsupported router: {router}. Use: {list(ROUTER_URLS.keys())}")
|
||||||
|
|
||||||
|
self.router = router
|
||||||
|
self.referer = referer
|
||||||
|
|
||||||
|
base_url = ROUTER_URLS[router]
|
||||||
|
self._client = AsyncOpenAI(base_url=base_url, api_key=api_key)
|
||||||
|
|
||||||
|
async def complete(
|
||||||
|
self,
|
||||||
|
model: str,
|
||||||
|
messages: list[dict],
|
||||||
|
system_prompt: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Get a completion from the AI model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model: Model short name (e.g., "claude") or full identifier.
|
||||||
|
messages: List of message dicts with "role" and "content".
|
||||||
|
system_prompt: Optional system prompt to prepend.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The model's response content as a string.
|
||||||
|
"""
|
||||||
|
# Resolve short model names
|
||||||
|
resolved_model = MODEL_MAP.get(model, model)
|
||||||
|
|
||||||
|
# Build message list
|
||||||
|
full_messages = []
|
||||||
|
if system_prompt:
|
||||||
|
full_messages.append({"role": "system", "content": system_prompt})
|
||||||
|
full_messages.extend(messages)
|
||||||
|
|
||||||
|
# Build extra headers for OpenRouter
|
||||||
|
extra_headers = {}
|
||||||
|
if self.router == "openrouter" and self.referer:
|
||||||
|
extra_headers["HTTP-Referer"] = self.referer
|
||||||
|
|
||||||
|
# Make the API call
|
||||||
|
response = await self._client.chat.completions.create(
|
||||||
|
model=resolved_model,
|
||||||
|
messages=full_messages,
|
||||||
|
extra_headers=extra_headers if extra_headers else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.choices[0].message.content or ""
|
||||||
|
|
||||||
|
|
||||||
|
# Module-level singleton
|
||||||
|
_client: AIClient | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def init_ai_client(config: BotConfig) -> AIClient:
|
||||||
|
"""Initialize the global AI client from config.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: BotConfig instance with AI settings.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The initialized AIClient instance.
|
||||||
|
"""
|
||||||
|
global _client
|
||||||
|
_client = AIClient(config.ai_router, config.ai_api_key, config.ai_referer)
|
||||||
|
return _client
|
||||||
|
|
||||||
|
|
||||||
|
def get_ai_client() -> AIClient:
|
||||||
|
"""Get the global AI client instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The initialized AIClient instance.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If AI client has not been initialized.
|
||||||
|
"""
|
||||||
|
if _client is None:
|
||||||
|
raise RuntimeError("AI client not initialized. Call init_ai_client() first.")
|
||||||
|
return _client
|
||||||
106
src/moai/core/database.py
Normal file
106
src/moai/core/database.py
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
"""Database session management for MoAI.
|
||||||
|
|
||||||
|
Provides async session management using SQLAlchemy 2.0 async support.
|
||||||
|
|
||||||
|
Usage pattern:
|
||||||
|
from moai.core.database import init_db, create_tables, get_session, close_db
|
||||||
|
|
||||||
|
# Initialize on startup
|
||||||
|
init_db("sqlite+aiosqlite:///./moai.db")
|
||||||
|
await create_tables()
|
||||||
|
|
||||||
|
# Use sessions for database operations
|
||||||
|
async with get_session() as session:
|
||||||
|
project = Project(name="My Project")
|
||||||
|
session.add(project)
|
||||||
|
# Auto-commits on context exit, rollback on exception
|
||||||
|
|
||||||
|
# Cleanup on shutdown
|
||||||
|
await close_db()
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import (
|
||||||
|
AsyncEngine,
|
||||||
|
AsyncSession,
|
||||||
|
async_sessionmaker,
|
||||||
|
create_async_engine,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Module-level state
|
||||||
|
DATABASE_URL: str = "sqlite+aiosqlite:///./moai.db"
|
||||||
|
engine: AsyncEngine | None = None
|
||||||
|
async_session_factory: async_sessionmaker[AsyncSession] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def init_db(url: str | None = None) -> None:
|
||||||
|
"""Initialize the database engine and session factory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: Database URL. Defaults to module-level DATABASE_URL if not provided.
|
||||||
|
Use "sqlite+aiosqlite:///:memory:" for in-memory testing.
|
||||||
|
"""
|
||||||
|
global engine, async_session_factory, DATABASE_URL
|
||||||
|
|
||||||
|
if url is not None:
|
||||||
|
DATABASE_URL = url
|
||||||
|
|
||||||
|
engine = create_async_engine(DATABASE_URL, echo=False)
|
||||||
|
async_session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_tables() -> None:
|
||||||
|
"""Create all database tables defined in models.
|
||||||
|
|
||||||
|
Must be called after init_db(). Creates tables if they don't exist.
|
||||||
|
"""
|
||||||
|
from moai.core.models import Base
|
||||||
|
|
||||||
|
if engine is None:
|
||||||
|
raise RuntimeError("Database not initialized. Call init_db() first.")
|
||||||
|
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
"""Async context manager providing a database session.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
AsyncSession: Database session for operations.
|
||||||
|
|
||||||
|
The session auto-commits on successful context exit.
|
||||||
|
On exception, the session is rolled back automatically.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
async with get_session() as session:
|
||||||
|
project = Project(name="Test")
|
||||||
|
session.add(project)
|
||||||
|
# Commits automatically on exit
|
||||||
|
"""
|
||||||
|
if async_session_factory is None:
|
||||||
|
raise RuntimeError("Database not initialized. Call init_db() first.")
|
||||||
|
|
||||||
|
async with async_session_factory() as session:
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
await session.commit()
|
||||||
|
except Exception:
|
||||||
|
await session.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
async def close_db() -> None:
|
||||||
|
"""Dispose of the database engine and release connections.
|
||||||
|
|
||||||
|
Should be called during application shutdown.
|
||||||
|
"""
|
||||||
|
global engine, async_session_factory
|
||||||
|
|
||||||
|
if engine is not None:
|
||||||
|
await engine.dispose()
|
||||||
|
engine = None
|
||||||
|
async_session_factory = None
|
||||||
191
src/moai/core/models.py
Normal file
191
src/moai/core/models.py
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
"""SQLAlchemy models for MoAI multi-AI discussion platform.
|
||||||
|
|
||||||
|
Data model hierarchy:
|
||||||
|
Project (has many) → Discussion (has many) → Round (has many) → Message
|
||||||
|
↘ Discussion (has one) → Consensus
|
||||||
|
|
||||||
|
All IDs use UUID stored as String(36) for SQLite compatibility.
|
||||||
|
Enums are stored as strings for database portability.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from sqlalchemy import JSON, DateTime, ForeignKey, String, Text
|
||||||
|
from sqlalchemy import Enum as SAEnum
|
||||||
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
"""Base class for all SQLAlchemy models."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DiscussionType(str, Enum):
|
||||||
|
"""Type of discussion mode."""
|
||||||
|
|
||||||
|
OPEN = "open"
|
||||||
|
DISCUSS = "discuss"
|
||||||
|
|
||||||
|
|
||||||
|
class DiscussionStatus(str, Enum):
|
||||||
|
"""Status of a discussion."""
|
||||||
|
|
||||||
|
ACTIVE = "active"
|
||||||
|
COMPLETED = "completed"
|
||||||
|
|
||||||
|
|
||||||
|
class RoundType(str, Enum):
|
||||||
|
"""Type of round in a discussion."""
|
||||||
|
|
||||||
|
PARALLEL = "parallel"
|
||||||
|
SEQUENTIAL = "sequential"
|
||||||
|
|
||||||
|
|
||||||
|
def _uuid() -> str:
|
||||||
|
"""Generate a new UUID string."""
|
||||||
|
return str(uuid4())
|
||||||
|
|
||||||
|
|
||||||
|
class Project(Base):
|
||||||
|
"""A project container for related discussions.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: Unique identifier (UUID).
|
||||||
|
name: Human-readable project name.
|
||||||
|
created_at: When the project was created.
|
||||||
|
updated_at: When the project was last modified.
|
||||||
|
models: List of AI model identifiers (e.g., ["claude", "gpt", "gemini"]).
|
||||||
|
settings: Configuration dict (default_rounds, consensus_threshold, system_prompt_override).
|
||||||
|
discussions: Related discussions in this project.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "project"
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid)
|
||||||
|
name: Mapped[str] = mapped_column(String(255))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
|
||||||
|
)
|
||||||
|
models: Mapped[Any] = mapped_column(JSON, default=list)
|
||||||
|
settings: Mapped[Any] = mapped_column(JSON, default=dict)
|
||||||
|
|
||||||
|
discussions: Mapped[list[Discussion]] = relationship(
|
||||||
|
back_populates="project", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Discussion(Base):
|
||||||
|
"""A discussion within a project.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: Unique identifier (UUID).
|
||||||
|
project_id: FK to parent project.
|
||||||
|
question: The question or topic being discussed.
|
||||||
|
type: Whether this is an "open" or "discuss" mode discussion.
|
||||||
|
status: Current status (active or completed).
|
||||||
|
created_at: When the discussion started.
|
||||||
|
project: Parent project relationship.
|
||||||
|
rounds: Discussion rounds.
|
||||||
|
consensus: Generated consensus (if any).
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "discussion"
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid)
|
||||||
|
project_id: Mapped[str] = mapped_column(ForeignKey("project.id"))
|
||||||
|
question: Mapped[str] = mapped_column(Text)
|
||||||
|
type: Mapped[DiscussionType] = mapped_column(SAEnum(DiscussionType))
|
||||||
|
status: Mapped[DiscussionStatus] = mapped_column(
|
||||||
|
SAEnum(DiscussionStatus), default=DiscussionStatus.ACTIVE
|
||||||
|
)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
project: Mapped[Project] = relationship(back_populates="discussions")
|
||||||
|
rounds: Mapped[list[Round]] = relationship(
|
||||||
|
back_populates="discussion", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
consensus: Mapped[Consensus | None] = relationship(
|
||||||
|
back_populates="discussion", uselist=False, cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Round(Base):
|
||||||
|
"""A round within a discussion.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: Unique identifier (UUID).
|
||||||
|
discussion_id: FK to parent discussion.
|
||||||
|
round_number: Sequential round number within the discussion.
|
||||||
|
type: Whether this round is parallel or sequential.
|
||||||
|
discussion: Parent discussion relationship.
|
||||||
|
messages: Messages from AI models in this round.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "round"
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid)
|
||||||
|
discussion_id: Mapped[str] = mapped_column(ForeignKey("discussion.id"))
|
||||||
|
round_number: Mapped[int]
|
||||||
|
type: Mapped[RoundType] = mapped_column(SAEnum(RoundType))
|
||||||
|
|
||||||
|
discussion: Mapped[Discussion] = relationship(back_populates="rounds")
|
||||||
|
messages: Mapped[list[Message]] = relationship(
|
||||||
|
back_populates="round", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Message(Base):
|
||||||
|
"""A message from an AI model within a round.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: Unique identifier (UUID).
|
||||||
|
round_id: FK to parent round.
|
||||||
|
model: AI model identifier (e.g., "claude", "gpt", "gemini").
|
||||||
|
content: The message content.
|
||||||
|
timestamp: When the message was created.
|
||||||
|
is_direct: True if this was a direct @mention to this model.
|
||||||
|
round: Parent round relationship.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "message"
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid)
|
||||||
|
round_id: Mapped[str] = mapped_column(ForeignKey("round.id"))
|
||||||
|
model: Mapped[str] = mapped_column(String(50))
|
||||||
|
content: Mapped[str] = mapped_column(Text)
|
||||||
|
timestamp: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
|
is_direct: Mapped[bool] = mapped_column(default=False)
|
||||||
|
|
||||||
|
round: Mapped[Round] = relationship(back_populates="messages")
|
||||||
|
|
||||||
|
|
||||||
|
class Consensus(Base):
|
||||||
|
"""Generated consensus summary for a discussion.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: Unique identifier (UUID).
|
||||||
|
discussion_id: FK to parent discussion (unique - one consensus per discussion).
|
||||||
|
agreements: List of bullet point strings for agreed items.
|
||||||
|
disagreements: List of {topic, positions: {model: position}} dicts.
|
||||||
|
generated_at: When the consensus was generated.
|
||||||
|
generated_by: Which model generated this summary.
|
||||||
|
discussion: Parent discussion relationship.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "consensus"
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid)
|
||||||
|
discussion_id: Mapped[str] = mapped_column(ForeignKey("discussion.id"), unique=True)
|
||||||
|
agreements: Mapped[Any] = mapped_column(JSON, default=list)
|
||||||
|
disagreements: Mapped[Any] = mapped_column(JSON, default=list)
|
||||||
|
generated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
|
generated_by: Mapped[str] = mapped_column(String(50))
|
||||||
|
|
||||||
|
discussion: Mapped[Discussion] = relationship(back_populates="consensus")
|
||||||
229
src/moai/core/orchestrator.py
Normal file
229
src/moai/core/orchestrator.py
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
"""AI orchestrator for managing multi-model discussions.
|
||||||
|
|
||||||
|
Provides functions for orchestrating parallel and sequential AI queries
|
||||||
|
across multiple models, building context, and managing discussion flow.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from moai.core.ai_client import get_ai_client
|
||||||
|
from moai.core.models import Discussion, RoundType
|
||||||
|
from moai.core.services.discussion import create_message, create_round
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# System prompt for roundtable discussions
|
||||||
|
SYSTEM_PROMPT = """You are participating in a roundtable discussion with other AI models.
|
||||||
|
Other participants: {models}
|
||||||
|
Current topic: {topic}
|
||||||
|
|
||||||
|
Guidelines:
|
||||||
|
- Be concise but substantive
|
||||||
|
- You can agree, disagree, or build upon others' points
|
||||||
|
- Reference other models by name when responding to their points
|
||||||
|
- Focus on practical, actionable insights
|
||||||
|
- If you reach agreement with others, state it clearly"""
|
||||||
|
|
||||||
|
|
||||||
|
async def query_models_parallel(
|
||||||
|
models: list[str],
|
||||||
|
question: str,
|
||||||
|
project_name: str,
|
||||||
|
) -> dict[str, str]:
|
||||||
|
"""Query multiple AI models in parallel.
|
||||||
|
|
||||||
|
Sends the same question to all models simultaneously and collects
|
||||||
|
their responses. Each model receives a system prompt identifying
|
||||||
|
the other participants and the discussion topic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
models: List of model short names (e.g., ["claude", "gpt", "gemini"]).
|
||||||
|
question: The question to ask all models.
|
||||||
|
project_name: The project name for context.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict mapping model name to response text. If a model fails,
|
||||||
|
its value will be an error message string.
|
||||||
|
"""
|
||||||
|
client = get_ai_client()
|
||||||
|
|
||||||
|
# Build system prompt with participant info
|
||||||
|
models_str = ", ".join(models)
|
||||||
|
system_prompt = SYSTEM_PROMPT.format(models=models_str, topic=project_name)
|
||||||
|
|
||||||
|
async def query_single_model(model: str) -> tuple[str, str]:
|
||||||
|
"""Query a single model and return (model_name, response)."""
|
||||||
|
try:
|
||||||
|
response = await client.complete(
|
||||||
|
model=model,
|
||||||
|
messages=[{"role": "user", "content": question}],
|
||||||
|
system_prompt=system_prompt,
|
||||||
|
)
|
||||||
|
logger.info("Model %s responded successfully", model)
|
||||||
|
return (model, response)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Model %s failed: %s", model, e)
|
||||||
|
return (model, f"[Error: {e}]")
|
||||||
|
|
||||||
|
# Run all queries in parallel
|
||||||
|
tasks = [query_single_model(model) for model in models]
|
||||||
|
results = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
return dict(results)
|
||||||
|
|
||||||
|
|
||||||
|
def build_context(discussion: Discussion) -> list[dict]:
|
||||||
|
"""Build conversation context from all rounds and messages in a discussion.
|
||||||
|
|
||||||
|
Converts the discussion history into OpenAI message format. The original
|
||||||
|
question becomes the first user message, and all model responses are
|
||||||
|
formatted as user messages with model attribution for context.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
discussion: Discussion object with eager-loaded rounds and messages.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of message dicts in OpenAI format:
|
||||||
|
[{"role": "user", "content": "..."}]
|
||||||
|
"""
|
||||||
|
messages = []
|
||||||
|
|
||||||
|
# Original question as first message
|
||||||
|
messages.append({"role": "user", "content": discussion.question})
|
||||||
|
|
||||||
|
# Sort rounds by round_number
|
||||||
|
sorted_rounds = sorted(discussion.rounds, key=lambda r: r.round_number)
|
||||||
|
|
||||||
|
for round_ in sorted_rounds:
|
||||||
|
# Sort messages by timestamp within round
|
||||||
|
sorted_messages = sorted(round_.messages, key=lambda m: m.timestamp)
|
||||||
|
|
||||||
|
for msg in sorted_messages:
|
||||||
|
# Format: "**Model:** response" as user context
|
||||||
|
formatted = f"**{msg.model.title()}:** {msg.content}"
|
||||||
|
messages.append({"role": "user", "content": formatted})
|
||||||
|
|
||||||
|
return messages
|
||||||
|
|
||||||
|
|
||||||
|
async def query_model_direct(
|
||||||
|
model: str,
|
||||||
|
message: str,
|
||||||
|
discussion: Discussion | None,
|
||||||
|
project_name: str,
|
||||||
|
) -> str:
|
||||||
|
"""Query a single model directly with optional discussion context.
|
||||||
|
|
||||||
|
Used for @mention messages where user addresses a specific model.
|
||||||
|
If a discussion is provided, includes full context so the model
|
||||||
|
can reference prior responses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model: Model short name (e.g., "claude", "gpt", "gemini").
|
||||||
|
message: The direct message to the model.
|
||||||
|
discussion: Optional Discussion object for context (with eager-loaded rounds/messages).
|
||||||
|
project_name: Project name for context.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The model's response text, or error message if the query fails.
|
||||||
|
"""
|
||||||
|
client = get_ai_client()
|
||||||
|
|
||||||
|
# Build system prompt indicating this is a direct message
|
||||||
|
system_prompt = f"""You are participating in a discussion about: {project_name}
|
||||||
|
|
||||||
|
This is a direct message to you specifically. The user has chosen to address you
|
||||||
|
directly for your unique perspective.
|
||||||
|
|
||||||
|
Respond helpfully and concisely."""
|
||||||
|
|
||||||
|
# Build messages with optional discussion context
|
||||||
|
if discussion is not None:
|
||||||
|
messages = build_context(discussion)
|
||||||
|
# Add the direct message
|
||||||
|
messages.append({"role": "user", "content": f"[Direct to you]: {message}"})
|
||||||
|
else:
|
||||||
|
messages = [{"role": "user", "content": message}]
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await client.complete(
|
||||||
|
model=model,
|
||||||
|
messages=messages,
|
||||||
|
system_prompt=system_prompt,
|
||||||
|
)
|
||||||
|
logger.info("Direct query to %s successful", model)
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Direct query to %s failed: %s", model, e)
|
||||||
|
return f"[Error: {e}]"
|
||||||
|
|
||||||
|
|
||||||
|
async def run_discussion_round(
|
||||||
|
discussion: Discussion,
|
||||||
|
models: list[str],
|
||||||
|
project_name: str,
|
||||||
|
round_number: int,
|
||||||
|
) -> dict[str, str]:
|
||||||
|
"""Run a single round of sequential discussion.
|
||||||
|
|
||||||
|
Each model is queried in sequence, seeing all prior responses including
|
||||||
|
those from earlier in this same round. This creates a true sequential
|
||||||
|
discussion where GPT sees Claude's response before responding.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
discussion: Discussion object with eager-loaded rounds and messages.
|
||||||
|
models: List of model short names in execution order.
|
||||||
|
project_name: Project name for context.
|
||||||
|
round_number: The round number being executed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict mapping model name to response text.
|
||||||
|
"""
|
||||||
|
client = get_ai_client()
|
||||||
|
|
||||||
|
# Build system prompt with participant info
|
||||||
|
models_str = ", ".join(models)
|
||||||
|
system_prompt = SYSTEM_PROMPT.format(models=models_str, topic=project_name)
|
||||||
|
|
||||||
|
# Create the round record
|
||||||
|
round_ = await create_round(
|
||||||
|
discussion_id=discussion.id,
|
||||||
|
round_number=round_number,
|
||||||
|
round_type=RoundType.SEQUENTIAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build initial context from prior rounds
|
||||||
|
context_messages = build_context(discussion)
|
||||||
|
|
||||||
|
# Store responses as we go (for sequential context building)
|
||||||
|
responses: dict[str, str] = {}
|
||||||
|
|
||||||
|
# Query each model SEQUENTIALLY
|
||||||
|
for model in models:
|
||||||
|
try:
|
||||||
|
response = await client.complete(
|
||||||
|
model=model,
|
||||||
|
messages=context_messages,
|
||||||
|
system_prompt=system_prompt,
|
||||||
|
)
|
||||||
|
logger.info("Model %s responded successfully (round %d)", model, round_number)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Model %s failed (round %d): %s", model, round_number, e)
|
||||||
|
response = f"[Error: {e}]"
|
||||||
|
|
||||||
|
# Persist message
|
||||||
|
await create_message(
|
||||||
|
round_id=round_.id,
|
||||||
|
model=model,
|
||||||
|
content=response,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add to responses
|
||||||
|
responses[model] = response
|
||||||
|
|
||||||
|
# Add this response to context for next model in this round
|
||||||
|
formatted = f"**{model.title()}:** {response}"
|
||||||
|
context_messages.append({"role": "user", "content": formatted})
|
||||||
|
|
||||||
|
return responses
|
||||||
33
src/moai/core/services/__init__.py
Normal file
33
src/moai/core/services/__init__.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
"""Service layer for MoAI business logic.
|
||||||
|
|
||||||
|
Services encapsulate database operations and business rules,
|
||||||
|
providing a clean interface for handlers to use.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from moai.core.services.discussion import (
|
||||||
|
complete_discussion,
|
||||||
|
create_discussion,
|
||||||
|
create_message,
|
||||||
|
create_round,
|
||||||
|
get_active_discussion,
|
||||||
|
get_current_round,
|
||||||
|
get_discussion,
|
||||||
|
get_round_messages,
|
||||||
|
list_discussions,
|
||||||
|
)
|
||||||
|
from moai.core.services.project import create_project, get_project, list_projects
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"complete_discussion",
|
||||||
|
"create_discussion",
|
||||||
|
"create_message",
|
||||||
|
"create_project",
|
||||||
|
"create_round",
|
||||||
|
"get_active_discussion",
|
||||||
|
"get_current_round",
|
||||||
|
"get_discussion",
|
||||||
|
"get_project",
|
||||||
|
"get_round_messages",
|
||||||
|
"list_discussions",
|
||||||
|
"list_projects",
|
||||||
|
]
|
||||||
219
src/moai/core/services/discussion.py
Normal file
219
src/moai/core/services/discussion.py
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
"""Discussion service for MoAI.
|
||||||
|
|
||||||
|
Provides CRUD operations for discussions, rounds, and messages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
from moai.core.database import get_session
|
||||||
|
from moai.core.models import (
|
||||||
|
Discussion,
|
||||||
|
DiscussionStatus,
|
||||||
|
DiscussionType,
|
||||||
|
Message,
|
||||||
|
Round,
|
||||||
|
RoundType,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_discussion(
|
||||||
|
project_id: str,
|
||||||
|
question: str,
|
||||||
|
discussion_type: DiscussionType,
|
||||||
|
) -> Discussion:
|
||||||
|
"""Create a new discussion within a project.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_id: The parent project's UUID.
|
||||||
|
question: The question or topic being discussed.
|
||||||
|
discussion_type: Whether this is OPEN or DISCUSS mode.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created Discussion object.
|
||||||
|
"""
|
||||||
|
async with get_session() as session:
|
||||||
|
discussion = Discussion(
|
||||||
|
project_id=project_id,
|
||||||
|
question=question,
|
||||||
|
type=discussion_type,
|
||||||
|
)
|
||||||
|
session.add(discussion)
|
||||||
|
await session.flush()
|
||||||
|
await session.refresh(discussion)
|
||||||
|
return discussion
|
||||||
|
|
||||||
|
|
||||||
|
async def get_discussion(discussion_id: str) -> Discussion | None:
|
||||||
|
"""Get a discussion by ID with eager loading of rounds and messages.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
discussion_id: The discussion's UUID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The Discussion object if found, None otherwise.
|
||||||
|
"""
|
||||||
|
async with get_session() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(Discussion)
|
||||||
|
.where(Discussion.id == discussion_id)
|
||||||
|
.options(
|
||||||
|
selectinload(Discussion.rounds).selectinload(Round.messages),
|
||||||
|
selectinload(Discussion.consensus),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_active_discussion(project_id: str) -> Discussion | None:
|
||||||
|
"""Get the active discussion for a project.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_id: The project's UUID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The active Discussion object if found, None otherwise.
|
||||||
|
"""
|
||||||
|
async with get_session() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(Discussion)
|
||||||
|
.where(Discussion.project_id == project_id)
|
||||||
|
.where(Discussion.status == DiscussionStatus.ACTIVE)
|
||||||
|
.options(
|
||||||
|
selectinload(Discussion.rounds).selectinload(Round.messages),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def list_discussions(project_id: str) -> list[Discussion]:
|
||||||
|
"""List all discussions for a project ordered by creation date (newest first).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_id: The project's UUID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of Discussion objects.
|
||||||
|
"""
|
||||||
|
async with get_session() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(Discussion)
|
||||||
|
.where(Discussion.project_id == project_id)
|
||||||
|
.order_by(Discussion.created_at.desc())
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
|
||||||
|
async def complete_discussion(discussion_id: str) -> Discussion | None:
|
||||||
|
"""Mark a discussion as completed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
discussion_id: The discussion's UUID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The updated Discussion object if found, None otherwise.
|
||||||
|
"""
|
||||||
|
async with get_session() as session:
|
||||||
|
result = await session.execute(select(Discussion).where(Discussion.id == discussion_id))
|
||||||
|
discussion = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if discussion is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
discussion.status = DiscussionStatus.COMPLETED
|
||||||
|
await session.flush()
|
||||||
|
await session.refresh(discussion)
|
||||||
|
return discussion
|
||||||
|
|
||||||
|
|
||||||
|
async def create_round(
|
||||||
|
discussion_id: str,
|
||||||
|
round_number: int,
|
||||||
|
round_type: RoundType,
|
||||||
|
) -> Round:
|
||||||
|
"""Create a new round within a discussion.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
discussion_id: The parent discussion's UUID.
|
||||||
|
round_number: Sequential round number within the discussion.
|
||||||
|
round_type: Whether this round is PARALLEL or SEQUENTIAL.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created Round object.
|
||||||
|
"""
|
||||||
|
async with get_session() as session:
|
||||||
|
round_ = Round(
|
||||||
|
discussion_id=discussion_id,
|
||||||
|
round_number=round_number,
|
||||||
|
type=round_type,
|
||||||
|
)
|
||||||
|
session.add(round_)
|
||||||
|
await session.flush()
|
||||||
|
await session.refresh(round_)
|
||||||
|
return round_
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_round(discussion_id: str) -> Round | None:
|
||||||
|
"""Get the current (most recent) round for a discussion.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
discussion_id: The discussion's UUID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The most recent Round object if found, None otherwise.
|
||||||
|
"""
|
||||||
|
async with get_session() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(Round)
|
||||||
|
.where(Round.discussion_id == discussion_id)
|
||||||
|
.order_by(Round.round_number.desc())
|
||||||
|
.limit(1)
|
||||||
|
.options(selectinload(Round.messages))
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def create_message(
|
||||||
|
round_id: str,
|
||||||
|
model: str,
|
||||||
|
content: str,
|
||||||
|
is_direct: bool = False,
|
||||||
|
) -> Message:
|
||||||
|
"""Create a new message within a round.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
round_id: The parent round's UUID.
|
||||||
|
model: AI model identifier (e.g., "claude", "gpt", "gemini").
|
||||||
|
content: The message content.
|
||||||
|
is_direct: True if this was a direct @mention to this model.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created Message object.
|
||||||
|
"""
|
||||||
|
async with get_session() as session:
|
||||||
|
message = Message(
|
||||||
|
round_id=round_id,
|
||||||
|
model=model,
|
||||||
|
content=content,
|
||||||
|
is_direct=is_direct,
|
||||||
|
)
|
||||||
|
session.add(message)
|
||||||
|
await session.flush()
|
||||||
|
await session.refresh(message)
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
|
async def get_round_messages(round_id: str) -> list[Message]:
|
||||||
|
"""Get all messages for a round ordered by timestamp.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
round_id: The round's UUID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of Message objects ordered by timestamp.
|
||||||
|
"""
|
||||||
|
async with get_session() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(Message).where(Message.round_id == round_id).order_by(Message.timestamp)
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
116
src/moai/core/services/project.py
Normal file
116
src/moai/core/services/project.py
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
"""Project service for MoAI.
|
||||||
|
|
||||||
|
Provides CRUD operations for projects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from moai.core.database import get_session
|
||||||
|
from moai.core.models import Project
|
||||||
|
|
||||||
|
DEFAULT_MODELS = ["claude", "gpt", "gemini"]
|
||||||
|
|
||||||
|
|
||||||
|
async def list_projects() -> list[Project]:
|
||||||
|
"""List all projects ordered by creation date (newest first).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of Project objects.
|
||||||
|
"""
|
||||||
|
async with get_session() as session:
|
||||||
|
result = await session.execute(select(Project).order_by(Project.created_at.desc()))
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
|
||||||
|
async def create_project(name: str, models: list[str] | None = None) -> Project:
|
||||||
|
"""Create a new project.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Human-readable project name.
|
||||||
|
models: List of AI model identifiers. Defaults to ["claude", "gpt", "gemini"].
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created Project object.
|
||||||
|
"""
|
||||||
|
if models is None:
|
||||||
|
models = DEFAULT_MODELS.copy()
|
||||||
|
|
||||||
|
async with get_session() as session:
|
||||||
|
project = Project(name=name, models=models)
|
||||||
|
session.add(project)
|
||||||
|
await session.flush()
|
||||||
|
await session.refresh(project)
|
||||||
|
return project
|
||||||
|
|
||||||
|
|
||||||
|
async def get_project(project_id: str) -> Project | None:
|
||||||
|
"""Get a project by ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_id: The project's UUID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The Project object if found, None otherwise.
|
||||||
|
"""
|
||||||
|
async with get_session() as session:
|
||||||
|
result = await session.execute(select(Project).where(Project.id == project_id))
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_project_by_name(name: str) -> Project | None:
|
||||||
|
"""Get a project by name (case-insensitive).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The project name to search for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The Project object if found, None otherwise.
|
||||||
|
"""
|
||||||
|
async with get_session() as session:
|
||||||
|
result = await session.execute(select(Project).where(Project.name.ilike(name)))
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def update_project_models(project_id: str, models: list[str]) -> Project | None:
|
||||||
|
"""Update a project's models list.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_id: The project's UUID.
|
||||||
|
models: List of AI model identifiers.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The updated Project object if found, None otherwise.
|
||||||
|
"""
|
||||||
|
async with get_session() as session:
|
||||||
|
result = await session.execute(select(Project).where(Project.id == project_id))
|
||||||
|
project = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if project is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
project.models = models
|
||||||
|
await session.flush()
|
||||||
|
await session.refresh(project)
|
||||||
|
return project
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_project(project_id: str) -> bool:
|
||||||
|
"""Delete a project by ID.
|
||||||
|
|
||||||
|
Cascade delete will handle discussions/rounds/messages via SQLAlchemy relationship config.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_id: The project's UUID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if deleted, False if not found.
|
||||||
|
"""
|
||||||
|
async with get_session() as session:
|
||||||
|
result = await session.execute(select(Project).where(Project.id == project_id))
|
||||||
|
project = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if project is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
await session.delete(project)
|
||||||
|
return True
|
||||||
4
tests/__init__.py
Normal file
4
tests/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
"""Test suite for MoAI.
|
||||||
|
|
||||||
|
Contains unit tests, integration tests, and fixtures for the MoAI platform.
|
||||||
|
"""
|
||||||
215
tests/test_models.py
Normal file
215
tests/test_models.py
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
"""Tests for MoAI SQLAlchemy models.
|
||||||
|
|
||||||
|
Tests verify model creation, relationships, and cascade delete behavior
|
||||||
|
using an in-memory SQLite database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from moai.core.database import close_db, create_tables, get_session, init_db
|
||||||
|
from moai.core.models import (
|
||||||
|
Consensus,
|
||||||
|
Discussion,
|
||||||
|
DiscussionStatus,
|
||||||
|
DiscussionType,
|
||||||
|
Message,
|
||||||
|
Project,
|
||||||
|
Round,
|
||||||
|
RoundType,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def db_session():
|
||||||
|
"""Provide a database session with in-memory SQLite for testing."""
|
||||||
|
init_db("sqlite+aiosqlite:///:memory:")
|
||||||
|
await create_tables()
|
||||||
|
async with get_session() as session:
|
||||||
|
yield session
|
||||||
|
await close_db()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_project(db_session):
|
||||||
|
"""Test creating a project with basic attributes."""
|
||||||
|
project = Project(name="Test Project", models=["claude", "gpt"])
|
||||||
|
db_session.add(project)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
assert project.id is not None
|
||||||
|
assert len(project.id) == 36 # UUID format
|
||||||
|
assert project.name == "Test Project"
|
||||||
|
assert project.models == ["claude", "gpt"]
|
||||||
|
assert project.created_at is not None
|
||||||
|
assert project.updated_at is not None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_discussion_with_project(db_session):
|
||||||
|
"""Test creating a discussion linked to a project."""
|
||||||
|
project = Project(name="Test Project", models=["claude"])
|
||||||
|
db_session.add(project)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
discussion = Discussion(
|
||||||
|
project_id=project.id,
|
||||||
|
question="What is the meaning of life?",
|
||||||
|
type=DiscussionType.OPEN,
|
||||||
|
)
|
||||||
|
db_session.add(discussion)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
assert discussion.id is not None
|
||||||
|
assert discussion.project_id == project.id
|
||||||
|
assert discussion.status == DiscussionStatus.ACTIVE
|
||||||
|
assert discussion.type == DiscussionType.OPEN
|
||||||
|
|
||||||
|
# Verify relationship
|
||||||
|
await db_session.refresh(project, ["discussions"])
|
||||||
|
assert len(project.discussions) == 1
|
||||||
|
assert project.discussions[0].id == discussion.id
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_full_discussion_chain(db_session):
|
||||||
|
"""Test creating a full chain: Project -> Discussion -> Round -> Message."""
|
||||||
|
# Create project
|
||||||
|
project = Project(name="Full Chain Test", models=["claude", "gpt", "gemini"])
|
||||||
|
db_session.add(project)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
# Create discussion
|
||||||
|
discussion = Discussion(
|
||||||
|
project_id=project.id,
|
||||||
|
question="How should we approach this problem?",
|
||||||
|
type=DiscussionType.DISCUSS,
|
||||||
|
)
|
||||||
|
db_session.add(discussion)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
# Create round
|
||||||
|
round_ = Round(
|
||||||
|
discussion_id=discussion.id,
|
||||||
|
round_number=1,
|
||||||
|
type=RoundType.SEQUENTIAL,
|
||||||
|
)
|
||||||
|
db_session.add(round_)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
# Create messages
|
||||||
|
message1 = Message(
|
||||||
|
round_id=round_.id,
|
||||||
|
model="claude",
|
||||||
|
content="I think we should consider option A.",
|
||||||
|
is_direct=False,
|
||||||
|
)
|
||||||
|
message2 = Message(
|
||||||
|
round_id=round_.id,
|
||||||
|
model="gpt",
|
||||||
|
content="I agree with Claude, option A seems best.",
|
||||||
|
is_direct=False,
|
||||||
|
)
|
||||||
|
db_session.add_all([message1, message2])
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
# Verify all relationships
|
||||||
|
await db_session.refresh(round_, ["messages"])
|
||||||
|
assert len(round_.messages) == 2
|
||||||
|
assert round_.discussion_id == discussion.id
|
||||||
|
|
||||||
|
await db_session.refresh(discussion, ["rounds"])
|
||||||
|
assert len(discussion.rounds) == 1
|
||||||
|
assert discussion.project_id == project.id
|
||||||
|
|
||||||
|
await db_session.refresh(project, ["discussions"])
|
||||||
|
assert len(project.discussions) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_consensus(db_session):
|
||||||
|
"""Test creating a consensus for a discussion."""
|
||||||
|
project = Project(name="Consensus Test", models=["claude"])
|
||||||
|
db_session.add(project)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
discussion = Discussion(
|
||||||
|
project_id=project.id,
|
||||||
|
question="What should we do?",
|
||||||
|
type=DiscussionType.OPEN,
|
||||||
|
)
|
||||||
|
db_session.add(discussion)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
consensus = Consensus(
|
||||||
|
discussion_id=discussion.id,
|
||||||
|
agreements=["We should prioritize user experience", "Performance is important"],
|
||||||
|
disagreements=[{"topic": "Timeline", "positions": {"claude": "2 weeks", "gpt": "3 weeks"}}],
|
||||||
|
generated_by="claude",
|
||||||
|
)
|
||||||
|
db_session.add(consensus)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
# Verify consensus attributes
|
||||||
|
assert consensus.id is not None
|
||||||
|
assert consensus.discussion_id == discussion.id
|
||||||
|
assert len(consensus.agreements) == 2
|
||||||
|
assert len(consensus.disagreements) == 1
|
||||||
|
assert consensus.generated_by == "claude"
|
||||||
|
assert consensus.generated_at is not None
|
||||||
|
|
||||||
|
# Verify relationship
|
||||||
|
await db_session.refresh(discussion, ["consensus"])
|
||||||
|
assert discussion.consensus is not None
|
||||||
|
assert discussion.consensus.id == consensus.id
|
||||||
|
|
||||||
|
|
||||||
|
async def test_project_cascade_delete(db_session):
|
||||||
|
"""Test that deleting a project cascades to all children."""
|
||||||
|
# Create full hierarchy
|
||||||
|
project = Project(name="Cascade Test", models=["claude"])
|
||||||
|
db_session.add(project)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
discussion = Discussion(
|
||||||
|
project_id=project.id,
|
||||||
|
question="Test question",
|
||||||
|
type=DiscussionType.OPEN,
|
||||||
|
)
|
||||||
|
db_session.add(discussion)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
round_ = Round(
|
||||||
|
discussion_id=discussion.id,
|
||||||
|
round_number=1,
|
||||||
|
type=RoundType.PARALLEL,
|
||||||
|
)
|
||||||
|
db_session.add(round_)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
message = Message(
|
||||||
|
round_id=round_.id,
|
||||||
|
model="claude",
|
||||||
|
content="Test message",
|
||||||
|
)
|
||||||
|
db_session.add(message)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
# Store IDs for verification
|
||||||
|
project_id = project.id
|
||||||
|
discussion_id = discussion.id
|
||||||
|
round_id = round_.id
|
||||||
|
message_id = message.id
|
||||||
|
|
||||||
|
# Delete project
|
||||||
|
await db_session.delete(project)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
# Verify all children are deleted (cascade)
|
||||||
|
result = await db_session.execute(select(Project).where(Project.id == project_id))
|
||||||
|
assert result.scalar_one_or_none() is None
|
||||||
|
|
||||||
|
result = await db_session.execute(select(Discussion).where(Discussion.id == discussion_id))
|
||||||
|
assert result.scalar_one_or_none() is None
|
||||||
|
|
||||||
|
result = await db_session.execute(select(Round).where(Round.id == round_id))
|
||||||
|
assert result.scalar_one_or_none() is None
|
||||||
|
|
||||||
|
result = await db_session.execute(select(Message).where(Message.id == message_id))
|
||||||
|
assert result.scalar_one_or_none() is None
|
||||||
Loading…
Add table
Reference in a new issue