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>
422 lines
12 KiB
Python
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)
|