- Add _cmd_create_arrangement_audio_pattern with 5-method fallback chain - Method 1: track.insert_arrangement_clip() [Live 12+] - Method 2: track.create_audio_clip() [Live 11+] - Method 3: arrangement_clips.add_new_clip() [Live 12+] - Method 4: Session->duplicate_clip_to_arrangement [Legacy] - Method 5: Session->Recording [Universal] - Add _cmd_duplicate_clip_to_arrangement for session-to-arrangement workflow - Update skills documentation - Verified: 3 clips created at positions [0, 4, 8] in Arrangement View Closes: Audio injection in Arrangement View
821 lines
27 KiB
Python
821 lines
27 KiB
Python
"""
|
|
RationaleLogger - Tracks all AI decisions for auditability and analysis.
|
|
|
|
This module provides comprehensive logging of all AI-driven decisions in the
|
|
production pipeline, including sample selection, kit assembly, variations, and
|
|
mixing choices. All entries are stored in SQLite for queryable analysis.
|
|
"""
|
|
|
|
import sqlite3
|
|
import json
|
|
import os
|
|
import uuid
|
|
from datetime import datetime
|
|
from typing import Dict, List, Optional, Any, Tuple
|
|
from dataclasses import dataclass, asdict
|
|
from pathlib import Path
|
|
|
|
|
|
@dataclass
|
|
class SampleSelectionRationale:
|
|
"""Rationale for a sample selection decision."""
|
|
decision: str
|
|
reasoning: List[str]
|
|
rejected: List[Dict[str, str]]
|
|
confidence: float
|
|
role: str
|
|
selected_sample: str
|
|
similarity_scores: Dict[str, float]
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return asdict(self)
|
|
|
|
|
|
@dataclass
|
|
class KitAssemblyRationale:
|
|
"""Rationale for a drum kit assembly decision."""
|
|
kit_samples: Dict[str, str] # role -> sample path
|
|
coherence_score: float
|
|
weak_links: List[Dict[str, Any]]
|
|
reasoning: List[str]
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return asdict(self)
|
|
|
|
|
|
@dataclass
|
|
class SectionVariationRationale:
|
|
"""Rationale for a section variation decision."""
|
|
section_name: str
|
|
base_kit: Dict[str, str]
|
|
evolved_kit: Dict[str, str]
|
|
coherence_with_base: float
|
|
changes: List[str]
|
|
reasoning: List[str]
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return asdict(self)
|
|
|
|
|
|
@dataclass
|
|
class MixDecisionRationale:
|
|
"""Rationale for a mixing decision."""
|
|
track_index: int
|
|
track_name: str
|
|
effect: str
|
|
parameters: Dict[str, Any]
|
|
reasoning: List[str]
|
|
before_state: Optional[Dict[str, Any]]
|
|
after_state: Optional[Dict[str, Any]]
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return asdict(self)
|
|
|
|
|
|
class RationaleLogger:
|
|
"""
|
|
Logs and queries AI decisions for auditability.
|
|
|
|
Provides a complete audit trail of all AI-driven decisions including:
|
|
- Sample selection with similarity scores and alternatives
|
|
- Kit assembly with coherence analysis
|
|
- Section variations with change tracking
|
|
- Mix decisions with before/after states
|
|
|
|
All data is stored in SQLite for efficient querying and analysis.
|
|
"""
|
|
|
|
def __init__(self, db_path: Optional[str] = None):
|
|
"""
|
|
Initialize the RationaleLogger.
|
|
|
|
Args:
|
|
db_path: Path to SQLite database. If None, uses default location.
|
|
"""
|
|
if db_path is None:
|
|
# Store in the same directory as the engine files
|
|
base_dir = Path(__file__).parent.parent
|
|
db_path = str(base_dir / "data" / "rationale.db")
|
|
|
|
self.db_path = db_path
|
|
self._ensure_data_dir()
|
|
self._init_database()
|
|
self._current_session_id: Optional[str] = None
|
|
|
|
def _ensure_data_dir(self) -> None:
|
|
"""Create data directory if it doesn't exist."""
|
|
data_dir = Path(self.db_path).parent
|
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
def _init_database(self) -> None:
|
|
"""Initialize the SQLite database with required tables."""
|
|
with sqlite3.connect(self.db_path) as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Create rationale_entries table
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS rationale_entries (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
session_id TEXT,
|
|
track_name TEXT,
|
|
decision_type TEXT,
|
|
decision_description TEXT,
|
|
inputs TEXT,
|
|
outputs TEXT,
|
|
scores TEXT,
|
|
rationale TEXT,
|
|
alternatives_considered TEXT
|
|
)
|
|
""")
|
|
|
|
# Create index for efficient queries
|
|
cursor.execute("""
|
|
CREATE INDEX IF NOT EXISTS idx_session
|
|
ON rationale_entries(session_id)
|
|
""")
|
|
|
|
cursor.execute("""
|
|
CREATE INDEX IF NOT EXISTS idx_decision_type
|
|
ON rationale_entries(decision_type)
|
|
""")
|
|
|
|
cursor.execute("""
|
|
CREATE INDEX IF NOT EXISTS idx_timestamp
|
|
ON rationale_entries(timestamp)
|
|
""")
|
|
|
|
# Create stats tracking table
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS decision_stats (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
decision_type TEXT UNIQUE,
|
|
count INTEGER DEFAULT 0,
|
|
avg_confidence REAL DEFAULT 0.0,
|
|
last_updated DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
""")
|
|
|
|
conn.commit()
|
|
|
|
def start_session(self, track_name: Optional[str] = None) -> str:
|
|
"""
|
|
Start a new logging session.
|
|
|
|
Args:
|
|
track_name: Name of the track/project being worked on
|
|
|
|
Returns:
|
|
The generated session ID
|
|
"""
|
|
self._current_session_id = str(uuid.uuid4())[:8]
|
|
self._current_track_name = track_name or "untitled"
|
|
return self._current_session_id
|
|
|
|
def get_session_id(self) -> str:
|
|
"""Get current session ID, creating one if needed."""
|
|
if self._current_session_id is None:
|
|
self.start_session()
|
|
return self._current_session_id
|
|
|
|
def _insert_entry(
|
|
self,
|
|
decision_type: str,
|
|
description: str,
|
|
inputs: Dict[str, Any],
|
|
outputs: Dict[str, Any],
|
|
scores: Dict[str, Any],
|
|
rationale: Dict[str, Any],
|
|
alternatives: List[Dict[str, Any]]
|
|
) -> int:
|
|
"""Insert a rationale entry into the database."""
|
|
with sqlite3.connect(self.db_path) as conn:
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
INSERT INTO rationale_entries (
|
|
session_id, track_name, decision_type, decision_description,
|
|
inputs, outputs, scores, rationale, alternatives_considered
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
self.get_session_id(),
|
|
getattr(self, '_current_track_name', 'untitled'),
|
|
decision_type,
|
|
description,
|
|
json.dumps(inputs, default=str),
|
|
json.dumps(outputs, default=str),
|
|
json.dumps(scores, default=str),
|
|
json.dumps(rationale, default=str),
|
|
json.dumps(alternatives, default=str)
|
|
))
|
|
|
|
entry_id = cursor.lastrowid
|
|
|
|
# Update stats
|
|
self._update_stats(conn, cursor, decision_type, rationale.get('confidence', 0.5))
|
|
|
|
conn.commit()
|
|
return entry_id
|
|
|
|
def _update_stats(
|
|
self,
|
|
conn: sqlite3.Connection,
|
|
cursor: sqlite3.Cursor,
|
|
decision_type: str,
|
|
confidence: float
|
|
) -> None:
|
|
"""Update decision statistics."""
|
|
cursor.execute("""
|
|
INSERT INTO decision_stats (decision_type, count, avg_confidence)
|
|
VALUES (?, 1, ?)
|
|
ON CONFLICT(decision_type) DO UPDATE SET
|
|
count = count + 1,
|
|
avg_confidence = (avg_confidence * count + ?) / (count + 1),
|
|
last_updated = CURRENT_TIMESTAMP
|
|
""", (decision_type, confidence, confidence))
|
|
|
|
def log_sample_selection(
|
|
self,
|
|
role: str,
|
|
selected_sample: str,
|
|
alternatives: List[str],
|
|
similarity_scores: Dict[str, float],
|
|
rationale: str,
|
|
reasoning: Optional[List[str]] = None,
|
|
rejected_details: Optional[List[Dict[str, str]]] = None,
|
|
confidence: float = 0.0
|
|
) -> int:
|
|
"""
|
|
Log a sample selection decision.
|
|
|
|
Args:
|
|
role: Sample role (kick, snare, hihat, etc.)
|
|
selected_sample: Path or name of selected sample
|
|
alternatives: List of alternative samples considered
|
|
similarity_scores: Dict of similarity metrics
|
|
rationale: Human-readable explanation
|
|
reasoning: List of detailed reasoning points
|
|
rejected_details: List of rejected options with reasons
|
|
confidence: Confidence score (0.0-1.0)
|
|
|
|
Returns:
|
|
Entry ID
|
|
"""
|
|
inputs = {
|
|
'role': role,
|
|
'candidates': alternatives + [selected_sample],
|
|
'criteria': similarity_scores.get('criteria', 'similarity')
|
|
}
|
|
|
|
outputs = {
|
|
'selected': selected_sample,
|
|
'alternatives_count': len(alternatives)
|
|
}
|
|
|
|
scores = {
|
|
'confidence': confidence,
|
|
'similarity_to_reference': similarity_scores.get('reference_similarity', 0.0),
|
|
'genre_match': similarity_scores.get('genre_match', 0.0),
|
|
'energy_match': similarity_scores.get('energy_match', 0.0)
|
|
}
|
|
|
|
rationale_dict = {
|
|
'decision': f"Selected {os.path.basename(selected_sample)} as {role}",
|
|
'reasoning': reasoning or [rationale],
|
|
'rejected': rejected_details or [],
|
|
'confidence': confidence
|
|
}
|
|
|
|
alternatives_list = [
|
|
{'sample': alt, 'reason': 'Lower similarity score'}
|
|
for alt in alternatives
|
|
]
|
|
if rejected_details:
|
|
alternatives_list.extend(rejected_details)
|
|
|
|
return self._insert_entry(
|
|
decision_type='sample_selection',
|
|
description=f"{role}: {os.path.basename(selected_sample)}",
|
|
inputs=inputs,
|
|
outputs=outputs,
|
|
scores=scores,
|
|
rationale=rationale_dict,
|
|
alternatives=alternatives_list
|
|
)
|
|
|
|
def log_kit_assembly(
|
|
self,
|
|
kit_samples: Dict[str, str],
|
|
coherence_score: float,
|
|
weak_links: List[Dict[str, Any]],
|
|
reasoning: Optional[List[str]] = None
|
|
) -> int:
|
|
"""
|
|
Log a drum kit assembly decision.
|
|
|
|
Args:
|
|
kit_samples: Dict mapping roles to sample paths
|
|
coherence_score: Overall kit coherence (0.0-1.0)
|
|
weak_links: List of weak coherence points with details
|
|
reasoning: List of reasoning points
|
|
|
|
Returns:
|
|
Entry ID
|
|
"""
|
|
inputs = {
|
|
'available_samples': len(kit_samples),
|
|
'target_coherence': 0.8
|
|
}
|
|
|
|
outputs = {
|
|
'kit_configuration': {role: os.path.basename(path) for role, path in kit_samples.items()},
|
|
'size': len(kit_samples)
|
|
}
|
|
|
|
scores = {
|
|
'coherence': coherence_score,
|
|
'weak_link_count': len(weak_links),
|
|
'confidence': coherence_score # Use coherence as confidence
|
|
}
|
|
|
|
rationale_dict = {
|
|
'decision': f"Assembled {len(kit_samples)}-piece drum kit",
|
|
'reasoning': reasoning or [f"Kit coherence: {coherence_score:.2f}"],
|
|
'rejected': weak_links,
|
|
'confidence': coherence_score
|
|
}
|
|
|
|
return self._insert_entry(
|
|
decision_type='kit_assembly',
|
|
description=f"Drum kit with {len(kit_samples)} samples",
|
|
inputs=inputs,
|
|
outputs=outputs,
|
|
scores=scores,
|
|
rationale=rationale_dict,
|
|
alternatives=weak_links
|
|
)
|
|
|
|
def log_section_variation(
|
|
self,
|
|
section_name: str,
|
|
base_kit: Dict[str, str],
|
|
evolved_kit: Dict[str, str],
|
|
coherence_with_base: float,
|
|
changes: Optional[List[str]] = None,
|
|
reasoning: Optional[List[str]] = None
|
|
) -> int:
|
|
"""
|
|
Log a section variation decision.
|
|
|
|
Args:
|
|
section_name: Name of section (verse, chorus, bridge, etc.)
|
|
base_kit: Original kit configuration
|
|
evolved_kit: Modified kit configuration
|
|
coherence_with_base: How well variation matches base
|
|
changes: List of specific changes made
|
|
reasoning: List of reasoning points
|
|
|
|
Returns:
|
|
Entry ID
|
|
"""
|
|
# Calculate differences
|
|
changed_samples = []
|
|
for role in set(base_kit.keys()) | set(evolved_kit.keys()):
|
|
if base_kit.get(role) != evolved_kit.get(role):
|
|
changed_samples.append(role)
|
|
|
|
inputs = {
|
|
'section': section_name,
|
|
'base_kit': {k: os.path.basename(v) for k, v in base_kit.items()}
|
|
}
|
|
|
|
outputs = {
|
|
'evolved_kit': {k: os.path.basename(v) for k, v in evolved_kit.items()},
|
|
'changed_roles': changed_samples,
|
|
'unchanged_roles': list(set(base_kit.keys()) - set(changed_samples))
|
|
}
|
|
|
|
scores = {
|
|
'coherence_with_base': coherence_with_base,
|
|
'change_ratio': len(changed_samples) / max(len(base_kit), 1),
|
|
'confidence': coherence_with_base
|
|
}
|
|
|
|
rationale_dict = {
|
|
'decision': f"Created {section_name} variation from base kit",
|
|
'reasoning': reasoning or [f"Coherence with base: {coherence_with_base:.2f}"],
|
|
'rejected': [],
|
|
'confidence': coherence_with_base
|
|
}
|
|
|
|
return self._insert_entry(
|
|
decision_type='variation',
|
|
description=f"{section_name} kit variation",
|
|
inputs=inputs,
|
|
outputs=outputs,
|
|
scores=scores,
|
|
rationale=rationale_dict,
|
|
alternatives=[]
|
|
)
|
|
|
|
def log_mix_decision(
|
|
self,
|
|
track_index: int,
|
|
effect: str,
|
|
parameters: Dict[str, Any],
|
|
rationale: str,
|
|
track_name: Optional[str] = None,
|
|
reasoning: Optional[List[str]] = None,
|
|
before_state: Optional[Dict[str, Any]] = None,
|
|
after_state: Optional[Dict[str, Any]] = None,
|
|
alternatives: Optional[List[Dict[str, Any]]] = None
|
|
) -> int:
|
|
"""
|
|
Log a mixing decision.
|
|
|
|
Args:
|
|
track_index: Index of affected track
|
|
effect: Effect/processor name
|
|
parameters: Effect parameters applied
|
|
rationale: Human-readable explanation
|
|
track_name: Name of track
|
|
reasoning: List of detailed reasoning points
|
|
before_state: State before the change
|
|
after_state: State after the change
|
|
alternatives: Alternative approaches considered
|
|
|
|
Returns:
|
|
Entry ID
|
|
"""
|
|
inputs = {
|
|
'track_index': track_index,
|
|
'track_name': track_name or f"Track {track_index}",
|
|
'before_state': before_state or {}
|
|
}
|
|
|
|
outputs = {
|
|
'effect': effect,
|
|
'parameters': parameters,
|
|
'after_state': after_state or {}
|
|
}
|
|
|
|
scores = {
|
|
'impact_score': parameters.get('impact', 0.5),
|
|
'confidence': 0.8 # Mix decisions typically have good confidence
|
|
}
|
|
|
|
rationale_dict = {
|
|
'decision': f"Applied {effect} to {track_name or f'track {track_index}'}",
|
|
'reasoning': reasoning or [rationale],
|
|
'rejected': alternatives or [],
|
|
'confidence': 0.8
|
|
}
|
|
|
|
return self._insert_entry(
|
|
decision_type='mix',
|
|
description=f"{effect} on {track_name or f'track {track_index}'}",
|
|
inputs=inputs,
|
|
outputs=outputs,
|
|
scores=scores,
|
|
rationale=rationale_dict,
|
|
alternatives=alternatives or []
|
|
)
|
|
|
|
def get_session_rationale(self, session_id: str) -> List[Dict[str, Any]]:
|
|
"""
|
|
Retrieve all decisions for a session.
|
|
|
|
Args:
|
|
session_id: Session ID to query
|
|
|
|
Returns:
|
|
List of rationale entries
|
|
"""
|
|
with sqlite3.connect(self.db_path) as conn:
|
|
conn.row_factory = sqlite3.Row
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
SELECT * FROM rationale_entries
|
|
WHERE session_id = ?
|
|
ORDER BY timestamp
|
|
""", (session_id,))
|
|
|
|
rows = cursor.fetchall()
|
|
return [dict(row) for row in rows]
|
|
|
|
def get_decision_stats(self) -> Dict[str, Any]:
|
|
"""
|
|
Get analytics on all decisions.
|
|
|
|
Returns:
|
|
Dict with statistics including counts, averages, trends
|
|
"""
|
|
with sqlite3.connect(self.db_path) as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Get per-type stats
|
|
cursor.execute("""
|
|
SELECT decision_type, count, avg_confidence, last_updated
|
|
FROM decision_stats
|
|
ORDER BY count DESC
|
|
""")
|
|
|
|
type_stats = {}
|
|
for row in cursor.fetchall():
|
|
type_stats[row[0]] = {
|
|
'count': row[1],
|
|
'avg_confidence': row[2],
|
|
'last_updated': row[3]
|
|
}
|
|
|
|
# Get overall stats
|
|
cursor.execute("""
|
|
SELECT
|
|
COUNT(*) as total_decisions,
|
|
COUNT(DISTINCT session_id) as total_sessions,
|
|
AVG(
|
|
CASE
|
|
WHEN json_extract(scores, '$.confidence') IS NOT NULL
|
|
THEN json_extract(scores, '$.confidence')
|
|
ELSE 0.5
|
|
END
|
|
) as overall_confidence
|
|
FROM rationale_entries
|
|
""")
|
|
|
|
row = cursor.fetchone()
|
|
overall = {
|
|
'total_decisions': row[0] or 0,
|
|
'total_sessions': row[1] or 0,
|
|
'overall_confidence': row[2] or 0.0
|
|
}
|
|
|
|
# Get recent activity (last 24 hours)
|
|
cursor.execute("""
|
|
SELECT COUNT(*)
|
|
FROM rationale_entries
|
|
WHERE timestamp > datetime('now', '-1 day')
|
|
""")
|
|
|
|
recent_count = cursor.fetchone()[0]
|
|
|
|
return {
|
|
'by_type': type_stats,
|
|
'overall': overall,
|
|
'recent_24h': recent_count
|
|
}
|
|
|
|
def find_similar_decisions(
|
|
self,
|
|
decision_type: str,
|
|
min_confidence: float = 0.7,
|
|
limit: int = 10
|
|
) -> List[Dict[str, Any]]:
|
|
"""
|
|
Find similar past decisions with high confidence.
|
|
|
|
Args:
|
|
decision_type: Type of decision to query
|
|
min_confidence: Minimum confidence threshold
|
|
limit: Maximum results to return
|
|
|
|
Returns:
|
|
List of similar decisions
|
|
"""
|
|
with sqlite3.connect(self.db_path) as conn:
|
|
conn.row_factory = sqlite3.Row
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
SELECT * FROM rationale_entries
|
|
WHERE decision_type = ?
|
|
AND json_extract(scores, '$.confidence') >= ?
|
|
ORDER BY json_extract(scores, '$.confidence') DESC, timestamp DESC
|
|
LIMIT ?
|
|
""", (decision_type, min_confidence, limit))
|
|
|
|
rows = cursor.fetchall()
|
|
return [dict(row) for row in rows]
|
|
|
|
def get_most_used_samples(self, role: Optional[str] = None, limit: int = 20) -> List[Dict[str, Any]]:
|
|
"""
|
|
Track which samples are used most frequently.
|
|
|
|
Args:
|
|
role: Filter by specific role (optional)
|
|
limit: Maximum results to return
|
|
|
|
Returns:
|
|
List of samples with usage counts
|
|
"""
|
|
with sqlite3.connect(self.db_path) as conn:
|
|
cursor = conn.cursor()
|
|
|
|
if role:
|
|
cursor.execute("""
|
|
SELECT
|
|
json_extract(outputs, '$.selected') as sample,
|
|
json_extract(inputs, '$.role') as sample_role,
|
|
COUNT(*) as usage_count,
|
|
AVG(json_extract(scores, '$.confidence')) as avg_confidence
|
|
FROM rationale_entries
|
|
WHERE decision_type = 'sample_selection'
|
|
AND json_extract(inputs, '$.role') = ?
|
|
GROUP BY json_extract(outputs, '$.selected')
|
|
ORDER BY usage_count DESC
|
|
LIMIT ?
|
|
""", (role, limit))
|
|
else:
|
|
cursor.execute("""
|
|
SELECT
|
|
json_extract(outputs, '$.selected') as sample,
|
|
json_extract(inputs, '$.role') as sample_role,
|
|
COUNT(*) as usage_count,
|
|
AVG(json_extract(scores, '$.confidence')) as avg_confidence
|
|
FROM rationale_entries
|
|
WHERE decision_type = 'sample_selection'
|
|
GROUP BY json_extract(outputs, '$.selected')
|
|
ORDER BY usage_count DESC
|
|
LIMIT ?
|
|
""", (limit,))
|
|
|
|
results = []
|
|
for row in cursor.fetchall():
|
|
results.append({
|
|
'sample': row[0],
|
|
'role': row[1],
|
|
'usage_count': row[2],
|
|
'avg_confidence': row[3]
|
|
})
|
|
|
|
return results
|
|
|
|
def analyze_coherence_trends(self) -> Dict[str, Any]:
|
|
"""
|
|
Analyze coherence trends over time.
|
|
|
|
Returns:
|
|
Dict with trend analysis
|
|
"""
|
|
with sqlite3.connect(self.db_path) as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Get coherence scores over time by decision type
|
|
cursor.execute("""
|
|
SELECT
|
|
decision_type,
|
|
date(timestamp) as date,
|
|
AVG(json_extract(scores, '$.coherence')) as avg_coherence,
|
|
COUNT(*) as count
|
|
FROM rationale_entries
|
|
WHERE json_extract(scores, '$.coherence') IS NOT NULL
|
|
GROUP BY decision_type, date(timestamp)
|
|
ORDER BY date
|
|
""")
|
|
|
|
trends = {}
|
|
for row in cursor.fetchall():
|
|
dec_type = row[0]
|
|
if dec_type not in trends:
|
|
trends[dec_type] = []
|
|
trends[dec_type].append({
|
|
'date': row[1],
|
|
'avg_coherence': row[2],
|
|
'count': row[3]
|
|
})
|
|
|
|
# Calculate overall trend
|
|
cursor.execute("""
|
|
SELECT
|
|
AVG(json_extract(scores, '$.coherence')) as overall_avg,
|
|
MIN(json_extract(scores, '$.coherence')) as min_coherence,
|
|
MAX(json_extract(scores, '$.coherence')) as max_coherence
|
|
FROM rationale_entries
|
|
WHERE json_extract(scores, '$.coherence') IS NOT NULL
|
|
""")
|
|
|
|
row = cursor.fetchone()
|
|
|
|
return {
|
|
'trends_by_type': trends,
|
|
'overall': {
|
|
'average': row[0] or 0.0,
|
|
'minimum': row[1] or 0.0,
|
|
'maximum': row[2] or 0.0
|
|
}
|
|
}
|
|
|
|
def export_session_report(self, session_id: str, output_path: Optional[str] = None) -> str:
|
|
"""
|
|
Export a detailed session report.
|
|
|
|
Args:
|
|
session_id: Session to export
|
|
output_path: Output file path (optional)
|
|
|
|
Returns:
|
|
Path to exported report
|
|
"""
|
|
entries = self.get_session_rationale(session_id)
|
|
|
|
if not entries:
|
|
return ""
|
|
|
|
# Generate report
|
|
report = {
|
|
'session_id': session_id,
|
|
'generated_at': datetime.now().isoformat(),
|
|
'total_decisions': len(entries),
|
|
'decisions': []
|
|
}
|
|
|
|
for entry in entries:
|
|
report['decisions'].append({
|
|
'timestamp': entry['timestamp'],
|
|
'type': entry['decision_type'],
|
|
'description': entry['decision_description'],
|
|
'rationale': json.loads(entry['rationale']),
|
|
'scores': json.loads(entry['scores'])
|
|
})
|
|
|
|
# Determine output path
|
|
if output_path is None:
|
|
base_dir = Path(self.db_path).parent
|
|
output_path = str(base_dir / f"session_report_{session_id}.json")
|
|
|
|
with open(output_path, 'w') as f:
|
|
json.dump(report, f, indent=2)
|
|
|
|
return output_path
|
|
|
|
def clear_session(self, session_id: str) -> int:
|
|
"""
|
|
Clear all entries for a session.
|
|
|
|
Args:
|
|
session_id: Session to clear
|
|
|
|
Returns:
|
|
Number of entries deleted
|
|
"""
|
|
with sqlite3.connect(self.db_path) as conn:
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
DELETE FROM rationale_entries
|
|
WHERE session_id = ?
|
|
""", (session_id,))
|
|
|
|
deleted = cursor.rowcount
|
|
conn.commit()
|
|
return deleted
|
|
|
|
def get_decision_by_id(self, entry_id: int) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Retrieve a specific decision by ID.
|
|
|
|
Args:
|
|
entry_id: Entry ID to retrieve
|
|
|
|
Returns:
|
|
Decision entry or None
|
|
"""
|
|
with sqlite3.connect(self.db_path) as conn:
|
|
conn.row_factory = sqlite3.Row
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
SELECT * FROM rationale_entries
|
|
WHERE id = ?
|
|
""", (entry_id,))
|
|
|
|
row = cursor.fetchone()
|
|
return dict(row) if row else None
|
|
|
|
|
|
# Singleton instance for module-level access
|
|
_default_logger: Optional[RationaleLogger] = None
|
|
|
|
|
|
def get_logger(db_path: Optional[str] = None) -> RationaleLogger:
|
|
"""
|
|
Get or create the default RationaleLogger instance.
|
|
|
|
Args:
|
|
db_path: Path to database (optional)
|
|
|
|
Returns:
|
|
RationaleLogger instance
|
|
"""
|
|
global _default_logger
|
|
if _default_logger is None:
|
|
_default_logger = RationaleLogger(db_path)
|
|
return _default_logger
|
|
|
|
|
|
def reset_logger() -> None:
|
|
"""Reset the singleton logger (useful for testing)."""
|
|
global _default_logger
|
|
_default_logger = None
|