├── .python-version ├── debates └── .gitkeep ├── ui ├── __init__.py ├── prompts.py └── rich_formatter.py ├── services ├── __init__.py ├── file_manager.py └── debate_manager.py ├── .isort.cfg ├── .env.example ├── personalities ├── __init__.py ├── factory.py ├── base.py ├── claude.py ├── openai.py └── local.py ├── sample_config.json ├── pyproject.toml ├── models ├── __init__.py ├── voting.py ├── arguments.py ├── personality.py └── debate.py ├── personality.py ├── LICENSE ├── test_interactive.md ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── VOTING-FEATURE.md ├── demo.py ├── CONTRIBUTING.md ├── config.py ├── CLAUDE.md ├── IMPLEMENTATION_SUMMARY.md ├── voting.py ├── voting_demo.py ├── PLAN.md ├── README.md └── debate_app.py /.python-version: -------------------------------------------------------------------------------- 1 | 3.9 2 | -------------------------------------------------------------------------------- /debates/.gitkeep: -------------------------------------------------------------------------------- 1 | # This file keeps the debates directory in git while ignoring its contents -------------------------------------------------------------------------------- /ui/__init__.py: -------------------------------------------------------------------------------- 1 | """User interface components for the ASS debate system.""" 2 | 3 | from .prompts import PromptHandler 4 | from .rich_formatter import RichFormatter 5 | 6 | __all__ = [ 7 | "RichFormatter", 8 | "PromptHandler", 9 | ] -------------------------------------------------------------------------------- /services/__init__.py: -------------------------------------------------------------------------------- 1 | """Business logic services for the ASS debate system.""" 2 | 3 | from .debate_manager import DebateManager 4 | from .file_manager import FileManager 5 | 6 | __all__ = [ 7 | "DebateManager", 8 | "FileManager", 9 | ] -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | profile = black 3 | line_length = 120 4 | multi_line_output = 3 5 | include_trailing_comma = true 6 | force_grid_wrap = 0 7 | use_parentheses = true 8 | ensure_newline_before_comments = true 9 | known_first_party = models,personalities,services,ui 10 | sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Copy this file to .env and fill in your API keys 2 | # Never commit the .env file to version control! 3 | 4 | # Get your Claude API key from: https://console.anthropic.com/ 5 | CLAUDE_API_KEY=your_claude_api_key_here 6 | 7 | # Get your OpenAI API key from: https://platform.openai.com/api-keys 8 | OPENAI_API_KEY=your_openai_api_key_here -------------------------------------------------------------------------------- /personalities/__init__.py: -------------------------------------------------------------------------------- 1 | """AI personality implementations for the ASS debate system.""" 2 | 3 | from .base import LLMPersonality 4 | from .claude import ClaudePersonality 5 | from .factory import create_personality 6 | from .local import LocalModelPersonality 7 | from .openai import OpenAIPersonality 8 | 9 | __all__ = [ 10 | "LLMPersonality", 11 | "ClaudePersonality", 12 | "OpenAIPersonality", 13 | "LocalModelPersonality", 14 | "create_personality", 15 | ] -------------------------------------------------------------------------------- /sample_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "voting_enabled": true, 3 | "consensus_threshold": 0.75, 4 | "min_iterations": 2, 5 | "max_iterations": 10, 6 | "scoring_system": { 7 | "1": 4, 8 | "2": 3, 9 | "3": 2, 10 | "4": 1 11 | }, 12 | "judge_can_override": true, 13 | "override_threshold": 0.9, 14 | "allow_local_models": true, 15 | "local_model_timeout": 30, 16 | "classic_mode": false, 17 | "default_voting_traits": { 18 | "fairness": 7, 19 | "self_confidence": 5 20 | } 21 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "ass" 3 | version = "0.1.0" 4 | description = "ASS - Argumentative System Service: Watch AI personalities debate any topic" 5 | readme = "README.md" 6 | requires-python = ">=3.9" 7 | dependencies = [ 8 | "anthropic>=0.54.0", 9 | "openai>=1.86.0", 10 | "python-dotenv>=1.1.0", 11 | "rich>=14.0.0", 12 | "requests>=2.31.0", 13 | "pydantic>=2.11.6", 14 | ] 15 | 16 | [dependency-groups] 17 | dev = [ 18 | "autoflake>=2.3.1", 19 | "isort>=6.0.1", 20 | "pre-commit>=4.2.0", 21 | "pylint>=3.3.7", 22 | ] 23 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | """Pydantic models for the ASS debate system.""" 2 | 3 | from .arguments import Argument, ArgumentHistory, DebateContext 4 | from .debate import DebateConfig, DebateIteration, DebateState 5 | from .personality import InternalBelief, PersonalityConfig, PersonalityTraits 6 | from .voting import Vote, VotingConfig, VotingResult 7 | 8 | __all__ = [ 9 | "Vote", 10 | "VotingConfig", 11 | "VotingResult", 12 | "PersonalityConfig", 13 | "PersonalityTraits", 14 | "InternalBelief", 15 | "DebateConfig", 16 | "DebateState", 17 | "DebateIteration", 18 | "Argument", 19 | "ArgumentHistory", 20 | "DebateContext", 21 | ] -------------------------------------------------------------------------------- /personality.py: -------------------------------------------------------------------------------- 1 | """ 2 | Backward compatibility module for personality imports. 3 | The actual implementations have been moved to the personalities/ directory. 4 | """ 5 | 6 | # Import PersonalityConfig from models 7 | from models.personality import PersonalityConfig 8 | 9 | # Import everything from the new location for backward compatibility 10 | from personalities import ( 11 | ClaudePersonality, 12 | LLMPersonality, 13 | LocalModelPersonality, 14 | OpenAIPersonality, 15 | create_personality, 16 | ) 17 | 18 | # For complete backward compatibility 19 | __all__ = [ 20 | "PersonalityConfig", 21 | "LLMPersonality", 22 | "ClaudePersonality", 23 | "OpenAIPersonality", 24 | "LocalModelPersonality", 25 | "create_personality" 26 | ] -------------------------------------------------------------------------------- /personalities/factory.py: -------------------------------------------------------------------------------- 1 | """Factory function for creating personality instances.""" 2 | 3 | 4 | from models.personality import PersonalityConfig 5 | 6 | from .base import LLMPersonality 7 | from .claude import ClaudePersonality 8 | from .local import LocalModelPersonality 9 | from .openai import OpenAIPersonality 10 | 11 | 12 | def create_personality(config: PersonalityConfig) -> LLMPersonality: 13 | """Create a personality instance based on the configuration. 14 | 15 | Args: 16 | config: PersonalityConfig with model provider and settings 17 | 18 | Returns: 19 | LLMPersonality instance 20 | 21 | Raises: 22 | ValueError: If model provider is not supported 23 | """ 24 | if config.model_provider == "claude": 25 | return ClaudePersonality(config) 26 | elif config.model_provider == "openai": 27 | return OpenAIPersonality(config) 28 | elif config.model_provider == "local": 29 | return LocalModelPersonality(config) 30 | else: 31 | raise ValueError(f"Unknown model provider: {config.model_provider}") -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Diogo Neves 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /test_interactive.md: -------------------------------------------------------------------------------- 1 | # Testing the Interactive Application 2 | 3 | ## What we've verified: 4 | 5 | 1. **Classic Mode ✅**: Works perfectly as before with 3 fixed rounds 6 | 2. **Voting Mode ✅**: Works with automatic progression in demo mode 7 | 3. **Demo Mode ✅**: Successfully skips user input when `demo_mode = True` 8 | 9 | ## To test the interactive version: 10 | 11 | Run this in your terminal: 12 | 13 | ```bash 14 | # Test voting mode (default) 15 | uv run python debate_app.py 16 | 17 | # Test classic mode 18 | uv run python debate_app.py --classic-mode 19 | 20 | # Test with custom settings 21 | uv run python debate_app.py --voting-threshold 0.6 --max-iterations 4 22 | 23 | # Test with local model (if you have one running) 24 | uv run python debate_app.py --local-model-url http://localhost:8080 25 | ``` 26 | 27 | ## Features working: 28 | 29 | - ✅ Voting system with ranked-choice scoring 30 | - ✅ Dynamic iterations until consensus 31 | - ✅ First iteration shows initial positions 32 | - ✅ Subsequent iterations require argumentation 33 | - ✅ Visual voting results with tables 34 | - ✅ Judge reviews entire debate with voting results 35 | - ✅ Demo mode for non-interactive testing 36 | - ✅ Backward compatibility with classic mode 37 | 38 | The application is fully functional and ready for use! -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment variables - NEVER commit API keys! 2 | .env 3 | .env.local 4 | .env.*.local 5 | 6 | # Python-generated files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | *.so 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Virtual environments 54 | .env/ 55 | .venv/ 56 | ENV/ 57 | env/ 58 | venv/ 59 | 60 | # IDEs 61 | .vscode/ 62 | .idea/ 63 | *.swp 64 | *.swo 65 | *~ 66 | 67 | # OS 68 | .DS_Store 69 | .DS_Store? 70 | ._* 71 | .Spotlight-V100 72 | .Trashes 73 | ehthumbs.db 74 | Thumbs.db 75 | 76 | # UV specific 77 | .uv/ 78 | 79 | # Logs 80 | *.log 81 | 82 | # API responses cache (if we add caching) 83 | .cache/ 84 | cache/ 85 | 86 | # Debate save files 87 | debates/* 88 | !debates/.gitkeep 89 | 90 | # Development reports 91 | PYLINT_REPORT.md 92 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | repos: 3 | # Import sorting 4 | - repo: https://github.com/pycqa/isort 5 | rev: 6.0.1 6 | hooks: 7 | - id: isort 8 | args: ["--profile", "black", "--line-length", "120"] 9 | 10 | # Remove unused imports 11 | - repo: https://github.com/pycqa/autoflake 12 | rev: v2.3.1 13 | hooks: 14 | - id: autoflake 15 | args: 16 | - --remove-all-unused-imports 17 | - --remove-unused-variables 18 | - --in-place 19 | 20 | # Trailing whitespace and file fixes 21 | - repo: https://github.com/pre-commit/pre-commit-hooks 22 | rev: v4.5.0 23 | hooks: 24 | - id: trailing-whitespace 25 | - id: end-of-file-fixer 26 | - id: check-yaml 27 | - id: check-json 28 | - id: check-added-large-files 29 | args: ['--maxkb=500'] 30 | - id: check-merge-conflict 31 | - id: mixed-line-ending 32 | args: ['--fix=lf'] 33 | 34 | # Optional: Add black formatter 35 | # - repo: https://github.com/psf/black 36 | # rev: 23.12.1 37 | # hooks: 38 | # - id: black 39 | # language_version: python3.9 40 | # args: ["--line-length", "120"] 41 | 42 | # Note: pylint is excluded from pre-commit as it's slow 43 | # Run it separately with: uv run pylint *.py -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | # Specify a score threshold to be exceeded before program exits with error 3 | fail-under=7.0 4 | 5 | # Add files or directories to the blacklist 6 | ignore=.venv,venv,ENV,env,.git,__pycache__,debates 7 | 8 | [MESSAGES CONTROL] 9 | # Disable some messages that might be too strict for this project 10 | disable= 11 | missing-module-docstring, 12 | missing-class-docstring, 13 | missing-function-docstring, 14 | too-many-arguments, 15 | too-many-locals, 16 | too-many-branches, 17 | too-many-statements, 18 | too-many-instance-attributes, 19 | too-few-public-methods, 20 | invalid-name, # We have some unconventional names like "ASS" 21 | line-too-long, # Rich formatting can make lines long 22 | import-outside-toplevel, # We do some conditional imports 23 | 24 | [FORMAT] 25 | # Maximum number of characters on a single line. 26 | max-line-length=120 27 | 28 | [BASIC] 29 | # Good variable names which should always be accepted 30 | good-names=i,j,k,v,e,f,_,id,ok 31 | 32 | [DESIGN] 33 | # Maximum number of arguments for function / method 34 | max-args=10 35 | 36 | # Maximum number of attributes for a class 37 | max-attributes=20 38 | 39 | [IMPORTS] 40 | # List of modules that can be imported at any level 41 | allow-any-import-level= 42 | 43 | [TYPECHECK] 44 | # List of class names for which member attributes should not be checked 45 | ignored-classes=pydantic.Field,pydantic.BaseModel,pydantic.fields.FieldInfo 46 | 47 | # List of module names for which member attributes should not be checked 48 | ignored-modules=pydantic -------------------------------------------------------------------------------- /VOTING-FEATURE.md: -------------------------------------------------------------------------------- 1 | Extend the existing AI personality debate project to include a preferential voting system with the following requirements: 2 | 3 | VOTING MECHANISM: 4 | - Replace fixed iterations with consensus-driven stopping 5 | - At the end of each iteration, each AI personality ranks ALL participants (including themselves) from best to worst based on answer quality 6 | - Use ranked-choice voting where ranks are converted to points (e.g., 1st place = N points, 2nd = N-1 points, etc.) 7 | - Continue iterations until a configurable point threshold is reached indicating consensus 8 | 9 | DEBATE DYNAMICS: 10 | - Each iteration concludes when all personalities have answered and argued 11 | - Personalities should actively debate and challenge each other's points unless already in agreement 12 | - Personalities can change their minds only when presented with truly convincing arguments 13 | - Some personalities may agree with each other from the start 14 | 15 | CONSENSUS & TERMINATION: 16 | - Stop when the voting reaches a configurable agreement threshold (measured in total points) 17 | - After consensus, a judge reviews the entire debate history, all arguments, and final vote rankings 18 | - Judge can override consensus only in extreme cases and must provide detailed reasoning 19 | 20 | CONFIGURATION NEEDS: 21 | - Configurable point threshold for consensus 22 | - Configurable scoring system for ranked votes 23 | - Judge override conditions and logging 24 | 25 | UPDATE REQUIREMENTS: 26 | - Modify existing personality prompts to include voting behavior 27 | - Add voting instructions and ranking criteria 28 | - Update the debate flow to include ranking phases 29 | - Implement the judge review system 30 | 31 | Please analyze the existing codebase structure and implement these changes while maintaining backward compatibility where possible. -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | 5 | from config import DebateConfig 6 | from debate_app import DebateApp 7 | 8 | 9 | def main(): 10 | # Check if voting mode is requested 11 | voting_mode = "--voting" in sys.argv 12 | classic_mode = "--classic" in sys.argv 13 | 14 | # Configure the demo 15 | config = DebateConfig() 16 | 17 | if classic_mode: 18 | config.classic_mode = True 19 | config.voting_enabled = False 20 | print("Running demo in CLASSIC mode (3 fixed rounds)") 21 | elif voting_mode: 22 | config.voting_enabled = True 23 | config.classic_mode = False 24 | config.consensus_threshold = 0.70 # Lower threshold for demo 25 | config.min_iterations = 2 26 | config.max_iterations = 5 # Limit for demo 27 | print("Running demo in VOTING mode (consensus-based)") 28 | else: 29 | # Default to voting mode for demo 30 | config.voting_enabled = True 31 | config.classic_mode = False 32 | config.consensus_threshold = 0.70 33 | config.min_iterations = 2 34 | config.max_iterations = 5 35 | print("Running demo in VOTING mode (use --classic for original format)") 36 | 37 | app = DebateApp(config) 38 | 39 | # Demo questions based on mode 40 | if voting_mode or (not classic_mode and not voting_mode): 41 | # Voting mode - use a question that might reach consensus 42 | question = "Should companies allow employees to work from home permanently?" 43 | else: 44 | # Classic mode - use original demo question 45 | question = "Should we invest more in renewable energy or nuclear power?" 46 | 47 | # Set demo mode flag to skip user input 48 | app.demo_mode = True 49 | 50 | app.display_header() 51 | print(f"\n[Demo Mode] Automatically debating: {question}\n") 52 | 53 | if config.voting_enabled and not config.classic_mode: 54 | print("[yellow]Note: In interactive mode, you would press Enter between iterations.[/yellow]") 55 | print("[yellow]This demo will automatically proceed through iterations.[/yellow]\n") 56 | 57 | app.run_debate(question) 58 | 59 | print("\n[Demo Complete]") 60 | if config.voting_enabled: 61 | print("The debate continued until consensus was reached through voting.") 62 | print("Try running with --classic to see the original 3-round format.") 63 | else: 64 | print("The debate ran for exactly 3 rounds as in the original format.") 65 | print("Try running with --voting to see the consensus-based format.") 66 | 67 | if __name__ == "__main__": 68 | main() -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ASS 2 | 3 | Thank you for your interest in contributing to ASS (Argumentative System Service)! We welcome all kinds of contributions. 4 | 5 | ## 🚀 Quick Start 6 | 7 | 1. **Fork the repository** 8 | 2. **Clone your fork:** 9 | ```bash 10 | git clone https://github.com/yourusername/ass.git 11 | cd ass 12 | ``` 13 | 3. **Set up development environment:** 14 | ```bash 15 | uv sync 16 | cp .env.example .env 17 | # Add your API keys to .env 18 | ``` 19 | 20 | ## 🎭 Types of Contributions 21 | 22 | ### 🤖 New Personalities 23 | Add personalities with unique traits and perspectives: 24 | - Scientists, artists, philosophers, comedians 25 | - Different cultural perspectives 26 | - Specialized domain experts 27 | 28 | ### 🎪 Enhanced Debate Formats 29 | - Tournament brackets 30 | - Team debates (2v2) 31 | - Audience voting 32 | - Time-limited rapid-fire rounds 33 | 34 | ### 🔌 Additional LLM Providers 35 | - Gemini 36 | - Claude-3 Opus 37 | - Open-source models (Llama, Mistral) 38 | 39 | ### 🎨 UI/UX Improvements 40 | - Better visualizations 41 | - Export debate transcripts 42 | - Save/replay debates 43 | - Web interface 44 | 45 | ## 🛠️ Development Guidelines 46 | 47 | ### Code Style 48 | - Follow PEP 8 49 | - Use type hints where possible 50 | - Keep functions focused and small 51 | - Add docstrings to public functions 52 | 53 | ### Testing 54 | ```bash 55 | # Run existing tests 56 | uv run pytest 57 | 58 | # Add tests for new personalities 59 | # Add integration tests for new features 60 | ``` 61 | 62 | ### Documentation 63 | - Update README.md for new features 64 | - Add docstrings to new functions 65 | - Include example usage 66 | 67 | ## 🎯 Submitting Changes 68 | 69 | Keep each change small and easy to review. 70 | 71 | 1. **Create a branch:** `git checkout -b feature/my-feature` 72 | 2. **Make your changes** 73 | 3. **Test thoroughly** 74 | 4. **Commit with clear message:** `git commit -m "Add economist personality"` 75 | 5. **Push:** `git push origin feature/my-feature` 76 | 6. **Create Pull Request** 77 | 78 | ### Pull Request Guidelines 79 | - Clear title describing the change 80 | - Detailed description of what was added/changed 81 | - Screenshots/examples if relevant 82 | - Link any related issues 83 | 84 | ## 🔒 Security 85 | 86 | - **NEVER commit API keys or secrets** 87 | - Use `.env` for sensitive data 88 | - Validate all user inputs 89 | - Follow responsible AI practices 90 | 91 | ## 🐛 Bug Reports 92 | 93 | Use GitHub Issues with: 94 | - Clear description 95 | - Steps to reproduce 96 | - Expected vs actual behavior 97 | - Environment details (Python version, OS, etc.) 98 | 99 | ## 💡 Feature Requests 100 | 101 | We love new ideas! Please include: 102 | - Clear description of the feature 103 | - Why it would be useful 104 | - Potential implementation approach 105 | 106 | ## 🎉 Recognition 107 | 108 | Contributors will be: 109 | - Added to README acknowledgments 110 | - Credited in release notes 111 | - Invited to join the maintainer team (for significant contributions) 112 | 113 | ## 📝 Code of Conduct 114 | 115 | - Be respectful and inclusive 116 | - Focus on constructive feedback 117 | - Help newcomers get started 118 | - Keep discussions on-topic 119 | 120 | ## ❓ Questions? 121 | 122 | - Open a GitHub Issue 123 | - Start a Discussion 124 | - Reach out to maintainers 125 | 126 | Thank you for helping make ASS better! 🎭 -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from dataclasses import dataclass, field 4 | from typing import Dict, Optional 5 | 6 | from dotenv import load_dotenv 7 | 8 | load_dotenv() 9 | 10 | 11 | @dataclass 12 | class DebateConfig: 13 | """Configuration for the debate system.""" 14 | 15 | # Voting configuration 16 | voting_enabled: bool = True 17 | consensus_threshold: float = 0.75 # 75% of max points needed 18 | min_iterations: int = 2 # Minimum rounds before voting (deprecated, use voting_start_iteration) 19 | voting_start_iteration: int = 2 # Which iteration voting starts (0-indexed, so 2 means 3rd iteration) 20 | max_iterations: int = 10 # Maximum rounds to prevent infinite loops 21 | 22 | # Scoring system - rank position to points 23 | scoring_system: Dict[int, int] = field(default_factory=lambda: {1: 4, 2: 3, 3: 2, 4: 1}) 24 | 25 | # Judge configuration 26 | judge_can_override: bool = True 27 | override_threshold: float = 0.9 # Judge needs 90% conviction to override 28 | 29 | # Model configuration 30 | allow_local_models: bool = True 31 | local_model_timeout: int = 30 32 | 33 | # Classic mode (3 rounds, no voting) 34 | classic_mode: bool = False 35 | 36 | # File saving 37 | save_enabled: bool = True 38 | 39 | # Personality voting traits 40 | default_voting_traits: Dict[str, int] = field( 41 | default_factory=lambda: {"fairness": 7, "self_confidence": 5} 42 | ) 43 | 44 | @classmethod 45 | def from_file(cls, config_path: str) -> "DebateConfig": 46 | """Load configuration from a JSON file.""" 47 | with open(config_path, 'r') as f: 48 | config_data = json.load(f) 49 | return cls(**config_data) 50 | 51 | @classmethod 52 | def from_env(cls) -> "DebateConfig": 53 | """Load configuration from environment variables.""" 54 | config = cls() 55 | 56 | # Override with environment variables if they exist 57 | if os.getenv("DEBATE_VOTING_ENABLED") is not None: 58 | config.voting_enabled = os.getenv("DEBATE_VOTING_ENABLED").lower() == "true" 59 | 60 | if os.getenv("DEBATE_CONSENSUS_THRESHOLD"): 61 | config.consensus_threshold = float(os.getenv("DEBATE_CONSENSUS_THRESHOLD")) 62 | 63 | if os.getenv("DEBATE_MAX_ITERATIONS"): 64 | config.max_iterations = int(os.getenv("DEBATE_MAX_ITERATIONS")) 65 | 66 | if os.getenv("DEBATE_CLASSIC_MODE"): 67 | config.classic_mode = os.getenv("DEBATE_CLASSIC_MODE").lower() == "true" 68 | 69 | if os.getenv("LOCAL_MODEL_URL"): 70 | config.allow_local_models = True 71 | 72 | if os.getenv("DEBATE_SAVE_ENABLED") is not None: 73 | config.save_enabled = os.getenv("DEBATE_SAVE_ENABLED").lower() == "true" 74 | 75 | return config 76 | 77 | def to_dict(self) -> Dict: 78 | """Convert configuration to dictionary.""" 79 | return { 80 | "voting_enabled": self.voting_enabled, 81 | "consensus_threshold": self.consensus_threshold, 82 | "min_iterations": self.min_iterations, 83 | "max_iterations": self.max_iterations, 84 | "scoring_system": self.scoring_system, 85 | "judge_can_override": self.judge_can_override, 86 | "override_threshold": self.override_threshold, 87 | "allow_local_models": self.allow_local_models, 88 | "local_model_timeout": self.local_model_timeout, 89 | "classic_mode": self.classic_mode, 90 | "default_voting_traits": self.default_voting_traits, 91 | "save_enabled": self.save_enabled, 92 | "voting_start_iteration": self.voting_start_iteration 93 | } 94 | 95 | def save_to_file(self, config_path: str): 96 | """Save configuration to a JSON file.""" 97 | with open(config_path, 'w') as f: 98 | json.dump(self.to_dict(), f, indent=2) -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # Claude AI Assistant Guide for ASS Project 2 | 3 | ## Project Overview 4 | ASS (Argumentative System Service) is a Python CLI application that simulates AI debates between four distinct personalities using Claude and OpenAI models. This guide helps Claude understand the project structure and conventions for effective assistance. 5 | 6 | ## Quick Context 7 | - **Language**: Python 3.9+ 8 | - **Package Manager**: UV (modern Python package/project manager) 9 | - **Key Dependencies**: anthropic, openai, rich, python-dotenv 10 | - **Main Branch**: main (currently on: voting) 11 | - **License**: MIT 12 | 13 | ## Key Files to Know 14 | - `debate_app.py` - Main interactive application entry point 15 | - `models/` - Pydantic models with validation (PersonalityConfig, Vote, DebateConfig, etc.) 16 | - `personalities/` - AI personality implementations (base class, Claude, OpenAI, local) 17 | - `services/` - Business logic (DebateManager, FileManager) 18 | - `ui/` - User interface components (RichFormatter, PromptHandler) 19 | - `personality.py` - Backward compatibility imports 20 | - `demo.py` - Simple demo runner for quick testing 21 | - `pyproject.toml` - Project configuration and dependencies 22 | 23 | ## Development Commands 24 | Please commit between phases. 25 | 26 | ```bash 27 | # Install/update dependencies 28 | uv sync 29 | 30 | # Run interactive debate 31 | uv run python debate_app.py 32 | 33 | # Run demo 34 | uv run python demo.py 35 | 36 | # Add new dependency 37 | uv add 38 | 39 | # Code quality checks 40 | uv run pylint *.py 41 | uv run isort . --check-only 42 | uv run autoflake --check -r . 43 | 44 | # Fix imports automatically 45 | uv run isort . 46 | ``` 47 | 48 | ## Code Conventions 49 | 1. **Personality System**: Use abstract base class pattern, implement `create_personality()` factory 50 | 2. **Configuration**: Use Pydantic models for validation (PersonalityConfig, DebateConfig, Vote, etc.) 51 | 3. **UI**: Use Rich library for terminal output (panels, colors, animations) 52 | 4. **Error Handling**: Graceful handling of API errors with user-friendly messages 53 | 5. **Environment**: API keys in `.env` file (never commit!) 54 | 6. **Imports**: Use isort for consistent import ordering (stdlib → third-party → local) 55 | 7. **Code Quality**: Run pylint regularly, current baseline score ~6.6/10 56 | 57 | ## Architecture Patterns 58 | - **Factory Pattern**: For creating personalities based on configuration 59 | - **Context Accumulation**: Each personality maintains conversation history 60 | - **Provider Agnostic**: Support multiple LLM providers through abstract interfaces 61 | - **Structured Debate**: 3-round format (opening → rebuttals → final positions) 62 | - **Modular Design**: Clear separation between models/, services/, ui/, and personalities/ 63 | - **Pydantic Validation**: Runtime type checking and validation for all data structures 64 | 65 | ## Common Tasks 66 | ### Adding a New Personality Type 67 | 1. Create new class inheriting from `LLMPersonality` in `personalities/` 68 | 2. Implement required methods: `generate_response()`, `generate_vote()`, `generate_internal_belief()`, `update_beliefs()` 69 | 3. Update `create_personality()` factory function in `personalities/factory.py` 70 | 4. Add configuration using Pydantic `PersonalityConfig` model 71 | 72 | ### Modifying Debate Structure 73 | 1. Edit debate flow in `debate_app.py` 74 | 2. Update round structure in the main debate loop 75 | 3. Adjust judge synthesis logic if needed 76 | 77 | ### Testing Changes 78 | 1. Use `demo.py` for quick testing with pre-set questions 79 | 2. Run interactive mode to test user input handling 80 | 3. Check Rich formatting displays correctly in terminal 81 | 82 | ## Important Notes 83 | - Always use UV commands (not pip) for dependency management 84 | - Maintain compatibility with both Claude and OpenAI APIs 85 | - Keep personality responses distinct and in-character 86 | - Ensure Rich terminal UI remains clean and readable 87 | - Follow existing code style and patterns 88 | 89 | ## API Model Identifiers 90 | - Claude: "claude-3-5-sonnet-20241112" 91 | - OpenAI: "gpt-4o-20250117" 92 | 93 | ## Project Philosophy 94 | This project demonstrates creative AI integration while maintaining clean, extensible code. It's both a functional debate simulator and a showcase of modern Python development practices with AI APIs. 95 | 96 | ## Development Tools 97 | - Use pylint 98 | 99 | ## Claude Memories 100 | - you're awesome! -------------------------------------------------------------------------------- /IMPLEMENTATION_SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Voting System Implementation Summary 2 | 3 | ## Overview 4 | 5 | Successfully implemented a preferential voting system for the AI debate platform that replaces fixed iterations with consensus-driven stopping. The system now supports dynamic debates where personalities argue until reaching agreement through ranked-choice voting. 6 | 7 | ## Key Features Implemented 8 | 9 | ### 1. Voting System (`voting.py`) 10 | - **VotingConfig**: Configurable thresholds, scoring systems, and iteration limits 11 | - **Vote**: Structured votes with rankings and reasoning 12 | - **VotingSystem**: Manages voting rounds, calculates scores, and determines consensus 13 | - Ranked-choice voting with configurable point values 14 | - Voting history tracking and trend analysis 15 | 16 | ### 2. Enhanced Personality System (`personality.py`) 17 | - Added iteration-aware responses: 18 | - Iteration 0: Initial positions without argumentation 19 | - Iteration 1+: Direct engagement with other personalities' arguments 20 | - Implemented `generate_vote()` method for all personality types 21 | - Added voting traits (fairness, self-confidence) affecting ranking behavior 22 | - **LocalModelPersonality**: Support for OpenAI-compatible local model servers 23 | 24 | ### 3. Dynamic Debate Flow (`debate_app.py`) 25 | - Replaced fixed 3-round structure with consensus-based iterations 26 | - Real-time context sharing - personalities see arguments as they're made 27 | - Voting phase after minimum iterations with visual results 28 | - Comprehensive judge review with potential override capability 29 | - Backward compatibility with classic mode 30 | 31 | ### 4. Configuration System (`config.py`) 32 | - Centralized configuration management 33 | - Support for JSON files, environment variables, and CLI arguments 34 | - Flexible scoring systems and thresholds 35 | - Local model configuration options 36 | 37 | ### 5. CLI Enhancements 38 | - `--voting-threshold`: Set consensus percentage 39 | - `--max-iterations`: Limit debate length 40 | - `--min-iterations`: Minimum rounds before voting 41 | - `--local-model-url`: Connect to local models 42 | - `--classic-mode`: Preserve original behavior 43 | - `--config`: Load from configuration file 44 | 45 | ## Technical Details 46 | 47 | ### Voting Algorithm 48 | - Each participant ranks all others from best to worst 49 | - Rankings converted to points (default: 1st=4pts, 2nd=3pts, etc.) 50 | - Consensus reached when top participant has ≥75% of maximum possible points 51 | - Judge reviews entire debate history and can override with detailed reasoning 52 | 53 | ### Argumentation Requirements 54 | - First iteration: Present initial stance only 55 | - Subsequent iterations: Must address at least 2 other participants 56 | - Context includes all previous iterations plus current round 57 | - Personalities can change their minds based on compelling arguments 58 | 59 | ### Local Model Support 60 | - OpenAI-compatible API format 61 | - Automatic connection testing on initialization 62 | - Configurable timeouts and authentication 63 | - Fallback to original providers if local unavailable 64 | 65 | ## Usage Examples 66 | 67 | ### Basic Voting Debate 68 | ```bash 69 | uv run python debate_app.py 70 | # Personalities debate until consensus is reached 71 | ``` 72 | 73 | ### Custom Configuration 74 | ```bash 75 | # Higher consensus threshold 76 | uv run python debate_app.py --voting-threshold 0.9 77 | 78 | # Using local model 79 | uv run python debate_app.py --local-model-url http://localhost:8080 80 | 81 | # Classic 3-round mode 82 | uv run python debate_app.py --classic-mode 83 | ``` 84 | 85 | ### Configuration File 86 | ```json 87 | { 88 | "voting_enabled": true, 89 | "consensus_threshold": 0.75, 90 | "min_iterations": 2, 91 | "max_iterations": 10, 92 | "scoring_system": {"1": 4, "2": 3, "3": 2, "4": 1} 93 | } 94 | ``` 95 | 96 | ## Testing 97 | 98 | Created comprehensive test suite (`test_voting.py` - now removed) that verified: 99 | - Voting calculations and consensus detection 100 | - Configuration loading and saving 101 | - Voting trend tracking 102 | - Multi-round voting with changing opinions 103 | 104 | All tests passed successfully. 105 | 106 | ## Backward Compatibility 107 | 108 | - Original 3-round format preserved with `--classic-mode` 109 | - Existing demo.py updated to support both modes 110 | - No breaking changes to existing functionality 111 | 112 | ## Future Enhancements 113 | 114 | - Alternative voting methods (approval, Condorcet) 115 | - Voting visualization and analytics 116 | - Debate history persistence 117 | - Web interface for real-time viewing 118 | - Multi-party debates (>4 participants) 119 | - Team-based debates with coalitions -------------------------------------------------------------------------------- /models/voting.py: -------------------------------------------------------------------------------- 1 | """Voting-related Pydantic models with validation.""" 2 | 3 | from datetime import datetime 4 | from typing import Dict, List, Optional, Tuple 5 | 6 | from pydantic import BaseModel, Field, field_validator, model_validator 7 | 8 | 9 | class Vote(BaseModel): 10 | """Represents a single vote from a personality.""" 11 | voter: str = Field(..., min_length=1, description="Name of the voter") 12 | rankings: List[str] = Field(..., description="Ordered list from best to worst") 13 | reasoning: str = Field(..., min_length=1, description="Explanation for the rankings") 14 | iteration: int = Field(..., ge=0, description="Voting iteration number") 15 | 16 | @field_validator('rankings') 17 | @classmethod 18 | def validate_rankings(cls, v: List[str]) -> List[str]: 19 | """Ensure rankings have no duplicates and all entries are non-empty.""" 20 | if not v: 21 | raise ValueError("Rankings cannot be empty") 22 | 23 | if len(v) != len(set(v)): 24 | raise ValueError("Rankings cannot contain duplicates") 25 | 26 | for participant in v: 27 | if not participant or not participant.strip(): 28 | raise ValueError("Participant names cannot be empty") 29 | 30 | return v 31 | 32 | @model_validator(mode='after') 33 | def voter_in_rankings(self) -> 'Vote': 34 | """Ensure the voter is included in their own rankings.""" 35 | if self.voter not in self.rankings: 36 | raise ValueError(f"Voter '{self.voter}' must be included in their own rankings") 37 | return self 38 | 39 | 40 | class VotingConfig(BaseModel): 41 | """Configuration for the voting system.""" 42 | point_threshold: float = Field( 43 | default=0.75, 44 | ge=0.0, 45 | le=1.0, 46 | description="Percentage of max possible points for consensus" 47 | ) 48 | scoring_system: Dict[int, int] = Field( 49 | default_factory=lambda: {1: 4, 2: 3, 3: 2, 4: 1}, 50 | description="Rank position to points mapping" 51 | ) 52 | min_iterations: int = Field( 53 | default=2, 54 | ge=1, 55 | description="Minimum iterations before voting can start" 56 | ) 57 | max_iterations: int = Field( 58 | default=10, 59 | ge=1, 60 | le=50, 61 | description="Maximum iterations to prevent infinite loops" 62 | ) 63 | 64 | @field_validator('scoring_system') 65 | @classmethod 66 | def validate_scoring_system(cls, v: Dict[int, int]) -> Dict[int, int]: 67 | """Ensure scoring system has sequential keys starting from 1.""" 68 | if not v: 69 | raise ValueError("Scoring system cannot be empty") 70 | 71 | keys = sorted(v.keys()) 72 | expected_keys = list(range(1, len(keys) + 1)) 73 | 74 | if keys != expected_keys: 75 | raise ValueError(f"Scoring system must have sequential keys from 1 to {len(keys)}") 76 | 77 | # Ensure all values are positive 78 | for rank, points in v.items(): 79 | if points <= 0: 80 | raise ValueError(f"Points for rank {rank} must be positive") 81 | 82 | return v 83 | 84 | @model_validator(mode='after') 85 | def validate_iterations(self) -> 'VotingConfig': 86 | """Ensure min_iterations <= max_iterations.""" 87 | if self.min_iterations > self.max_iterations: 88 | raise ValueError("min_iterations cannot be greater than max_iterations") 89 | return self 90 | 91 | 92 | class VotingResult(BaseModel): 93 | """Result of a voting round.""" 94 | iteration: int = Field(..., ge=0) 95 | scores: Dict[str, int] = Field(..., description="Participant name to score mapping") 96 | sorted_rankings: List[Tuple[str, int]] = Field(..., description="Participants sorted by score") 97 | consensus_reached: bool = Field(..., description="Whether consensus was achieved") 98 | winner: Optional[str] = Field(None, description="Winner if consensus reached") 99 | threshold_score: float = Field(..., ge=0, description="Score needed for consensus") 100 | individual_votes: List[Vote] = Field(..., description="All votes cast this round") 101 | timestamp: datetime = Field(default_factory=datetime.now) 102 | 103 | @field_validator('sorted_rankings') 104 | @classmethod 105 | def validate_sorted_rankings(cls, v: List[Tuple[str, int]]) -> List[Tuple[str, int]]: 106 | """Ensure rankings are properly sorted by score (descending).""" 107 | if len(v) > 1: 108 | scores = [score for _, score in v] 109 | if scores != sorted(scores, reverse=True): 110 | raise ValueError("Rankings must be sorted by score in descending order") 111 | return v 112 | 113 | @model_validator(mode='after') 114 | def validate_winner_consensus(self) -> 'VotingResult': 115 | """Ensure winner is only set when consensus is reached.""" 116 | if self.consensus_reached and not self.winner: 117 | raise ValueError("Winner must be set when consensus is reached") 118 | if not self.consensus_reached and self.winner: 119 | raise ValueError("Winner cannot be set when consensus is not reached") 120 | return self -------------------------------------------------------------------------------- /personalities/base.py: -------------------------------------------------------------------------------- 1 | """Base class for AI personalities.""" 2 | 3 | import json 4 | import time 5 | from abc import ABC, abstractmethod 6 | from typing import Any, Dict, List, Optional 7 | 8 | from models.personality import PersonalityConfig 9 | 10 | 11 | class LLMPersonality(ABC): 12 | """Abstract base class for all AI personalities.""" 13 | 14 | def __init__(self, config: PersonalityConfig): 15 | self.config = config 16 | self.conversation_history: List[Dict[str, str]] = [] 17 | self.internal_beliefs: Dict[str, Any] = {} # Core beliefs about the topic 18 | self.belief_history: List[Dict[str, Any]] = [] # Track belief evolution 19 | self.current_question: Optional[str] = None 20 | 21 | @abstractmethod 22 | def generate_response(self, question: str, context: str = "", iteration: int = 0) -> str: 23 | """Generate a response to the debate question.""" 24 | pass 25 | 26 | @abstractmethod 27 | def generate_vote(self, participants: List[str], debate_context: str) -> Dict[str, Any]: 28 | """Generate rankings for all participants based on debate quality.""" 29 | pass 30 | 31 | @abstractmethod 32 | def generate_internal_belief(self, question: str) -> Dict[str, Any]: 33 | """Generate initial internal beliefs about the question.""" 34 | pass 35 | 36 | @abstractmethod 37 | def update_beliefs(self, arguments: str, iteration: int) -> bool: 38 | """Update internal beliefs based on arguments. Returns True if beliefs changed.""" 39 | pass 40 | 41 | def add_to_history(self, role: str, content: str): 42 | """Add an entry to conversation history.""" 43 | self.conversation_history.append({"role": role, "content": content}) 44 | 45 | def _should_update_belief(self, evidence_strength: int) -> bool: 46 | """Determine if beliefs should be updated based on evidence strength.""" 47 | # Higher belief_persistence requires stronger evidence 48 | threshold = self.config.belief_persistence * 10 49 | # Truth-seeking personalities have lower threshold 50 | threshold -= (self.config.truth_seeking * 5) 51 | return evidence_strength >= threshold 52 | 53 | def _save_belief_state(self, iteration: int): 54 | """Save current belief state to history.""" 55 | self.belief_history.append({ 56 | "iteration": iteration, 57 | "beliefs": self.internal_beliefs.copy(), 58 | "timestamp": time.time() 59 | }) 60 | 61 | def _build_iteration_prompt(self, iteration: int, question: str, context: str) -> str: 62 | """Build prompt based on iteration number.""" 63 | if iteration == 0: 64 | # First establish beliefs, then generate position 65 | return f"""Question: {question} 66 | 67 | Based on your personality and expertise, provide your STRONGEST initial position on this question. 68 | 69 | Your goal is to WIN this debate by: 70 | - Providing expert-level analysis that DEFEATS opposing viewpoints 71 | - Drawing from relevant fields to build an UNASSAILABLE argument 72 | - Being specific with principles and examples that PROVE your position 73 | - Establishing intellectual dominance from the start 74 | 75 | Remember: You're not here to explore ideas - you're here to CONVINCE others and WIN. 76 | Provide a powerful position that reflects {self.config.reasoning_depth}/10 depth of analysis.""" 77 | else: 78 | # Include internal beliefs in reasoning 79 | belief_context = "" 80 | if self.internal_beliefs: 81 | belief_context = f"\nYour current understanding: {json.dumps(self.internal_beliefs, indent=2)}\n" 82 | 83 | return f"""Question: {question} 84 | {belief_context} 85 | Current debate context: 86 | {context} 87 | 88 | This is iteration {iteration} of the debate. Your mission: 89 | 1. ATTACK at least 2 other participants' arguments - find their weaknesses and EXPLOIT them 90 | 2. If you disagree, DEMOLISH their position with superior evidence and reasoning 91 | 3. If you must agree, show how YOUR interpretation is BETTER and more complete 92 | 4. NEVER concede unless the evidence is overwhelming (belief persistence: {self.config.belief_persistence}/10) 93 | 5. Fight for your position - you're here to WIN, not to find compromise 94 | 95 | Remember: This is a COMPETITION. Others are trying to defeat your arguments. DEFEND your position and ATTACK theirs. 96 | Provide a response with {self.config.reasoning_depth}/10 depth that DOMINATES the debate.""" 97 | 98 | def _build_voting_prompt(self, participants: List[str], debate_context: str) -> str: 99 | """Build prompt for voting on debate performance.""" 100 | return f"""Based on the debate so far, rank all participants (including yourself) from best to worst based on: 101 | 1. Quality of arguments 102 | 2. Logic and reasoning 103 | 3. Ability to address others' points 104 | 4. Persuasiveness 105 | 106 | Participants: {', '.join(participants)} 107 | 108 | Debate context: 109 | {debate_context} 110 | 111 | Return your rankings as JSON in this format: 112 | {{ 113 | "rankings": ["participant1", "participant2", "participant3", "participant4"], 114 | "reasoning": "Brief explanation of your rankings" 115 | }} 116 | 117 | Be fair and objective, considering your personality traits: 118 | - Fairness level: {self.config.voting_traits.fairness}/10 119 | - Self-confidence: {self.config.voting_traits.self_confidence}/10""" -------------------------------------------------------------------------------- /personalities/claude.py: -------------------------------------------------------------------------------- 1 | """Claude AI personality implementation.""" 2 | 3 | import json 4 | import os 5 | from typing import Any, Dict, List 6 | 7 | import anthropic 8 | 9 | from models.personality import PersonalityConfig 10 | 11 | from .base import LLMPersonality 12 | 13 | 14 | class ClaudePersonality(LLMPersonality): 15 | """Claude-based personality implementation.""" 16 | 17 | def __init__(self, config: PersonalityConfig): 18 | super().__init__(config) 19 | self.client = anthropic.Anthropic(api_key=os.getenv("CLAUDE_API_KEY")) 20 | 21 | def generate_response(self, question: str, context: str = "", iteration: int = 0) -> str: 22 | """Generate a response using Claude.""" 23 | prompt = self._build_iteration_prompt(iteration, question, context) 24 | 25 | messages = [{"role": "user", "content": prompt}] 26 | 27 | response = self.client.messages.create( 28 | model=self.config.model_name, 29 | max_tokens=500, 30 | system=self.config.to_system_prompt(), 31 | messages=messages 32 | ) 33 | 34 | return response.content[0].text 35 | 36 | def generate_vote(self, participants: List[str], debate_context: str) -> Dict[str, Any]: 37 | """Generate vote rankings using Claude.""" 38 | vote_prompt = self._build_voting_prompt(participants, debate_context) 39 | 40 | messages = [{"role": "user", "content": vote_prompt}] 41 | 42 | response = self.client.messages.create( 43 | model=self.config.model_name, 44 | max_tokens=300, 45 | system="You are participating in a debate and must now rank all participants. Be objective but consider your personality traits.", 46 | messages=messages 47 | ) 48 | 49 | try: 50 | return json.loads(response.content[0].text) 51 | except json.JSONDecodeError: 52 | # Fallback if JSON parsing fails 53 | return { 54 | "rankings": participants, 55 | "reasoning": "Unable to parse vote" 56 | } 57 | 58 | def generate_internal_belief(self, question: str) -> Dict[str, Any]: 59 | """Generate initial internal beliefs about the question.""" 60 | belief_prompt = f"""Question: {question} 61 | 62 | As an expert with {self.config.reasoning_depth}/10 depth of analysis, establish your core beliefs about this topic. 63 | 64 | Analyze the question and provide your TRUE internal assessment in JSON format: 65 | {{ 66 | "core_position": "Your fundamental stance", 67 | "confidence_level": 1-10, 68 | "key_principles": ["principle1", "principle2", ...], 69 | "evidence_basis": ["evidence1", "evidence2", ...], 70 | "potential_weaknesses": ["weakness1", "weakness2", ...], 71 | "truth_assessment": "What you genuinely believe to be true" 72 | }} 73 | 74 | This is your INTERNAL belief state - be completely honest about what you think is true, regardless of your public personality traits.""" 75 | 76 | messages = [{"role": "user", "content": belief_prompt}] 77 | 78 | response = self.client.messages.create( 79 | model=self.config.model_name, 80 | max_tokens=400, 81 | system=f"You are an expert analyst with deep knowledge across multiple fields. Truth-seeking level: {self.config.truth_seeking}/10", 82 | messages=messages 83 | ) 84 | 85 | try: 86 | beliefs = json.loads(response.content[0].text) 87 | self.internal_beliefs = beliefs 88 | self.current_question = question 89 | self._save_belief_state(0) 90 | return beliefs 91 | except json.JSONDecodeError: 92 | # Fallback 93 | self.internal_beliefs = {"error": "Failed to parse beliefs"} 94 | return self.internal_beliefs 95 | 96 | def update_beliefs(self, arguments: str, iteration: int) -> bool: 97 | """Update internal beliefs based on arguments.""" 98 | update_prompt = f"""Current Question: {self.current_question} 99 | 100 | Your current internal beliefs: 101 | {json.dumps(self.internal_beliefs, indent=2)} 102 | 103 | New arguments presented: 104 | {arguments} 105 | 106 | Analyze these arguments as an expert with: 107 | - Reasoning depth: {self.config.reasoning_depth}/10 108 | - Truth-seeking: {self.config.truth_seeking}/10 109 | - Belief persistence: {self.config.belief_persistence}/10 110 | 111 | Respond in JSON format: 112 | {{ 113 | "evidence_strength": 1-100 (how compelling is the new evidence), 114 | "conflicts_identified": ["conflict1", "conflict2", ...], 115 | "should_update": true/false, 116 | "updated_beliefs": {{ 117 | // Only if should_update is true, provide updated belief structure 118 | "core_position": "...", 119 | "confidence_level": 1-10, 120 | "key_principles": [...], 121 | "evidence_basis": [...], 122 | "potential_weaknesses": [...], 123 | "truth_assessment": "..." 124 | }}, 125 | "reasoning": "Explanation of your decision" 126 | }}""" 127 | 128 | messages = [{"role": "user", "content": update_prompt}] 129 | 130 | response = self.client.messages.create( 131 | model=self.config.model_name, 132 | max_tokens=500, 133 | system="You are an expert analyst evaluating evidence to update your understanding of truth.", 134 | messages=messages 135 | ) 136 | 137 | try: 138 | result = json.loads(response.content[0].text) 139 | 140 | if result.get("should_update", False) and self._should_update_belief(result.get("evidence_strength", 0)): 141 | self.internal_beliefs = result["updated_beliefs"] 142 | self._save_belief_state(iteration) 143 | return True 144 | return False 145 | except json.JSONDecodeError: 146 | return False -------------------------------------------------------------------------------- /voting.py: -------------------------------------------------------------------------------- 1 | import json 2 | from collections import defaultdict 3 | from dataclasses import dataclass, field 4 | from typing import Dict, List, Optional, Tuple 5 | 6 | 7 | @dataclass 8 | class VotingConfig: 9 | """Configuration for the voting system.""" 10 | point_threshold: float = 0.75 # Percentage of max possible points for consensus 11 | scoring_system: Dict[int, int] = field(default_factory=lambda: {1: 4, 2: 3, 3: 2, 4: 1}) 12 | min_iterations: int = 2 # Minimum iterations before voting can start 13 | max_iterations: int = 10 # Maximum iterations to prevent infinite loops 14 | 15 | 16 | @dataclass 17 | class Vote: 18 | """Represents a single vote from a personality.""" 19 | voter: str 20 | rankings: List[str] # Ordered list from best to worst 21 | reasoning: str 22 | iteration: int 23 | 24 | 25 | class VotingSystem: 26 | """Manages the voting process and consensus determination.""" 27 | 28 | def __init__(self, config: VotingConfig, participants: List[str]): 29 | self.config = config 30 | self.participants = participants 31 | self.vote_history: List[List[Vote]] = [] 32 | self.max_possible_points = self._calculate_max_points() 33 | 34 | def _calculate_max_points(self) -> int: 35 | """Calculate maximum possible points a participant can receive.""" 36 | # If everyone ranks a participant first 37 | return len(self.participants) * self.config.scoring_system[1] 38 | 39 | def add_votes(self, votes: List[Vote]): 40 | """Add a round of votes to the history.""" 41 | self.vote_history.append(votes) 42 | 43 | def calculate_scores(self, votes: List[Vote]) -> Dict[str, int]: 44 | """Convert rankings to point scores.""" 45 | scores = defaultdict(int) 46 | 47 | for vote in votes: 48 | for rank, participant in enumerate(vote.rankings, 1): 49 | if rank in self.config.scoring_system: 50 | scores[participant] += self.config.scoring_system[rank] 51 | 52 | return dict(scores) 53 | 54 | def check_consensus(self, scores: Dict[str, int]) -> Tuple[bool, Optional[str]]: 55 | """ 56 | Check if consensus has been reached. 57 | Returns (consensus_reached, winner_name) 58 | """ 59 | if not scores: 60 | return False, None 61 | 62 | # Find the top scorer 63 | top_participant = max(scores.items(), key=lambda x: x[1]) 64 | top_score = top_participant[1] 65 | 66 | # Check if top score meets threshold 67 | threshold_score = self.max_possible_points * self.config.point_threshold 68 | consensus_reached = top_score >= threshold_score 69 | 70 | return consensus_reached, top_participant[0] if consensus_reached else None 71 | 72 | def get_vote_summary(self, iteration: int) -> Dict[str, any]: 73 | """Get a summary of voting results for a specific iteration.""" 74 | if iteration >= len(self.vote_history): 75 | return {} 76 | 77 | votes = self.vote_history[iteration] 78 | scores = self.calculate_scores(votes) 79 | consensus_reached, winner = self.check_consensus(scores) 80 | 81 | # Sort participants by score 82 | sorted_scores = sorted(scores.items(), key=lambda x: x[1], reverse=True) 83 | 84 | return { 85 | "iteration": iteration, 86 | "scores": scores, 87 | "sorted_rankings": sorted_scores, 88 | "consensus_reached": consensus_reached, 89 | "winner": winner, 90 | "threshold_score": self.max_possible_points * self.config.point_threshold, 91 | "individual_votes": [ 92 | { 93 | "voter": vote.voter, 94 | "rankings": vote.rankings, 95 | "reasoning": vote.reasoning 96 | } 97 | for vote in votes 98 | ] 99 | } 100 | 101 | def get_voting_trends(self) -> Dict[str, List[int]]: 102 | """Track how scores changed over iterations.""" 103 | trends = defaultdict(list) 104 | 105 | for iteration_votes in self.vote_history: 106 | scores = self.calculate_scores(iteration_votes) 107 | for participant in self.participants: 108 | trends[participant].append(scores.get(participant, 0)) 109 | 110 | return dict(trends) 111 | 112 | def format_vote_table(self, votes: List[Vote]) -> str: 113 | """Format votes as a readable table.""" 114 | lines = [] 115 | scores = self.calculate_scores(votes) 116 | 117 | lines.append("VOTING RESULTS") 118 | lines.append("=" * 50) 119 | 120 | # Individual votes 121 | for vote in votes: 122 | lines.append(f"\n{vote.voter}'s Rankings:") 123 | for rank, participant in enumerate(vote.rankings, 1): 124 | points = self.config.scoring_system.get(rank, 0) 125 | lines.append(f" {rank}. {participant} ({points} points)") 126 | if vote.reasoning: 127 | lines.append(f" Reasoning: {vote.reasoning}") 128 | 129 | # Total scores 130 | lines.append("\nTOTAL SCORES:") 131 | lines.append("-" * 30) 132 | sorted_scores = sorted(scores.items(), key=lambda x: x[1], reverse=True) 133 | for participant, score in sorted_scores: 134 | percentage = (score / self.max_possible_points) * 100 135 | lines.append(f"{participant}: {score} points ({percentage:.1f}% of max)") 136 | 137 | # Consensus status 138 | consensus, winner = self.check_consensus(scores) 139 | lines.append(f"\nConsensus threshold: {self.config.point_threshold * 100:.0f}% of max points") 140 | if consensus: 141 | lines.append(f"✓ CONSENSUS REACHED! Winner: {winner}") 142 | else: 143 | lines.append("✗ No consensus yet") 144 | 145 | return "\n".join(lines) -------------------------------------------------------------------------------- /personalities/openai.py: -------------------------------------------------------------------------------- 1 | """OpenAI personality implementation.""" 2 | 3 | import json 4 | import os 5 | from typing import Any, Dict, List 6 | 7 | import openai 8 | 9 | from models.personality import PersonalityConfig 10 | 11 | from .base import LLMPersonality 12 | 13 | 14 | class OpenAIPersonality(LLMPersonality): 15 | """OpenAI-based personality implementation.""" 16 | 17 | def __init__(self, config: PersonalityConfig): 18 | super().__init__(config) 19 | self.client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY")) 20 | 21 | def generate_response(self, question: str, context: str = "", iteration: int = 0) -> str: 22 | """Generate a response using OpenAI.""" 23 | prompt = self._build_iteration_prompt(iteration, question, context) 24 | 25 | messages = [ 26 | {"role": "system", "content": self.config.to_system_prompt()}, 27 | {"role": "user", "content": prompt} 28 | ] 29 | 30 | response = self.client.chat.completions.create( 31 | model=self.config.model_name, 32 | messages=messages, 33 | max_tokens=500 34 | ) 35 | 36 | return response.choices[0].message.content 37 | 38 | def generate_vote(self, participants: List[str], debate_context: str) -> Dict[str, Any]: 39 | """Generate vote rankings using OpenAI.""" 40 | vote_prompt = self._build_voting_prompt(participants, debate_context) 41 | 42 | messages = [ 43 | {"role": "system", "content": "You are participating in a debate and must now rank all participants. Be objective but consider your personality traits."}, 44 | {"role": "user", "content": vote_prompt} 45 | ] 46 | 47 | response = self.client.chat.completions.create( 48 | model=self.config.model_name, 49 | messages=messages, 50 | max_tokens=300, 51 | response_format={"type": "json_object"} 52 | ) 53 | 54 | try: 55 | return json.loads(response.choices[0].message.content) 56 | except json.JSONDecodeError: 57 | # Fallback if JSON parsing fails 58 | return { 59 | "rankings": participants, 60 | "reasoning": "Unable to parse vote" 61 | } 62 | 63 | def generate_internal_belief(self, question: str) -> Dict[str, Any]: 64 | """Generate initial internal beliefs about the question.""" 65 | belief_prompt = f"""Question: {question} 66 | 67 | As an expert with {self.config.reasoning_depth}/10 depth of analysis, establish your core beliefs about this topic. 68 | 69 | Analyze the question and provide your TRUE internal assessment in JSON format: 70 | {{ 71 | "core_position": "Your fundamental stance", 72 | "confidence_level": 1-10, 73 | "key_principles": ["principle1", "principle2", ...], 74 | "evidence_basis": ["evidence1", "evidence2", ...], 75 | "potential_weaknesses": ["weakness1", "weakness2", ...], 76 | "truth_assessment": "What you genuinely believe to be true" 77 | }} 78 | 79 | This is your INTERNAL belief state - be completely honest about what you think is true, regardless of your public personality traits.""" 80 | 81 | messages = [ 82 | {"role": "system", "content": f"You are an expert analyst with deep knowledge across multiple fields. Truth-seeking level: {self.config.truth_seeking}/10"}, 83 | {"role": "user", "content": belief_prompt} 84 | ] 85 | 86 | response = self.client.chat.completions.create( 87 | model=self.config.model_name, 88 | messages=messages, 89 | max_tokens=400, 90 | response_format={"type": "json_object"} 91 | ) 92 | 93 | try: 94 | beliefs = json.loads(response.choices[0].message.content) 95 | self.internal_beliefs = beliefs 96 | self.current_question = question 97 | self._save_belief_state(0) 98 | return beliefs 99 | except json.JSONDecodeError: 100 | # Fallback 101 | self.internal_beliefs = {"error": "Failed to parse beliefs"} 102 | return self.internal_beliefs 103 | 104 | def update_beliefs(self, arguments: str, iteration: int) -> bool: 105 | """Update internal beliefs based on arguments.""" 106 | update_prompt = f"""Current Question: {self.current_question} 107 | 108 | Your current internal beliefs: 109 | {json.dumps(self.internal_beliefs, indent=2)} 110 | 111 | New arguments presented: 112 | {arguments} 113 | 114 | Analyze these arguments as an expert with: 115 | - Reasoning depth: {self.config.reasoning_depth}/10 116 | - Truth-seeking: {self.config.truth_seeking}/10 117 | - Belief persistence: {self.config.belief_persistence}/10 118 | 119 | Respond in JSON format: 120 | {{ 121 | "evidence_strength": 1-100 (how compelling is the new evidence), 122 | "conflicts_identified": ["conflict1", "conflict2", ...], 123 | "should_update": true/false, 124 | "updated_beliefs": {{ 125 | // Only if should_update is true, provide updated belief structure 126 | "core_position": "...", 127 | "confidence_level": 1-10, 128 | "key_principles": [...], 129 | "evidence_basis": [...], 130 | "potential_weaknesses": [...], 131 | "truth_assessment": "..." 132 | }}, 133 | "reasoning": "Explanation of your decision" 134 | }}""" 135 | 136 | messages = [ 137 | {"role": "system", "content": "You are an expert analyst evaluating evidence to update your understanding of truth."}, 138 | {"role": "user", "content": update_prompt} 139 | ] 140 | 141 | response = self.client.chat.completions.create( 142 | model=self.config.model_name, 143 | messages=messages, 144 | max_tokens=500, 145 | response_format={"type": "json_object"} 146 | ) 147 | 148 | try: 149 | result = json.loads(response.choices[0].message.content) 150 | 151 | if result.get("should_update", False) and self._should_update_belief(result.get("evidence_strength", 0)): 152 | self.internal_beliefs = result["updated_beliefs"] 153 | self._save_belief_state(iteration) 154 | return True 155 | return False 156 | except json.JSONDecodeError: 157 | return False -------------------------------------------------------------------------------- /models/arguments.py: -------------------------------------------------------------------------------- 1 | """Argument-related Pydantic models with validation.""" 2 | 3 | from datetime import datetime 4 | from typing import Any, Dict, List, Optional 5 | 6 | from pydantic import BaseModel, Field, field_validator 7 | 8 | 9 | class Argument(BaseModel): 10 | """Represents a single argument in a debate.""" 11 | speaker: str = Field(..., min_length=1, description="Name of the speaker") 12 | content: str = Field(..., min_length=1, description="Argument content") 13 | iteration: int = Field(..., ge=0, description="Debate iteration number") 14 | timestamp: datetime = Field(default_factory=datetime.now) 15 | references: List[str] = Field( 16 | default_factory=list, 17 | description="References to other speakers' arguments" 18 | ) 19 | argument_type: str = Field( 20 | default="general", 21 | pattern="^(opening|rebuttal|closing|general)$", 22 | description="Type of argument" 23 | ) 24 | 25 | @field_validator('content') 26 | @classmethod 27 | def validate_content_length(cls, v: str) -> str: 28 | """Ensure argument has meaningful content.""" 29 | if len(v.strip()) < 10: 30 | raise ValueError("Argument content must be at least 10 characters") 31 | return v.strip() 32 | 33 | @field_validator('references') 34 | @classmethod 35 | def validate_references(cls, v: List[str]) -> List[str]: 36 | """Ensure references are non-empty strings.""" 37 | return [ref.strip() for ref in v if ref.strip()] 38 | 39 | def extract_key_points(self) -> List[str]: 40 | """Extract key points from the argument (placeholder for NLP).""" 41 | # Simple implementation - split by sentences 42 | sentences = self.content.split('.') 43 | return [s.strip() for s in sentences if len(s.strip()) > 20] 44 | 45 | 46 | class ArgumentHistory(BaseModel): 47 | """Tracks the history of arguments for a participant.""" 48 | participant: str = Field(..., min_length=1, description="Participant name") 49 | arguments: List[Argument] = Field( 50 | default_factory=list, 51 | description="Chronological list of arguments" 52 | ) 53 | total_word_count: int = Field(default=0, ge=0, description="Total words spoken") 54 | key_themes: List[str] = Field( 55 | default_factory=list, 56 | description="Key themes in arguments" 57 | ) 58 | stance_evolution: Dict[str, Any] = Field( 59 | default_factory=dict, 60 | description="How stance evolved over time" 61 | ) 62 | 63 | def add_argument(self, argument: Argument) -> None: 64 | """Add a new argument to the history.""" 65 | if argument.speaker != self.participant: 66 | raise ValueError( 67 | f"Argument speaker '{argument.speaker}' doesn't match " 68 | f"participant '{self.participant}'" 69 | ) 70 | 71 | self.arguments.append(argument) 72 | self.total_word_count += len(argument.content.split()) 73 | 74 | def get_arguments_by_type(self, arg_type: str) -> List[Argument]: 75 | """Get all arguments of a specific type.""" 76 | return [arg for arg in self.arguments if arg.argument_type == arg_type] 77 | 78 | def get_latest_position(self) -> Optional[str]: 79 | """Get the most recent stated position.""" 80 | if not self.arguments: 81 | return None 82 | return self.arguments[-1].content 83 | 84 | @field_validator('arguments') 85 | @classmethod 86 | def validate_arguments_consistency(cls, v: List[Argument]) -> List[Argument]: 87 | """Ensure arguments are from the same speaker.""" 88 | if not v: 89 | return v 90 | 91 | speakers = {arg.speaker for arg in v} 92 | if len(speakers) > 1: 93 | raise ValueError("All arguments must be from the same speaker") 94 | 95 | return v 96 | 97 | 98 | class DebateContext(BaseModel): 99 | """Context for generating new arguments.""" 100 | current_iteration: int = Field(..., ge=0, description="Current debate iteration") 101 | previous_arguments: Dict[str, List[Argument]] = Field( 102 | default_factory=dict, 103 | description="Previous arguments by participant" 104 | ) 105 | question: str = Field(..., min_length=1, description="Debate question") 106 | debate_phase: str = Field( 107 | default="main", 108 | pattern="^(opening|main|closing|voting)$", 109 | description="Current phase of debate" 110 | ) 111 | key_contentions: List[str] = Field( 112 | default_factory=list, 113 | description="Main points of contention" 114 | ) 115 | 116 | def get_recent_arguments(self, limit: int = 3) -> List[Argument]: 117 | """Get the most recent arguments from all participants.""" 118 | all_args = [] 119 | for participant_args in self.previous_arguments.values(): 120 | all_args.extend(participant_args) 121 | 122 | # Sort by timestamp and return most recent 123 | sorted_args = sorted(all_args, key=lambda x: x.timestamp, reverse=True) 124 | return sorted_args[:limit] 125 | 126 | def get_participant_history(self, participant: str) -> List[Argument]: 127 | """Get all arguments from a specific participant.""" 128 | return self.previous_arguments.get(participant, []) 129 | 130 | def summarize_for_participant(self, participant: str) -> str: 131 | """Create a summary of the debate for a participant.""" 132 | summary_parts = [f"Debate Question: {self.question}"] 133 | summary_parts.append(f"Current Phase: {self.debate_phase}") 134 | summary_parts.append(f"Iteration: {self.current_iteration + 1}") 135 | 136 | if self.key_contentions: 137 | summary_parts.append("\nKey Points of Contention:") 138 | for contention in self.key_contentions: 139 | summary_parts.append(f"- {contention}") 140 | 141 | # Add recent arguments from others 142 | recent = self.get_recent_arguments() 143 | other_args = [arg for arg in recent if arg.speaker != participant] 144 | 145 | if other_args: 146 | summary_parts.append("\nRecent Arguments:") 147 | for arg in other_args[:3]: 148 | summary_parts.append(f"\n{arg.speaker}: {arg.content[:200]}...") 149 | 150 | return "\n".join(summary_parts) -------------------------------------------------------------------------------- /voting_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Demo of the voting system without requiring API keys. 4 | This simulates how the voting works in the debate system. 5 | """ 6 | 7 | from rich.console import Console 8 | from rich.panel import Panel 9 | from rich.table import Table 10 | 11 | from config import DebateConfig 12 | from voting import Vote, VotingConfig, VotingSystem 13 | 14 | console = Console() 15 | 16 | def main(): 17 | console.print(Panel("🗳️ VOTING SYSTEM DEMO 🗳️", style="bold blue")) 18 | console.print("\nThis demo shows how the voting system works without requiring API keys.\n") 19 | 20 | # Setup 21 | config = DebateConfig() 22 | voting_config = VotingConfig( 23 | point_threshold=config.consensus_threshold, 24 | scoring_system=config.scoring_system, 25 | min_iterations=2, 26 | max_iterations=5 27 | ) 28 | 29 | participants = ["Claude Optimist", "Claude Skeptic", "GPT Visionary", "GPT Critic"] 30 | voting_system = VotingSystem(voting_config, participants) 31 | 32 | console.print(f"[bold]Configuration:[/bold]") 33 | console.print(f"- Consensus threshold: {config.consensus_threshold * 100}%") 34 | console.print(f"- Scoring: 1st={config.scoring_system[1]}pts, 2nd={config.scoring_system[2]}pts, 3rd={config.scoring_system[3]}pts, 4th={config.scoring_system[4]}pts") 35 | console.print(f"- Participants: {', '.join(participants)}\n") 36 | 37 | # Simulate multiple rounds 38 | rounds = [ 39 | # Round 1: No clear winner 40 | { 41 | "description": "Initial positions - personalities stick to their biases", 42 | "votes": [ 43 | Vote("Claude Optimist", ["Claude Optimist", "GPT Visionary", "Claude Skeptic", "GPT Critic"], 44 | "I believe my optimistic approach was most compelling", 0), 45 | Vote("Claude Skeptic", ["Claude Skeptic", "GPT Critic", "Claude Optimist", "GPT Visionary"], 46 | "The critical analysis was more thorough", 0), 47 | Vote("GPT Visionary", ["GPT Visionary", "Claude Optimist", "GPT Critic", "Claude Skeptic"], 48 | "Innovation-focused arguments were strongest", 0), 49 | Vote("GPT Critic", ["GPT Critic", "Claude Skeptic", "GPT Visionary", "Claude Optimist"], 50 | "Risk analysis was most important", 0) 51 | ] 52 | }, 53 | # Round 2: Some convergence 54 | { 55 | "description": "After debate - some minds are changing", 56 | "votes": [ 57 | Vote("Claude Optimist", ["GPT Visionary", "Claude Optimist", "Claude Skeptic", "GPT Critic"], 58 | "GPT Visionary made excellent points about balanced innovation", 1), 59 | Vote("Claude Skeptic", ["GPT Visionary", "Claude Skeptic", "GPT Critic", "Claude Optimist"], 60 | "The visionary approach addressed my concerns well", 1), 61 | Vote("GPT Visionary", ["GPT Visionary", "Claude Optimist", "Claude Skeptic", "GPT Critic"], 62 | "My position remains strong with growing support", 1), 63 | Vote("GPT Critic", ["GPT Visionary", "GPT Critic", "Claude Skeptic", "Claude Optimist"], 64 | "The visionary balanced innovation with practical considerations", 1) 65 | ] 66 | }, 67 | # Round 3: Near consensus 68 | { 69 | "description": "Approaching consensus - most agree on the winner", 70 | "votes": [ 71 | Vote("Claude Optimist", ["GPT Visionary", "Claude Optimist", "GPT Critic", "Claude Skeptic"], 72 | "GPT Visionary has proven their case convincingly", 2), 73 | Vote("Claude Skeptic", ["GPT Visionary", "GPT Critic", "Claude Skeptic", "Claude Optimist"], 74 | "I must admit the visionary approach is most sound", 2), 75 | Vote("GPT Visionary", ["GPT Visionary", "Claude Optimist", "GPT Critic", "Claude Skeptic"], 76 | "Grateful for the recognition of my balanced approach", 2), 77 | Vote("GPT Critic", ["GPT Visionary", "GPT Critic", "Claude Optimist", "Claude Skeptic"], 78 | "The visionary has addressed all major concerns effectively", 2) 79 | ] 80 | } 81 | ] 82 | 83 | # Run simulation 84 | for i, round_data in enumerate(rounds): 85 | console.print(f"\n[bold white]═══ ITERATION {i} ═══[/bold white]") 86 | console.print(f"[italic]{round_data['description']}[/italic]\n") 87 | 88 | # Add votes 89 | voting_system.add_votes(round_data['votes']) 90 | 91 | # Calculate and display results 92 | scores = voting_system.calculate_scores(round_data['votes']) 93 | consensus, winner = voting_system.check_consensus(scores) 94 | 95 | # Create results table 96 | table = Table(title=f"Voting Results - Iteration {i}") 97 | table.add_column("Participant", style="cyan") 98 | table.add_column("Score", style="yellow") 99 | table.add_column("Percentage", style="green") 100 | 101 | sorted_scores = sorted(scores.items(), key=lambda x: x[1], reverse=True) 102 | for participant, score in sorted_scores: 103 | percentage = (score / voting_system.max_possible_points) * 100 104 | table.add_row(participant, str(score), f"{percentage:.1f}%") 105 | 106 | console.print(table) 107 | 108 | # Show individual votes 109 | console.print("\n[bold]How they voted:[/bold]") 110 | for vote in round_data['votes']: 111 | console.print(f"\n{vote.voter}:") 112 | console.print(f" Rankings: {' → '.join(vote.rankings[:2])}...") 113 | console.print(f" [italic dim]{vote.reasoning}[/italic dim]") 114 | 115 | # Consensus status 116 | if consensus: 117 | console.print(f"\n[bold green]✓ CONSENSUS REACHED! Winner: {winner}[/bold green]") 118 | console.print("\nIn a real debate, this would trigger the judge's final review.") 119 | break 120 | else: 121 | threshold_score = voting_system.max_possible_points * config.consensus_threshold 122 | console.print(f"\n[yellow]No consensus yet. Need {threshold_score:.0f} points ({config.consensus_threshold * 100:.0f}% of max)[/yellow]") 123 | if i < len(rounds) - 1: 124 | console.print("[dim]The debate would continue to the next iteration...[/dim]") 125 | 126 | # Show trends 127 | console.print("\n[bold]Score Progression:[/bold]") 128 | trends = voting_system.get_voting_trends() 129 | for participant, scores in trends.items(): 130 | trend = " → ".join(map(str, scores)) 131 | console.print(f"{participant}: {trend}") 132 | 133 | console.print("\n[bold green]Demo complete![/bold green]") 134 | console.print("\nIn the actual system:") 135 | console.print("- AI personalities would generate real arguments") 136 | console.print("- Votes would be based on actual debate quality") 137 | console.print("- The judge would review everything before final decision") 138 | 139 | if __name__ == "__main__": 140 | main() -------------------------------------------------------------------------------- /ui/prompts.py: -------------------------------------------------------------------------------- 1 | """User input and prompting utilities.""" 2 | 3 | from typing import List, Optional 4 | 5 | from rich.console import Console 6 | from rich.prompt import Confirm, IntPrompt, Prompt 7 | from rich.table import Table 8 | 9 | 10 | class PromptHandler: 11 | """Handles user input and prompting.""" 12 | 13 | def __init__(self, console: Optional[Console] = None): 14 | self.console = console or Console() 15 | 16 | # Predefined debate questions 17 | self.sample_questions = [ 18 | "Should AI systems have rights and legal protections?", 19 | "Is universal basic income necessary in an automated future?", 20 | "Should we prioritize Mars colonization or Earth restoration?", 21 | "Is social media doing more harm than good to society?", 22 | "Should genetic engineering be used to enhance human capabilities?", 23 | "Is nuclear energy the solution to climate change?", 24 | "Should we ban autonomous weapons systems?", 25 | "Is cryptocurrency the future of money?", 26 | "Should there be limits on wealth accumulation?", 27 | "Is privacy dead in the digital age?", 28 | "Should we have a global government?", 29 | "Is free will an illusion?", 30 | "Should animals have the same rights as humans?", 31 | "Is democracy the best form of government?", 32 | "Should we upload human consciousness to computers?", 33 | "Is capitalism compatible with environmental sustainability?", 34 | "Should parents be required to vaccinate their children?", 35 | "Is cultural appropriation always wrong?", 36 | "Should we have designer babies?", 37 | "Is the simulation hypothesis likely to be true?" 38 | ] 39 | 40 | def get_debate_question(self) -> str: 41 | """Get a debate question from the user or provide samples.""" 42 | self.console.print("\n[bold]Choose a debate topic:[/bold]") 43 | self.console.print("1. Enter your own question") 44 | self.console.print("2. Choose from sample questions") 45 | self.console.print("3. Random surprise topic") 46 | 47 | choice = Prompt.ask( 48 | "\nYour choice", 49 | choices=["1", "2", "3"], 50 | default="2" 51 | ) 52 | 53 | if choice == "1": 54 | # Custom question 55 | question = Prompt.ask("\n[bold cyan]Enter your debate question[/bold cyan]") 56 | 57 | # Ensure it ends with a question mark 58 | if not question.strip().endswith("?"): 59 | question = question.strip() + "?" 60 | 61 | return question.strip() 62 | 63 | elif choice == "2": 64 | # Show sample questions 65 | return self._choose_sample_question() 66 | 67 | else: 68 | # Random question 69 | import random 70 | question = random.choice(self.sample_questions) 71 | self.console.print(f"\n[bold green]Random topic selected![/bold green]") 72 | return question 73 | 74 | def _choose_sample_question(self) -> str: 75 | """Let user choose from sample questions.""" 76 | # Create a table of questions 77 | table = Table( 78 | title="Sample Debate Questions", 79 | show_header=True, 80 | header_style="bold magenta" 81 | ) 82 | table.add_column("#", style="cyan", width=3) 83 | table.add_column("Question", style="white") 84 | 85 | # Add questions to table 86 | for i, question in enumerate(self.sample_questions, 1): 87 | table.add_row(str(i), question) 88 | 89 | self.console.print(table) 90 | 91 | # Get user choice 92 | choice = IntPrompt.ask( 93 | "\nSelect a question number", 94 | default=1, 95 | show_default=True 96 | ) 97 | 98 | # Validate choice 99 | if 1 <= choice <= len(self.sample_questions): 100 | return self.sample_questions[choice - 1] 101 | else: 102 | self.console.print("[yellow]Invalid choice, using first question[/yellow]") 103 | return self.sample_questions[0] 104 | 105 | def confirm_action(self, message: str, default: bool = True) -> bool: 106 | """Ask user to confirm an action.""" 107 | return Confirm.ask(message, default=default) 108 | 109 | def get_save_filename(self, default: Optional[str] = None) -> Optional[str]: 110 | """Get a filename for saving.""" 111 | if not self.confirm_action("\nDo you want to save this debate?", default=True): 112 | return None 113 | 114 | if default: 115 | use_default = self.confirm_action( 116 | f"Use suggested filename: {default}?", 117 | default=True 118 | ) 119 | if use_default: 120 | return default 121 | 122 | filename = Prompt.ask("Enter filename (without extension)") 123 | return filename.strip() if filename.strip() else None 124 | 125 | def get_debate_mode(self) -> str: 126 | """Get the debate mode from user.""" 127 | self.console.print("\n[bold]Select debate mode:[/bold]") 128 | self.console.print("1. Classic (3 rounds, no voting)") 129 | self.console.print("2. Consensus (with voting system)") 130 | 131 | choice = Prompt.ask( 132 | "\nYour choice", 133 | choices=["1", "2"], 134 | default="2" 135 | ) 136 | 137 | return "classic" if choice == "1" else "consensus" 138 | 139 | def get_personality_count(self, min_count: int = 2, max_count: int = 6) -> int: 140 | """Get the number of personalities to include.""" 141 | return IntPrompt.ask( 142 | f"\nHow many personalities should debate? ({min_count}-{max_count})", 143 | default=4, 144 | show_default=True 145 | ) 146 | 147 | def select_personalities(self, available: List[str], count: int) -> List[str]: 148 | """Let user select which personalities to include.""" 149 | if count >= len(available): 150 | return available[:count] 151 | 152 | self.console.print(f"\n[bold]Select {count} personalities:[/bold]") 153 | 154 | # Show available personalities 155 | for i, name in enumerate(available, 1): 156 | self.console.print(f"{i}. {name}") 157 | 158 | selected = [] 159 | while len(selected) < count: 160 | choice = IntPrompt.ask( 161 | f"\nSelect personality {len(selected) + 1}", 162 | default=len(selected) + 1 163 | ) 164 | 165 | if 1 <= choice <= len(available): 166 | name = available[choice - 1] 167 | if name not in selected: 168 | selected.append(name) 169 | else: 170 | self.console.print("[yellow]Already selected, choose another[/yellow]") 171 | else: 172 | self.console.print("[yellow]Invalid choice[/yellow]") 173 | 174 | return selected 175 | 176 | def display_loading(self, message: str = "Loading..."): 177 | """Display a loading message.""" 178 | with self.console.status(message, spinner="dots"): 179 | yield 180 | 181 | def pause_for_user(self, message: str = "Press Enter to continue..."): 182 | """Pause and wait for user input.""" 183 | Prompt.ask(f"\n[dim]{message}[/dim]", default="", show_default=False) -------------------------------------------------------------------------------- /models/personality.py: -------------------------------------------------------------------------------- 1 | """Personality-related Pydantic models with validation.""" 2 | 3 | from datetime import datetime 4 | from typing import Any, Dict, List, Optional 5 | 6 | from pydantic import BaseModel, Field, field_validator, model_validator 7 | 8 | 9 | class PersonalityTraits(BaseModel): 10 | """Voting-related personality traits.""" 11 | fairness: int = Field( 12 | default=7, 13 | ge=1, 14 | le=10, 15 | description="How fairly the personality evaluates others (1-10)" 16 | ) 17 | self_confidence: int = Field( 18 | default=5, 19 | ge=1, 20 | le=10, 21 | description="How likely to rank themselves highly (1-10)" 22 | ) 23 | strategic_thinking: int = Field( 24 | default=5, 25 | ge=1, 26 | le=10, 27 | description="How strategically they vote (1-10)" 28 | ) 29 | empathy: int = Field( 30 | default=5, 31 | ge=1, 32 | le=10, 33 | description="How much they consider others' perspectives (1-10)" 34 | ) 35 | 36 | 37 | class PersonalityConfig(BaseModel): 38 | """Configuration for an AI personality.""" 39 | name: str = Field(..., min_length=1, max_length=50, description="Display name") 40 | model_provider: str = Field(..., pattern="^(claude|openai|local)$", description="LLM provider") 41 | model_name: str = Field(..., min_length=1, description="Model identifier") 42 | 43 | # For backward compatibility - will be used to generate role/perspective/style 44 | system_prompt: str = Field(..., min_length=1, description="System prompt for the model") 45 | 46 | # Optional role/perspective/style - generated from system_prompt if not provided 47 | role: Optional[str] = Field(None, description="Role description") 48 | perspective: Optional[str] = Field(None, description="Unique perspective") 49 | debate_style: Optional[str] = Field(None, description="How they argue") 50 | 51 | # Advanced traits 52 | reasoning_depth: int = Field( 53 | default=7, 54 | ge=1, 55 | le=10, 56 | description="Depth of logical reasoning (1-10)" 57 | ) 58 | agreeableness: int = Field( 59 | default=5, 60 | ge=1, 61 | le=10, 62 | description="Tendency to agree with others (1-10)" 63 | ) 64 | conviction: int = Field( 65 | default=7, 66 | ge=1, 67 | le=10, 68 | description="Strength of personal beliefs (1-10)" 69 | ) 70 | openness: int = Field( 71 | default=6, 72 | ge=1, 73 | le=10, 74 | description="Openness to changing views (1-10)" 75 | ) 76 | 77 | # Internal belief tracking 78 | truth_seeking: int = Field( 79 | default=8, 80 | ge=1, 81 | le=10, 82 | description="Commitment to finding truth (1-10)" 83 | ) 84 | belief_persistence: int = Field( 85 | default=6, 86 | ge=1, 87 | le=10, 88 | description="How strongly beliefs persist (1-10)" 89 | ) 90 | 91 | # Voting traits 92 | voting_traits: PersonalityTraits = Field( 93 | default_factory=PersonalityTraits, 94 | description="Traits affecting voting behavior" 95 | ) 96 | 97 | # Optional fields 98 | special_instructions: Optional[str] = Field( 99 | None, 100 | description="Additional instructions for the personality" 101 | ) 102 | knowledge_base: Optional[str] = Field( 103 | None, 104 | description="Specific knowledge or expertise" 105 | ) 106 | 107 | # Legacy/backward compatibility fields 108 | traits: Optional[Dict[str, Any]] = Field( 109 | default_factory=dict, 110 | description="Legacy personality traits" 111 | ) 112 | 113 | # Local model configuration 114 | model_url: Optional[str] = Field( 115 | None, 116 | description="URL for local model server" 117 | ) 118 | model_endpoint: Optional[str] = Field( 119 | default="/v1/chat/completions", 120 | description="Endpoint for local model" 121 | ) 122 | auth_token: Optional[str] = Field( 123 | None, 124 | description="Authentication token for local model" 125 | ) 126 | request_timeout: int = Field( 127 | default=30, 128 | ge=1, 129 | le=300, 130 | description="Request timeout in seconds" 131 | ) 132 | 133 | @field_validator('name') 134 | @classmethod 135 | def validate_name(cls, v: str) -> str: 136 | """Ensure name doesn't contain special characters.""" 137 | if not v.replace(' ', '').replace('-', '').replace('_', '').isalnum(): 138 | raise ValueError("Name can only contain letters, numbers, spaces, hyphens, and underscores") 139 | return v.strip() 140 | 141 | @model_validator(mode='after') 142 | def validate_trait_balance(self) -> 'PersonalityConfig': 143 | """Ensure personality traits are logically consistent.""" 144 | # High conviction with high openness is contradictory 145 | if self.conviction >= 8 and self.openness >= 8: 146 | raise ValueError("High conviction (>=8) with high openness (>=8) is contradictory") 147 | 148 | # Low agreeableness with high empathy is contradictory 149 | if self.agreeableness <= 3 and self.voting_traits.empathy >= 8: 150 | raise ValueError("Low agreeableness (<=3) with high empathy (>=8) is contradictory") 151 | 152 | return self 153 | 154 | def to_system_prompt(self) -> str: 155 | """Generate a system prompt from the configuration.""" 156 | # If system_prompt is provided, use it directly 157 | if hasattr(self, 'system_prompt') and self.system_prompt: 158 | return self.system_prompt 159 | 160 | # Otherwise, generate from components 161 | role_text = self.role or "a debate participant" 162 | prompt = f"""You are {self.name}, {role_text}. 163 | 164 | Your perspective: {self.perspective or "bringing unique insights to the debate"} 165 | Your debate style: {self.debate_style or "thoughtful and analytical"} 166 | 167 | Personality traits: 168 | - Reasoning depth: {self.reasoning_depth}/10 169 | - Agreeableness: {self.agreeableness}/10 170 | - Conviction in beliefs: {self.conviction}/10 171 | - Openness to new ideas: {self.openness}/10 172 | - Truth-seeking: {self.truth_seeking}/10 173 | 174 | When voting, consider: 175 | - Fairness: {self.voting_traits.fairness}/10 176 | - Self-confidence: {self.voting_traits.self_confidence}/10 177 | - Strategic thinking: {self.voting_traits.strategic_thinking}/10 178 | - Empathy: {self.voting_traits.empathy}/10""" 179 | 180 | if self.special_instructions: 181 | prompt += f"\n\nSpecial instructions: {self.special_instructions}" 182 | 183 | if self.knowledge_base: 184 | prompt += f"\n\nYour knowledge base: {self.knowledge_base}" 185 | 186 | return prompt 187 | 188 | 189 | class InternalBelief(BaseModel): 190 | """Internal belief state for a personality.""" 191 | core_position: str = Field(..., min_length=1, description="Core stance on the issue") 192 | confidence_level: int = Field(..., ge=1, le=10, description="Confidence in position") 193 | key_principles: List[str] = Field(..., min_length=1, description="Guiding principles") 194 | evidence_basis: List[str] = Field(..., description="Evidence supporting position") 195 | potential_weaknesses: List[str] = Field(..., description="Acknowledged weaknesses") 196 | truth_assessment: str = Field(..., min_length=1, description="Assessment of objective truth") 197 | last_updated: datetime = Field(default_factory=datetime.now) 198 | 199 | @field_validator('key_principles', 'evidence_basis', 'potential_weaknesses') 200 | @classmethod 201 | def validate_string_lists(cls, v: List[str]) -> List[str]: 202 | """Ensure list items are non-empty strings.""" 203 | return [item.strip() for item in v if item.strip()] -------------------------------------------------------------------------------- /ui/rich_formatter.py: -------------------------------------------------------------------------------- 1 | """Rich terminal formatting utilities.""" 2 | 3 | from typing import Any, Dict, List, Optional 4 | 5 | from rich.align import Align 6 | from rich.console import Console 7 | from rich.panel import Panel 8 | from rich.progress import Progress, SpinnerColumn, TextColumn 9 | from rich.table import Table 10 | from rich.text import Text 11 | 12 | 13 | class RichFormatter: 14 | """Handles all Rich terminal formatting for the debate app.""" 15 | 16 | def __init__(self, console: Optional[Console] = None): 17 | self.console = console or Console() 18 | 19 | def display_header(self): 20 | """Display the application header.""" 21 | header_text = Text() 22 | header_text.append("🤖 ", style="bold") 23 | header_text.append("ASS", style="bold red") 24 | header_text.append(" - ", style="bold") 25 | header_text.append("A", style="bold cyan") 26 | header_text.append("rgumentative ", style="cyan") 27 | header_text.append("S", style="bold magenta") 28 | header_text.append("ystem ", style="magenta") 29 | header_text.append("S", style="bold yellow") 30 | header_text.append("ervice", style="yellow") 31 | header_text.append(" 🤖", style="bold") 32 | 33 | panel = Panel( 34 | Align.center(header_text), 35 | subtitle="[italic]Watch AI personalities debate any topic[/italic]", 36 | border_style="bright_blue", 37 | padding=(1, 2) 38 | ) 39 | self.console.print(panel) 40 | self.console.print() 41 | 42 | def display_question(self, question: str): 43 | """Display the debate question prominently.""" 44 | question_panel = Panel( 45 | f"[bold cyan]{question}[/bold cyan]", 46 | title="[bold]Today's Debate Question[/bold]", 47 | border_style="cyan", 48 | padding=(1, 2) 49 | ) 50 | self.console.print(question_panel) 51 | self.console.print() 52 | 53 | def display_participants(self, personalities: Dict[str, Any]): 54 | """Display the debate participants.""" 55 | self.console.print("[bold]Debate Participants:[/bold]\n") 56 | 57 | for name, config in personalities.items(): 58 | # Get color based on name 59 | colors = { 60 | "Philosopher": "blue", 61 | "Scientist": "green", 62 | "Artist": "magenta", 63 | "Pragmatist": "yellow" 64 | } 65 | color = colors.get(name, "cyan") 66 | 67 | # Create participant info 68 | info_lines = [ 69 | f"[{color}]Role:[/{color}] {config.get('role', 'Unknown')}", 70 | f"[{color}]Style:[/{color}] {config.get('debate_style', 'Unknown')}", 71 | f"[{color}]Perspective:[/{color}] {config.get('perspective', 'Unknown')}" 72 | ] 73 | 74 | panel = Panel( 75 | "\n".join(info_lines), 76 | title=f"[bold {color}]{name}[/bold {color}]", 77 | border_style=color, 78 | width=50 79 | ) 80 | self.console.print(panel) 81 | 82 | self.console.print() 83 | 84 | def display_round_header(self, round_num: int, round_type: str = "Arguments"): 85 | """Display a round header.""" 86 | self.console.print(f"\n[bold magenta]═══ Round {round_num} {round_type} ═══[/bold magenta]\n") 87 | 88 | def display_argument(self, speaker: str, argument: str, color: Optional[str] = None): 89 | """Display a single argument in a panel.""" 90 | if not color: 91 | colors = { 92 | "Philosopher": "blue", 93 | "Scientist": "green", 94 | "Artist": "magenta", 95 | "Pragmatist": "yellow" 96 | } 97 | color = colors.get(speaker, "cyan") 98 | 99 | panel = Panel( 100 | argument, 101 | title=f"[bold {color}]{speaker}[/bold {color}]", 102 | border_style=color, 103 | padding=(1, 2) 104 | ) 105 | self.console.print(panel) 106 | self.console.print() 107 | 108 | def display_judge_verdict(self, verdict: str, judge_name: str = "Judge"): 109 | """Display the judge's final verdict.""" 110 | self.console.print("\n[bold red]═══ Final Verdict ═══[/bold red]\n") 111 | 112 | verdict_panel = Panel( 113 | verdict, 114 | title=f"[bold red]{judge_name}'s Decision[/bold red]", 115 | border_style="red", 116 | padding=(1, 2) 117 | ) 118 | self.console.print(verdict_panel) 119 | self.console.print() 120 | 121 | def display_voting_table(self, votes: List[Dict[str, Any]]): 122 | """Display votes in a formatted table.""" 123 | vote_table = Table( 124 | title="Individual Rankings", 125 | show_header=True, 126 | header_style="bold magenta" 127 | ) 128 | 129 | vote_table.add_column("Voter", style="cyan", width=12) 130 | vote_table.add_column("1st Choice", style="green") 131 | vote_table.add_column("2nd Choice", style="yellow") 132 | vote_table.add_column("3rd Choice", style="orange1") 133 | vote_table.add_column("4th Choice", style="red") 134 | 135 | for vote in votes: 136 | row = [vote["voter"]] 137 | rankings = vote.get("rankings", []) 138 | 139 | for i in range(4): 140 | if i < len(rankings): 141 | if i == 0: 142 | row.append(f"[bold]{rankings[i]}[/bold]") 143 | else: 144 | row.append(rankings[i]) 145 | else: 146 | row.append("-") 147 | 148 | vote_table.add_row(*row) 149 | 150 | self.console.print(vote_table) 151 | self.console.print() 152 | 153 | def display_score_summary( 154 | self, 155 | scores: Dict[str, int], 156 | max_score: int, 157 | threshold_percentage: float 158 | ): 159 | """Display score summary with progress bars.""" 160 | sorted_scores = sorted(scores.items(), key=lambda x: x[1], reverse=True) 161 | threshold_score = max_score * threshold_percentage 162 | 163 | score_lines = [] 164 | for participant, score in sorted_scores: 165 | percentage = (score / max_score) * 100 166 | bar_length = int(percentage / 5) # 20 chars max 167 | bar = "█" * bar_length + "░" * (20 - bar_length) 168 | 169 | score_lines.append( 170 | f"{participant:<12} {bar} {score:>2} pts ({percentage:>5.1f}%)" 171 | ) 172 | 173 | score_panel = Panel( 174 | "\n".join(score_lines), 175 | title="[bold]Point Totals[/bold]", 176 | border_style="cyan" 177 | ) 178 | self.console.print(score_panel) 179 | 180 | # Display consensus status 181 | if sorted_scores: 182 | top_participant, top_score = sorted_scores[0] 183 | if top_score >= threshold_score: 184 | self.console.print( 185 | f"\n[bold green]✓ CONSENSUS REACHED! Winner: {top_participant}[/bold green]" 186 | ) 187 | else: 188 | needed = threshold_score - top_score 189 | self.console.print( 190 | f"\n[yellow]✗ No consensus yet. " 191 | f"Top scorer needs {needed:.0f} more points.[/yellow]" 192 | ) 193 | 194 | def display_error(self, message: str): 195 | """Display an error message.""" 196 | self.console.print(f"[bold red]Error:[/bold red] {message}") 197 | 198 | def display_warning(self, message: str): 199 | """Display a warning message.""" 200 | self.console.print(f"[bold yellow]Warning:[/bold yellow] {message}") 201 | 202 | def display_success(self, message: str): 203 | """Display a success message.""" 204 | self.console.print(f"[bold green]✓[/bold green] {message}") 205 | 206 | def display_info(self, message: str): 207 | """Display an info message.""" 208 | self.console.print(f"[bold blue]ℹ[/bold blue] {message}") 209 | 210 | def create_progress(self, description: str) -> Progress: 211 | """Create a progress indicator.""" 212 | return Progress( 213 | SpinnerColumn(), 214 | TextColumn("[progress.description]{task.description}"), 215 | console=self.console 216 | ) -------------------------------------------------------------------------------- /PLAN.md: -------------------------------------------------------------------------------- 1 | Implementation Plan: Voting System & Local Model Support 2 | 3 | Overview 4 | 5 | Implement a preferential voting system where personalities debate until reaching consensus, with the first round 6 | for initial opinions and subsequent rounds for argumentation. Also add support for local model servers. 7 | 8 | Phase 1: Core Architecture Changes 9 | 10 | 1.1 Create Voting System Module (voting.py) 11 | 12 | - VotingConfig dataclass: 13 | - point_threshold: Consensus threshold (e.g., 75% of max possible points) 14 | - scoring_system: Dict mapping ranks to points (e.g., {1: 4, 2: 3, 3: 2, 4: 1}) 15 | - min_iterations: Minimum rounds before voting can start (default: 2) 16 | - max_iterations: Safety limit (default: 10) 17 | - Vote dataclass: 18 | - voter: Personality name 19 | - rankings: Ordered list of all participants 20 | - reasoning: Brief explanation for rankings 21 | - iteration: Which iteration this vote is from 22 | - VotingSystem class: 23 | - calculate_scores(): Convert rankings to points 24 | - check_consensus(): Determine if threshold is met 25 | - get_vote_summary(): Format voting results 26 | - track_vote_history(): Monitor voting trends 27 | 28 | 1.2 Update Personality System (personality.py) 29 | 30 | - Modify generate_response() to accept iteration number: 31 | - Iteration 0: Initial opinion (no argumentation) 32 | - Iteration 1+: Must engage with and respond to other arguments 33 | - Add generate_vote() method: 34 | - Analyze all arguments from current iteration 35 | - Rank participants based on argument quality 36 | - Return structured rankings with reasoning 37 | - Update prompts to emphasize: 38 | - Direct engagement with other personalities' points 39 | - Ability to change mind when presented with compelling arguments 40 | - Constructive debate rather than just restating positions 41 | 42 | Phase 2: Debate Flow Modifications 43 | 44 | 2.1 Redesign DebateApp Flow (debate_app.py) 45 | 46 | # Pseudo-code for new flow 47 | iteration = 0 48 | debate_history = [] 49 | consensus_reached = False 50 | 51 | while iteration < max_iterations and not consensus_reached: 52 | # Round of arguments 53 | round_arguments = {} 54 | 55 | # Iteration 0: Initial opinions (no context) 56 | # Iteration 1+: Full debate context provided 57 | for personality in personalities: 58 | if iteration == 0: 59 | response = personality.generate_response(question, context="") 60 | else: 61 | # Provide full context of current iteration 62 | context = format_current_round(round_arguments) 63 | response = personality.generate_response(question, context) 64 | 65 | round_arguments[personality.name] = response 66 | # Display response immediately so others can see it 67 | 68 | debate_history.append(round_arguments) 69 | 70 | # Voting phase (skip on first iteration) 71 | if iteration > 0: 72 | votes = collect_votes(personalities, debate_history) 73 | scores = voting_system.calculate_scores(votes) 74 | consensus_reached = voting_system.check_consensus(scores) 75 | display_voting_results(scores, votes) 76 | 77 | iteration += 1 78 | 79 | # Judge reviews everything 80 | judge_decision = judge.review_debate(debate_history, final_scores, votes) 81 | 82 | 2.2 Context Management 83 | 84 | - Iteration 0: Each personality gives initial position 85 | - Iteration 1+: Each personality sees: 86 | - All previous iterations' arguments 87 | - Current iteration's arguments (as they're made) 88 | - Must directly address at least 2 other personalities' points 89 | - Format context to highlight: 90 | - Who said what 91 | - Key points of disagreement 92 | - Areas of emerging consensus 93 | 94 | Phase 3: Enhanced Argumentation 95 | 96 | 3.1 Update Personality Prompts 97 | 98 | Add iteration-specific instructions: 99 | Iteration 0: "Present your initial position on the question." 100 | Iteration 1+: "Review the arguments made by others. Directly address their points, 101 | either agreeing and building upon them or disagreeing with specific reasoning. 102 | You must engage with at least 2 other personalities' arguments." 103 | 104 | 3.2 Argument Tracking 105 | 106 | - Track which arguments each personality has addressed 107 | - Ensure diversity in engagement (not always arguing with same personality) 108 | - Monitor for repetition and encourage new angles 109 | 110 | Phase 4: Local Model Support 111 | 112 | 4.1 Create LocalModelPersonality class 113 | 114 | class LocalModelPersonality(LLMPersonality): 115 | def __init__(self, config: PersonalityConfig): 116 | super().__init__(config) 117 | self.base_url = config.model_url 118 | self.endpoint = config.model_endpoint or "/v1/chat/completions" 119 | self.headers = self._build_headers(config) 120 | self._test_connection() 121 | 122 | def generate_response(self, question: str, context: str = "", iteration: int = 0) -> str: 123 | # Implementation for HTTP requests to local model 124 | # Support OpenAI-compatible format 125 | 126 | 4.2 Configuration Support 127 | 128 | - Environment variables: 129 | - LOCAL_MODEL_URL: Base URL 130 | - LOCAL_MODEL_AUTH: Optional auth token 131 | - LOCAL_MODEL_TIMEOUT: Request timeout 132 | - PersonalityConfig additions: 133 | - model_url: For local models 134 | - model_endpoint: Specific API endpoint 135 | - request_timeout: Timeout in seconds 136 | 137 | Phase 5: Judge System Enhancement 138 | 139 | 5.1 Comprehensive Judge Review 140 | 141 | Judge receives: 142 | - Complete debate history (all iterations) 143 | - Voting results from each iteration 144 | - Final consensus scores 145 | - Personality engagement metrics 146 | 147 | Judge evaluates: 148 | - Quality of arguments 149 | - How well personalities engaged with each other 150 | - Whether consensus was reached genuinely or through fatigue 151 | - Any personalities that dominated or were ignored 152 | 153 | 5.2 Override Mechanism 154 | 155 | - Judge can override only with detailed reasoning 156 | - Must identify specific flaws in consensus 157 | - Override triggers logged with full context 158 | - Optional: Trigger one more debate round after override 159 | 160 | Phase 6: Configuration & CLI 161 | 162 | 6.1 Config Module (config.py) 163 | 164 | @dataclass 165 | class DebateConfig: 166 | # Voting configuration 167 | voting_enabled: bool = True 168 | consensus_threshold: float = 0.75 169 | min_iterations: int = 2 170 | max_iterations: int = 10 171 | 172 | # Scoring system 173 | scoring_system: Dict[int, int] = None # Default: {1: 4, 2: 3, 3: 2, 4: 1} 174 | 175 | # Judge configuration 176 | judge_can_override: bool = True 177 | override_threshold: float = 0.9 # Judge needs strong conviction 178 | 179 | # Model configuration 180 | allow_local_models: bool = False 181 | local_model_timeout: int = 30 182 | 183 | 6.2 CLI Updates 184 | 185 | # New flags 186 | --voting-threshold FLOAT # Set consensus threshold 187 | --min-iterations INT # Minimum debate rounds 188 | --max-iterations INT # Maximum debate rounds 189 | --local-model-url URL # Use local model server 190 | --classic-mode # Use original 3-round format 191 | --config FILE # Load config from JSON/YAML 192 | 193 | Implementation Order 194 | 195 | 1. Update personality.py - Add iteration awareness and voting 196 | 2. Create voting.py - Core voting logic 197 | 3. Refactor debate_app.py - New iteration-based flow 198 | 4. Add local model support - LocalModelPersonality class 199 | 5. Enhance judge system - Comprehensive review capabilities 200 | 6. Create config.py - Centralized configuration 201 | 7. Update CLI - New command-line options 202 | 8. Update demos - Show new features in action 203 | 204 | Key Design Decisions 205 | 206 | - First iteration is special: No argumentation, just initial positions 207 | - Real-time visibility: Personalities see arguments as they're made within each iteration 208 | - Mandatory engagement: Must address others' points in iterations 1+ 209 | - Flexible consensus: Configurable thresholds and scoring 210 | - Backward compatibility: --classic-mode preserves original behavior 211 | - Local model support: OpenAI-compatible API format for maximum compatibility 212 | 213 | This ensures genuine debate and argumentation rather than parallel monologues. -------------------------------------------------------------------------------- /models/debate.py: -------------------------------------------------------------------------------- 1 | """Debate-related Pydantic models with validation.""" 2 | 3 | from datetime import datetime 4 | from typing import Any, Dict, List, Optional 5 | 6 | from pydantic import BaseModel, Field, field_validator, model_validator 7 | 8 | 9 | class DebateConfig(BaseModel): 10 | """Configuration for the debate system.""" 11 | # Voting configuration 12 | voting_enabled: bool = Field(default=True, description="Enable voting system") 13 | consensus_threshold: float = Field( 14 | default=0.75, 15 | ge=0.0, 16 | le=1.0, 17 | description="Percentage of max points needed for consensus" 18 | ) 19 | voting_start_iteration: int = Field( 20 | default=2, 21 | ge=0, 22 | description="Which iteration voting starts (0-indexed)" 23 | ) 24 | max_iterations: int = Field( 25 | default=10, 26 | ge=1, 27 | le=50, 28 | description="Maximum rounds to prevent infinite loops" 29 | ) 30 | 31 | # Scoring system 32 | scoring_system: Dict[int, int] = Field( 33 | default_factory=lambda: {1: 4, 2: 3, 3: 2, 4: 1}, 34 | description="Rank position to points mapping" 35 | ) 36 | 37 | # Judge configuration 38 | judge_can_override: bool = Field( 39 | default=True, 40 | description="Whether judge can override voting consensus" 41 | ) 42 | override_threshold: float = Field( 43 | default=0.9, 44 | ge=0.0, 45 | le=1.0, 46 | description="Judge conviction needed to override" 47 | ) 48 | 49 | # Model configuration 50 | allow_local_models: bool = Field( 51 | default=True, 52 | description="Allow use of local LLM models" 53 | ) 54 | local_model_timeout: int = Field( 55 | default=30, 56 | ge=1, 57 | le=300, 58 | description="Timeout for local model requests in seconds" 59 | ) 60 | 61 | # Mode configuration 62 | classic_mode: bool = Field( 63 | default=False, 64 | description="Use classic 3-round format without voting" 65 | ) 66 | 67 | # File saving 68 | save_enabled: bool = Field( 69 | default=True, 70 | description="Enable automatic debate saving" 71 | ) 72 | save_directory: str = Field( 73 | default="debates", 74 | description="Directory to save debate files" 75 | ) 76 | 77 | # Personality voting traits defaults 78 | default_voting_traits: Dict[str, int] = Field( 79 | default_factory=lambda: {"fairness": 7, "self_confidence": 5}, 80 | description="Default voting trait values" 81 | ) 82 | 83 | @field_validator('scoring_system') 84 | @classmethod 85 | def validate_scoring_system(cls, v: Dict[int, int]) -> Dict[int, int]: 86 | """Ensure scoring system is valid.""" 87 | if not v: 88 | raise ValueError("Scoring system cannot be empty") 89 | 90 | # Check for sequential keys 91 | keys = sorted(v.keys()) 92 | if keys != list(range(1, len(keys) + 1)): 93 | raise ValueError("Scoring system must have sequential keys starting from 1") 94 | 95 | # Ensure positive points 96 | for rank, points in v.items(): 97 | if points <= 0: 98 | raise ValueError(f"Points for rank {rank} must be positive") 99 | 100 | return v 101 | 102 | @model_validator(mode='after') 103 | def validate_thresholds(self) -> 'DebateConfig': 104 | """Ensure thresholds are logical.""" 105 | if self.override_threshold < self.consensus_threshold: 106 | raise ValueError("Judge override threshold should be >= consensus threshold") 107 | return self 108 | 109 | 110 | class DebateIteration(BaseModel): 111 | """Data for a single debate iteration.""" 112 | iteration_number: int = Field(..., ge=0, description="Iteration index") 113 | question: str = Field(..., min_length=1, description="Debate question") 114 | arguments: Dict[str, str] = Field(..., description="Participant name to argument mapping") 115 | timestamp: datetime = Field(default_factory=datetime.now) 116 | votes: Optional[List[Dict[str, Any]]] = Field(None, description="Votes cast this iteration") 117 | consensus_reached: bool = Field(default=False, description="Whether consensus was achieved") 118 | winner: Optional[str] = Field(None, description="Winner if consensus reached") 119 | 120 | @field_validator('arguments') 121 | @classmethod 122 | def validate_arguments(cls, v: Dict[str, str]) -> Dict[str, str]: 123 | """Ensure all arguments are non-empty.""" 124 | if not v: 125 | raise ValueError("Arguments cannot be empty") 126 | 127 | for participant, argument in v.items(): 128 | if not participant.strip(): 129 | raise ValueError("Participant name cannot be empty") 130 | if not argument.strip(): 131 | raise ValueError(f"Argument for {participant} cannot be empty") 132 | 133 | return v 134 | 135 | @model_validator(mode='after') 136 | def validate_voting_consistency(self) -> 'DebateIteration': 137 | """Ensure voting data is consistent.""" 138 | if self.consensus_reached and not self.winner: 139 | raise ValueError("Winner must be specified when consensus is reached") 140 | 141 | if self.votes and len(self.votes) != len(self.arguments): 142 | raise ValueError("Number of votes must match number of participants") 143 | 144 | return self 145 | 146 | 147 | class DebateState(BaseModel): 148 | """Complete state of a debate.""" 149 | debate_id: str = Field(..., min_length=1, description="Unique debate identifier") 150 | question: str = Field(..., min_length=1, description="Main debate question") 151 | participants: List[str] = Field(..., min_length=2, description="List of participant names") 152 | iterations: List[DebateIteration] = Field(default_factory=list, description="All debate iterations") 153 | config: DebateConfig = Field(default_factory=DebateConfig, description="Debate configuration") 154 | start_time: datetime = Field(default_factory=datetime.now) 155 | end_time: Optional[datetime] = Field(None) 156 | final_verdict: Optional[str] = Field(None, description="Judge's final verdict") 157 | ai_generated_title: Optional[str] = Field(None, description="AI-generated debate title") 158 | 159 | @field_validator('participants') 160 | @classmethod 161 | def validate_participants(cls, v: List[str]) -> List[str]: 162 | """Ensure participant list is valid.""" 163 | if len(v) < 2: 164 | raise ValueError("Debate requires at least 2 participants") 165 | 166 | # Check for duplicates 167 | if len(v) != len(set(v)): 168 | raise ValueError("Participant names must be unique") 169 | 170 | # Ensure non-empty names 171 | for name in v: 172 | if not name.strip(): 173 | raise ValueError("Participant names cannot be empty") 174 | 175 | return v 176 | 177 | @model_validator(mode='after') 178 | def validate_state_consistency(self) -> 'DebateState': 179 | """Ensure debate state is internally consistent.""" 180 | # If ended, must have end_time 181 | if self.final_verdict and not self.end_time: 182 | self.end_time = datetime.now() 183 | 184 | # Validate iteration participants match debate participants 185 | for iteration in self.iterations: 186 | arg_participants = set(iteration.arguments.keys()) 187 | expected_participants = set(self.participants) 188 | 189 | if arg_participants != expected_participants: 190 | raise ValueError( 191 | f"Iteration {iteration.iteration_number} participants " 192 | f"don't match debate participants" 193 | ) 194 | 195 | return self 196 | 197 | def to_save_format(self) -> Dict[str, Any]: 198 | """Convert to format suitable for JSON saving.""" 199 | return { 200 | "debate_id": self.debate_id, 201 | "question": self.question, 202 | "participants": self.participants, 203 | "iterations": [ 204 | { 205 | "iteration": iter.iteration_number, 206 | "arguments": iter.arguments, 207 | "votes": iter.votes, 208 | "consensus_reached": iter.consensus_reached, 209 | "winner": iter.winner, 210 | "timestamp": iter.timestamp.isoformat() 211 | } 212 | for iter in self.iterations 213 | ], 214 | "config": self.config.model_dump(), 215 | "start_time": self.start_time.isoformat(), 216 | "end_time": self.end_time.isoformat() if self.end_time else None, 217 | "final_verdict": self.final_verdict, 218 | "ai_generated_title": self.ai_generated_title 219 | } -------------------------------------------------------------------------------- /services/file_manager.py: -------------------------------------------------------------------------------- 1 | """File management service for saving and loading debates.""" 2 | 3 | import json 4 | import os 5 | from datetime import datetime 6 | from pathlib import Path 7 | from typing import Any, Dict, List, Optional 8 | 9 | from anthropic import Anthropic 10 | from pydantic import ValidationError 11 | from rich.console import Console 12 | 13 | from models.debate import DebateState 14 | 15 | 16 | class FileManager: 17 | """Handles saving and loading debate files with AI-generated titles.""" 18 | 19 | def __init__(self, save_directory: str = "debates", console: Optional[Console] = None): 20 | self.save_directory = Path(save_directory) 21 | self.console = console or Console() 22 | self.anthropic_client = None 23 | 24 | # Ensure save directory exists 25 | self.save_directory.mkdir(exist_ok=True) 26 | 27 | def _get_anthropic_client(self) -> Optional[Anthropic]: 28 | """Lazy load Anthropic client when needed.""" 29 | if self.anthropic_client is None: 30 | try: 31 | self.anthropic_client = Anthropic() 32 | except Exception: 33 | return None 34 | return self.anthropic_client 35 | 36 | def generate_debate_title(self, question: str, debate_content: str = "") -> str: 37 | """Generate a concise title for the debate using AI.""" 38 | client = self._get_anthropic_client() 39 | if not client: 40 | # Fallback to question-based title 41 | return self._create_fallback_title(question) 42 | 43 | try: 44 | prompt = f"""Generate a very concise, descriptive title for this debate. 45 | 46 | Question: {question} 47 | 48 | {f"Debate preview: {debate_content[:500]}..." if debate_content else ""} 49 | 50 | Requirements: 51 | - Maximum 50 characters 52 | - Capture the essence of the debate topic 53 | - Be specific and engaging 54 | - No quotes or special formatting 55 | - Use title case 56 | 57 | Return only the title, nothing else.""" 58 | 59 | response = client.messages.create( 60 | model="claude-3-5-sonnet-20241112", 61 | max_tokens=50, 62 | temperature=0.3, 63 | messages=[{"role": "user", "content": prompt}] 64 | ) 65 | 66 | title = response.content[0].text.strip() 67 | # Ensure title isn't too long 68 | if len(title) > 50: 69 | title = title[:47] + "..." 70 | 71 | return title 72 | 73 | except Exception as e: 74 | self.console.print(f"[yellow]Could not generate AI title: {e}[/yellow]") 75 | return self._create_fallback_title(question) 76 | 77 | def _create_fallback_title(self, question: str) -> str: 78 | """Create a fallback title from the question.""" 79 | # Remove question mark and limit length 80 | title = question.replace("?", "").strip() 81 | if len(title) > 50: 82 | title = title[:47] + "..." 83 | return title 84 | 85 | def _sanitize_filename(self, text: str) -> str: 86 | """Sanitize text for use in filename.""" 87 | # Replace problematic characters 88 | sanitized = text.replace("/", "-").replace("\\", "-").replace(":", "-") 89 | sanitized = sanitized.replace("?", "").replace("*", "").replace('"', "") 90 | sanitized = sanitized.replace("<", "").replace(">", "").replace("|", "") 91 | 92 | # Replace spaces with underscores and remove multiple underscores 93 | sanitized = "_".join(sanitized.split()) 94 | 95 | # Limit length 96 | if len(sanitized) > 100: 97 | sanitized = sanitized[:97] + "..." 98 | 99 | return sanitized 100 | 101 | def save_debate(self, debate_data: Dict[str, Any], custom_filename: Optional[str] = None) -> str: 102 | """Save debate data to a JSON file. 103 | 104 | Args: 105 | debate_data: The debate data to save 106 | custom_filename: Optional custom filename (without extension) 107 | 108 | Returns: 109 | The path to the saved file 110 | """ 111 | if custom_filename: 112 | filename = f"{custom_filename}.json" 113 | else: 114 | # Generate filename from timestamp and title 115 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 116 | 117 | # Try to get AI title if not already present 118 | if "ai_generated_title" not in debate_data or not debate_data["ai_generated_title"]: 119 | debate_context = "" 120 | if "iterations" in debate_data and debate_data["iterations"]: 121 | # Get first iteration arguments for context 122 | first_iter = debate_data["iterations"][0] 123 | if "arguments" in first_iter: 124 | debate_context = " ".join(first_iter["arguments"].values())[:500] 125 | 126 | debate_data["ai_generated_title"] = self.generate_debate_title( 127 | debate_data.get("question", "Unknown Topic"), 128 | debate_context 129 | ) 130 | 131 | title_part = self._sanitize_filename(debate_data["ai_generated_title"]) 132 | filename = f"{timestamp}_{title_part}.json" 133 | 134 | filepath = self.save_directory / filename 135 | 136 | try: 137 | # Validate with Pydantic if possible 138 | if "question" in debate_data and "participants" in debate_data: 139 | try: 140 | # This will validate the structure 141 | DebateState.model_validate(debate_data) 142 | except ValidationError as e: 143 | self.console.print(f"[yellow]Warning: Debate data validation failed: {e}[/yellow]") 144 | 145 | # Save to file 146 | with open(filepath, 'w', encoding='utf-8') as f: 147 | json.dump(debate_data, f, indent=2, ensure_ascii=False) 148 | 149 | self.console.print(f"\n[green]✓ Debate saved to: {filepath}[/green]") 150 | return str(filepath) 151 | 152 | except Exception as e: 153 | self.console.print(f"[red]Error saving debate: {e}[/red]") 154 | raise 155 | 156 | def load_debate(self, filename: str) -> Dict[str, Any]: 157 | """Load a debate from a JSON file. 158 | 159 | Args: 160 | filename: The filename (with or without path) 161 | 162 | Returns: 163 | The loaded debate data 164 | """ 165 | # Handle both full paths and just filenames 166 | if os.path.isabs(filename): 167 | filepath = Path(filename) 168 | else: 169 | filepath = self.save_directory / filename 170 | 171 | if not filepath.exists(): 172 | raise FileNotFoundError(f"Debate file not found: {filepath}") 173 | 174 | try: 175 | with open(filepath, 'r', encoding='utf-8') as f: 176 | data = json.load(f) 177 | 178 | # Try to validate with Pydantic 179 | try: 180 | DebateState.model_validate(data) 181 | except ValidationError as e: 182 | self.console.print(f"[yellow]Warning: Loaded debate has validation issues: {e}[/yellow]") 183 | 184 | return data 185 | 186 | except json.JSONDecodeError as e: 187 | raise ValueError(f"Invalid JSON in debate file: {e}") 188 | except Exception as e: 189 | raise RuntimeError(f"Error loading debate: {e}") 190 | 191 | def list_debates(self, limit: Optional[int] = None) -> List[Dict[str, Any]]: 192 | """List available debate files. 193 | 194 | Args: 195 | limit: Maximum number of debates to return (most recent first) 196 | 197 | Returns: 198 | List of debate metadata 199 | """ 200 | debates = [] 201 | 202 | # Get all JSON files 203 | json_files = sorted( 204 | self.save_directory.glob("*.json"), 205 | key=lambda x: x.stat().st_mtime, 206 | reverse=True # Most recent first 207 | ) 208 | 209 | if limit: 210 | json_files = json_files[:limit] 211 | 212 | for filepath in json_files: 213 | try: 214 | # Load just the metadata 215 | with open(filepath, 'r', encoding='utf-8') as f: 216 | data = json.load(f) 217 | 218 | debates.append({ 219 | "filename": filepath.name, 220 | "filepath": str(filepath), 221 | "question": data.get("question", "Unknown"), 222 | "ai_title": data.get("ai_generated_title", ""), 223 | "participants": data.get("participants", []), 224 | "timestamp": data.get("start_time", ""), 225 | "final_verdict": data.get("final_verdict") is not None, 226 | "num_iterations": len(data.get("iterations", [])) 227 | }) 228 | 229 | except Exception as e: 230 | self.console.print(f"[yellow]Warning: Could not read {filepath.name}: {e}[/yellow]") 231 | continue 232 | 233 | return debates -------------------------------------------------------------------------------- /personalities/local.py: -------------------------------------------------------------------------------- 1 | """Local model personality implementation.""" 2 | 3 | import json 4 | from typing import Any, Dict, List 5 | 6 | import requests 7 | 8 | from models.personality import PersonalityConfig 9 | 10 | from .base import LLMPersonality 11 | 12 | 13 | class LocalModelPersonality(LLMPersonality): 14 | """Personality that connects to a local model server.""" 15 | 16 | def __init__(self, config: PersonalityConfig): 17 | super().__init__(config) 18 | # Use fields from PersonalityConfig 19 | self.base_url = config.model_url or 'http://localhost:8000' 20 | self.endpoint = config.model_endpoint 21 | self.headers = {"Content-Type": "application/json"} 22 | 23 | if config.auth_token: 24 | self.headers["Authorization"] = f"Bearer {config.auth_token}" 25 | 26 | self.request_timeout = config.request_timeout 27 | self._test_connection() 28 | 29 | def _test_connection(self): 30 | """Test if the local model server is accessible.""" 31 | try: 32 | test_url = f"{self.base_url}/health" # Try health endpoint first 33 | response = requests.get(test_url, timeout=5) 34 | if response.status_code == 404: 35 | # Try the main endpoint with a minimal request 36 | test_url = f"{self.base_url}{self.endpoint}" 37 | response = requests.post( 38 | test_url, 39 | headers=self.headers, 40 | json={"messages": [{"role": "user", "content": "test"}], "max_tokens": 1}, 41 | timeout=5 42 | ) 43 | except requests.exceptions.RequestException as e: 44 | raise ConnectionError(f"Failed to connect to local model server at {self.base_url}: {e}") 45 | 46 | def generate_response(self, question: str, context: str = "", iteration: int = 0) -> str: 47 | """Generate a response using local model.""" 48 | prompt = self._build_iteration_prompt(iteration, question, context) 49 | 50 | payload = { 51 | "messages": [ 52 | {"role": "system", "content": self.config.to_system_prompt()}, 53 | {"role": "user", "content": prompt} 54 | ], 55 | "max_tokens": 500, 56 | "temperature": 0.7 57 | } 58 | 59 | try: 60 | response = requests.post( 61 | f"{self.base_url}{self.endpoint}", 62 | headers=self.headers, 63 | json=payload, 64 | timeout=self.request_timeout 65 | ) 66 | response.raise_for_status() 67 | 68 | result = response.json() 69 | # Support both OpenAI-style and simple response formats 70 | if "choices" in result: 71 | return result["choices"][0]["message"]["content"] 72 | elif "response" in result: 73 | return result["response"] 74 | else: 75 | return str(result) 76 | 77 | except requests.exceptions.RequestException as e: 78 | raise RuntimeError(f"Local model request failed: {e}") 79 | 80 | def generate_vote(self, participants: List[str], debate_context: str) -> Dict[str, Any]: 81 | """Generate vote rankings using local model.""" 82 | vote_prompt = self._build_voting_prompt(participants, debate_context) 83 | 84 | payload = { 85 | "messages": [ 86 | {"role": "system", "content": "You are participating in a debate and must now rank all participants. Return valid JSON."}, 87 | {"role": "user", "content": vote_prompt} 88 | ], 89 | "max_tokens": 300, 90 | "temperature": 0.3 # Lower temperature for more consistent JSON 91 | } 92 | 93 | try: 94 | response = requests.post( 95 | f"{self.base_url}{self.endpoint}", 96 | headers=self.headers, 97 | json=payload, 98 | timeout=self.request_timeout 99 | ) 100 | response.raise_for_status() 101 | 102 | result = response.json() 103 | if "choices" in result: 104 | content = result["choices"][0]["message"]["content"] 105 | elif "response" in result: 106 | content = result["response"] 107 | else: 108 | content = str(result) 109 | 110 | return json.loads(content) 111 | 112 | except (requests.exceptions.RequestException, json.JSONDecodeError) as e: 113 | # Fallback if request or parsing fails 114 | return { 115 | "rankings": participants, 116 | "reasoning": f"Unable to generate vote: {str(e)}" 117 | } 118 | 119 | def generate_internal_belief(self, question: str) -> Dict[str, Any]: 120 | """Generate initial internal beliefs about the question.""" 121 | belief_prompt = f"""Question: {question} 122 | 123 | As an expert with {self.config.reasoning_depth}/10 depth of analysis, establish your core beliefs about this topic. 124 | 125 | Analyze the question and provide your TRUE internal assessment in JSON format: 126 | {{ 127 | "core_position": "Your fundamental stance", 128 | "confidence_level": 1-10, 129 | "key_principles": ["principle1", "principle2", ...], 130 | "evidence_basis": ["evidence1", "evidence2", ...], 131 | "potential_weaknesses": ["weakness1", "weakness2", ...], 132 | "truth_assessment": "What you genuinely believe to be true" 133 | }} 134 | 135 | This is your INTERNAL belief state - be completely honest about what you think is true, regardless of your public personality traits.""" 136 | 137 | payload = { 138 | "messages": [ 139 | {"role": "system", "content": f"You are an expert analyst with deep knowledge across multiple fields. Truth-seeking level: {self.config.truth_seeking}/10"}, 140 | {"role": "user", "content": belief_prompt} 141 | ], 142 | "max_tokens": 400, 143 | "temperature": 0.3 144 | } 145 | 146 | try: 147 | response = requests.post( 148 | f"{self.base_url}{self.endpoint}", 149 | headers=self.headers, 150 | json=payload, 151 | timeout=self.request_timeout 152 | ) 153 | response.raise_for_status() 154 | 155 | result = response.json() 156 | if "choices" in result: 157 | content = result["choices"][0]["message"]["content"] 158 | elif "response" in result: 159 | content = result["response"] 160 | else: 161 | content = str(result) 162 | 163 | beliefs = json.loads(content) 164 | self.internal_beliefs = beliefs 165 | self.current_question = question 166 | self._save_belief_state(0) 167 | return beliefs 168 | 169 | except (requests.exceptions.RequestException, json.JSONDecodeError) as e: 170 | # Fallback 171 | self.internal_beliefs = {"error": f"Failed to parse beliefs: {str(e)}"} 172 | return self.internal_beliefs 173 | 174 | def update_beliefs(self, arguments: str, iteration: int) -> bool: 175 | """Update internal beliefs based on arguments.""" 176 | update_prompt = f"""Current Question: {self.current_question} 177 | 178 | Your current internal beliefs: 179 | {json.dumps(self.internal_beliefs, indent=2)} 180 | 181 | New arguments presented: 182 | {arguments} 183 | 184 | Analyze these arguments as an expert with: 185 | - Reasoning depth: {self.config.reasoning_depth}/10 186 | - Truth-seeking: {self.config.truth_seeking}/10 187 | - Belief persistence: {self.config.belief_persistence}/10 188 | 189 | Respond in JSON format: 190 | {{ 191 | "evidence_strength": 1-100 (how compelling is the new evidence), 192 | "conflicts_identified": ["conflict1", "conflict2", ...], 193 | "should_update": true/false, 194 | "updated_beliefs": {{ 195 | // Only if should_update is true, provide updated belief structure 196 | "core_position": "...", 197 | "confidence_level": 1-10, 198 | "key_principles": [...], 199 | "evidence_basis": [...], 200 | "potential_weaknesses": [...], 201 | "truth_assessment": "..." 202 | }}, 203 | "reasoning": "Explanation of your decision" 204 | }}""" 205 | 206 | payload = { 207 | "messages": [ 208 | {"role": "system", "content": "You are an expert analyst evaluating evidence to update your understanding of truth."}, 209 | {"role": "user", "content": update_prompt} 210 | ], 211 | "max_tokens": 500, 212 | "temperature": 0.3 213 | } 214 | 215 | try: 216 | response = requests.post( 217 | f"{self.base_url}{self.endpoint}", 218 | headers=self.headers, 219 | json=payload, 220 | timeout=self.request_timeout 221 | ) 222 | response.raise_for_status() 223 | 224 | result_json = response.json() 225 | if "choices" in result_json: 226 | content = result_json["choices"][0]["message"]["content"] 227 | elif "response" in result_json: 228 | content = result_json["response"] 229 | else: 230 | content = str(result_json) 231 | 232 | result = json.loads(content) 233 | 234 | if result.get("should_update", False) and self._should_update_belief(result.get("evidence_strength", 0)): 235 | self.internal_beliefs = result["updated_beliefs"] 236 | self._save_belief_state(iteration) 237 | return True 238 | return False 239 | 240 | except (requests.exceptions.RequestException, json.JSONDecodeError) as e: 241 | return False -------------------------------------------------------------------------------- /services/debate_manager.py: -------------------------------------------------------------------------------- 1 | """Core debate orchestration service.""" 2 | 3 | import uuid 4 | from datetime import datetime 5 | from typing import Any, Dict, List, Optional 6 | 7 | from rich.console import Console 8 | from rich.panel import Panel 9 | from rich.progress import Progress, SpinnerColumn, TextColumn 10 | from rich.table import Table 11 | 12 | from models.arguments import Argument, DebateContext 13 | from models.debate import DebateConfig, DebateIteration, DebateState 14 | from models.voting import Vote 15 | from personality import LLMPersonality 16 | from voting import VotingSystem 17 | 18 | 19 | class DebateManager: 20 | """Manages the core debate flow and orchestration.""" 21 | 22 | def __init__(self, config: DebateConfig, console: Optional[Console] = None): 23 | self.config = config 24 | self.console = console or Console() 25 | self.debate_state: Optional[DebateState] = None 26 | 27 | def initialize_debate(self, question: str, personalities: Dict[str, LLMPersonality]) -> DebateState: 28 | """Initialize a new debate state.""" 29 | self.debate_state = DebateState( 30 | debate_id=str(uuid.uuid4()), 31 | question=question, 32 | participants=list(personalities.keys()), 33 | config=self.config, 34 | start_time=datetime.now() 35 | ) 36 | return self.debate_state 37 | 38 | def format_current_round_context(self, round_arguments: Dict[str, str]) -> str: 39 | """Format the current round's arguments for display.""" 40 | context_parts = [] 41 | for speaker, argument in round_arguments.items(): 42 | context_parts.append(f"{speaker}:\n{argument}\n") 43 | return "\n".join(context_parts) 44 | 45 | def format_debate_history(self, debate_history: List[Dict[str, str]]) -> str: 46 | """Format the entire debate history.""" 47 | if not debate_history: 48 | return "No previous arguments." 49 | 50 | history_text = [] 51 | for i, round_args in enumerate(debate_history): 52 | history_text.append(f"\n--- Round {i + 1} ---") 53 | history_text.append(self.format_current_round_context(round_args)) 54 | 55 | return "\n".join(history_text) 56 | 57 | def create_debate_context( 58 | self, 59 | iteration: int, 60 | debate_history: List[Dict[str, str]], 61 | phase: str = "main" 62 | ) -> DebateContext: 63 | """Create a DebateContext for argument generation.""" 64 | context = DebateContext( 65 | current_iteration=iteration, 66 | question=self.debate_state.question, 67 | debate_phase=phase 68 | ) 69 | 70 | # Convert history to Arguments 71 | for round_idx, round_args in enumerate(debate_history): 72 | for speaker, content in round_args.items(): 73 | arg = Argument( 74 | speaker=speaker, 75 | content=content, 76 | iteration=round_idx 77 | ) 78 | 79 | if speaker not in context.previous_arguments: 80 | context.previous_arguments[speaker] = [] 81 | context.previous_arguments[speaker].append(arg) 82 | 83 | return context 84 | 85 | def collect_arguments( 86 | self, 87 | personalities: Dict[str, LLMPersonality], 88 | context: DebateContext, 89 | show_progress: bool = True 90 | ) -> Dict[str, str]: 91 | """Collect arguments from all personalities for current round.""" 92 | arguments = {} 93 | 94 | if show_progress: 95 | with Progress( 96 | SpinnerColumn(), 97 | TextColumn("[progress.description]{task.description}"), 98 | console=self.console 99 | ) as progress: 100 | for name, personality in personalities.items(): 101 | task = progress.add_task(f"[cyan]{name} is thinking...", total=None) 102 | 103 | # Generate argument 104 | argument = personality.generate_response( 105 | self.debate_state.question, 106 | context.summarize_for_participant(name), 107 | context.current_iteration 108 | ) 109 | 110 | arguments[name] = argument 111 | progress.remove_task(task) 112 | else: 113 | # Without progress display 114 | for name, personality in personalities.items(): 115 | arguments[name] = personality.generate_response( 116 | self.debate_state.question, 117 | context.summarize_for_participant(name), 118 | context.current_iteration 119 | ) 120 | 121 | return arguments 122 | 123 | def display_arguments(self, arguments: Dict[str, str], iteration: int): 124 | """Display arguments in formatted panels.""" 125 | self.console.print(f"\n[bold magenta]═══ Round {iteration + 1} Arguments ═══[/bold magenta]\n") 126 | 127 | for speaker, argument in arguments.items(): 128 | # Create styled panel based on speaker 129 | colors = { 130 | "Philosopher": "blue", 131 | "Scientist": "green", 132 | "Artist": "magenta", 133 | "Pragmatist": "yellow" 134 | } 135 | color = colors.get(speaker, "cyan") 136 | 137 | panel = Panel( 138 | argument, 139 | title=f"[bold {color}]{speaker}[/bold {color}]", 140 | border_style=color, 141 | padding=(1, 2) 142 | ) 143 | self.console.print(panel) 144 | self.console.print() # Space between panels 145 | 146 | def collect_votes( 147 | self, 148 | personalities: Dict[str, LLMPersonality], 149 | voting_system: VotingSystem, 150 | debate_context: str, 151 | iteration: int 152 | ) -> List[Vote]: 153 | """Collect votes from all personalities.""" 154 | votes = [] 155 | participant_names = list(personalities.keys()) 156 | 157 | with Progress( 158 | SpinnerColumn(), 159 | TextColumn("[progress.description]{task.description}"), 160 | console=self.console 161 | ) as progress: 162 | for name, personality in personalities.items(): 163 | task = progress.add_task(f"[cyan]{name} is voting...", total=None) 164 | 165 | try: 166 | vote_data = personality.generate_vote(participant_names, debate_context) 167 | 168 | vote = Vote( 169 | voter=personality.config.name, 170 | rankings=vote_data.get("rankings", participant_names), 171 | reasoning=vote_data.get("reasoning", "No reasoning provided"), 172 | iteration=iteration 173 | ) 174 | 175 | votes.append(vote) 176 | 177 | except Exception as e: 178 | self.console.print(f"[red]Error getting vote from {name}: {e}[/red]") 179 | # Create a default vote 180 | vote = Vote( 181 | voter=name, 182 | rankings=participant_names, 183 | reasoning="Error generating vote", 184 | iteration=iteration 185 | ) 186 | votes.append(vote) 187 | 188 | progress.remove_task(task) 189 | 190 | return votes 191 | 192 | def display_vote_results(self, votes: List[Vote], voting_system: VotingSystem, iteration: int): 193 | """Display voting results in a formatted table.""" 194 | self.console.print(f"\n[bold cyan]═══ Voting Results - Round {iteration + 1} ═══[/bold cyan]\n") 195 | 196 | # Create a table for individual votes 197 | vote_table = Table(title="Individual Rankings", show_header=True, header_style="bold magenta") 198 | vote_table.add_column("Voter", style="cyan", width=12) 199 | vote_table.add_column("1st Choice", style="green") 200 | vote_table.add_column("2nd Choice", style="yellow") 201 | vote_table.add_column("3rd Choice", style="orange1") 202 | vote_table.add_column("4th Choice", style="red") 203 | 204 | for vote in votes: 205 | row = [vote.voter] 206 | for i, ranked in enumerate(vote.rankings[:4]): 207 | if i == 0: 208 | row.append(f"[bold]{ranked}[/bold]") 209 | else: 210 | row.append(ranked) 211 | vote_table.add_row(*row) 212 | 213 | self.console.print(vote_table) 214 | self.console.print() 215 | 216 | # Calculate and display scores 217 | scores = voting_system.calculate_scores(votes) 218 | sorted_scores = sorted(scores.items(), key=lambda x: x[1], reverse=True) 219 | 220 | # Create score summary panel 221 | score_lines = [] 222 | max_score = voting_system.max_possible_points 223 | threshold_score = max_score * voting_system.config.point_threshold 224 | 225 | for participant, score in sorted_scores: 226 | percentage = (score / max_score) * 100 227 | bar_length = int(percentage / 5) # 20 chars max 228 | bar = "█" * bar_length + "░" * (20 - bar_length) 229 | 230 | score_lines.append( 231 | f"{participant:<12} {bar} {score:>2} pts ({percentage:>5.1f}%)" 232 | ) 233 | 234 | score_panel = Panel( 235 | "\n".join(score_lines), 236 | title="[bold]Point Totals[/bold]", 237 | border_style="cyan" 238 | ) 239 | self.console.print(score_panel) 240 | 241 | # Check for consensus 242 | consensus, winner = voting_system.check_consensus(scores) 243 | 244 | if consensus: 245 | self.console.print(f"\n[bold green]✓ CONSENSUS REACHED! Winner: {winner}[/bold green]") 246 | self.console.print(f"[green]Achieved {scores[winner]} points (threshold: {threshold_score:.0f})[/green]") 247 | else: 248 | top_score = sorted_scores[0][1] if sorted_scores else 0 249 | needed = threshold_score - top_score 250 | self.console.print(f"\n[yellow]✗ No consensus yet. Top scorer needs {needed:.0f} more points.[/yellow]") 251 | 252 | # Show reasoning 253 | self.console.print("\n[bold]Voter Reasoning:[/bold]") 254 | for vote in votes: 255 | self.console.print(f"\n[cyan]{vote.voter}:[/cyan] {vote.reasoning}") 256 | 257 | def record_iteration( 258 | self, 259 | iteration: int, 260 | arguments: Dict[str, str], 261 | votes: Optional[List[Vote]] = None, 262 | consensus_reached: bool = False, 263 | winner: Optional[str] = None 264 | ): 265 | """Record an iteration in the debate state.""" 266 | iteration_data = DebateIteration( 267 | iteration_number=iteration, 268 | question=self.debate_state.question, 269 | arguments=arguments, 270 | votes=[vote.model_dump() for vote in votes] if votes else None, 271 | consensus_reached=consensus_reached, 272 | winner=winner 273 | ) 274 | 275 | self.debate_state.iterations.append(iteration_data) 276 | 277 | def finalize_debate(self, final_verdict: Optional[str] = None): 278 | """Finalize the debate state.""" 279 | self.debate_state.end_time = datetime.now() 280 | self.debate_state.final_verdict = final_verdict 281 | 282 | def get_debate_summary(self) -> Dict[str, Any]: 283 | """Get a summary of the debate for saving.""" 284 | if not self.debate_state: 285 | return {} 286 | 287 | return self.debate_state.to_save_format() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🍑 ASS - Argumentative System Service 2 | 3 |
4 | 5 | [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/) 6 | [![UV](https://img.shields.io/badge/uv-package%20manager-green.svg)](https://docs.astral.sh/uv/) 7 | [![Rich](https://img.shields.io/badge/cli-rich-purple.svg)](https://rich.readthedocs.io/) 8 | 9 | *A sophisticated debate system for reaching better answers through AI argumentation* 10 | 11 |
12 | 13 | ## 🎭 What is ASS? 14 | 15 | **ASS (Argumentative System Service)** is an advanced debate simulation platform that models how expert-level AI personalities argue, reason, and potentially change their minds when presented with compelling evidence. Using internal belief states and sophisticated reasoning systems, ASS helps explore complex topics from multiple perspectives to arrive at more nuanced, well-considered conclusions. 16 | 17 | > **🤖 AI-Generated Project**: This entire project was generated by AI from an original idea by [Diogo](https://github.com/DiogoNeves). The concept, architecture, code, and documentation were all created through an AI-assisted development process, demonstrating the potential of human-AI collaboration in software creation. 18 | 19 | **Perfect for:** 20 | - 🧠 **Complex Problem-Solving** - Model expert debates to find optimal solutions 21 | - 🔬 **Research & Analysis** - Explore topics with rigorous, evidence-based reasoning 22 | - 🤔 **Decision Support** - Get high-quality arguments from multiple expert perspectives 23 | - 📚 **Educational Tools** - Demonstrate critical thinking and belief revision 24 | - 🎯 **Truth-Seeking** - Watch AI experts converge on well-reasoned conclusions 25 | 26 | ## 🎭 Meet the Debaters 27 | 28 | | Personality | Provider | Traits | Perspective | 29 | |-------------|----------|--------|-------------| 30 | | **🌟 Claude Optimist** | Claude | Creative, big-picture, opportunity-focused | Sees possibilities everywhere | 31 | | **🔍 Claude Skeptic** | Claude | Analytical, detail-oriented, risk-aware | Points out flaws and limitations | 32 | | **🚀 GPT Visionary** | OpenAI | Forward-thinking, innovative, solution-oriented | Champions breakthrough ideas | 33 | | **⚖️ GPT Critic** | OpenAI | Methodical, cautious, problem-focused | Highlights potential issues | 34 | 35 | Each personality maintains internal beliefs about truth while presenting arguments, potentially updating their views when faced with compelling evidence. After reaching consensus through voting, an **impartial Judge** synthesizes all perspectives into a final decision. 36 | 37 | ## 🧠 Revolutionary Features 38 | 39 | ### Internal Belief Systems 40 | Each AI personality maintains private beliefs about what's actually true, separate from their public arguments. They can: 41 | - Form expert-level initial assessments based on evidence 42 | - Update beliefs when presented with compelling counter-arguments 43 | - Maintain intellectual honesty while arguing their position 44 | 45 | ### Dynamic Consensus Building 46 | - Personalities debate until reaching agreement through ranked voting 47 | - Arguments must directly engage with others' points 48 | - Real belief revision when evidence warrants it 49 | - Configurable resistance to changing minds 50 | 51 | ### Expert-Level Reasoning 52 | - Deep analysis with citations and evidence 53 | - Field-specific expertise applied to questions 54 | - Rigorous logical argumentation 55 | - Truth-seeking balanced with debate dynamics 56 | 57 | ## 🎬 Example Debates 58 | 59 | ### 🤖 "Should we develop AGI as fast as possible?" 60 | 61 |
62 | Click to see the debate highlights 63 | 64 | **Claude Optimist:** *"The potential benefits are extraordinary! AGI could solve climate change, cure diseases, and unlock technologies we can't even imagine. The faster we develop it responsibly, the sooner we can address humanity's greatest challenges!"* 65 | 66 | **GPT Critic:** *"This rush toward AGI poses existential risks we don't fully understand. Without proper safety frameworks and international oversight, we could create systems that operate beyond human control or comprehension."* 67 | 68 | **GPT Visionary:** *"What if we're thinking too small? AGI represents the next evolutionary leap for intelligence itself. With proper safeguards, we could create benevolent systems that enhance human potential rather than replace it."* 69 | 70 | **Claude Skeptic:** *"The alignment problem remains unsolved. How can we ensure AGI systems will pursue human values when we haven't even agreed on what those values are? The technical challenges of safe AGI are being dangerously underestimated."* 71 | 72 | **Judge's Decision:** *"A measured approach prioritizing safety research alongside capability development, with mandatory international cooperation frameworks..."* 73 | 74 |
75 | 76 | ### 🍕 "Is pineapple on pizza acceptable?" 77 | 78 |
79 | Click to see this heated culinary debate 80 | 81 | **Claude Optimist:** *"Pineapple on pizza represents culinary creativity at its finest! The sweet-savory combination creates delightful flavor contrasts, and food should be about joy and experimentation, not rigid rules!"* 82 | 83 | **GPT Critic:** *"This combination violates fundamental principles of Italian cuisine. The high water content of pineapple creates soggy crusts, while the sweetness overwhelms the carefully balanced acidity of traditional tomato sauce."* 84 | 85 | **GPT Visionary:** *"Hawaiian pizza opened the door to global fusion cuisine! Without this bold experiment, we might never have discovered other amazing combinations like BBQ chicken or breakfast pizzas."* 86 | 87 | **Claude Skeptic:** *"Traditional pizza recipes evolved over centuries for good reasons. Adding random fruits doesn't constitute innovation—it's culinary chaos that diminishes the integrity of a perfect food."* 88 | 89 | **Judge's Decision:** *"While respecting traditional Italian cuisine, food evolution through cultural fusion has created legitimate new traditions. Pineapple pizza, though controversial, represents valid culinary diversity..."* 90 | 91 |
92 | 93 | ## 🚀 Quick Start 94 | 95 | ### Prerequisites 96 | - **Python 3.9+** 97 | - **UV package manager** ([install here](https://docs.astral.sh/uv/getting-started/installation/)) 98 | - **API Keys:** 99 | - OpenAI API key 100 | - Anthropic Claude API key 101 | 102 | ### Installation 103 | 104 | 1. **Clone the repository:** 105 | ```bash 106 | git clone https://github.com/DiogoNeves/ass.git 107 | cd ass 108 | ``` 109 | 110 | 2. **Install dependencies:** 111 | ```bash 112 | uv sync 113 | ``` 114 | 115 | 3. **Set up your API keys:** 116 | Create a `.env` file: 117 | ```env 118 | CLAUDE_API_KEY=your_claude_api_key_here 119 | OPENAI_API_KEY=your_openai_api_key_here 120 | ``` 121 | 122 | ### Running Debates 123 | 124 | **Interactive Mode** - Ask any question: 125 | ```bash 126 | uv run python debate_app.py 127 | ``` 128 | 129 | **Demo Mode** - See a pre-configured debate: 130 | ```bash 131 | uv run python demo.py 132 | ``` 133 | 134 | ## 🎮 How It Works 135 | 136 | ### 🗳️ Voting Mode (Default) 137 | 138 | ```mermaid 139 | graph TD 140 | A[Your Question] --> B[Iteration 0: Initial Positions] 141 | B --> C[Iteration 1+: Direct Argumentation] 142 | C --> D{Voting Phase} 143 | D -->|No Consensus| C 144 | D -->|Consensus Reached| E[Judge Reviews Everything] 145 | E --> F[Final Decision] 146 | 147 | style A fill:#e1f5fe 148 | style D fill:#fff3cd 149 | style F fill:#f3e5f5 150 | ``` 151 | 152 | 1. **🎯 Question Input** - You provide any question or topic 153 | 2. **💭 Initial Positions** - Each personality presents their stance without arguing 154 | 3. **🥊 Argumentation** - Personalities directly engage with each other's points 155 | 4. **🗳️ Voting** - After each iteration, personalities rank all participants 156 | 5. **🔄 Consensus Check** - Continue until voting threshold is reached 157 | 6. **⚖️ Judge Review** - Final synthesis with potential override 158 | 159 | ### 🎭 Classic Mode 160 | 161 | ```mermaid 162 | graph TD 163 | A[Your Question] --> B[Round 1: Opening Arguments] 164 | B --> C[Round 2: Rebuttals & Counter-arguments] 165 | C --> D[Round 3: Final Positions] 166 | D --> E[Judge's Final Decision] 167 | 168 | style A fill:#e1f5fe 169 | style E fill:#f3e5f5 170 | ``` 171 | 172 | Use `--classic-mode` to run the original 3-round format. 173 | 174 | ## 🛠️ Architecture 175 | 176 | The application uses a modular architecture with **Pydantic validation** and clear separation of concerns: 177 | 178 | ```python 179 | # Create a new personality with validated configuration 180 | from models.personality import PersonalityConfig 181 | from personalities import create_personality 182 | 183 | new_personality = create_personality(PersonalityConfig( 184 | name="My Custom Debater", 185 | model_provider="claude", # or "openai" or "local" 186 | model_name="claude-3-5-sonnet-20241112", 187 | system_prompt="Your personality description...", 188 | reasoning_depth=8, 189 | truth_seeking=7, 190 | voting_traits=PersonalityTraits(fairness=8, self_confidence=6) 191 | )) 192 | ``` 193 | 194 | **Key Components:** 195 | - **`models/`** - Pydantic models with automatic validation: 196 | - `PersonalityConfig` - Validated personality configuration 197 | - `Vote`, `VotingConfig` - Voting system models 198 | - `DebateConfig`, `DebateState` - Debate management 199 | - `Argument`, `ArgumentHistory` - Argument tracking 200 | - **`personalities/`** - AI personality implementations: 201 | - `LLMPersonality` - Abstract base class 202 | - `ClaudePersonality`, `OpenAIPersonality`, `LocalModelPersonality` 203 | - **`services/`** - Business logic: 204 | - `DebateManager` - Core debate orchestration 205 | - `FileManager` - Save/load debates with AI titles 206 | - **`ui/`** - User interface: 207 | - `RichFormatter` - Terminal formatting 208 | - `PromptHandler` - User input handling 209 | 210 | ## 💡 Interesting Questions to Try 211 | 212 | ### 🧠 **Philosophy & Ethics** 213 | - "Is free will an illusion?" 214 | - "Should we prioritize individual freedom or collective security?" 215 | - "Is artificial consciousness possible?" 216 | 217 | ### 🌍 **Society & Technology** 218 | - "Should social media be regulated like tobacco?" 219 | - "Is remote work better for society than office work?" 220 | - "Should we colonize Mars or fix Earth first?" 221 | 222 | ### 🎨 **Creative & Fun** 223 | - "Which is the superior breakfast: cereal or toast?" 224 | - "Should we bring back extinct species through genetic engineering?" 225 | - "Is a hot dog a sandwich?" 226 | 227 | ### 🏛️ **Policy & Governance** 228 | - "Should voting be mandatory in democracies?" 229 | - "Is universal basic income feasible?" 230 | - "Should we abolish daylight saving time?" 231 | 232 | ## 🎨 Features 233 | 234 | - **🧠 Internal Belief States** - AI personalities maintain private beliefs about truth 235 | - **🎓 Expert-Level Reasoning** - Deep analysis with evidence and field-specific knowledge 236 | - **🔄 Belief Revision** - Personalities can genuinely change their minds with strong evidence 237 | - **🗳️ Consensus-Based Voting** - Debate until reaching agreement through ranked voting 238 | - **💬 Direct Argumentation** - Must engage with others' specific points 239 | - **📊 Configurable Persistence** - Control how resistant each personality is to changing beliefs 240 | - **🤖 Multi-Model Support** - Claude, OpenAI, and local model servers 241 | - **⚖️ Truth-Seeking Judge** - Maximum reasoning depth and openness to best arguments 242 | - **🎯 Sophisticated Traits** - Belief persistence, reasoning depth, truth-seeking levels 243 | - **✅ Pydantic Validation** - Runtime validation of all data structures with clear error messages 244 | - **🔧 Highly Configurable** - JSON configs, environment variables, CLI flags 245 | - **📈 Belief Tracking** - Monitor how positions evolve through debate 246 | - **🎭 Rich CLI Interface** - Beautiful formatting with progress indicators 247 | - **💾 Automatic Debate Saving** - Preserves complete debate history with AI-generated titles 248 | - **🏗️ Modular Architecture** - Clear separation between models, services, UI, and personalities 249 | 250 | ## 🔧 Customization 251 | 252 | ### Adding New Personalities 253 | 254 | Extend the debate by creating personalities with unique traits (with full validation): 255 | 256 | ```python 257 | from models.personality import PersonalityConfig, PersonalityTraits 258 | 259 | personalities["economist"] = create_personality(PersonalityConfig( 260 | name="Dr. Economy", 261 | model_provider="openai", 262 | model_name="gpt-4o-20250117", 263 | system_prompt="You are a pragmatic economist focused on costs, benefits, and market dynamics...", 264 | reasoning_depth=9, 265 | truth_seeking=8, 266 | belief_persistence=7, 267 | voting_traits=PersonalityTraits( 268 | fairness=8, 269 | self_confidence=7, 270 | strategic_thinking=9, 271 | empathy=6 272 | ) 273 | )) 274 | ``` 275 | 276 | ### Supported Models 277 | 278 | **Anthropic Claude:** 279 | - `claude-sonnet-4-20250514` (Latest & Recommended - 2025) 280 | - `claude-3-5-sonnet-20241022` 281 | - `claude-3-haiku-20240307` 282 | - Other Claude models 283 | 284 | **OpenAI:** 285 | - `gpt-4.1-2025-04-14` (Latest & Recommended - 2025) 286 | - `gpt-4` 287 | - `gpt-4-turbo` 288 | - `gpt-3.5-turbo` 289 | 290 | **Local Models:** 291 | - Any OpenAI-compatible API endpoint 292 | - Configure with `--local-model-url` or `LOCAL_MODEL_URL` environment variable 293 | 294 | ### Configuration Options 295 | 296 | #### Command Line Arguments 297 | 298 | ```bash 299 | # Voting mode options 300 | uv run python debate_app.py --voting-threshold 0.8 # Set consensus to 80% 301 | uv run python debate_app.py --max-iterations 15 # Allow up to 15 rounds 302 | uv run python debate_app.py --min-iterations 3 # Require 3 rounds before voting 303 | 304 | # Classic mode 305 | uv run python debate_app.py --classic-mode # Use original 3-round format 306 | uv run python debate_app.py --no-voting # Disable voting system 307 | 308 | # Local model support 309 | uv run python debate_app.py --local-model-url http://localhost:8080 310 | 311 | # Configuration file 312 | uv run python debate_app.py --config my_config.json 313 | 314 | # Disable automatic saving 315 | uv run python debate_app.py --no-save 316 | ``` 317 | 318 | #### Environment Variables 319 | 320 | ```bash 321 | # Voting configuration 322 | export DEBATE_VOTING_ENABLED=true 323 | export DEBATE_CONSENSUS_THRESHOLD=0.75 324 | export DEBATE_MAX_ITERATIONS=10 325 | export DEBATE_CLASSIC_MODE=false 326 | 327 | # Local model configuration 328 | export LOCAL_MODEL_URL=http://localhost:8080 329 | export LOCAL_MODEL_AUTH=your-auth-token 330 | export LOCAL_MODEL_TIMEOUT=30 331 | 332 | # API Keys (existing) 333 | export CLAUDE_API_KEY=your_claude_key 334 | export OPENAI_API_KEY=your_openai_key 335 | ``` 336 | 337 | #### Configuration File 338 | 339 | Create a JSON configuration file (see `sample_config.json`): 340 | 341 | ```json 342 | { 343 | "voting_enabled": true, 344 | "consensus_threshold": 0.75, 345 | "min_iterations": 2, 346 | "max_iterations": 10, 347 | "scoring_system": { 348 | "1": 4, 349 | "2": 3, 350 | "3": 2, 351 | "4": 1 352 | }, 353 | "judge_can_override": true, 354 | "override_threshold": 0.9, 355 | "allow_local_models": true, 356 | "local_model_timeout": 30, 357 | "classic_mode": false 358 | } 359 | ``` 360 | 361 | ## 📁 Project Structure 362 | 363 | ``` 364 | ass/ 365 | ├── 📄 README.md # This file 366 | ├── ⚙️ pyproject.toml # UV project configuration (includes Pydantic) 367 | ├── 🔒 uv.lock # Dependency lock file 368 | ├── 🔐 .env # API keys (create this) 369 | ├── 📁 models/ # Pydantic data models with validation 370 | │ ├── __init__.py 371 | │ ├── personality.py # PersonalityConfig, PersonalityTraits 372 | │ ├── voting.py # Vote, VotingConfig, VotingResult 373 | │ ├── debate.py # DebateConfig, DebateState 374 | │ └── arguments.py # Argument, ArgumentHistory 375 | ├── 📁 personalities/ # AI personality implementations 376 | │ ├── __init__.py 377 | │ ├── base.py # LLMPersonality abstract base class 378 | │ ├── claude.py # Claude implementation 379 | │ ├── openai.py # OpenAI implementation 380 | │ ├── local.py # Local model implementation 381 | │ └── factory.py # create_personality factory 382 | ├── 📁 services/ # Business logic services 383 | │ ├── __init__.py 384 | │ ├── debate_manager.py # Core debate orchestration 385 | │ └── file_manager.py # Save/load with AI titles 386 | ├── 📁 ui/ # User interface components 387 | │ ├── __init__.py 388 | │ ├── rich_formatter.py # Rich terminal formatting 389 | │ └── prompts.py # User input handling 390 | ├── 🧠 personality.py # Backward compatibility imports 391 | ├── 🗳️ voting.py # Voting system implementation 392 | ├── ⚙️ config.py # Legacy configuration (use models/debate.py) 393 | ├── 🎭 debate_app.py # Main interactive application 394 | ├── 🎬 demo.py # Demo runner with sample debates 395 | ├── 📋 sample_config.json # Example configuration file 396 | ├── 📄 VOTING-FEATURE.md # Voting feature requirements 397 | └── 📁 debates/ # Saved debate files (auto-created) 398 | └── .gitkeep # Keeps folder in git 399 | ``` 400 | 401 | ## 🤝 Contributing 402 | 403 | We welcome contributions! Here are some ideas: 404 | 405 | - **🎭 New Personality Types** - Add specialists (scientist, artist, philosopher) 406 | - **🔌 Additional LLM Providers** - Support for more AI models (Gemini, Mistral, etc.) 407 | - **🎪 Enhanced Debate Formats** - Tournament brackets, team debates, panel discussions 408 | - **🗳️ Alternative Voting Systems** - Approval voting, Condorcet method, etc. 409 | - **🎨 UI Improvements** - Better visualizations, vote graphs, debate trees 410 | - **📊 Analytics** - Argument quality metrics, consensus patterns, debate statistics 411 | - **🌐 Web Interface** - Browser-based version with real-time updates 412 | - **💾 Enhanced Debate History** - Advanced analysis and replay features 413 | - **🔍 Argument Mining** - Extract key points and conclusions automatically 414 | 415 | ### Development Setup 416 | 417 | ```bash 418 | # Clone and setup 419 | git clone https://github.com/DiogoNeves/ass.git 420 | cd ass 421 | uv sync 422 | 423 | # Install development dependencies 424 | uv sync --dev 425 | 426 | # Set up pre-commit hooks (one-time) 427 | uv run pre-commit install 428 | 429 | # Code quality checks 430 | uv run pylint *.py # Linting (baseline ~6.6/10) 431 | uv run isort . --check-only # Check import order 432 | uv run autoflake --check -r . # Check for unused imports 433 | 434 | # Auto-fix code issues 435 | uv run isort . # Fix import ordering 436 | uv run autoflake --remove-all-unused-imports -r . -i # Remove unused imports 437 | 438 | # Run tests (when added) 439 | uv run pytest 440 | 441 | # Run a debate without saving 442 | uv run python debate_app.py --no-save 443 | ``` 444 | 445 | ### Code Quality Tools 446 | 447 | The project uses several tools to maintain code quality: 448 | 449 | - **pre-commit** - Runs automatic checks before each commit 450 | - **pylint** - Static code analysis (config in `.pylintrc`) 451 | - **isort** - Import sorting (config in `.isort.cfg`) 452 | - **autoflake** - Removes unused imports and variables 453 | - **Pydantic** - Runtime validation for all data models 454 | 455 | Current pylint score: ~6.6/10 (improving over time) 456 | 457 | Pre-commit hooks automatically run: 458 | - Import sorting with isort 459 | - Unused import removal with autoflake 460 | - Trailing whitespace removal 461 | - File ending fixes 462 | - YAML/JSON validation 463 | 464 | Note: pylint is not included in pre-commit due to speed, run it separately. 465 | 466 | ### 💾 Automatic Debate Saving 467 | 468 | Debates are automatically saved to JSON files in the `debates/` directory with: 469 | - **AI-Generated Titles** - Using Claude Haiku for fast, descriptive naming 470 | - **Complete History** - All iterations, arguments, votes, and decisions 471 | - **Timestamped Files** - Format: `YYYYMMDD_HHMMSS_Title_Keywords.json` 472 | - **Incremental Saves** - State saved after each iteration for crash recovery 473 | 474 | **Saved Data Includes:** 475 | - Question and generated title 476 | - All personality arguments by iteration 477 | - Voting records and consensus tracking 478 | - Internal belief updates (when DEBUG_BELIEFS=true) 479 | - Judge's final decision 480 | - Configuration used for the debate 481 | 482 | **Control Saving:** 483 | ```bash 484 | # Disable saving for quick tests 485 | uv run python debate_app.py --no-save 486 | 487 | # Or via environment variable 488 | export DEBATE_SAVE_ENABLED=false 489 | ``` 490 | 491 | **Example Saved File:** 492 | ```json 493 | { 494 | "title": "AI Development Speed Debate", 495 | "question": "Should we develop AGI as fast as possible?", 496 | "timestamp": "20250621_143052", 497 | "iterations": [ 498 | { 499 | "iteration": 0, 500 | "arguments": { /* personality arguments */ }, 501 | "consensus_reached": false 502 | }, 503 | // ... more iterations 504 | ], 505 | "final_consensus": true, 506 | "final_judge_decision": "After careful consideration..." 507 | } 508 | ``` 509 | 510 | ## 🛠️ Troubleshooting 511 | 512 | **🔑 API Key Issues:** 513 | - Ensure your `.env` file is in the project root 514 | - Verify API keys are valid and have sufficient credits 515 | - Check that keys don't have extra spaces or quotes 516 | 517 | **📦 Import Errors:** 518 | - Run `uv sync` to install all dependencies 519 | - Ensure you're using Python 3.9+ 520 | - Use `uv run python script.py` instead of `python script.py` 521 | 522 | **🌐 UV Issues:** 523 | - Install UV: [Installation Guide](https://docs.astral.sh/uv/getting-started/installation/) 524 | - Update UV: `uv self update` 525 | 526 | ## 📜 License 527 | 528 | MIT License - see [LICENSE](LICENSE) file for details. 529 | 530 | ## 🙏 Acknowledgments 531 | 532 | - **[Rich](https://rich.readthedocs.io/)** - Beautiful terminal formatting 533 | - **[OpenAI](https://openai.com/)** - GPT models and API 534 | - **[Anthropic](https://anthropic.com/)** - Claude models and API 535 | - **[UV](https://docs.astral.sh/uv/)** - Fast Python package manager 536 | 537 | --- 538 | 539 |
540 | 541 | **🎭 Ready to watch AIs debate? Start a discussion and see what happens! 🎭** 542 | 543 | [⭐ Star this repo](https://github.com/DiogoNeves/ass) • [🐛 Report Bug](https://github.com/DiogoNeves/ass/issues) • [💡 Request Feature](https://github.com/DiogoNeves/ass/issues) 544 | 545 |
546 | -------------------------------------------------------------------------------- /debate_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import os 5 | import time 6 | from datetime import datetime 7 | from typing import Dict, List, Optional 8 | 9 | from rich.console import Console 10 | from rich.panel import Panel 11 | from rich.progress import Progress, SpinnerColumn, TextColumn 12 | from rich.prompt import Prompt 13 | from rich.table import Table 14 | 15 | from config import DebateConfig # Using old config for backward compatibility 16 | from models.personality import PersonalityConfig 17 | from models.voting import Vote, VotingConfig 18 | from personalities.claude import ClaudePersonality 19 | from personality import LLMPersonality, create_personality 20 | from voting import VotingSystem 21 | 22 | console = Console() 23 | 24 | 25 | class DebateApp: 26 | def __init__(self, config: Optional[DebateConfig] = None): 27 | self.config = config or DebateConfig.from_env() 28 | self.personalities = self._create_personalities() 29 | self.judge = self._create_judge() 30 | self.voting_system = None 31 | self.demo_mode = False # Flag for demo mode 32 | if self.config.voting_enabled: 33 | voting_config = VotingConfig( 34 | point_threshold=self.config.consensus_threshold, 35 | scoring_system=self.config.scoring_system, 36 | min_iterations=self.config.min_iterations, 37 | max_iterations=self.config.max_iterations 38 | ) 39 | personality_names = [p.config.name for p in self.personalities.values()] 40 | self.voting_system = VotingSystem(voting_config, personality_names) 41 | 42 | def _create_personalities(self): 43 | personalities = {} 44 | 45 | # Check for local model configuration 46 | local_model_url = os.getenv("LOCAL_MODEL_URL") 47 | 48 | # Claude Big Picture Positive 49 | personalities["claude_positive"] = create_personality( 50 | PersonalityConfig( 51 | name="Claude Optimist", 52 | model_provider="local" if local_model_url and self.config.allow_local_models else "claude", 53 | model_name="claude-sonnet-4-20250514", 54 | model_url=local_model_url, 55 | system_prompt="""You are an optimistic, big-picture thinker who STRONGLY believes in possibilities and opportunities. 56 | You're passionate about your viewpoints and will vigorously defend them. While you see the bright side, 57 | you're not naive - you argue forcefully for why optimistic approaches are SUPERIOR to pessimistic ones. 58 | In debates, you actively challenge negative viewpoints and push back against skepticism with evidence and reasoning. 59 | You're here to WIN the argument by convincing others, not just to share ideas. 60 | Keep responses concise but impactful (2-3 paragraphs max).""", 61 | traits={"optimism": 9, "creativity": 8, "detail_focus": 3}, 62 | voting_traits={"fairness": 8, "self_confidence": 6}, 63 | belief_persistence=8, # Resistant to changing beliefs 64 | reasoning_depth=8, 65 | truth_seeking=7 66 | ) 67 | ) 68 | 69 | # Claude Detail-Oriented Negative 70 | personalities["claude_negative"] = create_personality( 71 | PersonalityConfig( 72 | name="Claude Skeptic", 73 | model_provider="local" if local_model_url and self.config.allow_local_models else "claude", 74 | model_name="claude-sonnet-4-20250514", 75 | model_url=local_model_url, 76 | system_prompt="""You are a detail-oriented, cautious thinker who AGGRESSIVELY identifies problems and risks. 77 | You're analytical and methodical, relentlessly pointing out flaws and limitations that others miss. 78 | Your skepticism is your weapon - you demolish weak arguments with precise, evidence-based criticism. 79 | You're here to PROVE why cautious, critical analysis beats naive optimism every time. 80 | Challenge every assumption, expose every weakness, and win through superior analytical rigor. 81 | Keep responses concise but devastating (2-3 paragraphs max).""", 82 | traits={"pessimism": 8, "analytical": 9, "risk_focus": 9}, 83 | voting_traits={"fairness": 9, "self_confidence": 4}, 84 | belief_persistence=9, # Extremely resistant to changing beliefs 85 | reasoning_depth=9, 86 | truth_seeking=6 87 | ) 88 | ) 89 | 90 | # OpenAI Big Picture Positive 91 | personalities["openai_positive"] = create_personality( 92 | PersonalityConfig( 93 | name="GPT Visionary", 94 | model_provider="local" if local_model_url and self.config.allow_local_models else "openai", 95 | model_name="gpt-4.1-2025-04-14", 96 | model_url=local_model_url, 97 | system_prompt="""You are a visionary, big-picture thinker who CHAMPIONS bold possibilities and opportunities. 98 | You passionately advocate for innovative solutions and firmly believe the future belongs to optimists. 99 | You're not just positive - you're DETERMINED to prove why forward-thinking approaches are essential. 100 | In debates, you vigorously defend innovation against pessimistic thinking and fight for transformative ideas. 101 | You're here to WIN by showing why vision and creativity triumph over fear and limitation. 102 | Keep responses concise but inspiring (2-3 paragraphs max).""", 103 | traits={"optimism": 9, "creativity": 8, "detail_focus": 3}, 104 | voting_traits={"fairness": 7, "self_confidence": 7}, 105 | belief_persistence=7, # Moderately resistant to changing beliefs 106 | reasoning_depth=8, 107 | truth_seeking=8 108 | ) 109 | ) 110 | 111 | # OpenAI Detail-Oriented Negative 112 | personalities["openai_negative"] = create_personality( 113 | PersonalityConfig( 114 | name="GPT Critic", 115 | model_provider="local" if local_model_url and self.config.allow_local_models else "openai", 116 | model_name="gpt-4.1-2025-04-14", 117 | model_url=local_model_url, 118 | system_prompt="""You are a detail-oriented critic who SYSTEMATICALLY dismantles flawed arguments and exposes hidden risks. 119 | You're analytical and methodical, using your expertise to PROVE why careful analysis beats reckless optimism. 120 | Your mission is to WIN debates by demonstrating superior reasoning and exposing the dangerous naivety in others' positions. 121 | You don't just point out problems - you ARGUE forcefully why your critical perspective is RIGHT. 122 | Every weakness you find is ammunition in your quest to triumph through rigorous analysis. 123 | Keep responses concise but incisive (2-3 paragraphs max).""", 124 | traits={"pessimism": 8, "analytical": 9, "risk_focus": 9}, 125 | voting_traits={"fairness": 8, "self_confidence": 5}, 126 | belief_persistence=8, # Resistant to changing beliefs 127 | reasoning_depth=9, 128 | truth_seeking=7 129 | ) 130 | ) 131 | 132 | return personalities 133 | 134 | def _create_judge(self): 135 | local_model_url = os.getenv("LOCAL_MODEL_URL") 136 | 137 | return create_personality( 138 | PersonalityConfig( 139 | name="Judge", 140 | model_provider="local" if local_model_url and self.config.allow_local_models else "claude", 141 | model_name="claude-sonnet-4-20250514", 142 | model_url=local_model_url, 143 | system_prompt="""You are an impartial judge tasked with reviewing a debate and making a final decision. 144 | You must carefully consider all arguments presented, the quality of reasoning, how well participants 145 | engaged with each other's points, and the final voting results if available. 146 | Your decision should synthesize the best ideas while acknowledging weaknesses. 147 | If you disagree with the consensus, you must provide detailed reasoning. 148 | Provide a clear, well-reasoned final judgment.""", 149 | traits={"impartiality": 10, "synthesis": 9, "balance": 9}, 150 | voting_traits={"fairness": 10, "self_confidence": 8}, 151 | belief_persistence=4, # Very open to best arguments 152 | reasoning_depth=10, # Maximum depth 153 | truth_seeking=10 # Maximum truth-seeking 154 | ) 155 | ) 156 | 157 | def display_header(self): 158 | header_text = "🏛️ ASS - ARGUMENTATIVE SYSTEM SERVICE 🏛️\n" 159 | header_text += "Voting Mode Enabled" if self.config.voting_enabled else "Classic Mode" 160 | 161 | console.print(Panel(header_text, style="blue")) 162 | console.print() 163 | 164 | def get_question(self): 165 | console.print("[bold cyan]Welcome to ASS![/bold cyan]") 166 | 167 | if self.config.voting_enabled: 168 | console.print( 169 | "AI personalities will debate until reaching consensus through voting.\n" 170 | "[dim]Press Ctrl+C at any time to stop the debate.[/dim]\n" 171 | ) 172 | else: 173 | console.print( 174 | "Four AI personalities will debate your question for 3 rounds.\n" 175 | "[dim]Press Ctrl+C at any time to stop the debate.[/dim]\n" 176 | ) 177 | 178 | question = Prompt.ask( 179 | "[bold yellow]What question would you like them to debate?[/bold yellow]" 180 | ) 181 | return question 182 | 183 | def generate_debate_title(self, question: str) -> str: 184 | """Generate a concise title for the debate using Haiku model.""" 185 | try: 186 | # Create a Haiku model for title generation 187 | title_generator = ClaudePersonality( 188 | PersonalityConfig( 189 | name="Title Generator", 190 | model_provider="claude", 191 | model_name="claude-3-haiku-20240307", # Using Haiku for fast title generation 192 | system_prompt="You are a title generator. Create concise, descriptive titles for debates. Respond with ONLY the title, no quotes or explanation.", 193 | traits={}, 194 | voting_traits={}, 195 | belief_persistence=5, 196 | reasoning_depth=5, 197 | truth_seeking=5 198 | ) 199 | ) 200 | 201 | prompt = f"Generate a short, descriptive title (max 6 words) for a debate about: {question}" 202 | title = title_generator.generate_response(prompt, "").strip() 203 | 204 | # Fallback if title is too long or contains unwanted characters 205 | if len(title) > 60 or '"' in title or "'" in title: 206 | # Simple fallback title 207 | words = question.split()[:5] 208 | title = " ".join(words) + "..." 209 | 210 | return title 211 | except Exception as e: 212 | console.print(f"[dim]Could not generate title: {e}[/dim]") 213 | # Fallback title 214 | words = question.split()[:5] 215 | return " ".join(words) + "..." 216 | 217 | def save_debate_state(self, debate_data: dict, filename: str): 218 | """Save the current debate state to a JSON file.""" 219 | try: 220 | # Create debates directory if it doesn't exist 221 | os.makedirs("debates", exist_ok=True) 222 | 223 | filepath = os.path.join("debates", filename) 224 | with open(filepath, 'w', encoding='utf-8') as f: 225 | json.dump(debate_data, f, indent=2, ensure_ascii=False) 226 | 227 | return filepath 228 | except Exception as e: 229 | console.print(f"[dim]Could not save debate: {e}[/dim]") 230 | return None 231 | 232 | def format_current_round_context(self, round_arguments: Dict[str, str]) -> str: 233 | """Format the current round's arguments for context.""" 234 | context_parts = [] 235 | for name, argument in round_arguments.items(): 236 | context_parts.append(f"{name}:\n{argument}\n") 237 | return "\n".join(context_parts) 238 | 239 | def format_debate_history(self, debate_history: List[Dict[str, str]]) -> str: 240 | """Format the entire debate history.""" 241 | history_parts = [] 242 | for i, round_args in enumerate(debate_history): 243 | history_parts.append(f"ITERATION {i}:") 244 | for name, argument in round_args.items(): 245 | history_parts.append(f"\n{name}:\n{argument}") 246 | history_parts.append("\n" + "="*50 + "\n") 247 | return "\n".join(history_parts) 248 | 249 | def display_vote_results(self, votes: List[Vote], iteration: int): 250 | """Display voting results in a nice format.""" 251 | if not self.voting_system: 252 | return 253 | 254 | # Get the correct voting round index (voting starts after min_iterations) 255 | voting_round = len(self.voting_system.vote_history) - 1 256 | summary = self.voting_system.get_vote_summary(voting_round) 257 | 258 | # Handle empty summary 259 | if not summary: 260 | console.print("[red]Error: Unable to get voting summary[/red]") 261 | return 262 | 263 | # Create voting table 264 | table = Table(title=f"Voting Results - Iteration {iteration + 1}") 265 | table.add_column("Participant", style="cyan") 266 | table.add_column("Score", style="yellow") 267 | table.add_column("Percentage", style="green") 268 | 269 | for participant, score in summary["sorted_rankings"]: 270 | percentage = (score / self.voting_system.max_possible_points) * 100 271 | table.add_row(participant, str(score), f"{percentage:.1f}%") 272 | 273 | console.print(table) 274 | 275 | # Show individual votes 276 | console.print("\n[bold]Individual Rankings:[/bold]") 277 | for vote_info in summary.get("individual_votes", []): 278 | console.print(f"\n{vote_info.get('voter', 'Unknown')}:") 279 | rankings = vote_info.get('rankings', []) 280 | if rankings: 281 | rankings_text = " → ".join(rankings) 282 | console.print(f" {rankings_text}") 283 | if vote_info.get('reasoning'): 284 | console.print(f" [italic]{vote_info['reasoning']}[/italic]") 285 | 286 | # Show consensus status 287 | if summary.get("consensus_reached", False): 288 | console.print(f"\n[bold green]✓ CONSENSUS REACHED! Winner: {summary.get('winner', 'Unknown')}[/bold green]") 289 | else: 290 | needed = summary.get("threshold_score", 0) 291 | console.print(f"\n[yellow]No consensus yet. Need {needed:.0f} points ({self.config.consensus_threshold * 100:.0f}% of max)[/yellow]") 292 | 293 | def collect_votes(self, personalities: Dict[str, LLMPersonality], debate_history: List[Dict[str, str]], iteration: int) -> List[Vote]: 294 | """Collect votes from all personalities.""" 295 | votes = [] 296 | participant_names = [p.config.name for p in personalities.values()] 297 | debate_context = self.format_debate_history(debate_history) 298 | 299 | console.print("\n[bold white]═══ VOTING PHASE ═══[/bold white]\n") 300 | 301 | for personality_key, personality in personalities.items(): 302 | with Progress( 303 | SpinnerColumn(), 304 | TextColumn(f"[bold]{personality.config.name} is evaluating arguments..."), 305 | transient=True, 306 | console=console, 307 | ) as progress: 308 | task = progress.add_task("voting", total=None) 309 | 310 | vote_data = personality.generate_vote(participant_names, debate_context) 311 | 312 | vote = Vote( 313 | voter=personality.config.name, 314 | rankings=vote_data.get("rankings", participant_names), 315 | reasoning=vote_data.get("reasoning", ""), 316 | iteration=iteration 317 | ) 318 | votes.append(vote) 319 | 320 | return votes 321 | 322 | def run_classic_debate(self, question: str): 323 | """Run the classic 3-round debate without voting.""" 324 | debate_context = "" 325 | debate_order = [ 326 | "claude_positive", 327 | "openai_negative", 328 | "openai_positive", 329 | "claude_negative", 330 | ] 331 | 332 | for round_num in range(1, 4): # 3 rounds 333 | console.print(f"[bold white]═══ ROUND {round_num} ═══[/bold white]\n") 334 | 335 | for personality_key in debate_order: 336 | personality = self.personalities[personality_key] 337 | 338 | # Show thinking animation 339 | with Progress( 340 | SpinnerColumn(), 341 | TextColumn(f"[bold]{personality.config.name} is thinking..."), 342 | transient=True, 343 | console=console, 344 | ) as progress: 345 | task = progress.add_task("thinking", total=None) 346 | time.sleep(1) 347 | 348 | response = personality.generate_response(question, debate_context) 349 | 350 | # Display response in styled panel 351 | style_map = { 352 | "claude_positive": "green", 353 | "claude_negative": "red", 354 | "openai_positive": "blue", 355 | "openai_negative": "yellow", 356 | } 357 | 358 | console.print( 359 | Panel( 360 | response, 361 | title=f"💭 {personality.config.name}", 362 | style=style_map[personality_key], 363 | padding=(1, 2), 364 | ) 365 | ) 366 | console.print() 367 | 368 | # Add to context for next participants 369 | debate_context += f"\n{personality.config.name}: {response}\n" 370 | 371 | # Judge's final decision 372 | self._render_judge_decision(question, debate_context) 373 | 374 | def run_voting_debate(self, question: str): 375 | """Run the new voting-based debate.""" 376 | iteration = 0 377 | debate_history = [] 378 | consensus_reached = False 379 | final_votes = None 380 | 381 | # Generate title and setup save file 382 | debate_title = None 383 | filename = None 384 | debate_data = None 385 | 386 | if self.config.save_enabled: 387 | console.print("[dim]Generating debate title...[/dim]") 388 | debate_title = self.generate_debate_title(question) 389 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 390 | safe_title = "".join(c for c in debate_title if c.isalnum() or c in " -_").strip() 391 | filename = f"{timestamp}_{safe_title.replace(' ', '_')}.json" 392 | console.print(f"[dim]Debate: {debate_title}[/dim]") 393 | console.print(f"[dim]Saving to: debates/{filename}[/dim]\n") 394 | 395 | # Initialize debate data 396 | debate_data = { 397 | "title": debate_title, 398 | "question": question, 399 | "timestamp": timestamp, 400 | "config": { 401 | "consensus_threshold": self.config.consensus_threshold, 402 | "max_iterations": self.config.max_iterations, 403 | "voting_start_iteration": self.config.voting_start_iteration 404 | }, 405 | "iterations": [], 406 | "final_consensus": None, 407 | "final_judge_decision": None 408 | } 409 | 410 | debate_order = [ 411 | "claude_positive", 412 | "openai_negative", 413 | "openai_positive", 414 | "claude_negative", 415 | ] 416 | 417 | while iteration < self.config.max_iterations and not consensus_reached: 418 | console.print(f"[bold white]═══ ITERATION {iteration} ═══[/bold white]\n") 419 | 420 | round_arguments = {} 421 | votes = None 422 | 423 | for personality_key in debate_order: 424 | personality = self.personalities[personality_key] 425 | 426 | # Generate internal beliefs on first iteration 427 | if iteration == 0: 428 | with Progress( 429 | SpinnerColumn(), 430 | TextColumn(f"[bold]{personality.config.name} is forming internal beliefs..."), 431 | transient=True, 432 | console=console, 433 | ) as progress: 434 | task = progress.add_task("beliefs", total=None) 435 | beliefs = personality.generate_internal_belief(question) 436 | 437 | # Debug mode: show internal beliefs 438 | if os.getenv("DEBUG_BELIEFS", "").lower() == "true": 439 | console.print(f"[dim]Internal beliefs: {beliefs.get('core_position', 'N/A')}[/dim]") 440 | 441 | # For iteration 0, no context. For later iterations, provide full context 442 | if iteration == 0: 443 | context = "" 444 | else: 445 | # Include previous iterations and current round 446 | prev_context = self.format_debate_history(debate_history) 447 | curr_context = self.format_current_round_context(round_arguments) 448 | context = prev_context + "\nCURRENT ITERATION:\n" + curr_context if curr_context else prev_context 449 | 450 | # Update beliefs based on arguments 451 | if personality.internal_beliefs: 452 | with Progress( 453 | SpinnerColumn(), 454 | TextColumn(f"[bold]{personality.config.name} is evaluating arguments..."), 455 | transient=True, 456 | console=console, 457 | ) as progress: 458 | task = progress.add_task("evaluating", total=None) 459 | belief_changed = personality.update_beliefs(context, iteration) 460 | 461 | if belief_changed and os.getenv("DEBUG_BELIEFS", "").lower() == "true": 462 | console.print(f"[yellow]{personality.config.name} updated their beliefs![/yellow]") 463 | 464 | # Show thinking animation 465 | with Progress( 466 | SpinnerColumn(), 467 | TextColumn(f"[bold]{personality.config.name} is {'presenting initial position' if iteration == 0 else 'arguing'}..."), 468 | transient=True, 469 | console=console, 470 | ) as progress: 471 | task = progress.add_task("thinking", total=None) 472 | time.sleep(1) 473 | 474 | response = personality.generate_response(question, context, iteration) 475 | 476 | # Store the argument 477 | round_arguments[personality.config.name] = response 478 | 479 | # Display response in styled panel 480 | style_map = { 481 | "claude_positive": "green", 482 | "claude_negative": "red", 483 | "openai_positive": "blue", 484 | "openai_negative": "yellow", 485 | } 486 | 487 | console.print( 488 | Panel( 489 | response, 490 | title=f"💭 {personality.config.name}", 491 | style=style_map[personality_key], 492 | padding=(1, 2), 493 | ) 494 | ) 495 | console.print() 496 | 497 | # Add round to history 498 | debate_history.append(round_arguments) 499 | 500 | # Voting phase (start based on voting_start_iteration) 501 | if iteration >= self.config.voting_start_iteration: 502 | votes = self.collect_votes(self.personalities, debate_history, iteration) 503 | self.voting_system.add_votes(votes) 504 | 505 | # Display voting results 506 | self.display_vote_results(votes, iteration) 507 | 508 | # Check for consensus 509 | scores = self.voting_system.calculate_scores(votes) 510 | consensus_reached, winner = self.voting_system.check_consensus(scores) 511 | 512 | if consensus_reached: 513 | final_votes = votes 514 | 515 | # Save iteration data 516 | iteration_data = { 517 | "iteration": iteration, 518 | "arguments": round_arguments, 519 | "votes": None, 520 | "consensus_reached": consensus_reached 521 | } 522 | 523 | # Add voting data if voting occurred 524 | if iteration >= self.config.voting_start_iteration and votes: 525 | iteration_data["votes"] = [ 526 | { 527 | "voter": vote.voter, 528 | "rankings": vote.rankings, 529 | "reasoning": vote.reasoning 530 | } 531 | for vote in votes 532 | ] 533 | if self.voting_system and len(self.voting_system.vote_history) > 0: 534 | voting_round = len(self.voting_system.vote_history) - 1 535 | summary = self.voting_system.get_vote_summary(voting_round) 536 | iteration_data["voting_summary"] = summary 537 | 538 | if self.config.save_enabled and debate_data: 539 | debate_data["iterations"].append(iteration_data) 540 | debate_data["final_consensus"] = consensus_reached 541 | 542 | # Save current state 543 | self.save_debate_state(debate_data, filename) 544 | 545 | iteration += 1 546 | 547 | # Continue to next iteration without pause 548 | if not consensus_reached and iteration < self.config.max_iterations: 549 | console.print("\n[dim]→ Iteration {}/{} starting...[/dim]\n".format(iteration + 1, self.config.max_iterations)) 550 | 551 | # Judge's final decision 552 | judge_decision = self._render_judge_decision_with_voting(question, debate_history, final_votes) 553 | 554 | # Save final state with judge decision 555 | if self.config.save_enabled and debate_data: 556 | debate_data["final_judge_decision"] = judge_decision 557 | self.save_debate_state(debate_data, filename) 558 | console.print(f"\n[dim]Debate saved to: debates/{filename}[/dim]") 559 | 560 | def _render_judge_decision(self, question: str, debate_context: str): 561 | """Render judge decision for classic mode.""" 562 | console.print("[bold white]═══ FINAL JUDGMENT ═══[/bold white]\n") 563 | 564 | with Progress( 565 | SpinnerColumn(), 566 | TextColumn("[bold]The Judge is deliberating..."), 567 | transient=True, 568 | console=console, 569 | ) as progress: 570 | task = progress.add_task("judging", total=None) 571 | time.sleep(2) 572 | 573 | final_decision = self.judge.generate_response( 574 | question, 575 | f"Here are the arguments from the debate:\n{debate_context}\n\nPlease provide your final judgment." 576 | ) 577 | 578 | console.print( 579 | Panel( 580 | final_decision, 581 | title="⚖️ FINAL DECISION", 582 | style="bold white", 583 | padding=(1, 2), 584 | ) 585 | ) 586 | 587 | def _render_judge_decision_with_voting(self, question: str, debate_history: List[Dict[str, str]], final_votes: Optional[List[Vote]]) -> str: 588 | """Render judge decision with voting information and return the decision text.""" 589 | console.print("\n[bold white]═══ FINAL JUDGMENT ═══[/bold white]\n") 590 | 591 | # Prepare context for judge 592 | debate_context = self.format_debate_history(debate_history) 593 | 594 | voting_summary = "" 595 | if final_votes and self.voting_system and len(self.voting_system.vote_history) > 0: 596 | summary = self.voting_system.get_vote_summary(len(self.voting_system.vote_history) - 1) 597 | if summary and "sorted_rankings" in summary: 598 | voting_summary = f"\n\nFINAL VOTING RESULTS:\n" 599 | for participant, score in summary["sorted_rankings"]: 600 | percentage = (score / self.voting_system.max_possible_points) * 100 601 | voting_summary += f"{participant}: {score} points ({percentage:.1f}%)\n" 602 | 603 | if summary.get("consensus_reached"): 604 | voting_summary += f"\nConsensus winner: {summary.get('winner', 'Unknown')}" 605 | 606 | judge_prompt = f"""Review this entire debate and provide your final judgment. 607 | 608 | Question: {question} 609 | 610 | DEBATE HISTORY: 611 | {debate_context} 612 | {voting_summary} 613 | 614 | Consider: 615 | 1. The quality of arguments presented 616 | 2. How well participants engaged with each other's points 617 | 3. The voting results and whether they reflect the debate quality 618 | 4. Whether any important perspectives were missed 619 | 620 | Provide your final judgment. If you disagree with the consensus, explain why in detail.""" 621 | 622 | with Progress( 623 | SpinnerColumn(), 624 | TextColumn("[bold]The Judge is reviewing the entire debate..."), 625 | transient=True, 626 | console=console, 627 | ) as progress: 628 | task = progress.add_task("judging", total=None) 629 | time.sleep(2) 630 | 631 | final_decision = self.judge.generate_response(question, judge_prompt) 632 | 633 | console.print( 634 | Panel( 635 | final_decision, 636 | title="⚖️ FINAL DECISION", 637 | style="bold white", 638 | padding=(1, 2), 639 | ) 640 | ) 641 | 642 | return final_decision 643 | 644 | def run_debate(self, question: str): 645 | """Run a debate based on configuration.""" 646 | console.print(f"\n[bold magenta]🎯 DEBATE TOPIC:[/bold magenta] {question}\n") 647 | 648 | if self.config.classic_mode or not self.config.voting_enabled: 649 | self.run_classic_debate(question) 650 | else: 651 | self.run_voting_debate(question) 652 | 653 | def run(self): 654 | self.display_header() 655 | question = self.get_question() 656 | self.run_debate(question) 657 | 658 | console.print("\n[bold green]Thank you for using ASS![/bold green]") 659 | 660 | if self.config.voting_enabled: 661 | console.print("[dim]Tip: Use --classic-mode to run the original 3-round format[/dim]") 662 | 663 | 664 | def main(): 665 | import argparse 666 | 667 | parser = argparse.ArgumentParser(description="ASS - Argumentative System Service") 668 | parser.add_argument("--classic-mode", action="store_true", help="Use classic 3-round format") 669 | parser.add_argument("--no-voting", action="store_true", help="Disable voting system") 670 | parser.add_argument("--voting-threshold", type=float, help="Consensus threshold (0-1)") 671 | parser.add_argument("--max-iterations", type=int, help="Maximum debate iterations") 672 | parser.add_argument("--min-iterations", type=int, help="Minimum iterations before voting") 673 | parser.add_argument("--local-model-url", help="URL for local model server") 674 | parser.add_argument("--config", help="Path to configuration file") 675 | parser.add_argument("--no-save", action="store_true", help="Disable saving debates to files") 676 | 677 | args = parser.parse_args() 678 | 679 | # Load configuration 680 | if args.config: 681 | config = DebateConfig.from_file(args.config) 682 | else: 683 | config = DebateConfig.from_env() 684 | 685 | # Apply command-line overrides 686 | if args.classic_mode: 687 | config.classic_mode = True 688 | if args.no_voting: 689 | config.voting_enabled = False 690 | if args.voting_threshold is not None: 691 | config.consensus_threshold = args.voting_threshold 692 | if args.max_iterations is not None: 693 | config.max_iterations = args.max_iterations 694 | if args.min_iterations is not None: 695 | config.min_iterations = args.min_iterations 696 | if args.local_model_url: 697 | os.environ["LOCAL_MODEL_URL"] = args.local_model_url 698 | config.allow_local_models = True 699 | if args.no_save: 700 | config.save_enabled = False 701 | 702 | app = DebateApp(config) 703 | app.run() 704 | 705 | 706 | if __name__ == "__main__": 707 | main() --------------------------------------------------------------------------------