entari-plugin-hyw 0.3.5__py3-none-any.whl → 4.0.0rc14__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.

Files changed (78) hide show
  1. entari_plugin_hyw/Untitled-1 +1865 -0
  2. entari_plugin_hyw/__init__.py +979 -116
  3. entari_plugin_hyw/filters.py +83 -0
  4. entari_plugin_hyw/history.py +251 -0
  5. entari_plugin_hyw/misc.py +214 -0
  6. entari_plugin_hyw/search_cache.py +154 -0
  7. entari_plugin_hyw-4.0.0rc14.dist-info/METADATA +118 -0
  8. entari_plugin_hyw-4.0.0rc14.dist-info/RECORD +72 -0
  9. {entari_plugin_hyw-0.3.5.dist-info → entari_plugin_hyw-4.0.0rc14.dist-info}/WHEEL +1 -1
  10. {entari_plugin_hyw-0.3.5.dist-info → entari_plugin_hyw-4.0.0rc14.dist-info}/top_level.txt +1 -0
  11. hyw_core/__init__.py +94 -0
  12. hyw_core/agent.py +768 -0
  13. hyw_core/browser_control/__init__.py +63 -0
  14. hyw_core/browser_control/assets/card-dist/index.html +425 -0
  15. hyw_core/browser_control/assets/card-dist/logos/anthropic.svg +1 -0
  16. hyw_core/browser_control/assets/card-dist/logos/cerebras.svg +9 -0
  17. hyw_core/browser_control/assets/card-dist/logos/deepseek.png +0 -0
  18. hyw_core/browser_control/assets/card-dist/logos/gemini.svg +1 -0
  19. hyw_core/browser_control/assets/card-dist/logos/google.svg +1 -0
  20. hyw_core/browser_control/assets/card-dist/logos/grok.png +0 -0
  21. hyw_core/browser_control/assets/card-dist/logos/huggingface.png +0 -0
  22. hyw_core/browser_control/assets/card-dist/logos/microsoft.svg +15 -0
  23. hyw_core/browser_control/assets/card-dist/logos/minimax.png +0 -0
  24. hyw_core/browser_control/assets/card-dist/logos/mistral.png +0 -0
  25. hyw_core/browser_control/assets/card-dist/logos/nvida.png +0 -0
  26. hyw_core/browser_control/assets/card-dist/logos/openai.svg +1 -0
  27. hyw_core/browser_control/assets/card-dist/logos/openrouter.png +0 -0
  28. hyw_core/browser_control/assets/card-dist/logos/perplexity.svg +24 -0
  29. hyw_core/browser_control/assets/card-dist/logos/qwen.png +0 -0
  30. hyw_core/browser_control/assets/card-dist/logos/xai.png +0 -0
  31. hyw_core/browser_control/assets/card-dist/logos/xiaomi.png +0 -0
  32. hyw_core/browser_control/assets/card-dist/logos/zai.png +0 -0
  33. hyw_core/browser_control/assets/card-dist/vite.svg +1 -0
  34. hyw_core/browser_control/assets/index.html +5691 -0
  35. hyw_core/browser_control/assets/logos/anthropic.svg +1 -0
  36. hyw_core/browser_control/assets/logos/cerebras.svg +9 -0
  37. hyw_core/browser_control/assets/logos/deepseek.png +0 -0
  38. hyw_core/browser_control/assets/logos/gemini.svg +1 -0
  39. hyw_core/browser_control/assets/logos/google.svg +1 -0
  40. hyw_core/browser_control/assets/logos/grok.png +0 -0
  41. hyw_core/browser_control/assets/logos/huggingface.png +0 -0
  42. hyw_core/browser_control/assets/logos/microsoft.svg +15 -0
  43. hyw_core/browser_control/assets/logos/minimax.png +0 -0
  44. hyw_core/browser_control/assets/logos/mistral.png +0 -0
  45. hyw_core/browser_control/assets/logos/nvida.png +0 -0
  46. hyw_core/browser_control/assets/logos/openai.svg +1 -0
  47. hyw_core/browser_control/assets/logos/openrouter.png +0 -0
  48. hyw_core/browser_control/assets/logos/perplexity.svg +24 -0
  49. hyw_core/browser_control/assets/logos/qwen.png +0 -0
  50. hyw_core/browser_control/assets/logos/xai.png +0 -0
  51. hyw_core/browser_control/assets/logos/xiaomi.png +0 -0
  52. hyw_core/browser_control/assets/logos/zai.png +0 -0
  53. hyw_core/browser_control/engines/__init__.py +15 -0
  54. hyw_core/browser_control/engines/base.py +13 -0
  55. hyw_core/browser_control/engines/default.py +166 -0
  56. hyw_core/browser_control/engines/duckduckgo.py +171 -0
  57. hyw_core/browser_control/landing.html +172 -0
  58. hyw_core/browser_control/manager.py +173 -0
  59. hyw_core/browser_control/renderer.py +446 -0
  60. hyw_core/browser_control/service.py +940 -0
  61. hyw_core/config.py +154 -0
  62. hyw_core/core.py +462 -0
  63. hyw_core/crawling/__init__.py +18 -0
  64. hyw_core/crawling/completeness.py +437 -0
  65. hyw_core/crawling/models.py +88 -0
  66. hyw_core/definitions.py +104 -0
  67. hyw_core/image_cache.py +274 -0
  68. hyw_core/pipeline.py +502 -0
  69. hyw_core/search.py +171 -0
  70. hyw_core/stages/__init__.py +21 -0
  71. hyw_core/stages/base.py +95 -0
  72. hyw_core/stages/summary.py +191 -0
  73. entari_plugin_hyw/agent.py +0 -419
  74. entari_plugin_hyw/compressor.py +0 -59
  75. entari_plugin_hyw/tools.py +0 -236
  76. entari_plugin_hyw/vision.py +0 -35
  77. entari_plugin_hyw-0.3.5.dist-info/METADATA +0 -112
  78. entari_plugin_hyw-0.3.5.dist-info/RECORD +0 -9
