entari-plugin-hyw 3.2.104__py3-none-any.whl → 3.2.106__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.
Potentially problematic release.
This version of entari-plugin-hyw might be problematic. Click here for more details.
- entari_plugin_hyw/__init__.py +126 -7
- entari_plugin_hyw/assets/libs/tailwind.css +1 -1
- entari_plugin_hyw/assets/package-lock.json +953 -0
- entari_plugin_hyw/assets/package.json +16 -0
- entari_plugin_hyw/assets/tailwind.config.js +1 -1
- entari_plugin_hyw/assets/tailwind.input.css +8 -8
- entari_plugin_hyw/assets/template.html +39 -43
- entari_plugin_hyw/assets/template.html.bak +157 -0
- entari_plugin_hyw/assets/template.j2 +259 -0
- entari_plugin_hyw/core/config.py +2 -4
- entari_plugin_hyw/core/pipeline.py +192 -23
- entari_plugin_hyw/core/render.py +235 -571
- entari_plugin_hyw/core/render.py.bak +926 -0
- entari_plugin_hyw/utils/prompts.py +6 -6
- {entari_plugin_hyw-3.2.104.dist-info → entari_plugin_hyw-3.2.106.dist-info}/METADATA +2 -1
- {entari_plugin_hyw-3.2.104.dist-info → entari_plugin_hyw-3.2.106.dist-info}/RECORD +18 -13
- {entari_plugin_hyw-3.2.104.dist-info → entari_plugin_hyw-3.2.106.dist-info}/WHEEL +0 -0
- {entari_plugin_hyw-3.2.104.dist-info → entari_plugin_hyw-3.2.106.dist-info}/top_level.txt +0 -0
entari_plugin_hyw/core/config.py
CHANGED
|
@@ -20,19 +20,17 @@ class HYWConfig:
|
|
|
20
20
|
extra_body: Optional[Dict[str, Any]] = None
|
|
21
21
|
temperature: float = 0.4
|
|
22
22
|
max_turns: int = 10
|
|
23
|
+
icon: str = "openai" # logo for primary model
|
|
24
|
+
vision_icon: Optional[str] = None # logo for vision model (falls back to icon when absent)
|
|
23
25
|
enable_browser_fallback: bool = False
|
|
24
26
|
vision_system_prompt: Optional[str] = None
|
|
25
27
|
intruct_system_prompt: Optional[str] = None
|
|
26
28
|
agent_system_prompt: Optional[str] = None
|
|
27
29
|
playwright_mcp_command: str = "npx"
|
|
28
30
|
playwright_mcp_args: Optional[List[str]] = None
|
|
29
|
-
# Billing configuration (price per million tokens)
|
|
30
|
-
# Main model pricing - if not set, billing is disabled
|
|
31
31
|
input_price: Optional[float] = None # $ per 1M input tokens
|
|
32
32
|
output_price: Optional[float] = None # $ per 1M output tokens
|
|
33
|
-
# Vision model pricing overrides (defaults to main model pricing if not set)
|
|
34
33
|
vision_input_price: Optional[float] = None
|
|
35
34
|
vision_output_price: Optional[float] = None
|
|
36
|
-
# Instruct model pricing overrides (defaults to main model pricing if not set)
|
|
37
35
|
intruct_input_price: Optional[float] = None
|
|
38
36
|
intruct_output_price: Optional[float] = None
|
|
@@ -111,6 +111,10 @@ class ProcessingPipeline:
|
|
|
111
111
|
|
|
112
112
|
# Vision stage
|
|
113
113
|
vision_text = ""
|
|
114
|
+
vision_start = time.time()
|
|
115
|
+
vision_time = 0
|
|
116
|
+
vision_cost = 0.0
|
|
117
|
+
vision_usage = {}
|
|
114
118
|
if images:
|
|
115
119
|
vision_model = (
|
|
116
120
|
selected_vision_model
|
|
@@ -129,6 +133,15 @@ class ProcessingPipeline:
|
|
|
129
133
|
# Add vision usage with vision-specific pricing
|
|
130
134
|
usage_totals["input_tokens"] += vision_usage.get("input_tokens", 0)
|
|
131
135
|
usage_totals["output_tokens"] += vision_usage.get("output_tokens", 0)
|
|
136
|
+
|
|
137
|
+
# Calculate Vision Cost
|
|
138
|
+
v_in_price = float(getattr(self.config, "vision_input_price", None) or getattr(self.config, "input_price", 0.0) or 0.0)
|
|
139
|
+
v_out_price = float(getattr(self.config, "vision_output_price", None) or getattr(self.config, "output_price", 0.0) or 0.0)
|
|
140
|
+
if v_in_price > 0 or v_out_price > 0:
|
|
141
|
+
vision_cost = (vision_usage.get("input_tokens", 0) / 1_000_000 * v_in_price) + (vision_usage.get("output_tokens", 0) / 1_000_000 * v_out_price)
|
|
142
|
+
|
|
143
|
+
vision_time = time.time() - vision_start
|
|
144
|
+
|
|
132
145
|
trace["vision"] = {
|
|
133
146
|
"model": vision_model,
|
|
134
147
|
"base_url": getattr(self.config, "vision_base_url", None) or self.config.base_url,
|
|
@@ -137,18 +150,33 @@ class ProcessingPipeline:
|
|
|
137
150
|
"images_count": len(images or []),
|
|
138
151
|
"output": vision_text,
|
|
139
152
|
"usage": vision_usage,
|
|
153
|
+
"time": vision_time,
|
|
154
|
+
"cost": vision_cost
|
|
140
155
|
}
|
|
141
156
|
|
|
142
157
|
# Intruct + pre-search
|
|
158
|
+
instruct_start = time.time()
|
|
143
159
|
instruct_model = getattr(self.config, "intruct_model_name", None) or active_model
|
|
144
|
-
instruct_text, search_payloads, intruct_trace, intruct_usage = await self._run_instruct_stage(
|
|
160
|
+
instruct_text, search_payloads, intruct_trace, intruct_usage, search_time = await self._run_instruct_stage(
|
|
145
161
|
user_input=user_input,
|
|
146
162
|
vision_text=vision_text,
|
|
147
163
|
model=instruct_model,
|
|
148
164
|
)
|
|
165
|
+
instruct_time = time.time() - instruct_start
|
|
166
|
+
|
|
167
|
+
# Calculate Instruct Cost
|
|
168
|
+
instruct_cost = 0.0
|
|
169
|
+
i_in_price = float(getattr(self.config, "intruct_input_price", None) or getattr(self.config, "input_price", 0.0) or 0.0)
|
|
170
|
+
i_out_price = float(getattr(self.config, "intruct_output_price", None) or getattr(self.config, "output_price", 0.0) or 0.0)
|
|
171
|
+
if i_in_price > 0 or i_out_price > 0:
|
|
172
|
+
instruct_cost = (intruct_usage.get("input_tokens", 0) / 1_000_000 * i_in_price) + (intruct_usage.get("output_tokens", 0) / 1_000_000 * i_out_price)
|
|
173
|
+
|
|
149
174
|
# Add instruct usage
|
|
150
175
|
usage_totals["input_tokens"] += intruct_usage.get("input_tokens", 0)
|
|
151
176
|
usage_totals["output_tokens"] += intruct_usage.get("output_tokens", 0)
|
|
177
|
+
|
|
178
|
+
intruct_trace["time"] = instruct_time
|
|
179
|
+
intruct_trace["cost"] = instruct_cost
|
|
152
180
|
trace["intruct"] = intruct_trace
|
|
153
181
|
|
|
154
182
|
explicit_mcp_intent = "mcp" in (user_input or "").lower()
|
|
@@ -162,6 +190,7 @@ class ProcessingPipeline:
|
|
|
162
190
|
logger.warning(f"MCP Playwright granted for this request: reason={intruct_trace.get('grant_reason')!r}")
|
|
163
191
|
|
|
164
192
|
# Start agent loop
|
|
193
|
+
agent_start_time = time.time()
|
|
165
194
|
current_history.append({"role": "user", "content": user_input or "..."})
|
|
166
195
|
|
|
167
196
|
max_steps = 6
|
|
@@ -269,6 +298,22 @@ class ProcessingPipeline:
|
|
|
269
298
|
structured = self._parse_tagged_response(final_response_content)
|
|
270
299
|
final_content = structured.get("response") or final_response_content
|
|
271
300
|
|
|
301
|
+
agent_time = time.time() - agent_start_time
|
|
302
|
+
|
|
303
|
+
# Calculate Agent Cost (accumulated steps)
|
|
304
|
+
agent_cost = 0.0
|
|
305
|
+
a_in_price = float(getattr(self.config, "input_price", 0.0) or 0.0)
|
|
306
|
+
a_out_price = float(getattr(self.config, "output_price", 0.0) or 0.0)
|
|
307
|
+
|
|
308
|
+
# Agent usage is already in usage_totals, but that includes ALL stages.
|
|
309
|
+
# We need just Agent tokens for Agent cost.
|
|
310
|
+
# Agent inputs = Total inputs - Vision inputs - Instruct inputs
|
|
311
|
+
agent_input_tokens = usage_totals["input_tokens"] - vision_usage.get("input_tokens", 0) - intruct_usage.get("input_tokens", 0)
|
|
312
|
+
agent_output_tokens = usage_totals["output_tokens"] - vision_usage.get("output_tokens", 0) - intruct_usage.get("output_tokens", 0)
|
|
313
|
+
|
|
314
|
+
if a_in_price > 0 or a_out_price > 0:
|
|
315
|
+
agent_cost = (agent_input_tokens / 1_000_000 * a_in_price) + (agent_output_tokens / 1_000_000 * a_out_price)
|
|
316
|
+
|
|
272
317
|
trace["agent"] = {
|
|
273
318
|
"model": active_model,
|
|
274
319
|
"base_url": self.config.base_url,
|
|
@@ -276,6 +321,8 @@ class ProcessingPipeline:
|
|
|
276
321
|
"steps": agent_trace_steps,
|
|
277
322
|
"final_output": final_response_content,
|
|
278
323
|
"mcp_granted": grant_mcp,
|
|
324
|
+
"time": agent_time,
|
|
325
|
+
"cost": agent_cost
|
|
279
326
|
}
|
|
280
327
|
trace_markdown = self._render_trace_markdown(trace)
|
|
281
328
|
|
|
@@ -297,7 +344,96 @@ class ProcessingPipeline:
|
|
|
297
344
|
input_cost = (usage_totals["input_tokens"] / 1_000_000) * input_price
|
|
298
345
|
output_cost = (usage_totals["output_tokens"] / 1_000_000) * output_price
|
|
299
346
|
billing_info["total_cost"] = input_cost + output_cost
|
|
300
|
-
logger.info(f"Billing: {usage_totals['input_tokens']} in @ ${input_price}/M + {usage_totals['output_tokens']} out @ ${output_price}/M = ${billing_info['total_cost']:.6f}")
|
|
347
|
+
# logger.info(f"Billing: {usage_totals['input_tokens']} in @ ${input_price}/M + {usage_totals['output_tokens']} out @ ${output_price}/M = ${billing_info['total_cost']:.6f}")
|
|
348
|
+
|
|
349
|
+
# Build stages_used list for UI display
|
|
350
|
+
# Order: Vision (if used) -> Search (if performed) -> Agent
|
|
351
|
+
stages_used = []
|
|
352
|
+
|
|
353
|
+
# Helper to infer icon from model name or base_url
|
|
354
|
+
def infer_icon(model_name: str, base_url: str) -> str:
|
|
355
|
+
model_lower = (model_name or "").lower()
|
|
356
|
+
url_lower = (base_url or "").lower()
|
|
357
|
+
|
|
358
|
+
if "deepseek" in model_lower or "deepseek" in url_lower:
|
|
359
|
+
return "deepseek"
|
|
360
|
+
elif "claude" in model_lower or "anthropic" in url_lower:
|
|
361
|
+
return "anthropic"
|
|
362
|
+
elif "gemini" in model_lower or "google" in url_lower:
|
|
363
|
+
return "google"
|
|
364
|
+
elif "gpt" in model_lower or "openai" in url_lower:
|
|
365
|
+
return "openai"
|
|
366
|
+
elif "qwen" in model_lower:
|
|
367
|
+
return "qwen"
|
|
368
|
+
elif "openrouter" in url_lower:
|
|
369
|
+
return "openrouter"
|
|
370
|
+
return "openai" # Default fallback
|
|
371
|
+
|
|
372
|
+
# Helper to infer provider from base_url
|
|
373
|
+
def infer_provider(base_url: str) -> str:
|
|
374
|
+
url_lower = (base_url or "").lower()
|
|
375
|
+
if "openrouter" in url_lower:
|
|
376
|
+
return "OpenRouter"
|
|
377
|
+
elif "openai" in url_lower:
|
|
378
|
+
return "OpenAI"
|
|
379
|
+
elif "anthropic" in url_lower:
|
|
380
|
+
return "Anthropic"
|
|
381
|
+
elif "google" in url_lower:
|
|
382
|
+
return "Google"
|
|
383
|
+
elif "deepseek" in url_lower:
|
|
384
|
+
return "DeepSeek"
|
|
385
|
+
return "" # Empty string = don't show provider
|
|
386
|
+
|
|
387
|
+
if trace.get("vision"):
|
|
388
|
+
v = trace["vision"]
|
|
389
|
+
v_model = v.get("model", "")
|
|
390
|
+
v_base_url = v.get("base_url", "") or self.config.base_url
|
|
391
|
+
stages_used.append({
|
|
392
|
+
"name": "Vision",
|
|
393
|
+
"model": v_model,
|
|
394
|
+
"icon_config": getattr(self.config, "vision_icon", None) or infer_icon(v_model, v_base_url),
|
|
395
|
+
"provider": infer_provider(v_base_url),
|
|
396
|
+
"time": v.get("time", 0),
|
|
397
|
+
"cost": v.get("cost", 0.0)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
if trace.get("intruct"):
|
|
401
|
+
i = trace["intruct"]
|
|
402
|
+
i_model = i.get("model", "")
|
|
403
|
+
i_base_url = i.get("base_url", "") or self.config.base_url
|
|
404
|
+
stages_used.append({
|
|
405
|
+
"name": "Instruct",
|
|
406
|
+
"model": i_model,
|
|
407
|
+
"icon_config": getattr(self.config, "intruct_icon", None) or infer_icon(i_model, i_base_url),
|
|
408
|
+
"provider": infer_provider(i_base_url),
|
|
409
|
+
"time": i.get("time", 0),
|
|
410
|
+
"cost": i.get("cost", 0.0)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
# Show Search stage only when search was actually performed
|
|
414
|
+
if search_payloads:
|
|
415
|
+
# Use dedicated SearXNG metadata as requested
|
|
416
|
+
stages_used.append({
|
|
417
|
+
"name": "Search",
|
|
418
|
+
"model": "SearXNG",
|
|
419
|
+
"icon_config": "search", # Ensure mapping exists or handle specially in render
|
|
420
|
+
"provider": "SearXNG",
|
|
421
|
+
"time": search_time,
|
|
422
|
+
"cost": 0.0 # Search is free in this plugin
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
if trace.get("agent"):
|
|
426
|
+
a = trace["agent"]
|
|
427
|
+
a_model = a.get("model", "") or active_model
|
|
428
|
+
a_base_url = a.get("base_url", "") or self.config.base_url
|
|
429
|
+
stages_used.append({
|
|
430
|
+
"name": "Agent",
|
|
431
|
+
"model": a_model,
|
|
432
|
+
"icon_config": getattr(self.config, "icon", None) or infer_icon(a_model, a_base_url),
|
|
433
|
+
"provider": infer_provider(a_base_url),
|
|
434
|
+
"time": a.get("time", 0),
|
|
435
|
+
"cost": a.get("cost", 0.0)
|
|
436
|
+
})
|
|
301
437
|
|
|
302
438
|
return {
|
|
303
439
|
"llm_response": final_content,
|
|
@@ -308,6 +444,7 @@ class ProcessingPipeline:
|
|
|
308
444
|
"conversation_history": current_history,
|
|
309
445
|
"trace_markdown": trace_markdown,
|
|
310
446
|
"billing_info": billing_info,
|
|
447
|
+
"stages_used": stages_used,
|
|
311
448
|
}
|
|
312
449
|
|
|
313
450
|
except Exception as e:
|
|
@@ -358,29 +495,53 @@ class ProcessingPipeline:
|
|
|
358
495
|
current_step = None
|
|
359
496
|
|
|
360
497
|
for line in lines:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
498
|
+
line_stripped = line.strip()
|
|
499
|
+
if not line_stripped: continue
|
|
500
|
+
|
|
501
|
+
# New Format: "1. [icon] name: description" OR "[icon] name: description"
|
|
502
|
+
# Regex details:
|
|
503
|
+
# ^(?:(?:\d+\.|[-*])\s+)? -> Optional numbering (1. or - or *)
|
|
504
|
+
# \[(\w+)\] -> Icon in brackets [icon] -> group 1
|
|
505
|
+
# \s+ -> separating space
|
|
506
|
+
# ([^:]+) -> Tool Name (chars before colon) -> group 2
|
|
507
|
+
# : -> Colon separator
|
|
508
|
+
# \s*(.+) -> Description -> group 3
|
|
509
|
+
new_format_match = re.match(r'^(?:(?:\d+\.|[-*])\s+)?\[(\w+)\]\s+([^:]+):\s*(.+)$', line_stripped)
|
|
510
|
+
|
|
511
|
+
# Old/Flexible Format: "[icon] name" (description might be on next line)
|
|
512
|
+
flexible_match = re.match(r'^(?:(?:\d+\.|[-*])\s+)?\[(\w+)\]\s+(.+)$', line_stripped)
|
|
513
|
+
|
|
514
|
+
if new_format_match:
|
|
515
|
+
if current_step: parsed["mcp_steps"].append(current_step)
|
|
367
516
|
current_step = {
|
|
368
|
-
"icon":
|
|
369
|
-
"name":
|
|
370
|
-
"description":
|
|
517
|
+
"icon": new_format_match.group(1).lower(),
|
|
518
|
+
"name": new_format_match.group(2).strip(),
|
|
519
|
+
"description": new_format_match.group(3).strip()
|
|
371
520
|
}
|
|
372
|
-
elif
|
|
373
|
-
#
|
|
374
|
-
current_step["
|
|
375
|
-
elif line.strip() and not line.startswith("[") and current_step is None:
|
|
376
|
-
# Fallback: plain tool name without icon
|
|
521
|
+
elif flexible_match:
|
|
522
|
+
# Could be just "[icon] name" without description, or mixed
|
|
523
|
+
if current_step: parsed["mcp_steps"].append(current_step)
|
|
377
524
|
current_step = {
|
|
378
|
-
"icon":
|
|
379
|
-
"name":
|
|
525
|
+
"icon": flexible_match.group(1).lower(),
|
|
526
|
+
"name": flexible_match.group(2).strip(),
|
|
380
527
|
"description": ""
|
|
381
528
|
}
|
|
529
|
+
elif line.startswith(" ") and current_step:
|
|
530
|
+
# Indented description line (continuation)
|
|
531
|
+
if current_step["description"]:
|
|
532
|
+
current_step["description"] += " " + line.strip()
|
|
533
|
+
else:
|
|
534
|
+
current_step["description"] = line.strip()
|
|
535
|
+
elif line_stripped and not line_stripped.startswith("[") and current_step is None:
|
|
536
|
+
# Plain text line without icon, treat as name if no current step
|
|
537
|
+
# (This handles cases where LLM forgets brackets but lists steps)
|
|
538
|
+
if current_step: parsed["mcp_steps"].append(current_step)
|
|
539
|
+
current_step = {
|
|
540
|
+
"icon": "default",
|
|
541
|
+
"name": line_stripped,
|
|
542
|
+
"description": ""
|
|
543
|
+
}
|
|
382
544
|
|
|
383
|
-
# Don't forget the last step
|
|
384
545
|
if current_step:
|
|
385
546
|
parsed["mcp_steps"].append(current_step)
|
|
386
547
|
remaining_text = remaining_text.replace(mcp_block_match.group(0), "").strip()
|
|
@@ -470,8 +631,8 @@ class ProcessingPipeline:
|
|
|
470
631
|
|
|
471
632
|
async def _run_instruct_stage(
|
|
472
633
|
self, user_input: str, vision_text: str, model: str
|
|
473
|
-
) -> Tuple[str, List[str], Dict[str, Any], Dict[str, int]]:
|
|
474
|
-
"""Returns (instruct_text, search_payloads, trace_dict, usage_dict)."""
|
|
634
|
+
) -> Tuple[str, List[str], Dict[str, Any], Dict[str, int], float]:
|
|
635
|
+
"""Returns (instruct_text, search_payloads, trace_dict, usage_dict, search_time)."""
|
|
475
636
|
tools = [self.web_search_tool, self.grant_mcp_playwright_tool]
|
|
476
637
|
tools_desc = "\n".join([t["function"]["name"] for t in tools])
|
|
477
638
|
|
|
@@ -511,12 +672,20 @@ class ProcessingPipeline:
|
|
|
511
672
|
"tool_results": [],
|
|
512
673
|
"output": "",
|
|
513
674
|
}
|
|
675
|
+
|
|
676
|
+
search_time = 0.0
|
|
677
|
+
|
|
514
678
|
if response.tool_calls:
|
|
515
679
|
plan_dict = response.model_dump() if hasattr(response, "model_dump") else response
|
|
516
680
|
history.append(plan_dict)
|
|
517
681
|
|
|
518
682
|
tasks = [self._safe_route_tool(tc) for tc in response.tool_calls]
|
|
683
|
+
|
|
684
|
+
# Measure search/tool execution time
|
|
685
|
+
st = time.time()
|
|
519
686
|
results = await asyncio.gather(*tasks)
|
|
687
|
+
search_time = time.time() - st
|
|
688
|
+
|
|
520
689
|
for i, result in enumerate(results):
|
|
521
690
|
tc = response.tool_calls[i]
|
|
522
691
|
history.append(
|
|
@@ -537,11 +706,11 @@ class ProcessingPipeline:
|
|
|
537
706
|
# and the grant decision; avoid wasting tokens/time.
|
|
538
707
|
intruct_trace["output"] = ""
|
|
539
708
|
intruct_trace["usage"] = usage
|
|
540
|
-
return "", search_payloads, intruct_trace, usage
|
|
709
|
+
return "", search_payloads, intruct_trace, usage, search_time
|
|
541
710
|
|
|
542
711
|
intruct_trace["output"] = (response.content or "").strip()
|
|
543
712
|
intruct_trace["usage"] = usage
|
|
544
|
-
return "", search_payloads, intruct_trace, usage
|
|
713
|
+
return "", search_payloads, intruct_trace, usage, 0.0
|
|
545
714
|
|
|
546
715
|
def _format_search_msgs(self, search_payloads: List[str]) -> str:
|
|
547
716
|
"""
|