Files
agentic-govbot/src/govbot/governance/primitives.py
Nathan Schneider a92236e528 Switch to LLM-driven agent with zero hard-coded governance logic
Replace old rule-based agent with pure LLM interpretation system.

Agent Changes:
- Rename agent.py → agent_legacy.py (preserve old hard-coded agent)
- Rename agent_refactored.py → agent.py (make LLM agent primary)
- Agent now interprets constitution to understand authority and processes
- No hard-coded checks for specific users, roles, or governance models
- Fully generic: works with any constitutional design

Constitution Interpreter:
- Updated interpret_proposal() to detect authority structures from text
- LLM determines who has decision-making power from constitution
- No assumptions about voting, proposals, or specific governance models

Mastodon Formatting:
- Improved line break handling for bullet points and paragraphs
- Better plain-text formatting for Mastodon posts

Primitives:
- Added support for admin_approval threshold type

Architecture:
- Bot now uses pure LLM interpretation instead of scripted logic
- Each instance can develop implementation guidelines separately
- Guidelines not included in main codebase (instance-specific)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-08 15:20:00 -07:00

422 lines
12 KiB
Python

"""
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)
elif threshold_type == "admin_approval":
# Admin decision model - no voting threshold
# This type means admin must approve, not vote counting
return False # Requires manual admin approval
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)