Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e5e316d24 | |||
| 69574c8c6b | |||
| e5108c6705 | |||
| 152d6173d6 | |||
| edb4ab5593 | |||
| ee9f8ca3a4 | |||
| 8242de5289 | |||
| ddb0de0757 |
18 changed files with 1362 additions and 164 deletions
31
.planning/MILESTONES.md
Normal file
31
.planning/MILESTONES.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Project Milestones: MoAI
|
||||||
|
|
||||||
|
## v1.0 MVP (Shipped: 2026-01-17)
|
||||||
|
|
||||||
|
**Delivered:** A fully functional multi-AI collaborative brainstorming Telegram bot where Claude, GPT, and Gemini discuss topics together and generate consensus.
|
||||||
|
|
||||||
|
**Phases completed:** 1-6 (16 plans total)
|
||||||
|
|
||||||
|
**Key accomplishments:**
|
||||||
|
|
||||||
|
- Complete Python project scaffolding with ruff, pre-commit, pytest, and src layout
|
||||||
|
- Working Telegram bot with /help, /status and full project CRUD commands
|
||||||
|
- AI client abstraction layer supporting Requesty/OpenRouter model routing
|
||||||
|
- Single-model /ask and multi-model discussions (parallel /open, sequential /discuss)
|
||||||
|
- @mention direct messages to specific AI models during discussions
|
||||||
|
- Consensus generation and markdown export of full discussion transcripts
|
||||||
|
|
||||||
|
**Stats:**
|
||||||
|
|
||||||
|
- 21 Python files created
|
||||||
|
- 2,732 lines of Python
|
||||||
|
- 6 phases, 16 plans, ~64 tasks
|
||||||
|
- 1 day from start to ship (2026-01-16 → 2026-01-17)
|
||||||
|
|
||||||
|
**Git range:** `feat(01-01)` → `feat(06-02)`
|
||||||
|
|
||||||
|
**Archive:** [v1.0-ROADMAP.md](milestones/v1.0-ROADMAP.md), [v1.0-REQUIREMENTS.md](milestones/v1.0-REQUIREMENTS.md)
|
||||||
|
|
||||||
|
**What's next:** Define requirements for v1.1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## What This Is
|
## 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.
|
A multi-AI collaborative brainstorming Telegram bot where multiple AI models (Claude, GPT, Gemini) discuss topics together, see each other's responses, and work toward consensus. Users can run parallel queries, conduct sequential discussions, @mention specific models, and export conversations with AI-generated consensus summaries.
|
||||||
|
|
||||||
## Core Value
|
## Core Value
|
||||||
|
|
||||||
|
|
@ -12,19 +12,19 @@ Get richer, more diverse AI insights through structured multi-model discussions
|
||||||
|
|
||||||
### Validated
|
### Validated
|
||||||
|
|
||||||
(None yet — ship to validate)
|
- ✓ Project scaffolding (pyproject.toml, ruff, pre-commit, src layout) — v1.0
|
||||||
|
- ✓ M1: Bot responds to /help, /status — v1.0
|
||||||
|
- ✓ M2: Project CRUD (/projects, /project new, select, delete, models, info) — v1.0
|
||||||
|
- ✓ M3: Single model Q&A working (/ask command) — v1.0
|
||||||
|
- ✓ M4: Open mode (parallel) with multiple models — v1.0
|
||||||
|
- ✓ M5: Discuss mode (sequential rounds) — v1.0
|
||||||
|
- ✓ M6: Consensus generation (/consensus) — v1.0
|
||||||
|
- ✓ M7: Export to markdown (/export) — v1.0
|
||||||
|
- ✓ M8: @mention direct messages — v1.0
|
||||||
|
|
||||||
### Active
|
### Active
|
||||||
|
|
||||||
- [ ] Project scaffolding (pyproject.toml, ruff, pre-commit, src layout)
|
(None — define requirements for next milestone)
|
||||||
- [ ] 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
|
### Out of Scope
|
||||||
|
|
||||||
|
|
@ -38,15 +38,17 @@ Get richer, more diverse AI insights through structured multi-model discussions
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
**SPEC.md contains:**
|
**Current state:** v1.0 shipped. Telegram bot fully functional with 2,732 LOC Python.
|
||||||
- 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).
|
**Tech stack:** Python 3.11+, python-telegram-bot, SQLAlchemy + SQLite, OpenAI SDK (Requesty/OpenRouter compatible), pytest, ruff.
|
||||||
|
|
||||||
|
**Architecture:** Telegram command → Handler → Service → Orchestrator → AI Client → Requesty/OpenRouter → AI APIs
|
||||||
|
|
||||||
|
**Known tech debt (from v1.0 audit):**
|
||||||
|
- Missing test coverage for database error paths
|
||||||
|
- Error handling enhancement deferred (comprehensive retry/timeout)
|
||||||
|
- Allowed users middleware not enforced (defined but unchecked)
|
||||||
|
- Minor code patterns: orphaned export, inconsistent get_selected_project, missing re-exports
|
||||||
|
|
||||||
## Constraints
|
## Constraints
|
||||||
|
|
||||||
|
|
@ -65,9 +67,16 @@ Get richer, more diverse AI insights through structured multi-model discussions
|
||||||
|
|
||||||
| Decision | Rationale | Outcome |
|
| Decision | Rationale | Outcome |
|
||||||
|----------|-----------|---------|
|
|----------|-----------|---------|
|
||||||
| AI client as abstraction layer | Support Requesty, OpenRouter, direct APIs without changing core code | — Pending |
|
| AI client as abstraction layer | Support Requesty, OpenRouter, direct APIs without changing core code | ✓ Good — OpenAI SDK works with both routers |
|
||||||
| Full project scaffolding first | Consistent tooling from day one; prevents tech debt | — Pending |
|
| Full project scaffolding first | Consistent tooling from day one; prevents tech debt | ✓ Good — ruff/pre-commit caught issues early |
|
||||||
| User allowlist auth (Phase 1) | Simple for single-user POC, each user brings own AI credentials later | — Pending |
|
| User allowlist auth (Phase 1) | Simple for single-user POC | ⚠️ Revisit — middleware defined but not enforced |
|
||||||
|
| hatchling as build backend | Explicit src layout config | ✓ Good |
|
||||||
|
| String(36) for UUID storage | SQLite compatibility | ✓ Good |
|
||||||
|
| Module-level singletons | Simple pattern for AI client and database | ✓ Good — consistent access everywhere |
|
||||||
|
| asyncio.gather for parallel | Concurrent model queries in /open | ✓ Good — graceful per-model error handling |
|
||||||
|
| user_data for state | Store discussion context across commands | ✓ Good — clean multi-command flows |
|
||||||
|
| JSON format for consensus | Structured AI output parsing | ✓ Good — reliable extraction |
|
||||||
|
| BytesIO for export | In-memory file generation | ✓ Good — no temp files needed |
|
||||||
|
|
||||||
---
|
---
|
||||||
*Last updated: 2026-01-16 after initialization*
|
*Last updated: 2026-01-17 after v1.0 milestone*
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
# 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 | - |
|
|
||||||
|
|
@ -2,86 +2,53 @@
|
||||||
|
|
||||||
## Project Reference
|
## Project Reference
|
||||||
|
|
||||||
See: .planning/PROJECT.md (updated 2026-01-16)
|
See: .planning/PROJECT.md (updated 2026-01-17)
|
||||||
|
|
||||||
**Core value:** Get richer, more diverse AI insights through structured multi-model discussions—ask a team of AIs instead of just one.
|
**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 focus:** Planning next milestone (v1.1)
|
||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Phase: 5 of 6 (Multi-Model Discussions)
|
Phase: Planning
|
||||||
Plan: 4 of 4 in current phase
|
Plan: Not started
|
||||||
Status: Phase complete
|
Status: v1.0 complete, ready for next milestone
|
||||||
Last activity: 2026-01-16 — Completed 05-04-PLAN.md (mention mode)
|
Last activity: 2026-01-17 — v1.0 milestone shipped
|
||||||
|
|
||||||
Progress: █████████░ ~85%
|
Progress: v1.0 ██████████ 100% shipped
|
||||||
|
|
||||||
## Performance Metrics
|
## Performance Metrics
|
||||||
|
|
||||||
**Velocity:**
|
**v1.0 Summary:**
|
||||||
- Total plans completed: 14
|
- Total plans completed: 16
|
||||||
- Average duration: 4 min
|
- Total phases: 6
|
||||||
- Total execution time: 0.95 hours
|
- Average plan duration: 4 min
|
||||||
|
- Total execution time: ~1.1 hours
|
||||||
**By Phase:**
|
- Lines of code: 2,732 Python
|
||||||
|
|
||||||
| 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
|
## Accumulated Context
|
||||||
|
|
||||||
### Decisions
|
### Decisions
|
||||||
|
|
||||||
Decisions are logged in PROJECT.md Key Decisions table.
|
Decisions are logged in PROJECT.md Key Decisions table.
|
||||||
Recent decisions affecting current work:
|
v1.0 decisions archived with outcomes.
|
||||||
|
|
||||||
- **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
|
### Deferred Issues
|
||||||
|
|
||||||
None yet.
|
None active — v1.0 tech debt documented in PROJECT.md Context section.
|
||||||
|
|
||||||
### Blockers/Concerns
|
### Blockers/Concerns
|
||||||
|
|
||||||
None yet.
|
None.
|
||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-01-16T19:58:00Z
|
Last session: 2026-01-17
|
||||||
Stopped at: Completed 05-04-PLAN.md (mention mode) - Phase 5 complete
|
Stopped at: v1.0 milestone complete
|
||||||
Resume file: None
|
Resume file: None
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. `/gsd:discuss-milestone` — thinking partner, creates context file
|
||||||
|
2. `/gsd:new-milestone` — update PROJECT.md with new goals
|
||||||
|
3. `/gsd:define-requirements` — scope what to build
|
||||||
|
4. `/gsd:create-roadmap` — plan how to build it
|
||||||
|
|
|
||||||
142
.planning/milestones/v1.0-MILESTONE-AUDIT.md
Normal file
142
.planning/milestones/v1.0-MILESTONE-AUDIT.md
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
---
|
||||||
|
milestone: 1.0
|
||||||
|
audited: 2026-01-17
|
||||||
|
status: passed
|
||||||
|
scores:
|
||||||
|
requirements: 9/9
|
||||||
|
phases: 6/6
|
||||||
|
integration: 24/24
|
||||||
|
flows: 4/4
|
||||||
|
gaps:
|
||||||
|
requirements: []
|
||||||
|
integration: []
|
||||||
|
flows: []
|
||||||
|
tech_debt:
|
||||||
|
- phase: 01-foundation
|
||||||
|
items:
|
||||||
|
- "Missing test coverage for error handling paths in database.py (lines 62, 85, 91-93)"
|
||||||
|
- phase: 04-single-model-qa
|
||||||
|
items:
|
||||||
|
- "Error handling enhancement deferred (original roadmap estimated 04-03 plan)"
|
||||||
|
- phase: general
|
||||||
|
items:
|
||||||
|
- "Orphaned export: get_round_messages defined but unused"
|
||||||
|
- "Inconsistent pattern: export.py has local get_selected_project instead of importing from projects.py"
|
||||||
|
- "Services __init__.py missing re-exports for save_consensus, get_consensus"
|
||||||
|
- "Allowed users middleware not enforced (BotConfig.allowed_users defined but unchecked)"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Milestone v1.0 Audit Report
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
**Status: PASSED**
|
||||||
|
|
||||||
|
All 9 requirements satisfied. All 6 phases complete. All 4 E2E flows verified. Cross-phase integration verified with 24 exports properly wired.
|
||||||
|
|
||||||
|
## Requirements Coverage
|
||||||
|
|
||||||
|
| Requirement | Phase | Status | Evidence |
|
||||||
|
|-------------|-------|--------|----------|
|
||||||
|
| Project scaffolding | Phase 1 | ✓ Satisfied | pyproject.toml, .pre-commit-config.yaml, src/moai/ structure |
|
||||||
|
| M1: /help, /status | Phase 2 | ✓ Satisfied | handlers/commands.py, handlers/status.py |
|
||||||
|
| M2: Project CRUD | Phase 3 | ✓ Satisfied | handlers/projects.py with new/select/delete/models/info |
|
||||||
|
| M3: Single model Q&A | Phase 4 | ✓ Satisfied | /ask command in handlers/discussion.py |
|
||||||
|
| M4: Open mode (parallel) | Phase 5 | ✓ Satisfied | /open command with asyncio.gather |
|
||||||
|
| M5: Discuss mode (sequential) | Phase 5 | ✓ Satisfied | /discuss, /next, /stop commands |
|
||||||
|
| M6: Consensus generation | Phase 6 | ✓ Satisfied | /consensus command |
|
||||||
|
| M7: Export to markdown | Phase 6 | ✓ Satisfied | /export command |
|
||||||
|
| M8: @mention direct messages | Phase 5 | ✓ Satisfied | @mention MessageHandler |
|
||||||
|
|
||||||
|
## Phase Completion
|
||||||
|
|
||||||
|
| Phase | Plans | Status | SUMMARY.md |
|
||||||
|
|-------|-------|--------|------------|
|
||||||
|
| 1. Foundation | 3/3 | Complete | ✓ All present |
|
||||||
|
| 2. Bot Core | 2/2 | Complete | ✓ All present |
|
||||||
|
| 3. Project CRUD | 3/3 | Complete | ✓ All present |
|
||||||
|
| 4. Single Model Q&A | 2/2 | Complete | ✓ All present |
|
||||||
|
| 5. Multi-Model Discussions | 4/4 | Complete | ✓ All present |
|
||||||
|
| 6. Consensus & Export | 2/2 | Complete | ✓ All present |
|
||||||
|
|
||||||
|
**Total:** 16 plans completed, 16 SUMMARY.md files present
|
||||||
|
|
||||||
|
## Cross-Phase Integration
|
||||||
|
|
||||||
|
### Wiring Status
|
||||||
|
|
||||||
|
| From Phase | Export | Used By | Status |
|
||||||
|
|------------|--------|---------|--------|
|
||||||
|
| Phase 1 | SQLAlchemy models | services, handlers, orchestrator, exporter | CONNECTED |
|
||||||
|
| Phase 1 | database.py functions | main.py, services | CONNECTED |
|
||||||
|
| Phase 2 | BotConfig, register_handlers | main.py, ai_client | CONNECTED |
|
||||||
|
| Phase 3 | project service functions | handlers | CONNECTED |
|
||||||
|
| Phase 3 | get_selected_project | discussion.py, status.py | CONNECTED |
|
||||||
|
| Phase 4 | AIClient, MODEL_MAP | main.py, orchestrator, handlers | CONNECTED |
|
||||||
|
| Phase 5 | discussion service | handlers, orchestrator | CONNECTED |
|
||||||
|
| Phase 5 | orchestrator functions | handlers | CONNECTED |
|
||||||
|
| Phase 6 | exporter functions | export handler | CONNECTED |
|
||||||
|
| Phase 6 | consensus service | discussion handler | CONNECTED |
|
||||||
|
|
||||||
|
**Connected:** 24 key exports properly used
|
||||||
|
**Orphaned:** 1 (get_round_messages - low severity)
|
||||||
|
**Missing:** 0
|
||||||
|
|
||||||
|
## E2E Flow Verification
|
||||||
|
|
||||||
|
### Flow 1: Single Model Q&A
|
||||||
|
`/project new → /project select → /ask → response`
|
||||||
|
|
||||||
|
**Status: COMPLETE** - All steps verified functional
|
||||||
|
|
||||||
|
### Flow 2: Open Mode (Parallel)
|
||||||
|
`/project new → /project select → /open → parallel responses`
|
||||||
|
|
||||||
|
**Status: COMPLETE** - asyncio.gather orchestration verified
|
||||||
|
|
||||||
|
### Flow 3: Full Discussion Flow
|
||||||
|
`/project new → /project select → /discuss → /next → /stop → /consensus → /export`
|
||||||
|
|
||||||
|
**Status: COMPLETE** - State management via user_data verified, persistence verified
|
||||||
|
|
||||||
|
### Flow 4: @mention During Discussion
|
||||||
|
`@claude message → direct response (with context if discussion active)`
|
||||||
|
|
||||||
|
**Status: COMPLETE** - MessageHandler regex filter verified, optional context loading verified
|
||||||
|
|
||||||
|
## Tech Debt Summary
|
||||||
|
|
||||||
|
### Phase 1: Foundation
|
||||||
|
- **Coverage gap:** Error handling paths in database.py untested (lines 62, 85, 91-93)
|
||||||
|
- **Severity:** Low - happy path tested, edge cases deferred
|
||||||
|
|
||||||
|
### Phase 4: Single Model Q&A
|
||||||
|
- **Deferred plan:** 04-03 error handling enhancement not implemented
|
||||||
|
- **Severity:** Low - basic error handling exists, comprehensive retry/timeout logic deferred
|
||||||
|
|
||||||
|
### General
|
||||||
|
- **Orphaned export:** `get_round_messages` in discussion service defined but unused
|
||||||
|
- **Pattern inconsistency:** export.py has local `get_selected_project` sync function vs importing async version
|
||||||
|
- **Missing re-exports:** `save_consensus`, `get_consensus` not in services/__init__.py
|
||||||
|
- **Feature gap:** `allowed_users` auth middleware not enforced (defined in config but unchecked)
|
||||||
|
|
||||||
|
### Total Tech Debt: 7 items across 3 categories
|
||||||
|
- Critical blockers: 0
|
||||||
|
- Non-blocking items: 7
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
**READY TO COMPLETE**
|
||||||
|
|
||||||
|
Milestone v1.0 has:
|
||||||
|
- All 9 requirements satisfied
|
||||||
|
- All 6 phases complete with SUMMARY.md files
|
||||||
|
- All 4 E2E user flows verified
|
||||||
|
- Cross-phase integration fully connected
|
||||||
|
- Tech debt documented but non-blocking
|
||||||
|
|
||||||
|
Proceed with `/gsd:complete-milestone v1.0`
|
||||||
|
|
||||||
|
---
|
||||||
|
*Audited: 2026-01-17*
|
||||||
|
*Auditor: gsd-integration-checker*
|
||||||
50
.planning/milestones/v1.0-REQUIREMENTS.md
Normal file
50
.planning/milestones/v1.0-REQUIREMENTS.md
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
# Requirements Archive: v1.0 MVP
|
||||||
|
|
||||||
|
**Archived:** 2026-01-17
|
||||||
|
**Status:** ✅ SHIPPED
|
||||||
|
|
||||||
|
This is the archived requirements specification for v1.0.
|
||||||
|
For current requirements, see `.planning/PROJECT.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0 Requirements
|
||||||
|
|
||||||
|
### Core Requirements
|
||||||
|
|
||||||
|
- [x] Project scaffolding (pyproject.toml, ruff, pre-commit, src layout)
|
||||||
|
- [x] M1: Bot responds to /help, /status
|
||||||
|
- [x] M2: Project CRUD (/projects, /project new, select, delete, models, info)
|
||||||
|
- [x] M3: Single model Q&A working
|
||||||
|
- [x] M4: Open mode (parallel) with multiple models
|
||||||
|
- [x] M5: Discuss mode (sequential rounds)
|
||||||
|
- [x] M6: Consensus generation (/consensus)
|
||||||
|
- [x] M7: Export to markdown (/export)
|
||||||
|
- [x] M8: @mention direct messages
|
||||||
|
|
||||||
|
### Traceability
|
||||||
|
|
||||||
|
| Requirement | Phase | Status | Evidence |
|
||||||
|
|-------------|-------|--------|----------|
|
||||||
|
| Project scaffolding | Phase 1 | Complete | pyproject.toml, .pre-commit-config.yaml, src/moai/ |
|
||||||
|
| M1: /help, /status | Phase 2 | Complete | handlers/commands.py, handlers/status.py |
|
||||||
|
| M2: Project CRUD | Phase 3 | Complete | handlers/projects.py |
|
||||||
|
| M3: Single model Q&A | Phase 4 | Complete | /ask in handlers/discussion.py |
|
||||||
|
| M4: Open mode | Phase 5 | Complete | /open with asyncio.gather |
|
||||||
|
| M5: Discuss mode | Phase 5 | Complete | /discuss, /next, /stop commands |
|
||||||
|
| M6: Consensus | Phase 6 | Complete | /consensus command |
|
||||||
|
| M7: Export | Phase 6 | Complete | /export command |
|
||||||
|
| M8: @mentions | Phase 5 | Complete | @mention MessageHandler |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Milestone Summary
|
||||||
|
|
||||||
|
**Shipped:** 9 of 9 v1.0 requirements
|
||||||
|
**Adjusted:** None
|
||||||
|
**Dropped:** None
|
||||||
|
|
||||||
|
All requirements delivered as originally specified. No scope changes during milestone.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Archived: 2026-01-17 as part of v1.0 milestone completion*
|
||||||
125
.planning/milestones/v1.0-ROADMAP.md
Normal file
125
.planning/milestones/v1.0-ROADMAP.md
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
# Milestone v1.0: MVP
|
||||||
|
|
||||||
|
**Status:** ✅ SHIPPED 2026-01-17
|
||||||
|
**Phases:** 1-6
|
||||||
|
**Total Plans:** 16
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## Phases
|
||||||
|
|
||||||
|
### Phase 1: Foundation ✓
|
||||||
|
|
||||||
|
**Goal**: Complete project scaffolding with pyproject.toml, ruff, pre-commit, src layout, and SQLAlchemy models
|
||||||
|
**Depends on**: Nothing (first phase)
|
||||||
|
**Plans**: 3 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
- [x] 01-01: Project scaffolding (pyproject.toml, ruff, pre-commit)
|
||||||
|
- [x] 01-02: SQLAlchemy models (Project, Discussion, Round, Message, Consensus)
|
||||||
|
- [x] 01-03: Database session management and tests
|
||||||
|
|
||||||
|
**Completed:** 2026-01-16
|
||||||
|
|
||||||
|
### Phase 2: Bot Core ✓
|
||||||
|
|
||||||
|
**Goal**: Working Telegram bot responding to /help and /status commands
|
||||||
|
**Depends on**: Phase 1
|
||||||
|
**Plans**: 2 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
- [x] 02-01: Bot infrastructure (Application, config, lifecycle)
|
||||||
|
- [x] 02-02: /help and /status command handlers
|
||||||
|
|
||||||
|
**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
|
||||||
|
**Plans**: 3 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
- [x] 03-01: Project service layer, /projects and /project new
|
||||||
|
- [x] 03-02: /project select and /project info
|
||||||
|
- [x] 03-03: /project delete and /project 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
|
||||||
|
**Plans**: 2 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
- [x] 04-01: AI client abstraction layer (Requesty/OpenRouter)
|
||||||
|
- [x] 04-02: /ask command handler
|
||||||
|
|
||||||
|
**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
|
||||||
|
**Plans**: 4 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
- [x] 05-01: Discussion service (CRUD operations)
|
||||||
|
- [x] 05-02: /open command (parallel mode)
|
||||||
|
- [x] 05-03: /discuss, /next, /stop commands (sequential mode)
|
||||||
|
- [x] 05-04: @mention message handler
|
||||||
|
|
||||||
|
**Completed:** 2026-01-16
|
||||||
|
|
||||||
|
### Phase 6: Consensus & Export ✓
|
||||||
|
|
||||||
|
**Goal**: Consensus generation from discussions and markdown export
|
||||||
|
**Depends on**: Phase 5
|
||||||
|
**Plans**: 2 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
- [x] 06-01: /consensus command (AI-generated synthesis)
|
||||||
|
- [x] 06-02: /export command (markdown file)
|
||||||
|
|
||||||
|
**Completed:** 2026-01-17
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Milestone Summary
|
||||||
|
|
||||||
|
**Decimal Phases:** None (clean milestone)
|
||||||
|
|
||||||
|
**Key Decisions:**
|
||||||
|
|
||||||
|
- hatchling as build backend with explicit src layout config
|
||||||
|
- String(36) for UUID storage (SQLite compatibility)
|
||||||
|
- Module-level singletons for database and AI client
|
||||||
|
- OpenAI SDK for router abstraction (Requesty/OpenRouter compatible)
|
||||||
|
- Service layer pattern for business logic
|
||||||
|
- asyncio.gather for parallel model queries
|
||||||
|
- user_data dict for discussion state across commands
|
||||||
|
- JSON format for consensus AI output
|
||||||
|
- BytesIO for in-memory markdown export
|
||||||
|
|
||||||
|
**Issues Resolved:**
|
||||||
|
|
||||||
|
- None (greenfield development)
|
||||||
|
|
||||||
|
**Issues Deferred:**
|
||||||
|
|
||||||
|
- Comprehensive error handling (retry/timeout logic)
|
||||||
|
- User allowlist middleware enforcement
|
||||||
|
- Test coverage for database error paths
|
||||||
|
|
||||||
|
**Technical Debt Incurred:**
|
||||||
|
|
||||||
|
- get_round_messages defined but unused
|
||||||
|
- Inconsistent get_selected_project pattern in export.py
|
||||||
|
- Missing re-exports in services/__init__.py
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*For current project status, see .planning/PROJECT.md*
|
||||||
129
.planning/phases/06-consensus-export/06-01-PLAN.md
Normal file
129
.planning/phases/06-consensus-export/06-01-PLAN.md
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
---
|
||||||
|
phase: 06-consensus-export
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Implement consensus generation for multi-model discussions.
|
||||||
|
|
||||||
|
Purpose: Enable users to synthesize discussion outcomes into structured agreements/disagreements (M6 milestone).
|
||||||
|
Output: Working /consensus command that generates and displays AI-summarized consensus.
|
||||||
|
</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 (discussion infrastructure):
|
||||||
|
@.planning/phases/05-multi-model-discussions/05-03-SUMMARY.md
|
||||||
|
|
||||||
|
# Source files to modify/reference:
|
||||||
|
@src/moai/core/models.py
|
||||||
|
@src/moai/core/orchestrator.py
|
||||||
|
@src/moai/core/services/discussion.py
|
||||||
|
@src/moai/bot/handlers/discussion.py
|
||||||
|
@src/moai/bot/handlers/commands.py
|
||||||
|
|
||||||
|
**Tech stack available:** python-telegram-bot, sqlalchemy, openai SDK, asyncio
|
||||||
|
**Established patterns:** Service layer in core/services/, orchestrator for AI calls, handler delegation to services
|
||||||
|
|
||||||
|
**Constraining decisions:**
|
||||||
|
- Phase 05-03: build_context() assembles discussion history for AI context
|
||||||
|
- Phase 05-01: Discussion service with eager loading via selectinload
|
||||||
|
- Phase 04-01: AI client singleton pattern, MODEL_MAP for short names
|
||||||
|
|
||||||
|
**SPEC.md consensus format:**
|
||||||
|
- agreements: string[] (bullet points)
|
||||||
|
- disagreements: [{topic, positions: {model: position}}]
|
||||||
|
- Consensus model already exists with these fields
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add consensus generation to orchestrator and service</name>
|
||||||
|
<files>src/moai/core/orchestrator.py, src/moai/core/services/discussion.py</files>
|
||||||
|
<action>
|
||||||
|
1. In orchestrator.py, add CONSENSUS_PROMPT constant with instructions to analyze discussion and return JSON with "agreements" (list of strings) and "disagreements" (list of {topic, positions} objects).
|
||||||
|
|
||||||
|
2. Add `generate_consensus(discussion: Discussion, model: str = "claude") -> dict` function:
|
||||||
|
- Build context from discussion using existing build_context()
|
||||||
|
- Append CONSENSUS_PROMPT as final user message
|
||||||
|
- Call AI client with system prompt explaining the task
|
||||||
|
- Parse JSON response (handle parsing errors gracefully)
|
||||||
|
- Return dict with "agreements" and "disagreements" keys
|
||||||
|
|
||||||
|
3. In discussion.py service, add:
|
||||||
|
- `save_consensus(discussion_id: str, agreements: list, disagreements: list, generated_by: str) -> Consensus`
|
||||||
|
- `get_consensus(discussion_id: str) -> Consensus | None`
|
||||||
|
|
||||||
|
Use existing session patterns (async with get_session()). Import Consensus model.
|
||||||
|
</action>
|
||||||
|
<verify>Python imports succeed: `python -c "from moai.core.orchestrator import generate_consensus; from moai.core.services.discussion import save_consensus, get_consensus"`</verify>
|
||||||
|
<done>generate_consensus() returns dict with agreements/disagreements, save_consensus() persists to DB, get_consensus() retrieves</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Create /consensus command handler</name>
|
||||||
|
<files>src/moai/bot/handlers/discussion.py, src/moai/bot/handlers/commands.py, src/moai/bot/handlers/__init__.py</files>
|
||||||
|
<action>
|
||||||
|
1. In discussion.py handlers, add `consensus_command(update, context)`:
|
||||||
|
- Get selected project (error if none)
|
||||||
|
- Get active discussion for project (error if none, suggest /open or /discuss first)
|
||||||
|
- Check if consensus already exists (get_consensus) - if so, display existing
|
||||||
|
- Otherwise call generate_consensus() with discussion
|
||||||
|
- Save consensus via save_consensus()
|
||||||
|
- Format and display:
|
||||||
|
```
|
||||||
|
**Consensus Summary**
|
||||||
|
|
||||||
|
**Agreements:**
|
||||||
|
- point 1
|
||||||
|
- point 2
|
||||||
|
|
||||||
|
**Disagreements:**
|
||||||
|
- **Topic:** {topic}
|
||||||
|
- Claude: {position}
|
||||||
|
- GPT: {position}
|
||||||
|
|
||||||
|
_Generated by {model}_
|
||||||
|
```
|
||||||
|
- Show typing indicator while generating
|
||||||
|
|
||||||
|
2. Register handler in __init__.py: `CommandHandler("consensus", consensus_command)`
|
||||||
|
|
||||||
|
3. Update HELP_TEXT in commands.py to add consensus under Output section:
|
||||||
|
`/consensus` - Generate consensus summary
|
||||||
|
</action>
|
||||||
|
<verify>Bot responds to /consensus with either consensus output or appropriate error message</verify>
|
||||||
|
<done>/consensus generates and displays consensus for active discussion, or shows existing if already generated</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `ruff check src/moai/core/orchestrator.py src/moai/core/services/discussion.py src/moai/bot/handlers/`
|
||||||
|
- [ ] `python -c "from moai.core.orchestrator import generate_consensus"` succeeds
|
||||||
|
- [ ] `python -c "from moai.core.services.discussion import save_consensus, get_consensus"` succeeds
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
|
||||||
|
- generate_consensus() calls AI and returns structured dict
|
||||||
|
- Consensus persisted to database via save_consensus()
|
||||||
|
- /consensus command displays formatted consensus
|
||||||
|
- Help text updated
|
||||||
|
- No ruff errors
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/06-consensus-export/06-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
99
.planning/phases/06-consensus-export/06-01-SUMMARY.md
Normal file
99
.planning/phases/06-consensus-export/06-01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
---
|
||||||
|
phase: 06-consensus-export
|
||||||
|
plan: 01
|
||||||
|
subsystem: ai, discussion
|
||||||
|
tags: [consensus, json-parsing, orchestrator, telegram-commands]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 05-multi-model-discussions
|
||||||
|
provides: build_context(), discussion service, orchestrator patterns
|
||||||
|
provides:
|
||||||
|
- generate_consensus() for AI-powered discussion synthesis
|
||||||
|
- save_consensus() and get_consensus() service functions
|
||||||
|
- /consensus command handler
|
||||||
|
affects: [06-export, future-consensus-features]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [json-structured-ai-output, consensus-persistence]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: []
|
||||||
|
modified:
|
||||||
|
- src/moai/core/orchestrator.py
|
||||||
|
- src/moai/core/services/discussion.py
|
||||||
|
- src/moai/bot/handlers/discussion.py
|
||||||
|
- src/moai/bot/handlers/__init__.py
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "JSON format for consensus output with agreements array and disagreements array"
|
||||||
|
- "Graceful error handling returns empty consensus on parse failure"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "CONSENSUS_PROMPT: structured prompt for JSON output from AI"
|
||||||
|
- "Consensus retrieval before generation to avoid duplicates"
|
||||||
|
|
||||||
|
issues-created: []
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 5 min
|
||||||
|
completed: 2026-01-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 6 Plan 1: Consensus Generation Summary
|
||||||
|
|
||||||
|
**AI-powered consensus generation via CONSENSUS_PROMPT constant, generate_consensus() orchestrator function, and /consensus command handler**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 5 min
|
||||||
|
- **Started:** 2026-01-16T20:02:13Z
|
||||||
|
- **Completed:** 2026-01-16T20:07:55Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 4
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Added CONSENSUS_PROMPT constant with JSON output instructions for agreements/disagreements
|
||||||
|
- Implemented generate_consensus() function using existing build_context() and AI client
|
||||||
|
- Created save_consensus() and get_consensus() service functions for persistence
|
||||||
|
- Added /consensus command handler with Markdown formatting
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Add consensus generation to orchestrator and service** - `8242de5` (feat)
|
||||||
|
2. **Task 2: Create /consensus command handler** - `ee9f8ca` (feat)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `src/moai/core/orchestrator.py` - Added CONSENSUS_PROMPT, generate_consensus()
|
||||||
|
- `src/moai/core/services/discussion.py` - Added save_consensus(), get_consensus()
|
||||||
|
- `src/moai/bot/handlers/discussion.py` - Added consensus_command handler
|
||||||
|
- `src/moai/bot/handlers/__init__.py` - Registered consensus CommandHandler
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- JSON format for consensus output (agreements: string[], disagreements: [{topic, positions}])
|
||||||
|
- Graceful error handling on JSON parse failure returns empty consensus
|
||||||
|
- Check for existing consensus before generating new one
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- Consensus generation complete, ready for 06-02 (export functionality)
|
||||||
|
- /consensus command fully functional for active discussions
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 06-consensus-export*
|
||||||
|
*Completed: 2026-01-16*
|
||||||
157
.planning/phases/06-consensus-export/06-02-PLAN.md
Normal file
157
.planning/phases/06-consensus-export/06-02-PLAN.md
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
---
|
||||||
|
phase: 06-consensus-export
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Implement markdown export for discussions and projects.
|
||||||
|
|
||||||
|
Purpose: Enable users to export discussion history and consensus to shareable markdown documents (M7 milestone).
|
||||||
|
Output: Working /export command that generates and sends markdown files via Telegram.
|
||||||
|
</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/06-consensus-export/06-01-SUMMARY.md
|
||||||
|
|
||||||
|
# Source files to reference:
|
||||||
|
@src/moai/core/models.py
|
||||||
|
@src/moai/core/services/discussion.py
|
||||||
|
@src/moai/core/services/project.py
|
||||||
|
@src/moai/bot/handlers/__init__.py
|
||||||
|
@src/moai/bot/handlers/commands.py
|
||||||
|
|
||||||
|
**Tech stack available:** python-telegram-bot, sqlalchemy
|
||||||
|
**Established patterns:** Service layer, handler registration, get_selected_project helper
|
||||||
|
|
||||||
|
**SPEC.md export format:**
|
||||||
|
```markdown
|
||||||
|
# {Project Name}
|
||||||
|
|
||||||
|
**Date:** {date}
|
||||||
|
**Models:** Claude, GPT, Gemini
|
||||||
|
**Discussions:** {count}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Discussion 1: {Question}
|
||||||
|
|
||||||
|
### Initial Responses (Open)
|
||||||
|
**Claude:**
|
||||||
|
> {response}
|
||||||
|
|
||||||
|
### Discussion (N rounds)
|
||||||
|
**Round 1:**
|
||||||
|
**Claude:** {response}
|
||||||
|
**GPT:** {response}
|
||||||
|
|
||||||
|
### Consensus
|
||||||
|
**Agreements:**
|
||||||
|
- point 1
|
||||||
|
|
||||||
|
**Disagreements:**
|
||||||
|
- **Topic:** {topic}
|
||||||
|
- Claude: {position}
|
||||||
|
```
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create exporter module</name>
|
||||||
|
<files>src/moai/core/exporter.py</files>
|
||||||
|
<action>
|
||||||
|
Create new file src/moai/core/exporter.py with:
|
||||||
|
|
||||||
|
1. Module docstring explaining markdown export functionality.
|
||||||
|
|
||||||
|
2. `export_discussion(discussion: Discussion) -> str` function:
|
||||||
|
- Format discussion as markdown following SPEC format
|
||||||
|
- Include question as heading
|
||||||
|
- Group messages by round, label round type (parallel = "Initial Responses", sequential = "Round N")
|
||||||
|
- Format each message as `**Model:** response`
|
||||||
|
- Include consensus section if consensus exists (agreements as bullets, disagreements with positions)
|
||||||
|
- Return markdown string
|
||||||
|
|
||||||
|
3. `export_project(project: Project, discussions: list[Discussion]) -> str` function:
|
||||||
|
- Header with project name, date, models list, discussion count
|
||||||
|
- Separator (---)
|
||||||
|
- Each discussion formatted via export_discussion()
|
||||||
|
- Separators between discussions
|
||||||
|
- Return full markdown string
|
||||||
|
|
||||||
|
4. Helper `_format_consensus(consensus: Consensus) -> str`:
|
||||||
|
- Format agreements as bullet list
|
||||||
|
- Format disagreements with topic and model positions
|
||||||
|
- Return formatted string or empty if no consensus
|
||||||
|
|
||||||
|
Use datetime.now().strftime("%Y-%m-%d") for date formatting.
|
||||||
|
</action>
|
||||||
|
<verify>`python -c "from moai.core.exporter import export_discussion, export_project"` succeeds</verify>
|
||||||
|
<done>Exporter module creates properly formatted markdown matching SPEC format</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Create /export command handler</name>
|
||||||
|
<files>src/moai/bot/handlers/export.py, src/moai/bot/handlers/__init__.py, src/moai/bot/handlers/commands.py</files>
|
||||||
|
<action>
|
||||||
|
1. Create new file src/moai/bot/handlers/export.py with:
|
||||||
|
- Import exporter functions, services, telegram types
|
||||||
|
- `export_command(update, context)` handler:
|
||||||
|
- Get selected project (error if none)
|
||||||
|
- Parse args: no args = export active discussion, "project" = export full project
|
||||||
|
- For discussion export:
|
||||||
|
- Get active discussion (error if none)
|
||||||
|
- Call export_discussion()
|
||||||
|
- Send as document: `update.message.reply_document(io.BytesIO(md.encode()), filename=f"{project.name}-discussion.md")`
|
||||||
|
- For project export:
|
||||||
|
- Get all discussions for project via list_discussions()
|
||||||
|
- Fetch each with full eager loading via get_discussion()
|
||||||
|
- Call export_project()
|
||||||
|
- Send as document with filename `{project.name}-export.md`
|
||||||
|
- Use io.BytesIO for in-memory file, import io module
|
||||||
|
|
||||||
|
2. Update handlers/__init__.py:
|
||||||
|
- Import export_command from export module
|
||||||
|
- Register `CommandHandler("export", export_command)`
|
||||||
|
|
||||||
|
3. Update HELP_TEXT in commands.py, Output section:
|
||||||
|
`/export` - Export current discussion as markdown
|
||||||
|
`/export project` - Export entire project as markdown
|
||||||
|
</action>
|
||||||
|
<verify>Bot responds to /export with a markdown document attachment</verify>
|
||||||
|
<done>/export sends discussion markdown, /export project sends full project markdown as Telegram documents</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `ruff check src/moai/core/exporter.py src/moai/bot/handlers/export.py`
|
||||||
|
- [ ] `python -c "from moai.core.exporter import export_discussion, export_project"` succeeds
|
||||||
|
- [ ] `python -c "from moai.bot.handlers.export import export_command"` succeeds
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
|
||||||
|
- Exporter module generates SPEC-compliant markdown
|
||||||
|
- /export sends discussion as .md document
|
||||||
|
- /export project sends full project as .md document
|
||||||
|
- Help text updated
|
||||||
|
- No ruff errors
|
||||||
|
- Phase 6 complete (M6 + M7 milestones)
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/06-consensus-export/06-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
97
.planning/phases/06-consensus-export/06-02-SUMMARY.md
Normal file
97
.planning/phases/06-consensus-export/06-02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
---
|
||||||
|
phase: 06-consensus-export
|
||||||
|
plan: 02
|
||||||
|
subsystem: api
|
||||||
|
tags: [markdown, export, telegram, io]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 06-01
|
||||||
|
provides: consensus model and generation
|
||||||
|
- phase: 05
|
||||||
|
provides: discussion service, rounds, messages
|
||||||
|
provides:
|
||||||
|
- Markdown exporter for discussions and projects
|
||||||
|
- /export command for Telegram document attachments
|
||||||
|
affects: []
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [io.BytesIO for in-memory file generation]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: [src/moai/core/exporter.py, src/moai/bot/handlers/export.py]
|
||||||
|
modified: [src/moai/bot/handlers/__init__.py, src/moai/bot/handlers/commands.py]
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "BytesIO for in-memory markdown file (no temp files needed)"
|
||||||
|
- "Filename sanitization with space→underscore replacement"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Document export via reply_document with BytesIO"
|
||||||
|
|
||||||
|
issues-created: []
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 3min
|
||||||
|
completed: 2026-01-17
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 06 Plan 02: Export Summary
|
||||||
|
|
||||||
|
**Markdown export module with /export command sending discussions and projects as Telegram document attachments**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 3 min
|
||||||
|
- **Started:** 2026-01-17T00:00:00Z
|
||||||
|
- **Completed:** 2026-01-17T00:03:00Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 4
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Created exporter module with SPEC-compliant markdown formatting
|
||||||
|
- /export command sends active discussion as .md document
|
||||||
|
- /export project sends full project with all discussions
|
||||||
|
- Help text updated with export options
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Create exporter module** - `152d617` (feat)
|
||||||
|
2. **Task 2: Create /export command handler** - `e5108c6` (feat)
|
||||||
|
|
||||||
|
**Plan metadata:** (this commit) (docs: complete plan)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `src/moai/core/exporter.py` - Markdown export functions (export_discussion, export_project, _format_consensus)
|
||||||
|
- `src/moai/bot/handlers/export.py` - /export command handler
|
||||||
|
- `src/moai/bot/handlers/__init__.py` - Register export_command handler
|
||||||
|
- `src/moai/bot/handlers/commands.py` - Updated HELP_TEXT with export options
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- Used io.BytesIO for in-memory file creation (no temp files, cleaner)
|
||||||
|
- Sanitize filename by replacing spaces with underscores
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- Phase 6 complete (M6 + M7 milestones achieved)
|
||||||
|
- All milestone features implemented: consensus generation and markdown export
|
||||||
|
- Milestone 1 complete - ready for /gsd:complete-milestone
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 06-consensus-export*
|
||||||
|
*Completed: 2026-01-17*
|
||||||
|
|
@ -9,12 +9,14 @@ from telegram.ext import Application, CommandHandler, MessageHandler, filters
|
||||||
from moai.bot.handlers.commands import help_command, start_command
|
from moai.bot.handlers.commands import help_command, start_command
|
||||||
from moai.bot.handlers.discussion import (
|
from moai.bot.handlers.discussion import (
|
||||||
ask_command,
|
ask_command,
|
||||||
|
consensus_command,
|
||||||
discuss_command,
|
discuss_command,
|
||||||
mention_handler,
|
mention_handler,
|
||||||
next_command,
|
next_command,
|
||||||
open_command,
|
open_command,
|
||||||
stop_command,
|
stop_command,
|
||||||
)
|
)
|
||||||
|
from moai.bot.handlers.export import export_command
|
||||||
from moai.bot.handlers.projects import project_command, projects_command
|
from moai.bot.handlers.projects import project_command, projects_command
|
||||||
from moai.bot.handlers.status import status_command
|
from moai.bot.handlers.status import status_command
|
||||||
|
|
||||||
|
|
@ -42,6 +44,8 @@ def register_handlers(app: Application) -> None:
|
||||||
app.add_handler(CommandHandler("discuss", discuss_command))
|
app.add_handler(CommandHandler("discuss", discuss_command))
|
||||||
app.add_handler(CommandHandler("next", next_command))
|
app.add_handler(CommandHandler("next", next_command))
|
||||||
app.add_handler(CommandHandler("stop", stop_command))
|
app.add_handler(CommandHandler("stop", stop_command))
|
||||||
|
app.add_handler(CommandHandler("consensus", consensus_command))
|
||||||
|
app.add_handler(CommandHandler("export", export_command))
|
||||||
|
|
||||||
# @mention handler - MessageHandler registered AFTER CommandHandlers
|
# @mention handler - MessageHandler registered AFTER CommandHandlers
|
||||||
# Matches messages starting with @claude, @gpt, or @gemini followed by content
|
# Matches messages starting with @claude, @gpt, or @gemini followed by content
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ Multi-AI collaborative brainstorming platform.
|
||||||
|
|
||||||
*Output Commands:*
|
*Output Commands:*
|
||||||
/consensus - Generate consensus summary
|
/consensus - Generate consensus summary
|
||||||
/export - Export project as markdown
|
/export - Export current discussion as markdown
|
||||||
|
/export project - Export entire project as markdown
|
||||||
|
|
||||||
*Utility:*
|
*Utility:*
|
||||||
/status - Show current state
|
/status - Show current state
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,22 @@ from telegram.ext import ContextTypes
|
||||||
from moai.bot.handlers.projects import get_selected_project
|
from moai.bot.handlers.projects import get_selected_project
|
||||||
from moai.core.ai_client import MODEL_MAP, get_ai_client
|
from moai.core.ai_client import MODEL_MAP, get_ai_client
|
||||||
from moai.core.models import DiscussionType, RoundType
|
from moai.core.models import DiscussionType, RoundType
|
||||||
from moai.core.orchestrator import query_model_direct, query_models_parallel, run_discussion_round
|
from moai.core.orchestrator import (
|
||||||
|
generate_consensus,
|
||||||
|
query_model_direct,
|
||||||
|
query_models_parallel,
|
||||||
|
run_discussion_round,
|
||||||
|
)
|
||||||
from moai.core.services.discussion import (
|
from moai.core.services.discussion import (
|
||||||
complete_discussion,
|
complete_discussion,
|
||||||
create_discussion,
|
create_discussion,
|
||||||
create_message,
|
create_message,
|
||||||
create_round,
|
create_round,
|
||||||
get_active_discussion,
|
get_active_discussion,
|
||||||
|
get_consensus,
|
||||||
get_current_round,
|
get_current_round,
|
||||||
get_discussion,
|
get_discussion,
|
||||||
|
save_consensus,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -425,3 +432,119 @@ async def mention_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await update.message.reply_text(f"Error: {e}")
|
await update.message.reply_text(f"Error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def consensus_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Handle /consensus command - generate or display consensus summary.
|
||||||
|
|
||||||
|
Requires a selected project with an active discussion. If a consensus
|
||||||
|
already exists, displays it. Otherwise generates a new one using AI.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
/consensus
|
||||||
|
"""
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Check if consensus already exists
|
||||||
|
existing_consensus = await get_consensus(discussion.id)
|
||||||
|
if existing_consensus is not None:
|
||||||
|
# Display existing consensus
|
||||||
|
response_text = _format_consensus(
|
||||||
|
agreements=existing_consensus.agreements,
|
||||||
|
disagreements=existing_consensus.disagreements,
|
||||||
|
generated_by=existing_consensus.generated_by,
|
||||||
|
)
|
||||||
|
await update.message.reply_text(response_text, parse_mode="Markdown")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Show typing indicator while generating
|
||||||
|
await update.message.chat.send_action("typing")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Reload discussion with full context
|
||||||
|
discussion = await get_discussion(discussion.id)
|
||||||
|
|
||||||
|
# Generate consensus
|
||||||
|
consensus_model = "claude"
|
||||||
|
result = await generate_consensus(discussion, model=consensus_model)
|
||||||
|
|
||||||
|
# Check for errors
|
||||||
|
if "error" in result:
|
||||||
|
await update.message.reply_text(f"Failed to generate consensus: {result['error']}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Save consensus
|
||||||
|
await save_consensus(
|
||||||
|
discussion_id=discussion.id,
|
||||||
|
agreements=result["agreements"],
|
||||||
|
disagreements=result["disagreements"],
|
||||||
|
generated_by=consensus_model,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format and display
|
||||||
|
response_text = _format_consensus(
|
||||||
|
agreements=result["agreements"],
|
||||||
|
disagreements=result["disagreements"],
|
||||||
|
generated_by=consensus_model,
|
||||||
|
)
|
||||||
|
await update.message.reply_text(response_text, parse_mode="Markdown")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await update.message.reply_text(f"Error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _format_consensus(
|
||||||
|
agreements: list,
|
||||||
|
disagreements: list,
|
||||||
|
generated_by: str,
|
||||||
|
) -> str:
|
||||||
|
"""Format consensus data into a Markdown string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
agreements: List of agreement strings.
|
||||||
|
disagreements: List of disagreement dicts with topic and positions.
|
||||||
|
generated_by: The model that generated the consensus.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted Markdown string.
|
||||||
|
"""
|
||||||
|
lines = ["*Consensus Summary*\n"]
|
||||||
|
|
||||||
|
# Agreements section
|
||||||
|
lines.append("*Agreements:*")
|
||||||
|
if agreements:
|
||||||
|
for point in agreements:
|
||||||
|
lines.append(f"- {point}")
|
||||||
|
else:
|
||||||
|
lines.append("- None identified")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Disagreements section
|
||||||
|
lines.append("*Disagreements:*")
|
||||||
|
if disagreements:
|
||||||
|
for disagreement in disagreements:
|
||||||
|
topic = disagreement.get("topic", "Unknown topic")
|
||||||
|
positions = disagreement.get("positions", {})
|
||||||
|
lines.append(f"- *{topic}*")
|
||||||
|
for model, position in positions.items():
|
||||||
|
lines.append(f" - {model.title()}: {position}")
|
||||||
|
else:
|
||||||
|
lines.append("- None identified")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Footer
|
||||||
|
lines.append(f"_Generated by {generated_by.title()}_")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
|
||||||
85
src/moai/bot/handlers/export.py
Normal file
85
src/moai/bot/handlers/export.py
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
"""Export command handlers for MoAI bot.
|
||||||
|
|
||||||
|
Provides /export command to export discussions and projects as markdown documents.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
|
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import ContextTypes
|
||||||
|
|
||||||
|
from moai.core.exporter import export_discussion, export_project
|
||||||
|
from moai.core.services.discussion import get_active_discussion, get_discussion, list_discussions
|
||||||
|
from moai.core.services.project import get_project
|
||||||
|
|
||||||
|
|
||||||
|
def get_selected_project(context: ContextTypes.DEFAULT_TYPE) -> str | None:
|
||||||
|
"""Get the currently selected project ID from user data."""
|
||||||
|
return context.user_data.get("selected_project_id")
|
||||||
|
|
||||||
|
|
||||||
|
async def export_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Handle /export command - export discussion or project as markdown.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
/export - Export current active discussion
|
||||||
|
/export project - Export entire project with all discussions
|
||||||
|
"""
|
||||||
|
project_id = get_selected_project(context)
|
||||||
|
if not project_id:
|
||||||
|
await update.message.reply_text("No project selected. Use /project select first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
project = await get_project(project_id)
|
||||||
|
if not project:
|
||||||
|
await update.message.reply_text("Selected project not found. Use /project select.")
|
||||||
|
return
|
||||||
|
|
||||||
|
args = context.args
|
||||||
|
export_full_project = args and args[0].lower() == "project"
|
||||||
|
|
||||||
|
if export_full_project:
|
||||||
|
# Export entire project
|
||||||
|
discussions_list = await list_discussions(project_id)
|
||||||
|
if not discussions_list:
|
||||||
|
await update.message.reply_text("No discussions in this project to export.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fetch each discussion with full eager loading
|
||||||
|
loaded_discussions = []
|
||||||
|
for disc in discussions_list:
|
||||||
|
full_disc = await get_discussion(disc.id)
|
||||||
|
if full_disc:
|
||||||
|
loaded_discussions.append(full_disc)
|
||||||
|
|
||||||
|
markdown = export_project(project, loaded_discussions)
|
||||||
|
filename = f"{project.name.replace(' ', '_')}-export.md"
|
||||||
|
|
||||||
|
await update.message.reply_document(
|
||||||
|
document=io.BytesIO(markdown.encode("utf-8")),
|
||||||
|
filename=filename,
|
||||||
|
caption=f"📄 Full project export: {project.name}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Export active discussion
|
||||||
|
discussion = await get_active_discussion(project_id)
|
||||||
|
if not discussion:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"No active discussion. Start one with /open or /discuss."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Re-fetch with full loading including consensus
|
||||||
|
full_discussion = await get_discussion(discussion.id)
|
||||||
|
if not full_discussion:
|
||||||
|
await update.message.reply_text("Failed to load discussion.")
|
||||||
|
return
|
||||||
|
|
||||||
|
markdown = export_discussion(full_discussion)
|
||||||
|
filename = f"{project.name.replace(' ', '_')}-discussion.md"
|
||||||
|
|
||||||
|
await update.message.reply_document(
|
||||||
|
document=io.BytesIO(markdown.encode("utf-8")),
|
||||||
|
filename=filename,
|
||||||
|
caption=f"📄 Discussion export: {full_discussion.question[:50]}...",
|
||||||
|
)
|
||||||
117
src/moai/core/exporter.py
Normal file
117
src/moai/core/exporter.py
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
"""Markdown export functionality for MoAI discussions and projects.
|
||||||
|
|
||||||
|
Exports discussions and projects to shareable markdown documents following
|
||||||
|
the SPEC format with sections for initial responses, rounds, and consensus.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from moai.core.models import Consensus, Discussion, Project, RoundType
|
||||||
|
|
||||||
|
|
||||||
|
def _format_consensus(consensus: Consensus | None) -> str:
|
||||||
|
"""Format consensus section as markdown.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
consensus: The Consensus object to format, or None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted markdown string for consensus, or empty string if none.
|
||||||
|
"""
|
||||||
|
if consensus is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
lines = ["### Consensus"]
|
||||||
|
|
||||||
|
if consensus.agreements:
|
||||||
|
lines.append("**Agreements:**")
|
||||||
|
for agreement in consensus.agreements:
|
||||||
|
lines.append(f"- {agreement}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
if consensus.disagreements:
|
||||||
|
lines.append("**Disagreements:**")
|
||||||
|
for disagreement in consensus.disagreements:
|
||||||
|
topic = disagreement.get("topic", "Unknown topic")
|
||||||
|
positions = disagreement.get("positions", {})
|
||||||
|
lines.append(f"- **{topic}:**")
|
||||||
|
for model, position in positions.items():
|
||||||
|
lines.append(f" - {model}: {position}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def export_discussion(discussion: Discussion) -> str:
|
||||||
|
"""Export a discussion as markdown.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
discussion: The Discussion object to export (with rounds/messages loaded).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Markdown string formatted according to SPEC.
|
||||||
|
"""
|
||||||
|
lines = [f"## Discussion: {discussion.question}", ""]
|
||||||
|
|
||||||
|
# Group messages by round
|
||||||
|
sorted_rounds = sorted(discussion.rounds, key=lambda r: r.round_number)
|
||||||
|
|
||||||
|
for round_ in sorted_rounds:
|
||||||
|
# Label round type appropriately
|
||||||
|
if round_.type == RoundType.PARALLEL:
|
||||||
|
lines.append("### Initial Responses (Open)")
|
||||||
|
else:
|
||||||
|
lines.append(f"### Round {round_.round_number}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Format each message
|
||||||
|
sorted_messages = sorted(round_.messages, key=lambda m: m.timestamp)
|
||||||
|
for message in sorted_messages:
|
||||||
|
model_name = message.model.capitalize()
|
||||||
|
lines.append(f"**{model_name}:**")
|
||||||
|
# Quote the response content
|
||||||
|
for content_line in message.content.split("\n"):
|
||||||
|
lines.append(f"> {content_line}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Add consensus section if exists
|
||||||
|
consensus_md = _format_consensus(discussion.consensus)
|
||||||
|
if consensus_md:
|
||||||
|
lines.append(consensus_md)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def export_project(project: Project, discussions: list[Discussion]) -> str:
|
||||||
|
"""Export a project and its discussions as markdown.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project: The Project object to export.
|
||||||
|
discussions: List of Discussion objects (with rounds/messages loaded).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Full markdown string for the project export.
|
||||||
|
"""
|
||||||
|
# Header section
|
||||||
|
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
models_str = ", ".join(m.capitalize() for m in project.models) if project.models else "None"
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
f"# {project.name}",
|
||||||
|
"",
|
||||||
|
f"**Date:** {date_str}",
|
||||||
|
f"**Models:** {models_str}",
|
||||||
|
f"**Discussions:** {len(discussions)}",
|
||||||
|
"",
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add each discussion
|
||||||
|
for i, discussion in enumerate(discussions):
|
||||||
|
if i > 0:
|
||||||
|
lines.append("---")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(export_discussion(discussion))
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
@ -5,6 +5,7 @@ across multiple models, building context, and managing discussion flow.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from moai.core.ai_client import get_ai_client
|
from moai.core.ai_client import get_ai_client
|
||||||
|
|
@ -25,6 +26,33 @@ Guidelines:
|
||||||
- Focus on practical, actionable insights
|
- Focus on practical, actionable insights
|
||||||
- If you reach agreement with others, state it clearly"""
|
- If you reach agreement with others, state it clearly"""
|
||||||
|
|
||||||
|
# Prompt for generating consensus summary
|
||||||
|
CONSENSUS_PROMPT = """Analyze the discussion above and provide a JSON summary with:
|
||||||
|
1. "agreements" - A list of strings, each being a point all participants agreed on
|
||||||
|
2. "disagreements" - A list of objects, each with:
|
||||||
|
- "topic": The topic of disagreement
|
||||||
|
- "positions": An object mapping model names to their positions
|
||||||
|
|
||||||
|
Only include items where there was clear agreement or disagreement.
|
||||||
|
Respond with ONLY valid JSON, no markdown formatting or explanation.
|
||||||
|
|
||||||
|
Example format:
|
||||||
|
{
|
||||||
|
"agreements": [
|
||||||
|
"Python is a great language for beginners",
|
||||||
|
"Documentation is important"
|
||||||
|
],
|
||||||
|
"disagreements": [
|
||||||
|
{
|
||||||
|
"topic": "Best web framework",
|
||||||
|
"positions": {
|
||||||
|
"claude": "FastAPI for its modern async support",
|
||||||
|
"gpt": "Django for its batteries-included approach"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"""
|
||||||
|
|
||||||
|
|
||||||
async def query_models_parallel(
|
async def query_models_parallel(
|
||||||
models: list[str],
|
models: list[str],
|
||||||
|
|
@ -227,3 +255,71 @@ async def run_discussion_round(
|
||||||
context_messages.append({"role": "user", "content": formatted})
|
context_messages.append({"role": "user", "content": formatted})
|
||||||
|
|
||||||
return responses
|
return responses
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_consensus(discussion: Discussion, model: str = "claude") -> dict:
|
||||||
|
"""Generate a consensus summary from a discussion.
|
||||||
|
|
||||||
|
Analyzes all rounds and messages in the discussion to identify
|
||||||
|
agreements and disagreements among the participating AI models.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
discussion: Discussion object with eager-loaded rounds and messages.
|
||||||
|
model: Model short name to use for generating the consensus (default: "claude").
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with "agreements" (list of strings) and "disagreements"
|
||||||
|
(list of {topic, positions} objects).
|
||||||
|
"""
|
||||||
|
client = get_ai_client()
|
||||||
|
|
||||||
|
# Build context from the discussion
|
||||||
|
context_messages = build_context(discussion)
|
||||||
|
|
||||||
|
# Add the consensus prompt as the final user message
|
||||||
|
context_messages.append({"role": "user", "content": CONSENSUS_PROMPT})
|
||||||
|
|
||||||
|
# System prompt for consensus generation
|
||||||
|
system_prompt = """You are an impartial analyst summarizing a multi-model AI discussion.
|
||||||
|
Your task is to identify clear agreements and disagreements from the conversation.
|
||||||
|
Be precise and only include items where there was genuine consensus or difference of opinion.
|
||||||
|
Respond with valid JSON only."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await client.complete(
|
||||||
|
model=model,
|
||||||
|
messages=context_messages,
|
||||||
|
system_prompt=system_prompt,
|
||||||
|
)
|
||||||
|
logger.info("Consensus generated successfully by %s", model)
|
||||||
|
|
||||||
|
# Parse JSON response, handling potential markdown code blocks
|
||||||
|
json_str = response.strip()
|
||||||
|
if json_str.startswith("```"):
|
||||||
|
# Strip markdown code block
|
||||||
|
lines = json_str.split("\n")
|
||||||
|
json_str = "\n".join(lines[1:-1]) if lines[-1] == "```" else "\n".join(lines[1:])
|
||||||
|
json_str = json_str.strip()
|
||||||
|
|
||||||
|
result = json.loads(json_str)
|
||||||
|
|
||||||
|
# Ensure required keys exist with defaults
|
||||||
|
return {
|
||||||
|
"agreements": result.get("agreements", []),
|
||||||
|
"disagreements": result.get("disagreements", []),
|
||||||
|
}
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error("Failed to parse consensus JSON: %s", e)
|
||||||
|
return {
|
||||||
|
"agreements": [],
|
||||||
|
"disagreements": [],
|
||||||
|
"error": f"Failed to parse AI response: {e}",
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Consensus generation failed: %s", e)
|
||||||
|
return {
|
||||||
|
"agreements": [],
|
||||||
|
"disagreements": [],
|
||||||
|
"error": str(e),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from moai.core.database import get_session
|
from moai.core.database import get_session
|
||||||
from moai.core.models import (
|
from moai.core.models import (
|
||||||
|
Consensus,
|
||||||
Discussion,
|
Discussion,
|
||||||
DiscussionStatus,
|
DiscussionStatus,
|
||||||
DiscussionType,
|
DiscussionType,
|
||||||
|
|
@ -217,3 +218,49 @@ async def get_round_messages(round_id: str) -> list[Message]:
|
||||||
select(Message).where(Message.round_id == round_id).order_by(Message.timestamp)
|
select(Message).where(Message.round_id == round_id).order_by(Message.timestamp)
|
||||||
)
|
)
|
||||||
return list(result.scalars().all())
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
|
||||||
|
async def save_consensus(
|
||||||
|
discussion_id: str,
|
||||||
|
agreements: list,
|
||||||
|
disagreements: list,
|
||||||
|
generated_by: str,
|
||||||
|
) -> Consensus:
|
||||||
|
"""Save a consensus summary for a discussion.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
discussion_id: The discussion's UUID.
|
||||||
|
agreements: List of agreement strings.
|
||||||
|
disagreements: List of disagreement dicts with topic and positions.
|
||||||
|
generated_by: The model that generated the consensus.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created Consensus object.
|
||||||
|
"""
|
||||||
|
async with get_session() as session:
|
||||||
|
consensus = Consensus(
|
||||||
|
discussion_id=discussion_id,
|
||||||
|
agreements=agreements,
|
||||||
|
disagreements=disagreements,
|
||||||
|
generated_by=generated_by,
|
||||||
|
)
|
||||||
|
session.add(consensus)
|
||||||
|
await session.flush()
|
||||||
|
await session.refresh(consensus)
|
||||||
|
return consensus
|
||||||
|
|
||||||
|
|
||||||
|
async def get_consensus(discussion_id: str) -> Consensus | None:
|
||||||
|
"""Get the consensus for a discussion if it exists.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
discussion_id: The discussion's UUID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The Consensus object if found, None otherwise.
|
||||||
|
"""
|
||||||
|
async with get_session() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(Consensus).where(Consensus.discussion_id == discussion_id)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue