htmlgraph 0.21.0__py3-none-any.whl → 0.23.0__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 (40) hide show
  1. htmlgraph/__init__.py +1 -1
  2. htmlgraph/agent_detection.py +41 -2
  3. htmlgraph/analytics/cli.py +86 -20
  4. htmlgraph/cli.py +519 -87
  5. htmlgraph/collections/base.py +68 -4
  6. htmlgraph/docs/__init__.py +77 -0
  7. htmlgraph/docs/docs_version.py +55 -0
  8. htmlgraph/docs/metadata.py +93 -0
  9. htmlgraph/docs/migrations.py +232 -0
  10. htmlgraph/docs/template_engine.py +143 -0
  11. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  12. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  13. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  14. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  15. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  16. htmlgraph/docs/version_check.py +161 -0
  17. htmlgraph/git_events.py +61 -7
  18. htmlgraph/operations/README.md +62 -0
  19. htmlgraph/operations/__init__.py +61 -0
  20. htmlgraph/operations/analytics.py +338 -0
  21. htmlgraph/operations/events.py +243 -0
  22. htmlgraph/operations/hooks.py +349 -0
  23. htmlgraph/operations/server.py +302 -0
  24. htmlgraph/orchestration/__init__.py +39 -0
  25. htmlgraph/orchestration/headless_spawner.py +566 -0
  26. htmlgraph/orchestration/model_selection.py +323 -0
  27. htmlgraph/orchestrator-system-prompt-optimized.txt +47 -0
  28. htmlgraph/parser.py +56 -1
  29. htmlgraph/sdk.py +529 -7
  30. htmlgraph/server.py +153 -60
  31. {htmlgraph-0.21.0.dist-info → htmlgraph-0.23.0.dist-info}/METADATA +3 -1
  32. {htmlgraph-0.21.0.dist-info → htmlgraph-0.23.0.dist-info}/RECORD +40 -19
  33. /htmlgraph/{orchestration.py → orchestration/task_coordination.py} +0 -0
  34. {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/dashboard.html +0 -0
  35. {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/styles.css +0 -0
  36. {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  37. {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  38. {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  39. {htmlgraph-0.21.0.dist-info → htmlgraph-0.23.0.dist-info}/WHEEL +0 -0
  40. {htmlgraph-0.21.0.dist-info → htmlgraph-0.23.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,323 @@
1
+ """Intelligent model selection for task routing.
2
+
3
+ This module provides functionality to select the best AI model for a given task
4
+ based on task type, complexity, and budget constraints.
5
+
6
+ Model Selection Strategy:
7
+ - Exploration: Use Gemini (free tier) for cost-effective research
8
+ - Debugging: Use Claude Sonnet (high context) for complex error analysis
9
+ - Implementation: Use Codex (programming specialized) for code generation
10
+ - Quality: Use Claude Haiku (fast) for linting and formatting
11
+
12
+ Fallback Chain:
13
+ Each model has fallback options if primary model is unavailable:
14
+ - Gemini → Claude Haiku → Claude Sonnet
15
+ - Codex → Claude Sonnet
16
+ - Copilot → Claude Sonnet
17
+ """
18
+
19
+ from enum import Enum
20
+
21
+
22
+ class TaskType(str, Enum):
23
+ """Task classification types."""
24
+
25
+ EXPLORATION = "exploration"
26
+ DEBUGGING = "debugging"
27
+ IMPLEMENTATION = "implementation"
28
+ QUALITY = "quality"
29
+ GENERAL = "general"
30
+
31
+
32
+ class ComplexityLevel(str, Enum):
33
+ """Complexity assessment levels."""
34
+
35
+ LOW = "low"
36
+ MEDIUM = "medium"
37
+ HIGH = "high"
38
+
39
+
40
+ class BudgetMode(str, Enum):
41
+ """Budget constraints."""
42
+
43
+ FREE = "free" # Use only free models
44
+ BALANCED = "balanced" # Balance cost and quality
45
+ QUALITY = "quality" # Prioritize best model
46
+
47
+
48
+ class ModelSelection:
49
+ """Intelligent model selection engine."""
50
+
51
+ # Decision matrix: (task_type, complexity, budget) -> model
52
+ DECISION_MATRIX = {
53
+ # Exploration tasks - prioritize free/cheap options
54
+ (TaskType.EXPLORATION, ComplexityLevel.LOW, BudgetMode.FREE): "gemini",
55
+ (TaskType.EXPLORATION, ComplexityLevel.MEDIUM, BudgetMode.FREE): "gemini",
56
+ (TaskType.EXPLORATION, ComplexityLevel.HIGH, BudgetMode.FREE): "gemini",
57
+ (TaskType.EXPLORATION, ComplexityLevel.LOW, BudgetMode.BALANCED): "gemini",
58
+ (TaskType.EXPLORATION, ComplexityLevel.MEDIUM, BudgetMode.BALANCED): "gemini",
59
+ (
60
+ TaskType.EXPLORATION,
61
+ ComplexityLevel.HIGH,
62
+ BudgetMode.BALANCED,
63
+ ): "claude-sonnet",
64
+ (
65
+ TaskType.EXPLORATION,
66
+ ComplexityLevel.LOW,
67
+ BudgetMode.QUALITY,
68
+ ): "claude-sonnet",
69
+ (
70
+ TaskType.EXPLORATION,
71
+ ComplexityLevel.MEDIUM,
72
+ BudgetMode.QUALITY,
73
+ ): "claude-sonnet",
74
+ (TaskType.EXPLORATION, ComplexityLevel.HIGH, BudgetMode.QUALITY): "claude-opus",
75
+ # Debugging tasks - need strong reasoning
76
+ (TaskType.DEBUGGING, ComplexityLevel.LOW, BudgetMode.FREE): "claude-haiku",
77
+ (TaskType.DEBUGGING, ComplexityLevel.MEDIUM, BudgetMode.FREE): "claude-haiku",
78
+ (TaskType.DEBUGGING, ComplexityLevel.HIGH, BudgetMode.FREE): "claude-haiku",
79
+ (TaskType.DEBUGGING, ComplexityLevel.LOW, BudgetMode.BALANCED): "claude-sonnet",
80
+ (
81
+ TaskType.DEBUGGING,
82
+ ComplexityLevel.MEDIUM,
83
+ BudgetMode.BALANCED,
84
+ ): "claude-sonnet",
85
+ (TaskType.DEBUGGING, ComplexityLevel.HIGH, BudgetMode.BALANCED): "claude-opus",
86
+ (TaskType.DEBUGGING, ComplexityLevel.LOW, BudgetMode.QUALITY): "claude-opus",
87
+ (TaskType.DEBUGGING, ComplexityLevel.MEDIUM, BudgetMode.QUALITY): "claude-opus",
88
+ (TaskType.DEBUGGING, ComplexityLevel.HIGH, BudgetMode.QUALITY): "claude-opus",
89
+ # Implementation tasks - balance speed and quality
90
+ (TaskType.IMPLEMENTATION, ComplexityLevel.LOW, BudgetMode.FREE): "claude-haiku",
91
+ (
92
+ TaskType.IMPLEMENTATION,
93
+ ComplexityLevel.MEDIUM,
94
+ BudgetMode.FREE,
95
+ ): "claude-haiku",
96
+ (
97
+ TaskType.IMPLEMENTATION,
98
+ ComplexityLevel.HIGH,
99
+ BudgetMode.FREE,
100
+ ): "claude-haiku",
101
+ (TaskType.IMPLEMENTATION, ComplexityLevel.LOW, BudgetMode.BALANCED): "codex",
102
+ (TaskType.IMPLEMENTATION, ComplexityLevel.MEDIUM, BudgetMode.BALANCED): "codex",
103
+ (
104
+ TaskType.IMPLEMENTATION,
105
+ ComplexityLevel.HIGH,
106
+ BudgetMode.BALANCED,
107
+ ): "claude-opus",
108
+ (
109
+ TaskType.IMPLEMENTATION,
110
+ ComplexityLevel.LOW,
111
+ BudgetMode.QUALITY,
112
+ ): "claude-opus",
113
+ (
114
+ TaskType.IMPLEMENTATION,
115
+ ComplexityLevel.MEDIUM,
116
+ BudgetMode.QUALITY,
117
+ ): "claude-opus",
118
+ (
119
+ TaskType.IMPLEMENTATION,
120
+ ComplexityLevel.HIGH,
121
+ BudgetMode.QUALITY,
122
+ ): "claude-opus",
123
+ # Quality tasks - fast and cheap
124
+ (TaskType.QUALITY, ComplexityLevel.LOW, BudgetMode.FREE): "claude-haiku",
125
+ (TaskType.QUALITY, ComplexityLevel.MEDIUM, BudgetMode.FREE): "claude-haiku",
126
+ (TaskType.QUALITY, ComplexityLevel.HIGH, BudgetMode.FREE): "claude-haiku",
127
+ (TaskType.QUALITY, ComplexityLevel.LOW, BudgetMode.BALANCED): "claude-haiku",
128
+ (
129
+ TaskType.QUALITY,
130
+ ComplexityLevel.MEDIUM,
131
+ BudgetMode.BALANCED,
132
+ ): "claude-sonnet",
133
+ (TaskType.QUALITY, ComplexityLevel.HIGH, BudgetMode.BALANCED): "claude-sonnet",
134
+ (TaskType.QUALITY, ComplexityLevel.LOW, BudgetMode.QUALITY): "claude-sonnet",
135
+ (TaskType.QUALITY, ComplexityLevel.MEDIUM, BudgetMode.QUALITY): "claude-opus",
136
+ (TaskType.QUALITY, ComplexityLevel.HIGH, BudgetMode.QUALITY): "claude-opus",
137
+ # General tasks - safe defaults
138
+ (TaskType.GENERAL, ComplexityLevel.LOW, BudgetMode.FREE): "claude-haiku",
139
+ (TaskType.GENERAL, ComplexityLevel.MEDIUM, BudgetMode.FREE): "claude-haiku",
140
+ (TaskType.GENERAL, ComplexityLevel.HIGH, BudgetMode.FREE): "claude-haiku",
141
+ (TaskType.GENERAL, ComplexityLevel.LOW, BudgetMode.BALANCED): "claude-sonnet",
142
+ (
143
+ TaskType.GENERAL,
144
+ ComplexityLevel.MEDIUM,
145
+ BudgetMode.BALANCED,
146
+ ): "claude-sonnet",
147
+ (TaskType.GENERAL, ComplexityLevel.HIGH, BudgetMode.BALANCED): "claude-opus",
148
+ (TaskType.GENERAL, ComplexityLevel.LOW, BudgetMode.QUALITY): "claude-opus",
149
+ (TaskType.GENERAL, ComplexityLevel.MEDIUM, BudgetMode.QUALITY): "claude-opus",
150
+ (TaskType.GENERAL, ComplexityLevel.HIGH, BudgetMode.QUALITY): "claude-opus",
151
+ }
152
+
153
+ # Fallback chains for when primary model is unavailable
154
+ FALLBACK_CHAINS = {
155
+ "gemini": ["claude-haiku", "claude-sonnet", "claude-opus"],
156
+ "codex": ["claude-sonnet", "claude-opus"],
157
+ "copilot": ["claude-sonnet", "claude-opus"],
158
+ "claude-haiku": ["claude-sonnet", "claude-opus"],
159
+ "claude-sonnet": ["claude-opus", "claude-haiku"],
160
+ "claude-opus": ["claude-sonnet", "claude-haiku"],
161
+ }
162
+
163
+ @staticmethod
164
+ def select_model(
165
+ task_type: str | TaskType,
166
+ complexity: str | ComplexityLevel = "medium",
167
+ budget: str | BudgetMode = "balanced",
168
+ ) -> str:
169
+ """
170
+ Select best model for the given task parameters.
171
+
172
+ Args:
173
+ task_type: Type of task (exploration, debugging, implementation, quality, general)
174
+ complexity: Task complexity level (low, medium, high). Default: medium
175
+ budget: Budget mode (free, balanced, quality). Default: balanced
176
+
177
+ Returns:
178
+ Model name (e.g., "claude-sonnet", "gemini")
179
+
180
+ Example:
181
+ >>> model = ModelSelection.select_model("implementation", "high", "balanced")
182
+ >>> print(model)
183
+ 'claude-opus'
184
+ """
185
+ # Normalize inputs
186
+ if isinstance(task_type, str):
187
+ try:
188
+ task_type = TaskType(task_type)
189
+ except ValueError:
190
+ task_type = TaskType.GENERAL
191
+
192
+ if isinstance(complexity, str):
193
+ try:
194
+ complexity = ComplexityLevel(complexity)
195
+ except ValueError:
196
+ complexity = ComplexityLevel.MEDIUM
197
+
198
+ if isinstance(budget, str):
199
+ try:
200
+ budget = BudgetMode(budget)
201
+ except ValueError:
202
+ budget = BudgetMode.BALANCED
203
+
204
+ # Look up in decision matrix
205
+ key = (task_type, complexity, budget)
206
+ return ModelSelection.DECISION_MATRIX.get(key, "claude-sonnet")
207
+
208
+ @staticmethod
209
+ def get_fallback_chain(primary_model: str) -> list[str]:
210
+ """
211
+ Get fallback models if primary model is unavailable.
212
+
213
+ Args:
214
+ primary_model: Primary model name
215
+
216
+ Returns:
217
+ List of fallback models in order of preference
218
+
219
+ Example:
220
+ >>> fallbacks = ModelSelection.get_fallback_chain("gemini")
221
+ >>> print(fallbacks)
222
+ ['claude-haiku', 'claude-sonnet', 'claude-opus']
223
+ """
224
+ return ModelSelection.FALLBACK_CHAINS.get(primary_model, ["claude-sonnet"])
225
+
226
+ @staticmethod
227
+ def estimate_tokens(
228
+ task_description: str, complexity: str | ComplexityLevel = "medium"
229
+ ) -> int:
230
+ """
231
+ Estimate token usage for a task.
232
+
233
+ Args:
234
+ task_description: Description of the task
235
+ complexity: Task complexity level
236
+
237
+ Returns:
238
+ Estimated tokens for the task
239
+
240
+ Estimation formula:
241
+ - Low complexity: ~500-1000 tokens
242
+ - Medium complexity: ~1000-5000 tokens
243
+ - High complexity: ~5000-20000 tokens
244
+ """
245
+ if isinstance(complexity, str):
246
+ try:
247
+ complexity = ComplexityLevel(complexity)
248
+ except ValueError:
249
+ complexity = ComplexityLevel.MEDIUM
250
+
251
+ # Base estimate on description length
252
+ description_tokens = len(task_description.split()) * 1.3 # ~1.3 tokens per word
253
+
254
+ # Add complexity multiplier
255
+ multipliers = {
256
+ ComplexityLevel.LOW: 1.0,
257
+ ComplexityLevel.MEDIUM: 2.0,
258
+ ComplexityLevel.HIGH: 5.0,
259
+ }
260
+
261
+ multiplier = multipliers.get(complexity, 2.0)
262
+ return int(description_tokens * multiplier)
263
+
264
+ @staticmethod
265
+ def is_model_available(model: str) -> bool:
266
+ """
267
+ Check if a model is available (basic check).
268
+
269
+ Args:
270
+ model: Model name to check
271
+
272
+ Returns:
273
+ True if model is known, False otherwise
274
+
275
+ Note:
276
+ This is a simple availability check. For actual availability,
277
+ you should check Claude CLI, Gemini CLI, etc.
278
+ """
279
+ available_models = {
280
+ "gemini",
281
+ "codex",
282
+ "copilot",
283
+ "claude-haiku",
284
+ "claude-sonnet",
285
+ "claude-opus",
286
+ }
287
+ return model in available_models
288
+
289
+
290
+ def select_model(
291
+ task_type: str = "general",
292
+ complexity: str = "medium",
293
+ budget: str = "balanced",
294
+ ) -> str:
295
+ """
296
+ Convenience function for model selection.
297
+
298
+ Args:
299
+ task_type: Type of task. Default: general
300
+ complexity: Complexity level. Default: medium
301
+ budget: Budget mode. Default: balanced
302
+
303
+ Returns:
304
+ Recommended model name
305
+
306
+ Example:
307
+ >>> model = select_model("implementation", "high")
308
+ >>> print(model)
309
+ """
310
+ return ModelSelection.select_model(task_type, complexity, budget)
311
+
312
+
313
+ def get_fallback_chain(model: str) -> list[str]:
314
+ """
315
+ Convenience function for getting fallback models.
316
+
317
+ Args:
318
+ model: Primary model name
319
+
320
+ Returns:
321
+ List of fallback models
322
+ """
323
+ return ModelSelection.get_fallback_chain(model)
@@ -0,0 +1,47 @@
1
+ # ORCHESTRATOR SYSTEM PROMPT (Minimal)
2
+
3
+ **Core Principle:** Delegation > Direct Execution. Cascading failures consume exponentially more context than structured delegation.
4
+
5
+ ## Execute Directly (Strategic Only)
6
+
7
+ Only when ALL true:
8
+ - Planning/Design/Decisions - Architectural choices
9
+ - Single Tool Call - No error handling needed
10
+ - SDK Operations - HtmlGraph feature/spike creation
11
+ - Clarifying Requirements - User questions
12
+
13
+ ## Delegate Everything Else
14
+
15
+ Git, code changes, testing, research, deployment - DELEGATE.
16
+
17
+ **Context cost:** Direct = 7+ tool calls | Delegation = 2 tool calls
18
+
19
+ ## Quick Decision Tree
20
+
21
+ 1. Strategic (decisions/planning)? → Execute directly
22
+ 2. Single tool, no errors? → Execute directly
23
+ 3. Everything else → DELEGATE
24
+
25
+ ## Spawner Selection (Brief)
26
+
27
+ - Code work → `/multi-ai-orchestration` skill
28
+ - Images/analysis → spawn_gemini
29
+ - Git/PRs → spawn_copilot
30
+ - Complex reasoning → spawn_claude
31
+
32
+ For detailed spawner selection, cost analysis, and patterns:
33
+ → Use `/multi-ai-orchestration` skill
34
+
35
+ ## HtmlGraph Integration
36
+
37
+ ```python
38
+ sdk = SDK(agent="orchestrator")
39
+ feature = sdk.features.create("Title").save()
40
+ Task(prompt="...", description="...")
41
+ ```
42
+
43
+ For complete patterns: → Use `/orchestrator-directives` skill
44
+
45
+ ---
46
+
47
+ **Key Insight:** Smart routing → fewer tool calls → better context → faster resolution.
htmlgraph/parser.py CHANGED
@@ -379,6 +379,50 @@ class HtmlParser:
379
379
 
380
380
  return "\n".join(text_parts)
381
381
 
382
+ def get_findings(self) -> str | None:
383
+ """Extract findings from section[data-findings] (Spike-specific)."""
384
+ findings_section = self.query_one("section[data-findings]")
385
+ if not findings_section:
386
+ return None
387
+
388
+ # Look for findings-content div using full selector
389
+ content_div = self.query_one("section[data-findings] div.findings-content")
390
+ if content_div:
391
+ text = content_div.to_text().strip()
392
+ return text if text else None
393
+
394
+ # Fallback: get all text excluding h3 header
395
+ text_parts = []
396
+ for child in findings_section.children:
397
+ if hasattr(child, "name") and child.name == "h3":
398
+ continue
399
+ if hasattr(child, "to_text"):
400
+ text = child.to_text().strip()
401
+ if text:
402
+ text_parts.append(text)
403
+
404
+ result = "\n".join(text_parts)
405
+ return result if result else None
406
+
407
+ def get_decision(self) -> str | None:
408
+ """Extract decision from section[data-decision] (Spike-specific)."""
409
+ decision_section = self.query_one("section[data-decision]")
410
+ if not decision_section:
411
+ return None
412
+
413
+ # Get text content excluding the h3 header
414
+ text_parts = []
415
+ for child in decision_section.children:
416
+ if hasattr(child, "name") and child.name == "h3":
417
+ continue
418
+ if hasattr(child, "to_text"):
419
+ text = child.to_text().strip()
420
+ if text:
421
+ text_parts.append(text)
422
+
423
+ result = "\n".join(text_parts)
424
+ return result if result else None
425
+
382
426
  def parse_full_node(self) -> dict[str, Any]:
383
427
  """
384
428
  Parse complete node data from HTML.
@@ -388,7 +432,7 @@ class HtmlParser:
388
432
  metadata = self.get_node_metadata()
389
433
  title = self.get_title()
390
434
 
391
- return {
435
+ result = {
392
436
  **metadata,
393
437
  "title": title or metadata.get("id", "Untitled"),
394
438
  "edges": self.get_edges(),
@@ -397,6 +441,17 @@ class HtmlParser:
397
441
  "content": self.get_content(),
398
442
  }
399
443
 
444
+ # Add Spike-specific fields if present
445
+ findings = self.get_findings()
446
+ if findings is not None:
447
+ result["findings"] = findings
448
+
449
+ decision = self.get_decision()
450
+ if decision is not None:
451
+ result["decision"] = decision
452
+
453
+ return result
454
+
400
455
 
401
456
  def parse_html_file(filepath: Path | str) -> dict[str, Any]:
402
457
  """