htmlgraph 0.9.3__py3-none-any.whl → 0.27.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +173 -17
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +127 -0
- htmlgraph/agent_registry.py +45 -30
- htmlgraph/agents.py +160 -107
- htmlgraph/analytics/__init__.py +9 -2
- htmlgraph/analytics/cli.py +190 -51
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +192 -100
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +190 -14
- htmlgraph/analytics_index.py +135 -51
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +208 -0
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/__init__.py +14 -0
- htmlgraph/builders/base.py +118 -29
- htmlgraph/builders/bug.py +150 -0
- htmlgraph/builders/chore.py +119 -0
- htmlgraph/builders/epic.py +150 -0
- htmlgraph/builders/feature.py +31 -6
- htmlgraph/builders/insight.py +195 -0
- htmlgraph/builders/metric.py +217 -0
- htmlgraph/builders/pattern.py +202 -0
- htmlgraph/builders/phase.py +162 -0
- htmlgraph/builders/spike.py +52 -19
- htmlgraph/builders/track.py +148 -72
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +18 -0
- htmlgraph/collections/base.py +415 -98
- htmlgraph/collections/bug.py +53 -0
- htmlgraph/collections/chore.py +53 -0
- htmlgraph/collections/epic.py +53 -0
- htmlgraph/collections/feature.py +12 -26
- htmlgraph/collections/insight.py +100 -0
- htmlgraph/collections/metric.py +92 -0
- htmlgraph/collections/pattern.py +97 -0
- htmlgraph/collections/phase.py +53 -0
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +56 -16
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +511 -0
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +344 -0
- htmlgraph/converter.py +216 -28
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2406 -307
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +19 -2
- htmlgraph/deploy.py +142 -125
- htmlgraph/deployment_models.py +474 -0
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +182 -27
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +100 -52
- htmlgraph/event_migration.py +13 -4
- htmlgraph/exceptions.py +49 -0
- htmlgraph/file_watcher.py +101 -28
- htmlgraph/find_api.py +75 -63
- htmlgraph/git_events.py +145 -63
- htmlgraph/graph.py +1122 -106
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +45 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +1314 -0
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/hooks-config.example.json +12 -0
- htmlgraph/hooks/installer.py +343 -0
- htmlgraph/hooks/orchestrator.py +674 -0
- htmlgraph/hooks/orchestrator_reflector.py +223 -0
- htmlgraph/hooks/post-checkout.sh +28 -0
- htmlgraph/hooks/post-commit.sh +24 -0
- htmlgraph/hooks/post-merge.sh +26 -0
- htmlgraph/hooks/post_tool_use_failure.py +273 -0
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +408 -0
- htmlgraph/hooks/pre-commit.sh +94 -0
- htmlgraph/hooks/pre-push.sh +28 -0
- htmlgraph/hooks/pretooluse.py +819 -0
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +255 -0
- htmlgraph/hooks/task_validator.py +177 -0
- htmlgraph/hooks/validator.py +628 -0
- htmlgraph/ids.py +41 -27
- htmlgraph/index.d.ts +286 -0
- htmlgraph/learning.py +767 -0
- htmlgraph/mcp_server.py +69 -23
- htmlgraph/models.py +1586 -87
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/orchestration/task_coordination.py +343 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +669 -0
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +328 -0
- htmlgraph/orchestrator_validator.py +133 -0
- htmlgraph/parallel.py +646 -0
- htmlgraph/parser.py +160 -35
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/planning.py +147 -52
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +109 -72
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/routing.py +8 -19
- htmlgraph/scripts/deploy.py +1 -2
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +685 -180
- htmlgraph/services/__init__.py +10 -0
- htmlgraph/services/claiming.py +199 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +1392 -175
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +201 -0
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/setup.py +34 -17
- htmlgraph/spike_index.py +143 -0
- htmlgraph/sync_docs.py +12 -15
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph/templates/GEMINI.md.template +87 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +146 -15
- htmlgraph/track_manager.py +69 -21
- htmlgraph/transcript.py +890 -0
- htmlgraph/transcript_analytics.py +699 -0
- htmlgraph/types.py +323 -0
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +8 -5
- htmlgraph/work_type_utils.py +3 -2
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
- htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -2688
- htmlgraph/sdk.py +0 -709
- htmlgraph-0.9.3.dist-info/RECORD +0 -61
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PreferenceManager - Learns user preferences from feedback.
|
|
3
|
+
|
|
4
|
+
This module provides preference learning and management:
|
|
5
|
+
1. Feedback collection - Record user acceptance/rejection of suggestions
|
|
6
|
+
2. Preference weighting - Calculate preference scores from feedback
|
|
7
|
+
3. User preferences - Store and retrieve individual user preferences
|
|
8
|
+
4. Recommendation tuning - Adjust suggestions based on learned preferences
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from htmlgraph.analytics.strategic import PreferenceManager
|
|
12
|
+
|
|
13
|
+
manager = PreferenceManager(db_path)
|
|
14
|
+
|
|
15
|
+
# Record feedback on a suggestion
|
|
16
|
+
manager.record_feedback(
|
|
17
|
+
suggestion_id="sug-abc123",
|
|
18
|
+
accepted=True,
|
|
19
|
+
outcome="successful"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Get user preferences
|
|
23
|
+
preferences = manager.get_preferences(user_id="claude")
|
|
24
|
+
|
|
25
|
+
# Reset preferences
|
|
26
|
+
manager.reset_preferences(user_id="claude")
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import logging
|
|
31
|
+
import sqlite3
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from datetime import datetime
|
|
34
|
+
from enum import Enum
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FeedbackType(Enum):
|
|
42
|
+
"""Types of feedback that can be recorded."""
|
|
43
|
+
|
|
44
|
+
ACCEPTED = "accepted" # User accepted suggestion
|
|
45
|
+
REJECTED = "rejected" # User rejected suggestion
|
|
46
|
+
IGNORED = "ignored" # User ignored suggestion
|
|
47
|
+
MODIFIED = "modified" # User modified suggestion before using
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class OutcomeType(Enum):
|
|
51
|
+
"""Outcome types for feedback tracking."""
|
|
52
|
+
|
|
53
|
+
SUCCESSFUL = "successful" # Task completed successfully
|
|
54
|
+
FAILED = "failed" # Task failed
|
|
55
|
+
PARTIAL = "partial" # Partially successful
|
|
56
|
+
UNKNOWN = "unknown" # Outcome not tracked
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class Feedback:
|
|
61
|
+
"""
|
|
62
|
+
Feedback on a suggestion.
|
|
63
|
+
|
|
64
|
+
Attributes:
|
|
65
|
+
feedback_id: Unique identifier
|
|
66
|
+
suggestion_id: ID of the suggestion being rated
|
|
67
|
+
user_id: User providing feedback
|
|
68
|
+
session_id: Session where feedback was given
|
|
69
|
+
feedback_type: Type of feedback (accepted, rejected, etc.)
|
|
70
|
+
outcome: Outcome of following the suggestion
|
|
71
|
+
rating: Optional numeric rating (1-5)
|
|
72
|
+
comment: Optional text comment
|
|
73
|
+
context: Additional context about the feedback
|
|
74
|
+
created_at: When feedback was recorded
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
feedback_id: str
|
|
78
|
+
suggestion_id: str
|
|
79
|
+
user_id: str
|
|
80
|
+
session_id: str
|
|
81
|
+
feedback_type: FeedbackType
|
|
82
|
+
outcome: OutcomeType = OutcomeType.UNKNOWN
|
|
83
|
+
rating: int | None = None
|
|
84
|
+
comment: str | None = None
|
|
85
|
+
context: dict[str, Any] = field(default_factory=dict)
|
|
86
|
+
created_at: datetime | None = None
|
|
87
|
+
|
|
88
|
+
def to_dict(self) -> dict[str, Any]:
|
|
89
|
+
"""Convert feedback to dictionary for serialization."""
|
|
90
|
+
return {
|
|
91
|
+
"feedback_id": self.feedback_id,
|
|
92
|
+
"suggestion_id": self.suggestion_id,
|
|
93
|
+
"user_id": self.user_id,
|
|
94
|
+
"session_id": self.session_id,
|
|
95
|
+
"feedback_type": self.feedback_type.value,
|
|
96
|
+
"outcome": self.outcome.value,
|
|
97
|
+
"rating": self.rating,
|
|
98
|
+
"comment": self.comment,
|
|
99
|
+
"context": self.context,
|
|
100
|
+
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class UserPreferences:
|
|
106
|
+
"""
|
|
107
|
+
Learned preferences for a user.
|
|
108
|
+
|
|
109
|
+
Attributes:
|
|
110
|
+
user_id: User identifier
|
|
111
|
+
model_preferences: Preference weights for different models
|
|
112
|
+
delegation_preferences: Preference weights for delegation types
|
|
113
|
+
tool_preferences: Preference weights for different tools
|
|
114
|
+
suggestion_type_preferences: Preference weights for suggestion types
|
|
115
|
+
quality_thresholds: Minimum thresholds for various quality metrics
|
|
116
|
+
cost_sensitivity: How much user values cost vs quality (0-1)
|
|
117
|
+
speed_sensitivity: How much user values speed vs thoroughness (0-1)
|
|
118
|
+
updated_at: When preferences were last updated
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
user_id: str
|
|
122
|
+
model_preferences: dict[str, float] = field(default_factory=dict)
|
|
123
|
+
delegation_preferences: dict[str, float] = field(default_factory=dict)
|
|
124
|
+
tool_preferences: dict[str, float] = field(default_factory=dict)
|
|
125
|
+
suggestion_type_preferences: dict[str, float] = field(default_factory=dict)
|
|
126
|
+
quality_thresholds: dict[str, float] = field(default_factory=dict)
|
|
127
|
+
cost_sensitivity: float = 0.5 # 0=ignore cost, 1=minimize cost
|
|
128
|
+
speed_sensitivity: float = 0.5 # 0=ignore speed, 1=maximize speed
|
|
129
|
+
updated_at: datetime | None = None
|
|
130
|
+
|
|
131
|
+
def to_dict(self) -> dict[str, Any]:
|
|
132
|
+
"""Convert preferences to dictionary for serialization."""
|
|
133
|
+
return {
|
|
134
|
+
"user_id": self.user_id,
|
|
135
|
+
"model_preferences": self.model_preferences,
|
|
136
|
+
"delegation_preferences": self.delegation_preferences,
|
|
137
|
+
"tool_preferences": self.tool_preferences,
|
|
138
|
+
"suggestion_type_preferences": self.suggestion_type_preferences,
|
|
139
|
+
"quality_thresholds": self.quality_thresholds,
|
|
140
|
+
"cost_sensitivity": self.cost_sensitivity,
|
|
141
|
+
"speed_sensitivity": self.speed_sensitivity,
|
|
142
|
+
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def default(cls, user_id: str) -> "UserPreferences":
|
|
147
|
+
"""
|
|
148
|
+
Create default preferences for a new user.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
user_id: User identifier
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
UserPreferences with default values
|
|
155
|
+
"""
|
|
156
|
+
return cls(
|
|
157
|
+
user_id=user_id,
|
|
158
|
+
model_preferences={
|
|
159
|
+
"claude-opus": 0.5,
|
|
160
|
+
"claude-sonnet": 0.5,
|
|
161
|
+
"claude-haiku": 0.5,
|
|
162
|
+
},
|
|
163
|
+
delegation_preferences={
|
|
164
|
+
"researcher": 0.5,
|
|
165
|
+
"coder": 0.5,
|
|
166
|
+
"tester": 0.5,
|
|
167
|
+
"debugger": 0.5,
|
|
168
|
+
},
|
|
169
|
+
tool_preferences={}, # Will be populated from usage
|
|
170
|
+
suggestion_type_preferences={
|
|
171
|
+
"next_action": 0.5,
|
|
172
|
+
"delegation": 0.5,
|
|
173
|
+
"parameter": 0.5,
|
|
174
|
+
"model_selection": 0.5,
|
|
175
|
+
"error_resolution": 0.5,
|
|
176
|
+
"workflow": 0.5,
|
|
177
|
+
},
|
|
178
|
+
quality_thresholds={
|
|
179
|
+
"min_test_coverage": 0.8,
|
|
180
|
+
"max_loc_reduction": 0.3,
|
|
181
|
+
"min_success_rate": 0.7,
|
|
182
|
+
},
|
|
183
|
+
cost_sensitivity=0.5,
|
|
184
|
+
speed_sensitivity=0.5,
|
|
185
|
+
updated_at=datetime.now(),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class PreferenceManager:
|
|
190
|
+
"""
|
|
191
|
+
Manages user preferences and feedback learning.
|
|
192
|
+
|
|
193
|
+
Collects feedback on suggestions and learns user preferences
|
|
194
|
+
to improve future recommendation quality.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def __init__(self, db_path: Path | str | None = None):
|
|
198
|
+
"""
|
|
199
|
+
Initialize preference manager.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
db_path: Path to HtmlGraph database. If None, uses default location.
|
|
203
|
+
"""
|
|
204
|
+
if db_path is None:
|
|
205
|
+
from htmlgraph.config import get_database_path
|
|
206
|
+
|
|
207
|
+
db_path = get_database_path()
|
|
208
|
+
|
|
209
|
+
self.db_path = Path(db_path)
|
|
210
|
+
self._conn: sqlite3.Connection | None = None
|
|
211
|
+
|
|
212
|
+
# Learning parameters
|
|
213
|
+
self._learning_rate = 0.1 # How fast preferences update
|
|
214
|
+
self._decay_factor = 0.95 # Older feedback matters less
|
|
215
|
+
|
|
216
|
+
def _get_connection(self) -> sqlite3.Connection:
|
|
217
|
+
"""Get database connection with row factory."""
|
|
218
|
+
if self._conn is None:
|
|
219
|
+
self._conn = sqlite3.connect(str(self.db_path))
|
|
220
|
+
self._conn.row_factory = sqlite3.Row
|
|
221
|
+
return self._conn
|
|
222
|
+
|
|
223
|
+
def close(self) -> None:
|
|
224
|
+
"""Close database connection."""
|
|
225
|
+
if self._conn:
|
|
226
|
+
self._conn.close()
|
|
227
|
+
self._conn = None
|
|
228
|
+
|
|
229
|
+
def record_feedback(
|
|
230
|
+
self,
|
|
231
|
+
suggestion_id: str,
|
|
232
|
+
accepted: bool,
|
|
233
|
+
user_id: str = "default",
|
|
234
|
+
session_id: str = "",
|
|
235
|
+
outcome: str = "unknown",
|
|
236
|
+
rating: int | None = None,
|
|
237
|
+
comment: str | None = None,
|
|
238
|
+
context: dict[str, Any] | None = None,
|
|
239
|
+
) -> str | None:
|
|
240
|
+
"""
|
|
241
|
+
Record feedback on a suggestion.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
suggestion_id: ID of the suggestion
|
|
245
|
+
accepted: Whether user accepted the suggestion
|
|
246
|
+
user_id: User providing feedback
|
|
247
|
+
session_id: Current session ID
|
|
248
|
+
outcome: Outcome of following suggestion (successful, failed, etc.)
|
|
249
|
+
rating: Optional 1-5 rating
|
|
250
|
+
comment: Optional text comment
|
|
251
|
+
context: Additional context
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Feedback ID if successful, None otherwise
|
|
255
|
+
"""
|
|
256
|
+
import uuid
|
|
257
|
+
|
|
258
|
+
conn = self._get_connection()
|
|
259
|
+
cursor = conn.cursor()
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
feedback_id = f"fb-{uuid.uuid4().hex[:8]}"
|
|
263
|
+
feedback_type = FeedbackType.ACCEPTED if accepted else FeedbackType.REJECTED
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
outcome_type = OutcomeType(outcome)
|
|
267
|
+
except ValueError:
|
|
268
|
+
outcome_type = OutcomeType.UNKNOWN
|
|
269
|
+
|
|
270
|
+
cursor.execute(
|
|
271
|
+
"""
|
|
272
|
+
INSERT INTO delegation_preferences
|
|
273
|
+
(feedback_id, suggestion_id, user_id, session_id, feedback_type,
|
|
274
|
+
outcome, rating, comment, context, created_at)
|
|
275
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
276
|
+
""",
|
|
277
|
+
(
|
|
278
|
+
feedback_id,
|
|
279
|
+
suggestion_id,
|
|
280
|
+
user_id,
|
|
281
|
+
session_id,
|
|
282
|
+
feedback_type.value,
|
|
283
|
+
outcome_type.value,
|
|
284
|
+
rating,
|
|
285
|
+
comment,
|
|
286
|
+
json.dumps(context) if context else None,
|
|
287
|
+
),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
conn.commit()
|
|
291
|
+
|
|
292
|
+
# Update user preferences based on feedback
|
|
293
|
+
self._update_preferences_from_feedback(
|
|
294
|
+
user_id, suggestion_id, feedback_type, outcome_type
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
return feedback_id
|
|
298
|
+
|
|
299
|
+
except sqlite3.Error as e:
|
|
300
|
+
logger.error(f"Error recording feedback: {e}")
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
def _update_preferences_from_feedback(
|
|
304
|
+
self,
|
|
305
|
+
user_id: str,
|
|
306
|
+
suggestion_id: str,
|
|
307
|
+
feedback_type: FeedbackType,
|
|
308
|
+
outcome: OutcomeType,
|
|
309
|
+
) -> None:
|
|
310
|
+
"""
|
|
311
|
+
Update user preferences based on feedback.
|
|
312
|
+
|
|
313
|
+
Uses exponential moving average to update preference weights.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
user_id: User whose preferences to update
|
|
317
|
+
suggestion_id: Suggestion that was rated
|
|
318
|
+
feedback_type: Type of feedback given
|
|
319
|
+
outcome: Outcome of following suggestion
|
|
320
|
+
"""
|
|
321
|
+
try:
|
|
322
|
+
# Get current preferences
|
|
323
|
+
preferences = self.get_preferences(user_id)
|
|
324
|
+
|
|
325
|
+
# Get suggestion details to determine what preferences to update
|
|
326
|
+
conn = self._get_connection()
|
|
327
|
+
cursor = conn.cursor()
|
|
328
|
+
|
|
329
|
+
cursor.execute(
|
|
330
|
+
"""
|
|
331
|
+
SELECT suggestion_type, metadata FROM delegation_suggestions
|
|
332
|
+
WHERE suggestion_id = ?
|
|
333
|
+
""",
|
|
334
|
+
(suggestion_id,),
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
row = cursor.fetchone()
|
|
338
|
+
if not row:
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
suggestion_type = row["suggestion_type"]
|
|
342
|
+
metadata = json.loads(row["metadata"]) if row["metadata"] else {}
|
|
343
|
+
|
|
344
|
+
# Calculate feedback value (-1 to 1)
|
|
345
|
+
feedback_value = 0.0
|
|
346
|
+
if feedback_type == FeedbackType.ACCEPTED:
|
|
347
|
+
if outcome == OutcomeType.SUCCESSFUL:
|
|
348
|
+
feedback_value = 1.0
|
|
349
|
+
elif outcome == OutcomeType.PARTIAL:
|
|
350
|
+
feedback_value = 0.5
|
|
351
|
+
elif outcome == OutcomeType.FAILED:
|
|
352
|
+
feedback_value = -0.5
|
|
353
|
+
else:
|
|
354
|
+
feedback_value = 0.3 # Unknown but accepted
|
|
355
|
+
elif feedback_type == FeedbackType.REJECTED:
|
|
356
|
+
feedback_value = -0.3
|
|
357
|
+
elif feedback_type == FeedbackType.IGNORED:
|
|
358
|
+
feedback_value = -0.1
|
|
359
|
+
|
|
360
|
+
# Update suggestion type preference
|
|
361
|
+
current = preferences.suggestion_type_preferences.get(suggestion_type, 0.5)
|
|
362
|
+
new_value = current + self._learning_rate * (
|
|
363
|
+
feedback_value - (current - 0.5)
|
|
364
|
+
)
|
|
365
|
+
new_value = max(0.0, min(1.0, new_value)) # Clamp to [0, 1]
|
|
366
|
+
preferences.suggestion_type_preferences[suggestion_type] = new_value
|
|
367
|
+
|
|
368
|
+
# Update model preference if model suggestion
|
|
369
|
+
if suggestion_type == "model_selection" and "recommended_model" in metadata:
|
|
370
|
+
model = metadata["recommended_model"]
|
|
371
|
+
current = preferences.model_preferences.get(model, 0.5)
|
|
372
|
+
new_value = current + self._learning_rate * feedback_value
|
|
373
|
+
new_value = max(0.0, min(1.0, new_value))
|
|
374
|
+
preferences.model_preferences[model] = new_value
|
|
375
|
+
|
|
376
|
+
# Update delegation preference if delegation suggestion
|
|
377
|
+
if suggestion_type == "delegation" and "next_agent" in metadata:
|
|
378
|
+
agent = metadata["next_agent"]
|
|
379
|
+
current = preferences.delegation_preferences.get(agent, 0.5)
|
|
380
|
+
new_value = current + self._learning_rate * feedback_value
|
|
381
|
+
new_value = max(0.0, min(1.0, new_value))
|
|
382
|
+
preferences.delegation_preferences[agent] = new_value
|
|
383
|
+
|
|
384
|
+
# Store updated preferences
|
|
385
|
+
self._store_preferences(preferences)
|
|
386
|
+
|
|
387
|
+
except Exception as e:
|
|
388
|
+
logger.warning(f"Error updating preferences: {e}")
|
|
389
|
+
|
|
390
|
+
def get_preferences(self, user_id: str) -> UserPreferences:
|
|
391
|
+
"""
|
|
392
|
+
Get preferences for a user.
|
|
393
|
+
|
|
394
|
+
Creates default preferences if user has no stored preferences.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
user_id: User to get preferences for
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
UserPreferences for the user
|
|
401
|
+
"""
|
|
402
|
+
conn = self._get_connection()
|
|
403
|
+
cursor = conn.cursor()
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
cursor.execute(
|
|
407
|
+
"""
|
|
408
|
+
SELECT preferences_json, updated_at FROM user_preferences
|
|
409
|
+
WHERE user_id = ?
|
|
410
|
+
""",
|
|
411
|
+
(user_id,),
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
row = cursor.fetchone()
|
|
415
|
+
if row and row["preferences_json"]:
|
|
416
|
+
data = json.loads(row["preferences_json"])
|
|
417
|
+
return UserPreferences(
|
|
418
|
+
user_id=user_id,
|
|
419
|
+
model_preferences=data.get("model_preferences", {}),
|
|
420
|
+
delegation_preferences=data.get("delegation_preferences", {}),
|
|
421
|
+
tool_preferences=data.get("tool_preferences", {}),
|
|
422
|
+
suggestion_type_preferences=data.get(
|
|
423
|
+
"suggestion_type_preferences", {}
|
|
424
|
+
),
|
|
425
|
+
quality_thresholds=data.get("quality_thresholds", {}),
|
|
426
|
+
cost_sensitivity=data.get("cost_sensitivity", 0.5),
|
|
427
|
+
speed_sensitivity=data.get("speed_sensitivity", 0.5),
|
|
428
|
+
updated_at=datetime.fromisoformat(row["updated_at"])
|
|
429
|
+
if row["updated_at"]
|
|
430
|
+
else None,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Return default preferences for new user
|
|
434
|
+
return UserPreferences.default(user_id)
|
|
435
|
+
|
|
436
|
+
except sqlite3.Error as e:
|
|
437
|
+
logger.warning(f"Error getting preferences: {e}")
|
|
438
|
+
return UserPreferences.default(user_id)
|
|
439
|
+
|
|
440
|
+
def _store_preferences(self, preferences: UserPreferences) -> bool:
|
|
441
|
+
"""
|
|
442
|
+
Store user preferences in database.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
preferences: Preferences to store
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
True if stored successfully, False otherwise
|
|
449
|
+
"""
|
|
450
|
+
conn = self._get_connection()
|
|
451
|
+
cursor = conn.cursor()
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
preferences_json = json.dumps(
|
|
455
|
+
{
|
|
456
|
+
"model_preferences": preferences.model_preferences,
|
|
457
|
+
"delegation_preferences": preferences.delegation_preferences,
|
|
458
|
+
"tool_preferences": preferences.tool_preferences,
|
|
459
|
+
"suggestion_type_preferences": preferences.suggestion_type_preferences,
|
|
460
|
+
"quality_thresholds": preferences.quality_thresholds,
|
|
461
|
+
"cost_sensitivity": preferences.cost_sensitivity,
|
|
462
|
+
"speed_sensitivity": preferences.speed_sensitivity,
|
|
463
|
+
}
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
cursor.execute(
|
|
467
|
+
"""
|
|
468
|
+
INSERT OR REPLACE INTO user_preferences
|
|
469
|
+
(user_id, preferences_json, updated_at)
|
|
470
|
+
VALUES (?, ?, datetime('now'))
|
|
471
|
+
""",
|
|
472
|
+
(preferences.user_id, preferences_json),
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
conn.commit()
|
|
476
|
+
return True
|
|
477
|
+
|
|
478
|
+
except sqlite3.Error as e:
|
|
479
|
+
logger.error(f"Error storing preferences: {e}")
|
|
480
|
+
return False
|
|
481
|
+
|
|
482
|
+
def calculate_weights(self, feedback_list: list[Feedback]) -> dict[str, float]:
|
|
483
|
+
"""
|
|
484
|
+
Calculate preference weights from a list of feedback.
|
|
485
|
+
|
|
486
|
+
Uses time-decayed weighted average.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
feedback_list: List of feedback to analyze
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Dictionary of preference weights by suggestion type
|
|
493
|
+
"""
|
|
494
|
+
if not feedback_list:
|
|
495
|
+
return {}
|
|
496
|
+
|
|
497
|
+
weights: dict[str, float] = {}
|
|
498
|
+
type_counts: dict[str, int] = {}
|
|
499
|
+
type_sums: dict[str, float] = {}
|
|
500
|
+
|
|
501
|
+
# Sort by creation time (oldest first for proper decay)
|
|
502
|
+
sorted_feedback = sorted(
|
|
503
|
+
feedback_list,
|
|
504
|
+
key=lambda f: f.created_at or datetime.min,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
for i, feedback in enumerate(sorted_feedback):
|
|
508
|
+
# Calculate decay factor based on position
|
|
509
|
+
decay = self._decay_factor ** (len(sorted_feedback) - i - 1)
|
|
510
|
+
|
|
511
|
+
# Calculate feedback value
|
|
512
|
+
value = 0.0
|
|
513
|
+
if feedback.feedback_type == FeedbackType.ACCEPTED:
|
|
514
|
+
if feedback.outcome == OutcomeType.SUCCESSFUL:
|
|
515
|
+
value = 1.0
|
|
516
|
+
elif feedback.outcome == OutcomeType.PARTIAL:
|
|
517
|
+
value = 0.5
|
|
518
|
+
else:
|
|
519
|
+
value = 0.3
|
|
520
|
+
elif feedback.feedback_type == FeedbackType.REJECTED:
|
|
521
|
+
value = -0.3
|
|
522
|
+
|
|
523
|
+
# Get suggestion type from context
|
|
524
|
+
suggestion_type = feedback.context.get("suggestion_type", "unknown")
|
|
525
|
+
|
|
526
|
+
if suggestion_type not in type_sums:
|
|
527
|
+
type_sums[suggestion_type] = 0.0
|
|
528
|
+
type_counts[suggestion_type] = 0
|
|
529
|
+
|
|
530
|
+
type_sums[suggestion_type] += value * decay
|
|
531
|
+
type_counts[suggestion_type] += 1
|
|
532
|
+
|
|
533
|
+
# Calculate weighted averages
|
|
534
|
+
for suggestion_type in type_sums:
|
|
535
|
+
if type_counts[suggestion_type] > 0:
|
|
536
|
+
avg = type_sums[suggestion_type] / type_counts[suggestion_type]
|
|
537
|
+
# Normalize to [0, 1]
|
|
538
|
+
weights[suggestion_type] = (avg + 1) / 2
|
|
539
|
+
|
|
540
|
+
return weights
|
|
541
|
+
|
|
542
|
+
def reset_preferences(self, user_id: str) -> bool:
|
|
543
|
+
"""
|
|
544
|
+
Reset preferences for a user to defaults.
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
user_id: User to reset
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
True if reset successfully, False otherwise
|
|
551
|
+
"""
|
|
552
|
+
default_prefs = UserPreferences.default(user_id)
|
|
553
|
+
return self._store_preferences(default_prefs)
|
|
554
|
+
|
|
555
|
+
def get_feedback_history(
|
|
556
|
+
self,
|
|
557
|
+
user_id: str,
|
|
558
|
+
limit: int = 100,
|
|
559
|
+
) -> list[Feedback]:
|
|
560
|
+
"""
|
|
561
|
+
Get feedback history for a user.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
user_id: User to get history for
|
|
565
|
+
limit: Maximum number of feedback entries
|
|
566
|
+
|
|
567
|
+
Returns:
|
|
568
|
+
List of feedback entries
|
|
569
|
+
"""
|
|
570
|
+
conn = self._get_connection()
|
|
571
|
+
cursor = conn.cursor()
|
|
572
|
+
|
|
573
|
+
try:
|
|
574
|
+
cursor.execute(
|
|
575
|
+
"""
|
|
576
|
+
SELECT * FROM delegation_preferences
|
|
577
|
+
WHERE user_id = ?
|
|
578
|
+
ORDER BY created_at DESC
|
|
579
|
+
LIMIT ?
|
|
580
|
+
""",
|
|
581
|
+
(user_id, limit),
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
feedback_list = []
|
|
585
|
+
for row in cursor.fetchall():
|
|
586
|
+
feedback = Feedback(
|
|
587
|
+
feedback_id=row["feedback_id"],
|
|
588
|
+
suggestion_id=row["suggestion_id"],
|
|
589
|
+
user_id=row["user_id"],
|
|
590
|
+
session_id=row["session_id"],
|
|
591
|
+
feedback_type=FeedbackType(row["feedback_type"]),
|
|
592
|
+
outcome=OutcomeType(row["outcome"]),
|
|
593
|
+
rating=row["rating"],
|
|
594
|
+
comment=row["comment"],
|
|
595
|
+
context=json.loads(row["context"]) if row["context"] else {},
|
|
596
|
+
created_at=datetime.fromisoformat(row["created_at"])
|
|
597
|
+
if row["created_at"]
|
|
598
|
+
else None,
|
|
599
|
+
)
|
|
600
|
+
feedback_list.append(feedback)
|
|
601
|
+
|
|
602
|
+
return feedback_list
|
|
603
|
+
|
|
604
|
+
except sqlite3.Error as e:
|
|
605
|
+
logger.error(f"Error getting feedback history: {e}")
|
|
606
|
+
return []
|
|
607
|
+
|
|
608
|
+
def get_acceptance_rate(
|
|
609
|
+
self, user_id: str, suggestion_type: str | None = None
|
|
610
|
+
) -> float:
|
|
611
|
+
"""
|
|
612
|
+
Calculate suggestion acceptance rate for a user.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
user_id: User to calculate for
|
|
616
|
+
suggestion_type: Optional filter by suggestion type
|
|
617
|
+
|
|
618
|
+
Returns:
|
|
619
|
+
Acceptance rate as percentage (0-100)
|
|
620
|
+
"""
|
|
621
|
+
conn = self._get_connection()
|
|
622
|
+
cursor = conn.cursor()
|
|
623
|
+
|
|
624
|
+
try:
|
|
625
|
+
if suggestion_type:
|
|
626
|
+
cursor.execute(
|
|
627
|
+
"""
|
|
628
|
+
SELECT
|
|
629
|
+
COUNT(*) as total,
|
|
630
|
+
SUM(CASE WHEN feedback_type = 'accepted' THEN 1 ELSE 0 END) as accepted
|
|
631
|
+
FROM delegation_preferences dp
|
|
632
|
+
JOIN delegation_suggestions ds ON dp.suggestion_id = ds.suggestion_id
|
|
633
|
+
WHERE dp.user_id = ?
|
|
634
|
+
AND ds.suggestion_type = ?
|
|
635
|
+
""",
|
|
636
|
+
(user_id, suggestion_type),
|
|
637
|
+
)
|
|
638
|
+
else:
|
|
639
|
+
cursor.execute(
|
|
640
|
+
"""
|
|
641
|
+
SELECT
|
|
642
|
+
COUNT(*) as total,
|
|
643
|
+
SUM(CASE WHEN feedback_type = 'accepted' THEN 1 ELSE 0 END) as accepted
|
|
644
|
+
FROM delegation_preferences
|
|
645
|
+
WHERE user_id = ?
|
|
646
|
+
""",
|
|
647
|
+
(user_id,),
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
row = cursor.fetchone()
|
|
651
|
+
if row and row["total"] > 0:
|
|
652
|
+
return float((row["accepted"] / row["total"]) * 100)
|
|
653
|
+
return 0.0
|
|
654
|
+
|
|
655
|
+
except sqlite3.Error as e:
|
|
656
|
+
logger.error(f"Error calculating acceptance rate: {e}")
|
|
657
|
+
return 0.0
|
|
658
|
+
|
|
659
|
+
def apply_preferences_to_suggestions(
|
|
660
|
+
self,
|
|
661
|
+
suggestions: list[Any], # List of Suggestion objects
|
|
662
|
+
user_id: str,
|
|
663
|
+
) -> list[Any]:
|
|
664
|
+
"""
|
|
665
|
+
Adjust suggestion scores based on user preferences.
|
|
666
|
+
|
|
667
|
+
Args:
|
|
668
|
+
suggestions: List of suggestions to adjust
|
|
669
|
+
user_id: User whose preferences to apply
|
|
670
|
+
|
|
671
|
+
Returns:
|
|
672
|
+
Adjusted suggestions (same list, modified in place)
|
|
673
|
+
"""
|
|
674
|
+
preferences = self.get_preferences(user_id)
|
|
675
|
+
|
|
676
|
+
for suggestion in suggestions:
|
|
677
|
+
# Get preference weight for this suggestion type
|
|
678
|
+
suggestion_type = suggestion.suggestion_type.value
|
|
679
|
+
pref_weight = preferences.suggestion_type_preferences.get(
|
|
680
|
+
suggestion_type, 0.5
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
# Adjust relevance based on preference
|
|
684
|
+
# Preferences > 0.5 boost relevance, < 0.5 reduce it
|
|
685
|
+
adjustment = (pref_weight - 0.5) * 0.4 # Max +/- 0.2 adjustment
|
|
686
|
+
suggestion.relevance = max(0.0, min(1.0, suggestion.relevance + adjustment))
|
|
687
|
+
|
|
688
|
+
# Apply model preference if model suggestion
|
|
689
|
+
if (
|
|
690
|
+
suggestion_type == "model_selection"
|
|
691
|
+
and "recommended_model" in suggestion.metadata
|
|
692
|
+
):
|
|
693
|
+
model = suggestion.metadata["recommended_model"]
|
|
694
|
+
model_pref = preferences.model_preferences.get(model, 0.5)
|
|
695
|
+
model_adjustment = (model_pref - 0.5) * 0.3
|
|
696
|
+
suggestion.confidence = max(
|
|
697
|
+
0.0, min(1.0, suggestion.confidence + model_adjustment)
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
# Apply delegation preference if delegation suggestion
|
|
701
|
+
if suggestion_type == "delegation" and "next_agent" in suggestion.metadata:
|
|
702
|
+
agent = suggestion.metadata["next_agent"]
|
|
703
|
+
agent_pref = preferences.delegation_preferences.get(agent, 0.5)
|
|
704
|
+
agent_adjustment = (agent_pref - 0.5) * 0.3
|
|
705
|
+
suggestion.confidence = max(
|
|
706
|
+
0.0, min(1.0, suggestion.confidence + agent_adjustment)
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
return suggestions
|