Compare commits
No commits in common. "v1.0" and "main" have entirely different histories.
18 changed files with 164 additions and 1362 deletions
|
|
@ -1,31 +0,0 @@
|
|||
# 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
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
|
|
@ -12,19 +12,19 @@ Get richer, more diverse AI insights through structured multi-model discussions
|
|||
|
||||
### Validated
|
||||
|
||||
- ✓ 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
|
||||
(None yet — ship to validate)
|
||||
|
||||
### Active
|
||||
|
||||
(None — define requirements for next milestone)
|
||||
- [ ] 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
|
||||
|
||||
|
|
@ -38,17 +38,15 @@ Get richer, more diverse AI insights through structured multi-model discussions
|
|||
|
||||
## Context
|
||||
|
||||
**Current state:** v1.0 shipped. Telegram bot fully functional with 2,732 LOC Python.
|
||||
**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
|
||||
|
||||
**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
|
||||
**Current state:** Greenfield. Only documentation exists (SPEC.md, README.md, CLAUDE.md).
|
||||
|
||||
## Constraints
|
||||
|
||||
|
|
@ -67,16 +65,9 @@ Get richer, more diverse AI insights through structured multi-model discussions
|
|||
|
||||
| Decision | Rationale | Outcome |
|
||||
|----------|-----------|---------|
|
||||
| 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 | ✓ Good — ruff/pre-commit caught issues early |
|
||||
| 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 |
|
||||
| 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-17 after v1.0 milestone*
|
||||
*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 | - |
|
||||
|
|
@ -2,53 +2,86 @@
|
|||
|
||||
## Project Reference
|
||||
|
||||
See: .planning/PROJECT.md (updated 2026-01-17)
|
||||
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:** Planning next milestone (v1.1)
|
||||
**Current focus:** Phase 6 — Consensus & Export (next)
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: Planning
|
||||
Plan: Not started
|
||||
Status: v1.0 complete, ready for next milestone
|
||||
Last activity: 2026-01-17 — v1.0 milestone shipped
|
||||
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: v1.0 ██████████ 100% shipped
|
||||
Progress: █████████░ ~85%
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
**v1.0 Summary:**
|
||||
- Total plans completed: 16
|
||||
- Total phases: 6
|
||||
- Average plan duration: 4 min
|
||||
- Total execution time: ~1.1 hours
|
||||
- Lines of code: 2,732 Python
|
||||
**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.
|
||||
v1.0 decisions archived with outcomes.
|
||||
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 active — v1.0 tech debt documented in PROJECT.md Context section.
|
||||
None yet.
|
||||
|
||||
### Blockers/Concerns
|
||||
|
||||
None.
|
||||
None yet.
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-01-17
|
||||
Stopped at: v1.0 milestone complete
|
||||
Last session: 2026-01-16T19:58:00Z
|
||||
Stopped at: Completed 05-04-PLAN.md (mention mode) - Phase 5 complete
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,142 +0,0 @@
|
|||
---
|
||||
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*
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
# 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*
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
# 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*
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
---
|
||||
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>
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
---
|
||||
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*
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
---
|
||||
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>
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
---
|
||||
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,14 +9,12 @@ 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,
|
||||
consensus_command,
|
||||
discuss_command,
|
||||
mention_handler,
|
||||
next_command,
|
||||
open_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.status import status_command
|
||||
|
||||
|
|
@ -44,8 +42,6 @@ def register_handlers(app: Application) -> None:
|
|||
app.add_handler(CommandHandler("discuss", discuss_command))
|
||||
app.add_handler(CommandHandler("next", next_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
|
||||
# Matches messages starting with @claude, @gpt, or @gemini followed by content
|
||||
|
|
|
|||
|
|
@ -28,8 +28,7 @@ Multi-AI collaborative brainstorming platform.
|
|||
|
||||
*Output Commands:*
|
||||
/consensus - Generate consensus summary
|
||||
/export - Export current discussion as markdown
|
||||
/export project - Export entire project as markdown
|
||||
/export - Export project as markdown
|
||||
|
||||
*Utility:*
|
||||
/status - Show current state
|
||||
|
|
|
|||
|
|
@ -6,22 +6,15 @@ 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 (
|
||||
generate_consensus,
|
||||
query_model_direct,
|
||||
query_models_parallel,
|
||||
run_discussion_round,
|
||||
)
|
||||
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_consensus,
|
||||
get_current_round,
|
||||
get_discussion,
|
||||
save_consensus,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -432,119 +425,3 @@ async def mention_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||
|
||||
except Exception as 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)
|
||||
|
|
|
|||
|
|
@ -1,85 +0,0 @@
|
|||
"""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]}...",
|
||||
)
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
"""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,7 +5,6 @@ across multiple models, building context, and managing discussion flow.
|
|||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
|
||||
from moai.core.ai_client import get_ai_client
|
||||
|
|
@ -26,33 +25,6 @@ Guidelines:
|
|||
- Focus on practical, actionable insights
|
||||
- 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(
|
||||
models: list[str],
|
||||
|
|
@ -255,71 +227,3 @@ async def run_discussion_round(
|
|||
context_messages.append({"role": "user", "content": formatted})
|
||||
|
||||
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,7 +8,6 @@ from sqlalchemy.orm import selectinload
|
|||
|
||||
from moai.core.database import get_session
|
||||
from moai.core.models import (
|
||||
Consensus,
|
||||
Discussion,
|
||||
DiscussionStatus,
|
||||
DiscussionType,
|
||||
|
|
@ -218,49 +217,3 @@ async def get_round_messages(round_id: str) -> list[Message]:
|
|||
select(Message).where(Message.round_id == round_id).order_by(Message.timestamp)
|
||||
)
|
||||
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