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.
- htmlgraph/__init__.py +1 -1
- htmlgraph/agent_detection.py +41 -2
- htmlgraph/analytics/cli.py +86 -20
- htmlgraph/cli.py +519 -87
- htmlgraph/collections/base.py +68 -4
- 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 +161 -0
- htmlgraph/git_events.py +61 -7
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +61 -0
- htmlgraph/operations/analytics.py +338 -0
- htmlgraph/operations/events.py +243 -0
- htmlgraph/operations/hooks.py +349 -0
- htmlgraph/operations/server.py +302 -0
- htmlgraph/orchestration/__init__.py +39 -0
- htmlgraph/orchestration/headless_spawner.py +566 -0
- htmlgraph/orchestration/model_selection.py +323 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +47 -0
- htmlgraph/parser.py +56 -1
- htmlgraph/sdk.py +529 -7
- htmlgraph/server.py +153 -60
- {htmlgraph-0.21.0.dist-info → htmlgraph-0.23.0.dist-info}/METADATA +3 -1
- {htmlgraph-0.21.0.dist-info → htmlgraph-0.23.0.dist-info}/RECORD +40 -19
- /htmlgraph/{orchestration.py → orchestration/task_coordination.py} +0 -0
- {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.21.0.dist-info → htmlgraph-0.23.0.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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
|
"""
|