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.
Files changed (331) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +173 -17
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +127 -0
  7. htmlgraph/agent_registry.py +45 -30
  8. htmlgraph/agents.py +160 -107
  9. htmlgraph/analytics/__init__.py +9 -2
  10. htmlgraph/analytics/cli.py +190 -51
  11. htmlgraph/analytics/cost_analyzer.py +391 -0
  12. htmlgraph/analytics/cost_monitor.py +664 -0
  13. htmlgraph/analytics/cost_reporter.py +675 -0
  14. htmlgraph/analytics/cross_session.py +617 -0
  15. htmlgraph/analytics/dependency.py +192 -100
  16. htmlgraph/analytics/pattern_learning.py +771 -0
  17. htmlgraph/analytics/session_graph.py +707 -0
  18. htmlgraph/analytics/strategic/__init__.py +80 -0
  19. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  20. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  21. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  22. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  23. htmlgraph/analytics/work_type.py +190 -14
  24. htmlgraph/analytics_index.py +135 -51
  25. htmlgraph/api/__init__.py +3 -0
  26. htmlgraph/api/cost_alerts_websocket.py +416 -0
  27. htmlgraph/api/main.py +2498 -0
  28. htmlgraph/api/static/htmx.min.js +1 -0
  29. htmlgraph/api/static/style-redesign.css +1344 -0
  30. htmlgraph/api/static/style.css +1079 -0
  31. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  32. htmlgraph/api/templates/dashboard.html +794 -0
  33. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  34. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  35. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  36. htmlgraph/api/templates/partials/agents.html +317 -0
  37. htmlgraph/api/templates/partials/event-traces.html +373 -0
  38. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  39. htmlgraph/api/templates/partials/features.html +578 -0
  40. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  41. htmlgraph/api/templates/partials/metrics.html +346 -0
  42. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  43. htmlgraph/api/templates/partials/orchestration.html +198 -0
  44. htmlgraph/api/templates/partials/spawners.html +375 -0
  45. htmlgraph/api/templates/partials/work-items.html +613 -0
  46. htmlgraph/api/websocket.py +538 -0
  47. htmlgraph/archive/__init__.py +24 -0
  48. htmlgraph/archive/bloom.py +234 -0
  49. htmlgraph/archive/fts.py +297 -0
  50. htmlgraph/archive/manager.py +583 -0
  51. htmlgraph/archive/search.py +244 -0
  52. htmlgraph/atomic_ops.py +560 -0
  53. htmlgraph/attribute_index.py +208 -0
  54. htmlgraph/bounded_paths.py +539 -0
  55. htmlgraph/builders/__init__.py +14 -0
  56. htmlgraph/builders/base.py +118 -29
  57. htmlgraph/builders/bug.py +150 -0
  58. htmlgraph/builders/chore.py +119 -0
  59. htmlgraph/builders/epic.py +150 -0
  60. htmlgraph/builders/feature.py +31 -6
  61. htmlgraph/builders/insight.py +195 -0
  62. htmlgraph/builders/metric.py +217 -0
  63. htmlgraph/builders/pattern.py +202 -0
  64. htmlgraph/builders/phase.py +162 -0
  65. htmlgraph/builders/spike.py +52 -19
  66. htmlgraph/builders/track.py +148 -72
  67. htmlgraph/cigs/__init__.py +81 -0
  68. htmlgraph/cigs/autonomy.py +385 -0
  69. htmlgraph/cigs/cost.py +475 -0
  70. htmlgraph/cigs/messages_basic.py +472 -0
  71. htmlgraph/cigs/messaging.py +365 -0
  72. htmlgraph/cigs/models.py +771 -0
  73. htmlgraph/cigs/pattern_storage.py +427 -0
  74. htmlgraph/cigs/patterns.py +503 -0
  75. htmlgraph/cigs/posttool_analyzer.py +234 -0
  76. htmlgraph/cigs/reporter.py +818 -0
  77. htmlgraph/cigs/tracker.py +317 -0
  78. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  79. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  80. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  81. htmlgraph/cli/__init__.py +42 -0
  82. htmlgraph/cli/__main__.py +6 -0
  83. htmlgraph/cli/analytics.py +1424 -0
  84. htmlgraph/cli/base.py +685 -0
  85. htmlgraph/cli/constants.py +206 -0
  86. htmlgraph/cli/core.py +954 -0
  87. htmlgraph/cli/main.py +147 -0
  88. htmlgraph/cli/models.py +475 -0
  89. htmlgraph/cli/templates/__init__.py +1 -0
  90. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  91. htmlgraph/cli/work/__init__.py +239 -0
  92. htmlgraph/cli/work/browse.py +115 -0
  93. htmlgraph/cli/work/features.py +568 -0
  94. htmlgraph/cli/work/orchestration.py +676 -0
  95. htmlgraph/cli/work/report.py +728 -0
  96. htmlgraph/cli/work/sessions.py +466 -0
  97. htmlgraph/cli/work/snapshot.py +559 -0
  98. htmlgraph/cli/work/tracks.py +486 -0
  99. htmlgraph/cli_commands/__init__.py +1 -0
  100. htmlgraph/cli_commands/feature.py +195 -0
  101. htmlgraph/cli_framework.py +115 -0
  102. htmlgraph/collections/__init__.py +18 -0
  103. htmlgraph/collections/base.py +415 -98
  104. htmlgraph/collections/bug.py +53 -0
  105. htmlgraph/collections/chore.py +53 -0
  106. htmlgraph/collections/epic.py +53 -0
  107. htmlgraph/collections/feature.py +12 -26
  108. htmlgraph/collections/insight.py +100 -0
  109. htmlgraph/collections/metric.py +92 -0
  110. htmlgraph/collections/pattern.py +97 -0
  111. htmlgraph/collections/phase.py +53 -0
  112. htmlgraph/collections/session.py +194 -0
  113. htmlgraph/collections/spike.py +56 -16
  114. htmlgraph/collections/task_delegation.py +241 -0
  115. htmlgraph/collections/todo.py +511 -0
  116. htmlgraph/collections/traces.py +487 -0
  117. htmlgraph/config/cost_models.json +56 -0
  118. htmlgraph/config.py +190 -0
  119. htmlgraph/context_analytics.py +344 -0
  120. htmlgraph/converter.py +216 -28
  121. htmlgraph/cost_analysis/__init__.py +5 -0
  122. htmlgraph/cost_analysis/analyzer.py +438 -0
  123. htmlgraph/dashboard.html +2406 -307
  124. htmlgraph/dashboard.html.backup +6592 -0
  125. htmlgraph/dashboard.html.bak +7181 -0
  126. htmlgraph/dashboard.html.bak2 +7231 -0
  127. htmlgraph/dashboard.html.bak3 +7232 -0
  128. htmlgraph/db/__init__.py +38 -0
  129. htmlgraph/db/queries.py +790 -0
  130. htmlgraph/db/schema.py +1788 -0
  131. htmlgraph/decorators.py +317 -0
  132. htmlgraph/dependency_models.py +19 -2
  133. htmlgraph/deploy.py +142 -125
  134. htmlgraph/deployment_models.py +474 -0
  135. htmlgraph/docs/API_REFERENCE.md +841 -0
  136. htmlgraph/docs/HTTP_API.md +750 -0
  137. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  138. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  139. htmlgraph/docs/README.md +532 -0
  140. htmlgraph/docs/__init__.py +77 -0
  141. htmlgraph/docs/docs_version.py +55 -0
  142. htmlgraph/docs/metadata.py +93 -0
  143. htmlgraph/docs/migrations.py +232 -0
  144. htmlgraph/docs/template_engine.py +143 -0
  145. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  146. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  147. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  148. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  149. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  150. htmlgraph/docs/version_check.py +163 -0
  151. htmlgraph/edge_index.py +182 -27
  152. htmlgraph/error_handler.py +544 -0
  153. htmlgraph/event_log.py +100 -52
  154. htmlgraph/event_migration.py +13 -4
  155. htmlgraph/exceptions.py +49 -0
  156. htmlgraph/file_watcher.py +101 -28
  157. htmlgraph/find_api.py +75 -63
  158. htmlgraph/git_events.py +145 -63
  159. htmlgraph/graph.py +1122 -106
  160. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  161. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  162. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  163. htmlgraph/hooks/__init__.py +45 -0
  164. htmlgraph/hooks/bootstrap.py +169 -0
  165. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  166. htmlgraph/hooks/concurrent_sessions.py +208 -0
  167. htmlgraph/hooks/context.py +350 -0
  168. htmlgraph/hooks/drift_handler.py +525 -0
  169. htmlgraph/hooks/event_tracker.py +1314 -0
  170. htmlgraph/hooks/git_commands.py +175 -0
  171. htmlgraph/hooks/hooks-config.example.json +12 -0
  172. htmlgraph/hooks/installer.py +343 -0
  173. htmlgraph/hooks/orchestrator.py +674 -0
  174. htmlgraph/hooks/orchestrator_reflector.py +223 -0
  175. htmlgraph/hooks/post-checkout.sh +28 -0
  176. htmlgraph/hooks/post-commit.sh +24 -0
  177. htmlgraph/hooks/post-merge.sh +26 -0
  178. htmlgraph/hooks/post_tool_use_failure.py +273 -0
  179. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  180. htmlgraph/hooks/posttooluse.py +408 -0
  181. htmlgraph/hooks/pre-commit.sh +94 -0
  182. htmlgraph/hooks/pre-push.sh +28 -0
  183. htmlgraph/hooks/pretooluse.py +819 -0
  184. htmlgraph/hooks/prompt_analyzer.py +637 -0
  185. htmlgraph/hooks/session_handler.py +668 -0
  186. htmlgraph/hooks/session_summary.py +395 -0
  187. htmlgraph/hooks/state_manager.py +504 -0
  188. htmlgraph/hooks/subagent_detection.py +202 -0
  189. htmlgraph/hooks/subagent_stop.py +369 -0
  190. htmlgraph/hooks/task_enforcer.py +255 -0
  191. htmlgraph/hooks/task_validator.py +177 -0
  192. htmlgraph/hooks/validator.py +628 -0
  193. htmlgraph/ids.py +41 -27
  194. htmlgraph/index.d.ts +286 -0
  195. htmlgraph/learning.py +767 -0
  196. htmlgraph/mcp_server.py +69 -23
  197. htmlgraph/models.py +1586 -87
  198. htmlgraph/operations/README.md +62 -0
  199. htmlgraph/operations/__init__.py +79 -0
  200. htmlgraph/operations/analytics.py +339 -0
  201. htmlgraph/operations/bootstrap.py +289 -0
  202. htmlgraph/operations/events.py +244 -0
  203. htmlgraph/operations/fastapi_server.py +231 -0
  204. htmlgraph/operations/hooks.py +350 -0
  205. htmlgraph/operations/initialization.py +597 -0
  206. htmlgraph/operations/initialization.py.backup +228 -0
  207. htmlgraph/operations/server.py +303 -0
  208. htmlgraph/orchestration/__init__.py +58 -0
  209. htmlgraph/orchestration/claude_launcher.py +179 -0
  210. htmlgraph/orchestration/command_builder.py +72 -0
  211. htmlgraph/orchestration/headless_spawner.py +281 -0
  212. htmlgraph/orchestration/live_events.py +377 -0
  213. htmlgraph/orchestration/model_selection.py +327 -0
  214. htmlgraph/orchestration/plugin_manager.py +140 -0
  215. htmlgraph/orchestration/prompts.py +137 -0
  216. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  217. htmlgraph/orchestration/spawners/__init__.py +16 -0
  218. htmlgraph/orchestration/spawners/base.py +194 -0
  219. htmlgraph/orchestration/spawners/claude.py +173 -0
  220. htmlgraph/orchestration/spawners/codex.py +435 -0
  221. htmlgraph/orchestration/spawners/copilot.py +294 -0
  222. htmlgraph/orchestration/spawners/gemini.py +471 -0
  223. htmlgraph/orchestration/subprocess_runner.py +36 -0
  224. htmlgraph/orchestration/task_coordination.py +343 -0
  225. htmlgraph/orchestration.md +563 -0
  226. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  227. htmlgraph/orchestrator.py +669 -0
  228. htmlgraph/orchestrator_config.py +357 -0
  229. htmlgraph/orchestrator_mode.py +328 -0
  230. htmlgraph/orchestrator_validator.py +133 -0
  231. htmlgraph/parallel.py +646 -0
  232. htmlgraph/parser.py +160 -35
  233. htmlgraph/path_query.py +608 -0
  234. htmlgraph/pattern_matcher.py +636 -0
  235. htmlgraph/planning.py +147 -52
  236. htmlgraph/pydantic_models.py +476 -0
  237. htmlgraph/quality_gates.py +350 -0
  238. htmlgraph/query_builder.py +109 -72
  239. htmlgraph/query_composer.py +509 -0
  240. htmlgraph/reflection.py +443 -0
  241. htmlgraph/refs.py +344 -0
  242. htmlgraph/repo_hash.py +512 -0
  243. htmlgraph/repositories/__init__.py +292 -0
  244. htmlgraph/repositories/analytics_repository.py +455 -0
  245. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  246. htmlgraph/repositories/feature_repository.py +581 -0
  247. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  248. htmlgraph/repositories/feature_repository_memory.py +607 -0
  249. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  250. htmlgraph/repositories/filter_service.py +620 -0
  251. htmlgraph/repositories/filter_service_standard.py +445 -0
  252. htmlgraph/repositories/shared_cache.py +621 -0
  253. htmlgraph/repositories/shared_cache_memory.py +395 -0
  254. htmlgraph/repositories/track_repository.py +552 -0
  255. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  256. htmlgraph/repositories/track_repository_memory.py +508 -0
  257. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  258. htmlgraph/routing.py +8 -19
  259. htmlgraph/scripts/deploy.py +1 -2
  260. htmlgraph/sdk/__init__.py +398 -0
  261. htmlgraph/sdk/__init__.pyi +14 -0
  262. htmlgraph/sdk/analytics/__init__.py +19 -0
  263. htmlgraph/sdk/analytics/engine.py +155 -0
  264. htmlgraph/sdk/analytics/helpers.py +178 -0
  265. htmlgraph/sdk/analytics/registry.py +109 -0
  266. htmlgraph/sdk/base.py +484 -0
  267. htmlgraph/sdk/constants.py +216 -0
  268. htmlgraph/sdk/core.pyi +308 -0
  269. htmlgraph/sdk/discovery.py +120 -0
  270. htmlgraph/sdk/help/__init__.py +12 -0
  271. htmlgraph/sdk/help/mixin.py +699 -0
  272. htmlgraph/sdk/mixins/__init__.py +15 -0
  273. htmlgraph/sdk/mixins/attribution.py +113 -0
  274. htmlgraph/sdk/mixins/mixin.py +410 -0
  275. htmlgraph/sdk/operations/__init__.py +12 -0
  276. htmlgraph/sdk/operations/mixin.py +427 -0
  277. htmlgraph/sdk/orchestration/__init__.py +17 -0
  278. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  279. htmlgraph/sdk/orchestration/spawner.py +204 -0
  280. htmlgraph/sdk/planning/__init__.py +19 -0
  281. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  282. htmlgraph/sdk/planning/mixin.py +211 -0
  283. htmlgraph/sdk/planning/parallel.py +186 -0
  284. htmlgraph/sdk/planning/queue.py +210 -0
  285. htmlgraph/sdk/planning/recommendations.py +87 -0
  286. htmlgraph/sdk/planning/smart_planning.py +319 -0
  287. htmlgraph/sdk/session/__init__.py +19 -0
  288. htmlgraph/sdk/session/continuity.py +57 -0
  289. htmlgraph/sdk/session/handoff.py +110 -0
  290. htmlgraph/sdk/session/info.py +309 -0
  291. htmlgraph/sdk/session/manager.py +103 -0
  292. htmlgraph/sdk/strategic/__init__.py +26 -0
  293. htmlgraph/sdk/strategic/mixin.py +563 -0
  294. htmlgraph/server.py +685 -180
  295. htmlgraph/services/__init__.py +10 -0
  296. htmlgraph/services/claiming.py +199 -0
  297. htmlgraph/session_hooks.py +300 -0
  298. htmlgraph/session_manager.py +1392 -175
  299. htmlgraph/session_registry.py +587 -0
  300. htmlgraph/session_state.py +436 -0
  301. htmlgraph/session_warning.py +201 -0
  302. htmlgraph/sessions/__init__.py +23 -0
  303. htmlgraph/sessions/handoff.py +756 -0
  304. htmlgraph/setup.py +34 -17
  305. htmlgraph/spike_index.py +143 -0
  306. htmlgraph/sync_docs.py +12 -15
  307. htmlgraph/system_prompts.py +450 -0
  308. htmlgraph/templates/AGENTS.md.template +366 -0
  309. htmlgraph/templates/CLAUDE.md.template +97 -0
  310. htmlgraph/templates/GEMINI.md.template +87 -0
  311. htmlgraph/templates/orchestration-view.html +350 -0
  312. htmlgraph/track_builder.py +146 -15
  313. htmlgraph/track_manager.py +69 -21
  314. htmlgraph/transcript.py +890 -0
  315. htmlgraph/transcript_analytics.py +699 -0
  316. htmlgraph/types.py +323 -0
  317. htmlgraph/validation.py +115 -0
  318. htmlgraph/watch.py +8 -5
  319. htmlgraph/work_type_utils.py +3 -2
  320. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
  321. htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
  322. htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
  323. htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
  324. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
  325. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  326. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  327. htmlgraph/cli.py +0 -2688
  328. htmlgraph/sdk.py +0 -709
  329. htmlgraph-0.9.3.dist-info/RECORD +0 -61
  330. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  331. {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