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:
416
src/govbot/governance/primitives.py
Normal file
416
src/govbot/governance/primitives.py
Normal 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)
|
||||
Reference in New Issue
Block a user