@@ -0,0 +1,21 @@
1
+ """
2
+ hyw_core.stages - Pipeline Stages
3
+
4
+ This subpackage provides the pipeline stage implementations:
5
+ - BaseStage: Abstract base class for all stages
6
+ - StageContext: Shared context between stages
7
+ - StageResult: Stage execution result
8
+ - InstructStage: Initial task planning and search execution
9
+ - SummaryStage: Final response generation
10
+ """
11
+
12
+ from .base import BaseStage, StageContext, StageResult
13
+
14
+ from .summary import SummaryStage
15
+
16
+ __all__ = [
17
+ "BaseStage",
18
+ "StageContext",
19
+ "StageResult",
20
+ "SummaryStage",
21
+ ]
@@ -0,0 +1,95 @@
1
+ """
2
+ Stage Base Classes
3
+
4
+ Abstract base classes for pipeline stages.
5
+ Each stage is a self-contained unit of work.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from openai import AsyncOpenAI
13
+
14
+
15
+ @dataclass
16
+ class StageContext:
17
+ """Shared context passed between stages."""
18
+ user_input: str
19
+ images: List[str] = field(default_factory=list)
20
+ conversation_history: List[Dict] = field(default_factory=list)
21
+ instruct_history: List[Dict] = field(default_factory=list) # History for Instruct stage rounds
22
+
23
+ # Accumulated data
24
+ web_results: List[Dict] = field(default_factory=list)
25
+ agent_context: str = ""
26
+ review_context: str = "" # Context passed from Instruct to Review stage
27
+
28
+ # Mode info (set by Instruct stage)
29
+ task_list: List[str] = field(default_factory=list)
30
+
31
+ # Control flags
32
+ should_refuse: bool = False
33
+ refuse_reason: str = ""
34
+ selected_mode: str = "fast" # "fast" or "deepsearch"
35
+
36
+ # ID counter for unified referencing
37
+ global_id_counter: int = 0
38
+
39
+ # Model capabilities
40
+ image_input_supported: bool = True
41
+
42
+ # Search timing
43
+ search_time: float = 0.0
44
+
45
+ def next_id(self) -> int:
46
+ """Get next global ID."""
47
+ self.global_id_counter += 1
48
+ return self.global_id_counter
49
+
50
+
51
+ @dataclass
52
+ class StageResult:
53
+ """Result from a stage execution."""
54
+ success: bool
55
+ data: Dict[str, Any] = field(default_factory=dict)
56
+ usage: Dict[str, int] = field(default_factory=lambda: {"input_tokens": 0, "output_tokens": 0})
57
+ trace: Dict[str, Any] = field(default_factory=dict)
58
+ error: Optional[str] = None
59
+
60
+
61
+ class BaseStage(ABC):
62
+ """Abstract base class for pipeline stages."""
63
+
64
+ def __init__(self, config: Any, search_service: Any, client: AsyncOpenAI):
65
+ self.config = config
66
+ self.search_service = search_service
67
+ self.client = client
68
+
69
+ @property
70
+ @abstractmethod
71
+ def name(self) -> str:
72
+ """Stage name for logging and tracing."""
73
+ pass
74
+
75
+ @abstractmethod
76
+ async def execute(self, context: StageContext) -> StageResult:
77
+ """
78
+ Execute the stage.
79
+
80
+ Args:
81
+ context: Shared context with accumulated data
82
+
83
+ Returns:
84
+ StageResult with success status, data, usage, and trace info
85
+ """
86
+ pass
87
+
88
+ def _client_for(self, api_key: Optional[str], base_url: Optional[str]) -> AsyncOpenAI:
89
+ """Get or create client with custom credentials."""
90
+ if api_key or base_url:
91
+ return AsyncOpenAI(
92
+ base_url=base_url or self.config.base_url,
93
+ api_key=api_key or self.config.api_key
94
+ )
95
+ return self.client
@@ -0,0 +1,191 @@
1
+ """
2
+ Summary Stage
3
+
4
+ Generates final response based on gathered information.
5
+ Different output formats for different modes.
6
+ """
7
+
8
+ import time
9
+ import re
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from loguru import logger
13
+ from openai import AsyncOpenAI
14
+
15
+ from .base import BaseStage, StageContext, StageResult
16
+ from ..definitions import SUMMARY_REPORT_SP, get_refuse_answer_tool
17
+
18
+
19
+ class SummaryStage(BaseStage):
20
+ """
21
+ Summary Stage: Generate final response.
22
+ """
23
+
24
+ @property
25
+ def name(self) -> str:
26
+ return "Summary"
27
+
28
+ async def execute(
29
+ self,
30
+ context: StageContext,
31
+ images: List[str] = None
32
+ ) -> StageResult:
33
+ """Generate summary."""
34
+ start_time = time.time()
35
+
36
+ # Format context from web results
37
+ web_content = self._format_web_content(context)
38
+
39
+ # Tools
40
+ refuse_tool = get_refuse_answer_tool()
41
+ full_context = f"{context.agent_context}\n\n{web_content}"
42
+
43
+ # Select prompt
44
+ language = getattr(self.config, "language", "Simplified Chinese")
45
+
46
+ system_prompt = SUMMARY_REPORT_SP + f"\n\n用户要求的语言: {language}"
47
+
48
+ # Build Context Message
49
+ context_message = f"## Web Search & Page Content\n\n```context\n{full_context}\n```"
50
+
51
+
52
+ # Build user content
53
+ user_text = context.user_input or "..."
54
+ if images:
55
+ # Add image context message for multimodal input
56
+ image_context = f"[System: The user has provided {len(images)} image(s). Please analyze these images together with the text query to provide a comprehensive response.]"
57
+ user_content: List[Dict[str, Any]] = [{"type": "text", "text": f"{image_context}\n\n{user_text}"}]
58
+ for img_b64 in images:
59
+ url = f"data:image/jpeg;base64,{img_b64}" if not img_b64.startswith("data:") else img_b64
60
+ user_content.append({"type": "image_url", "image_url": {"url": url}})
61
+ else:
62
+ user_content = user_text
63
+
64
+ messages = [
65
+ {"role": "system", "content": system_prompt},
66
+ {"role": "user", "content": context_message},
67
+ {"role": "user", "content": user_content}
68
+ ]
69
+
70
+ # Get model config
71
+ model_cfg = self.config.get_model_config("main")
72
+
73
+ client = self._client_for(
74
+ api_key=model_cfg.api_key,
75
+ base_url=model_cfg.base_url
76
+ )
77
+
78
+ model = model_cfg.model_name or self.config.model_name
79
+
80
+ try:
81
+ response = await client.chat.completions.create(
82
+ model=model,
83
+ messages=messages,
84
+ temperature=self.config.temperature,
85
+
86
+ extra_body=getattr(self.config, "summary_extra_body", None),
87
+ tools=[refuse_tool],
88
+ tool_choice="auto",
89
+ )
90
+ except Exception as e:
91
+ logger.error(f"SummaryStage LLM error: {e}")
92
+ return StageResult(
93
+ success=False,
94
+ error=str(e),
95
+ data={"content": f"Error generating summary: {e}"}
96
+ )
97
+
98
+ usage = {"input_tokens": 0, "output_tokens": 0}
99
+ if hasattr(response, "usage") and response.usage:
100
+ usage["input_tokens"] = getattr(response.usage, "prompt_tokens", 0) or 0
101
+ usage["output_tokens"] = getattr(response.usage, "completion_tokens", 0) or 0
102
+
103
+ # Handle Tool Calls (Refusal)
104
+ tool_calls = response.choices[0].message.tool_calls
105
+ if tool_calls:
106
+ for tc in tool_calls:
107
+ if tc.function.name == "refuse_answer":
108
+ import json
109
+ try:
110
+ args = json.loads(tc.function.arguments)
111
+ reason = args.get("reason", "Refused")
112
+ context.should_refuse = True
113
+ context.refuse_reason = reason
114
+ return StageResult(
115
+ success=True,
116
+ data={"content": f"Refused: {reason}"},
117
+ usage=usage,
118
+ trace={"skipped": True, "reason": reason}
119
+ )
120
+ except: pass
121
+
122
+ content = (response.choices[0].message.content or "").strip()
123
+
124
+ return StageResult(
125
+ success=True,
126
+ data={"content": content},
127
+ usage=usage,
128
+ trace={
129
+ "model": model,
130
+ "provider": model_cfg.model_provider or "Unknown",
131
+ "usage": usage,
132
+ "system_prompt": system_prompt,
133
+ "context_message": context_message, # Includes vision description + search results
134
+ "output": content,
135
+ "time": time.time() - start_time,
136
+ "images_count": len(images) if images else 0,
137
+ }
138
+ )
139
+
140
+ def _strip_links(self, text: str) -> str:
141
+ """Strip markdown links [text](url) -> text and remove bare URLs."""
142
+ # Replace [text](url) with text
143
+ text = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', text)
144
+ # Remove bare URLs (http/https) roughly, trying to preserve surrounding text if possible?
145
+ # A simple pattern for http/s
146
+ text = re.sub(r'https?://\S+', '', text)
147
+ return text
148
+
149
+ def _format_web_content(self, context: StageContext) -> str:
150
+ """Format web results for summary prompt."""
151
+ if not context.web_results:
152
+ return ""
153
+
154
+ # Sort results: pages first, then raw searches, then snippets
155
+ def get_priority(item_type):
156
+ if item_type == "page": return 0
157
+ if item_type == "search_raw_page": return 1
158
+ return 2 # search (snippets)
159
+
160
+ sorted_results = sorted(
161
+ context.web_results,
162
+ key=lambda x: get_priority(x.get("_type"))
163
+ )
164
+
165
+ lines = []
166
+ seen_urls = set()
167
+
168
+ for res in sorted_results:
169
+ type_ = res.get("_type")
170
+ idx = res.get("_id")
171
+ title = (res.get("title", "") or "").strip()
172
+ url = res.get("url", "")
173
+
174
+ # Deduplicate items by URL (keep higher priority item only)
175
+ if url:
176
+ if url in seen_urls:
177
+ continue
178
+ seen_urls.add(url)
179
+
180
+ # url = res.get("url", "") # Removed as requested
181
+
182
+ if type_ == "page":
183
+ content = (res.get("content", "") or "").strip()
184
+ content = self._strip_links(content)
185
+ lines.append(f"[{idx}] Title: {title}\nContent:\n{content}\n")
186
+ elif type_ == "search":
187
+ snippet = (res.get("content", "") or "").strip()
188
+ snippet = self._strip_links(snippet)
189
+ lines.append(f"[{idx}] Title: {title}\nSnippet: {snippet}\n")
190
+
191
+ return "\n".join(lines)