Initial commit: Platform-agnostic governance bot

Govbot is an AI-powered governance bot that interprets natural language
constitutions and facilitates collective decision-making across social
platforms.

Core features:
- Agentic architecture with constitutional reasoning (RAG)
- Platform-agnostic design (Mastodon, Discord, Telegram, etc.)
- Action primitives for flexible governance processes
- Temporal awareness for multi-day proposals and voting
- Audit trail with constitutional citations
- Reversible actions with supermajority veto
- Works with local (Ollama) and cloud AI models

Platform support:
- Mastodon: Full implementation with streaming, moderation, and admin skills
- Discord/Telegram: Platform abstraction ready for implementation

Documentation:
- README.md: Architecture and overview
- QUICKSTART.md: Getting started guide
- PLATFORMS.md: Platform implementation guide for developers
- MASTODON_SETUP.md: Complete Mastodon deployment guide
- constitution.md: Example governance constitution

Technical stack:
- Python 3.11+
- SQLAlchemy for state management
- llm CLI for model abstraction
- Mastodon.py for Mastodon integration
- Pydantic for configuration validation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Nathan Schneider
2026-02-06 17:09:26 -07:00
commit fbc37ecb8f
27 changed files with 6004 additions and 0 deletions

View File

@@ -0,0 +1,416 @@
"""
Action Primitives for Agentic Governance.
These are the low-level operations that the AI agent can orchestrate
to implement governance processes. Each primitive is simple and composable,
allowing the agent to flexibly implement constitutional procedures.
"""
from datetime import datetime, timedelta
from typing import Dict, Any, Optional, List
from sqlalchemy.orm import Session
import json
from ..db import queries
from ..db.models import GovernanceProcess, Action
class GovernancePrimitives:
"""
Provides primitive operations for governance actions.
These are called by the AI agent to implement constitutional procedures.
"""
def __init__(self, db_session: Session):
self.db = db_session
# Storage primitives
def store_record(
self,
record_type: str,
data: Dict[str, Any],
actor: str,
reasoning: Optional[str] = None,
citation: Optional[str] = None,
) -> int:
"""
Store a governance record (generic storage primitive).
Args:
record_type: Type of record (e.g., "proposal", "vote", "decision")
data: Record data as dictionary
actor: Who created this record
reasoning: Bot's reasoning for creating this record
citation: Constitutional citation
Returns:
Record ID
"""
action = queries.create_action(
session=self.db,
action_type=f"store_{record_type}",
actor=actor,
data=data,
bot_reasoning=reasoning,
constitutional_citation=citation,
)
return action.id
def query_records(
self,
record_type: Optional[str] = None,
criteria: Optional[Dict[str, Any]] = None,
limit: int = 50,
) -> List[Dict[str, Any]]:
"""
Query governance records (generic retrieval primitive).
Args:
record_type: Type of record to query (None for all)
criteria: Filter criteria (e.g., {"status": "active"})
limit: Maximum number of results
Returns:
List of matching records as dictionaries
"""
actions = queries.get_recent_actions(
session=self.db, limit=limit, action_type=record_type
)
if criteria:
# Filter by criteria in data field
filtered = []
for action in actions:
if action.data and all(
action.data.get(k) == v for k, v in criteria.items()
):
filtered.append(action.to_dict())
return filtered
return [action.to_dict() for action in actions]
# Process primitives
def create_process(
self,
process_type: str,
creator: str,
deadline_days: int,
constitutional_basis: str,
initial_state: Optional[Dict[str, Any]] = None,
mastodon_thread_id: Optional[str] = None,
) -> int:
"""
Create a new governance process.
Args:
process_type: Type of process (e.g., "standard_proposal")
creator: Who initiated the process
deadline_days: Days until deadline
constitutional_basis: Constitutional citation
initial_state: Initial state data
mastodon_thread_id: Link to Mastodon thread
Returns:
Process ID
"""
deadline = datetime.utcnow() + timedelta(days=deadline_days)
process = queries.create_process(
session=self.db,
process_type=process_type,
creator=creator,
constitutional_basis=constitutional_basis,
deadline=deadline,
state_data=initial_state or {},
mastodon_thread_id=mastodon_thread_id,
)
# Log the action
queries.create_action(
session=self.db,
action_type="process_created",
actor="bot",
data={
"process_id": process.id,
"process_type": process_type,
"creator": creator,
"deadline": deadline.isoformat(),
},
constitutional_citation=constitutional_basis,
)
return process.id
def update_process_state(
self, process_id: int, state_updates: Dict[str, Any], actor: str = "bot"
) -> bool:
"""
Update the state of a governance process.
Args:
process_id: ID of process to update
state_updates: Dictionary of state updates to merge
actor: Who is updating the state
Returns:
True if successful
"""
try:
process = queries.update_process_state(
session=self.db, process_id=process_id, state_data=state_updates
)
# Log the action
queries.create_action(
session=self.db,
action_type="process_updated",
actor=actor,
data={"process_id": process_id, "updates": state_updates},
)
return True
except Exception as e:
return False
def complete_process(
self, process_id: int, outcome: str, reasoning: str
) -> bool:
"""
Mark a governance process as completed.
Args:
process_id: ID of process to complete
outcome: Outcome description (e.g., "passed", "failed")
reasoning: Explanation of outcome
Returns:
True if successful
"""
try:
process = queries.complete_process(
session=self.db, process_id=process_id, outcome=outcome
)
# Log the action
queries.create_action(
session=self.db,
action_type="process_completed",
actor="bot",
data={"process_id": process_id, "outcome": outcome},
bot_reasoning=reasoning,
)
return True
except Exception as e:
return False
# Calculation primitives
def calculate(self, expression: str, variables: Dict[str, Any]) -> Any:
"""
Safely evaluate a mathematical expression.
Args:
expression: Math expression (e.g., "agree > disagree")
variables: Variable values (e.g., {"agree": 10, "disagree": 3})
Returns:
Result of calculation
"""
# Safe evaluation using eval with restricted globals
allowed_names = {
"abs": abs,
"max": max,
"min": min,
"sum": sum,
"len": len,
}
allowed_names.update(variables)
try:
result = eval(expression, {"__builtins__": {}}, allowed_names)
return result
except Exception as e:
raise ValueError(f"Invalid expression: {expression} - {e}")
def count_votes(
self, process_id: int
) -> Dict[str, int]:
"""
Count votes for a governance process.
Args:
process_id: ID of process to count votes for
Returns:
Dictionary with vote counts
"""
process = queries.get_process(self.db, process_id)
if not process:
return {}
# Get votes from process state
votes = process.state_data.get("votes", {})
# Count by type
counts = {"agree": 0, "disagree": 0, "abstain": 0, "block": 0}
for voter, vote_data in votes.items():
vote_type = vote_data.get("vote", "").lower()
if vote_type in counts:
counts[vote_type] += 1
return counts
def check_threshold(
self, counts: Dict[str, int], threshold_type: str
) -> bool:
"""
Check if vote counts meet a threshold.
Args:
counts: Vote counts dictionary
threshold_type: Type of threshold to check
Returns:
True if threshold is met
"""
agree = counts.get("agree", 0)
disagree = counts.get("disagree", 0)
block = counts.get("block", 0)
if threshold_type == "simple_majority":
return agree > disagree
elif threshold_type == "3x_majority":
return agree >= (disagree * 3)
elif threshold_type == "with_blocks":
# Require 9x more agree than disagree+block
return agree >= ((disagree + block) * 9)
elif threshold_type == "supermajority_2/3":
total = agree + disagree
if total == 0:
return False
return (agree / total) >= (2 / 3)
else:
raise ValueError(f"Unknown threshold type: {threshold_type}")
# Scheduling primitives
def schedule_reminder(
self, when: datetime, message: str, recipient: Optional[str] = None
) -> int:
"""
Schedule a reminder for a future time.
Args:
when: When to send reminder
message: Reminder message
recipient: Who to remind (None for broadcast)
Returns:
Reminder ID
"""
action = queries.create_action(
session=self.db,
action_type="reminder_scheduled",
actor="bot",
data={
"scheduled_for": when.isoformat(),
"message": message,
"recipient": recipient,
"status": "pending",
},
)
return action.id
def get_pending_reminders(self) -> List[Dict[str, Any]]:
"""
Get reminders that are due.
Returns:
List of reminder dictionaries
"""
actions = queries.get_recent_actions(
session=self.db, limit=100, action_type="reminder_scheduled"
)
now = datetime.utcnow()
due_reminders = []
for action in actions:
if action.data.get("status") == "pending":
scheduled_time = datetime.fromisoformat(
action.data["scheduled_for"]
)
if scheduled_time <= now:
due_reminders.append(action.to_dict())
return due_reminders
def mark_reminder_sent(self, reminder_id: int) -> bool:
"""
Mark a reminder as sent.
Args:
reminder_id: ID of reminder action
Returns:
True if successful
"""
action = queries.get_action(self.db, reminder_id)
if action and action.data:
action.data["status"] = "sent"
self.db.commit()
return True
return False
# Reversal primitives
def reverse_action(
self, action_id: int, actor: str, reason: str
) -> bool:
"""
Reverse a previous action.
Args:
action_id: ID of action to reverse
actor: Who is reversing the action
reason: Reason for reversal
Returns:
True if successful
"""
try:
queries.reverse_action(
session=self.db,
action_id=action_id,
reversing_actor=actor,
reason=reason,
)
return True
except Exception as e:
return False
def is_action_reversed(self, action_id: int) -> bool:
"""
Check if an action has been reversed.
Args:
action_id: ID of action to check
Returns:
True if action is reversed
"""
action = queries.get_action(self.db, action_id)
return action.status == "reversed" if action else False
def create_primitives(db_session: Session) -> GovernancePrimitives:
"""Factory function to create primitives instance"""
return GovernancePrimitives(db_session)