entari-plugin-hyw 3.5.0rc3__py3-none-any.whl → 3.5.0rc4__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.

@@ -40,6 +40,8 @@ class ProcessingPipeline:
40
40
  self.current_mode = "standard" # standard | agent
41
41
  # Global ID counter for all types (unified numbering)
42
42
  self.global_id_counter = 0
43
+ # Background tasks for async image search (not blocking agent)
44
+ self._image_search_tasks: List[asyncio.Task] = []
43
45
 
44
46
  self.web_search_tool = {
45
47
  "type": "function",
@@ -94,6 +96,23 @@ class ProcessingPipeline:
94
96
  },
95
97
  },
96
98
  }
99
+ self.refuse_answer_tool = {
100
+ "type": "function",
101
+ "function": {
102
+ "name": "refuse_answer",
103
+ "description": "拒绝回答问题。当用户问题涉及敏感、违规、不适宜内容时调用此工具,立即终止流程并返回拒绝回答的图片。",
104
+ "parameters": {
105
+ "type": "object",
106
+ "properties": {
107
+ "reason": {"type": "string", "description": "拒绝回答的原因(内部记录,不展示给用户)"},
108
+ },
109
+ "required": [],
110
+ },
111
+ },
112
+ }
113
+ # Flag to indicate refuse_answer was called
114
+ self._should_refuse = False
115
+ self._refuse_reason = ""
97
116
 
98
117
  async def execute(
99
118
  self,
@@ -122,6 +141,9 @@ class ProcessingPipeline:
122
141
  # Reset search cache and ID counter for this execution
123
142
  self.all_web_results = []
124
143
  self.global_id_counter = 0
144
+ # Reset refuse_answer flag
145
+ self._should_refuse = False
146
+ self._refuse_reason = ""
125
147
 
126
148
  try:
127
149
  logger.info(f"Pipeline: Starting workflow for '{user_input}' using {active_model}")
@@ -203,6 +225,21 @@ class ProcessingPipeline:
203
225
  instruct_trace["cost"] = instruct_cost
204
226
  trace["instruct"] = instruct_trace
205
227
 
228
+ # Check if refuse_answer was called - terminate early
229
+ if self._should_refuse:
230
+ logger.info(f"Pipeline: refuse_answer triggered. Reason: {self._refuse_reason}")
231
+ stats["total_time"] = time.time() - start_time
232
+ return {
233
+ "llm_response": "",
234
+ "structured_response": {},
235
+ "stats": stats,
236
+ "model_used": active_model,
237
+ "conversation_history": current_history,
238
+ "refuse_answer": True,
239
+ "refuse_reason": self._refuse_reason,
240
+ "stages_used": [],
241
+ }
242
+
206
243
  # Start agent loop
207
244
  agent_start_time = time.time()
208
245
  current_history.append({"role": "user", "content": user_input or "..."})
@@ -245,10 +282,10 @@ class ProcessingPipeline:
245
282
  user_msgs_text = user_input or ""
246
283
 
247
284
  search_msgs_text = self._format_search_msgs()
248
- image_msgs_text = self._format_image_search_msgs()
285
+ # Image search results are NOT passed to LLM - they're for UI rendering only
249
286
 
250
287
  has_search_results = any(r.get("_type") == "search" for r in self.all_web_results)
251
- has_image_results = any(r.get("_type") == "image" for r in self.all_web_results)
288
+ has_image_results = any(r.get("_type") == "image" for r in self.all_web_results) # For UI rendering only
252
289
 
253
290
  # Build agent system prompt
254
291
  mode_desc_text = AGENT_SP_TOOLS_AGENT_ADD.format(tools_desc=tools_desc) if mode == "agent" else AGENT_SP_TOOLS_STANDARD_ADD
@@ -263,15 +300,14 @@ class ProcessingPipeline:
263
300
  if vision_text:
264
301
  system_prompt += AGENT_SP_INSTRUCT_VISION_ADD.format(vision_msgs=vision_text)
265
302
 
266
- # Append all search results (text, page, image) in one block
303
+ # Append search results (text and page only, NOT images)
267
304
  page_msgs_text = self._format_page_msgs()
268
305
  all_search_parts = []
269
306
  if has_search_results and search_msgs_text:
270
307
  all_search_parts.append(search_msgs_text)
271
308
  if page_msgs_text:
272
309
  all_search_parts.append(page_msgs_text)
273
- if has_image_results and image_msgs_text:
274
- all_search_parts.append(image_msgs_text)
310
+ # Images are excluded from LLM prompt - they're for UI rendering only
275
311
 
276
312
  if all_search_parts:
277
313
  system_prompt += AGENT_SP_SEARCH_ADD.format(search_msgs="\n".join(all_search_parts))
@@ -348,7 +384,13 @@ class ProcessingPipeline:
348
384
 
349
385
  final_response_content = response.content or ""
350
386
  current_history.append({"role": "assistant", "content": final_response_content})
351
- agent_trace_steps.append({"step": step, "final": True, "output": final_response_content})
387
+ agent_trace_steps.append({
388
+ "step": step,
389
+ "final": True,
390
+ "output": final_response_content,
391
+ "llm_time": step_llm_time,
392
+ "usage": step_usage
393
+ })
352
394
  break
353
395
 
354
396
  if not final_response_content:
@@ -384,19 +426,20 @@ class ProcessingPipeline:
384
426
  stats["total_time"] = time.time() - start_time
385
427
  stats["steps"] = step
386
428
 
387
- # Calculate billing info
429
+ # Calculate billing info correctly by summing up all actual costs
430
+ total_cost_sum = vision_cost + instruct_cost
431
+ for s in agent_trace_steps:
432
+ s_usage = s.get("usage", {})
433
+ if s_usage:
434
+ s_in_price = float(getattr(self.config, "input_price", 0.0) or 0.0)
435
+ s_out_price = float(getattr(self.config, "output_price", 0.0) or 0.0)
436
+ total_cost_sum += (s_usage.get("input_tokens", 0) / 1_000_000 * s_in_price) + (s_usage.get("output_tokens", 0) / 1_000_000 * s_out_price)
437
+
388
438
  billing_info = {
389
439
  "input_tokens": usage_totals["input_tokens"],
390
440
  "output_tokens": usage_totals["output_tokens"],
391
- "total_cost": 0.0,
441
+ "total_cost": total_cost_sum,
392
442
  }
393
- input_price = getattr(self.config, "input_price", None) or 0.0
394
- output_price = getattr(self.config, "output_price", None) or 0.0
395
-
396
- if input_price > 0 or output_price > 0:
397
- input_cost = (usage_totals["input_tokens"] / 1_000_000) * input_price
398
- output_cost = (usage_totals["output_tokens"] / 1_000_000) * output_price
399
- billing_info["total_cost"] = input_cost + output_cost
400
443
 
401
444
  # Build stages_used list for UI display
402
445
  stages_used = []
@@ -598,12 +641,19 @@ class ProcessingPipeline:
598
641
  })
599
642
 
600
643
  elif s.get("final"):
644
+ # Correctly calculate final step cost
645
+ step_usage = s.get("usage", {})
646
+ step_cost = 0.0
647
+ if a_in_price > 0 or a_out_price > 0:
648
+ step_cost = (step_usage.get("input_tokens", 0) / 1_000_000 * a_in_price) + (step_usage.get("output_tokens", 0) / 1_000_000 * a_out_price)
649
+
601
650
  stages_used.append({
602
651
  "name": "Agent",
603
652
  "model": a_model,
604
653
  "icon_config": agent_icon,
605
654
  "provider": agent_provider,
606
- "time": 0, "cost": 0
655
+ "time": s.get("llm_time", 0),
656
+ "cost": step_cost
607
657
  })
608
658
 
609
659
  # Assign total time/cost to last Agent stage
@@ -668,10 +718,10 @@ class ProcessingPipeline:
668
718
  markdown_urls = markdown_img_pattern.findall(final_content)
669
719
  all_image_urls.update(markdown_urls)
670
720
 
671
- # Get cached versions (waits for pending downloads, with timeout)
721
+ # Get cached versions (waits for pending downloads until agent ends)
672
722
  if all_image_urls:
673
723
  try:
674
- cached_map = await get_cached_images(list(all_image_urls), wait_timeout=3.0)
724
+ cached_map = await get_cached_images(list(all_image_urls))
675
725
 
676
726
  # Apply cached URLs to structured response
677
727
  for img_ref in structured.get("image_references", []):
@@ -708,6 +758,29 @@ class ProcessingPipeline:
708
758
  except Exception as e:
709
759
  logger.warning(f"Failed to apply image cache: {e}")
710
760
 
761
+ # Cancel all background image search/download tasks when agent ends
762
+ if self._image_search_tasks:
763
+ logger.info(f"Cancelling {len(self._image_search_tasks)} background image search tasks")
764
+ for task in self._image_search_tasks:
765
+ if not task.done():
766
+ task.cancel()
767
+ # Wait a bit for tasks to handle cancellation gracefully
768
+ try:
769
+ await asyncio.gather(*self._image_search_tasks, return_exceptions=True)
770
+ except Exception:
771
+ pass
772
+ self._image_search_tasks.clear()
773
+
774
+ # Also cancel any pending image downloads in the cache
775
+ from .image_cache import get_image_cache
776
+ cache = get_image_cache()
777
+ if cache._pending:
778
+ logger.info(f"Cancelling {len(cache._pending)} pending image downloads")
779
+ for task in cache._pending.values():
780
+ if not task.done():
781
+ task.cancel()
782
+ cache._pending.clear()
783
+
711
784
  return {
712
785
  "llm_response": final_content,
713
786
  "structured_response": structured,
@@ -722,6 +795,19 @@ class ProcessingPipeline:
722
795
 
723
796
  except Exception as e:
724
797
  logger.exception("Pipeline Critical Failure")
798
+ # Cancel all background image tasks on error
799
+ if hasattr(self, '_image_search_tasks') and self._image_search_tasks:
800
+ for task in self._image_search_tasks:
801
+ if not task.done():
802
+ task.cancel()
803
+ self._image_search_tasks.clear()
804
+ from .image_cache import get_image_cache
805
+ cache = get_image_cache()
806
+ if cache._pending:
807
+ for task in cache._pending.values():
808
+ if not task.done():
809
+ task.cancel()
810
+ cache._pending.clear()
725
811
  return {
726
812
  "llm_response": f"I encountered a critical error: {e}",
727
813
  "stats": stats,
@@ -893,18 +979,32 @@ class ProcessingPipeline:
893
979
 
894
980
  if name == "internal_image_search":
895
981
  query = args.get("query")
896
- images = await self.search_service.image_search(query)
897
-
898
- # Cache results and assign global IDs
899
- for item in images:
900
- self.global_id_counter += 1
901
- item["_id"] = self.global_id_counter
902
- item["_type"] = "image"
903
- item["query"] = query
904
- item["is_image"] = True
905
- self.all_web_results.append(item)
906
-
907
- return json.dumps({"image_results_count": len(images), "status": "cached_for_prompt"}, ensure_ascii=False)
982
+ # Start image search in background (non-blocking)
983
+ # Images are for UI rendering only, not passed to LLM
984
+ async def _background_image_search():
985
+ try:
986
+ images = await self.search_service.image_search(query)
987
+ # Cache results and assign global IDs for UI rendering
988
+ for item in images:
989
+ self.global_id_counter += 1
990
+ item["_id"] = self.global_id_counter
991
+ item["_type"] = "image"
992
+ item["query"] = query
993
+ item["is_image"] = True
994
+ self.all_web_results.append(item)
995
+ logger.info(f"Background image search completed: {len(images)} images for query '{query}'")
996
+ except asyncio.CancelledError:
997
+ # Task was cancelled when agent ended - this is expected
998
+ logger.debug(f"Background image search cancelled for query '{query}' (agent ended)")
999
+ raise # Re-raise to properly handle cancellation
1000
+ except Exception as e:
1001
+ logger.error(f"Background image search failed for query '{query}': {e}")
1002
+
1003
+ task = asyncio.create_task(_background_image_search())
1004
+ self._image_search_tasks.append(task)
1005
+
1006
+ # Return immediately without waiting for search to complete
1007
+ return json.dumps({"image_results_count": 0, "status": "searching_in_background"}, ensure_ascii=False)
908
1008
 
909
1009
  if name == "crawl_page":
910
1010
  url = args.get("url")
@@ -939,6 +1039,13 @@ class ProcessingPipeline:
939
1039
  self.current_mode = mode
940
1040
  return f"Mode set to {mode}"
941
1041
 
1042
+ if name == "refuse_answer":
1043
+ reason = args.get("reason", "")
1044
+ self._should_refuse = True
1045
+ self._refuse_reason = reason
1046
+ logger.info(f"[Tool] refuse_answer called. Reason: {reason}")
1047
+ return "Refuse answer triggered. Pipeline will terminate early."
1048
+
942
1049
  return f"Unknown tool {name}"
943
1050
 
944
1051
 
@@ -1003,9 +1110,9 @@ class ProcessingPipeline:
1003
1110
  self, user_input: str, vision_text: str, model: str
1004
1111
  ) -> Tuple[str, List[str], Dict[str, Any], Dict[str, int], float]:
1005
1112
  """Returns (instruct_text, search_payloads, trace_dict, usage_dict, search_time)."""
1006
- # Instruct has access to: web_search, image_search, set_mode, crawl_page
1007
- tools = [self.web_search_tool, self.image_search_tool, self.set_mode_tool, self.crawl_page_tool]
1008
- tools_desc = "- internal_web_search: 搜索文本\n- internal_image_search: 搜索图片\n- crawl_page: 获取网页内容\n- set_mode: 设定standard/agent模式"
1113
+ # Instruct has access to: web_search, image_search, set_mode, crawl_page, refuse_answer
1114
+ tools = [self.web_search_tool, self.image_search_tool, self.set_mode_tool, self.crawl_page_tool, self.refuse_answer_tool]
1115
+ tools_desc = "- internal_web_search: 搜索文本\n- internal_image_search: 搜索图片\n- crawl_page: 获取网页内容\n- set_mode: 设定standard/agent模式\n- refuse_answer: 拒绝回答(敏感/违规内容)"
1009
1116
 
1010
1117
  prompt = INSTRUCT_SP.format(user_msgs=user_input or "", tools_desc=tools_desc)
1011
1118
 
@@ -27,6 +27,10 @@ INSTRUCT_SP = """# 你是一个专业的指导专家.
27
27
  - 绝大部分常规问题: standard
28
28
  - 用户要求研究/深度搜索: agent
29
29
  - 需要获取页面具体信息才能回答问题: agent
30
+ - 如果内容包含以下方向, 则调用 refuse_answer
31
+ - 鉴政、涉政内容
32
+ - 过于露骨的 r18+、r18g 内容
33
+ - 空白内容, 无意义内容
30
34
  > 所有工具需要在本次对话同时调用
31
35
 
32
36
  ## 调用工具
@@ -653,8 +653,8 @@ class SearchService:
653
653
  result = await crawler.arun(
654
654
  url=url,
655
655
  config=CrawlerRunConfig(
656
- wait_until="networkidle",
657
- wait_for_images=True,
656
+ wait_until="domcontentloaded", # 不需要等 networkidle
657
+ wait_for_images=False, # 只需要 img src,不需要图片内容
658
658
  wait_for="img",
659
659
  cache_mode=CacheMode.BYPASS,
660
660
  word_count_threshold=1,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entari_plugin_hyw
3
- Version: 3.5.0rc3
3
+ Version: 3.5.0rc4
4
4
  Summary: Use large language models to interpret chat messages
5
5
  Author-email: kumoSleeping <zjr2992@outlook.com>
6
6
  License: MIT
@@ -1,12 +1,12 @@
1
- entari_plugin_hyw/__init__.py,sha256=LHhCnOL5X6Il5OOrL4Q9HH1PHZtGfof9rTsRDQpVYpQ,15790
1
+ entari_plugin_hyw/__init__.py,sha256=MBdjeTlu2DyTbGCugR-0oJD-u1BVuDjzsonmtNbmOUs,16250
2
2
  entari_plugin_hyw/history.py,sha256=zYtON0FgkA_AcXerLV335OzpIP30eAxDEp7NHCFFXis,7016
3
- entari_plugin_hyw/image_cache.py,sha256=-GWNgzmIZv8OF2qEECqcQcuzGnhz5vmZ2-GWdPkEX4I,10373
4
- entari_plugin_hyw/misc.py,sha256=pW-eSRKGJjJhVfz8Z-N0bTIHL57Jq3ynPzVFuy7YWnI,3479
5
- entari_plugin_hyw/pipeline.py,sha256=Q4896twMwL_UbaabA7v-TFlLl1fduTrtPkFyiH3Qvyc,56173
6
- entari_plugin_hyw/prompts.py,sha256=ZE0UN4GuUuG6HCzH4RbfQFHVCC9zT4_xByiG2L-U70I,3961
3
+ entari_plugin_hyw/image_cache.py,sha256=t8pr1kgH2ngK9IhrBAhzUqhBWERNztUywMzgCFZEtQk,9899
4
+ entari_plugin_hyw/misc.py,sha256=gyjqPb9oWl7ZJOcqYeNQunrHooU6BtK1XmF0jK4ORcc,4395
5
+ entari_plugin_hyw/pipeline.py,sha256=BXnYsS7YFnIKFBFHWxWbb2MI87H9pJ5gLCDXAPu6l08,61746
6
+ entari_plugin_hyw/prompts.py,sha256=wUFtvvC6WdtA_FGbXHC8bZDNQOzgEPR9Q4ZRTEsJhDM,4118
7
7
  entari_plugin_hyw/render_vue.py,sha256=BkjfIB4JJ-CTvbreSHbczMu5NyDZtKjK-0HVQpYGacQ,12393
8
- entari_plugin_hyw/search.py,sha256=yauAkNs3bM79dWoMSbn5ngRsxGLAHRbiAYSaGOxzVUQ,28316
9
- entari_plugin_hyw/assets/card-dist/index.html,sha256=2DEOmrRDN6QwoqsywMfe8zdHvzHxrn5Rnhgg2bJA8UE,2016347
8
+ entari_plugin_hyw/search.py,sha256=RX1f2khqh4TBwdQZQv-k7OaG-2D6-op91Vd_MIZQjso,28403
9
+ entari_plugin_hyw/assets/card-dist/index.html,sha256=B5U91hlLEVK_zPNmMoB5daj475ncdRKbVxR35Xgb-Ns,2016492
10
10
  entari_plugin_hyw/assets/card-dist/vite.svg,sha256=SnSK_UQ5GLsWWRyDTEAdrjPoeGGrXbrQgRw6O0qSFPs,1497
11
11
  entari_plugin_hyw/assets/card-dist/logos/anthropic.svg,sha256=ASsy1ypo3osNc3n-B0R81tk_dIFsVgg7qQORrd5T2kA,558
12
12
  entari_plugin_hyw/assets/card-dist/logos/cerebras.svg,sha256=bpmiiYTODwc06knTmPj3GQ7NNtosMog5lkggvB_Z-7M,44166
@@ -72,17 +72,17 @@ entari_plugin_hyw/card-ui/public/logos/qwen.png,sha256=eqLbnIPbjh2_PsODU_mmqjeD8
72
72
  entari_plugin_hyw/card-ui/public/logos/xai.png,sha256=uSulvvDVqoA4RUOW0ZAkdvBVM2rpyGJRZIbn5dEFspw,362
73
73
  entari_plugin_hyw/card-ui/public/logos/xiaomi.png,sha256=WHxlDFGU5FCjb-ure3ngdGG18-efYZUUfqA3_lqCUN0,4084
74
74
  entari_plugin_hyw/card-ui/public/logos/zai.png,sha256=K-gnabdsjMLInppHA1Op7Nyt33iegrx1x-yNlvCZ0Tc,2351
75
- entari_plugin_hyw/card-ui/src/App.vue,sha256=DA5CLpWtl_bm3n3WBpEhdmun9IkLaRE1E_jWMeOmwwY,17656
75
+ entari_plugin_hyw/card-ui/src/App.vue,sha256=fwnw_SFFXK383w0BGAjopzPv7vAWForOLJa2K0tX03o,17821
76
76
  entari_plugin_hyw/card-ui/src/main.ts,sha256=rm653lPnK5fuTIj-iNLpgr8GAmayuCoKop7IWfo0IBk,111
77
- entari_plugin_hyw/card-ui/src/style.css,sha256=LnQEZyUqsj-IuESW1YBRqAz7T6LMayMDuiNKP5TAB1o,188
77
+ entari_plugin_hyw/card-ui/src/style.css,sha256=Qm0sv9em6k4VI_j5PI0_E_ng6u20eFpf2lN44H19zzc,671
78
78
  entari_plugin_hyw/card-ui/src/test_regex.js,sha256=cWmclm6LRKYfjeN1RT5HECdltmo1HvS2BwGCYY_4l14,3040
79
79
  entari_plugin_hyw/card-ui/src/types.ts,sha256=jzI8MXDKxz7WRZmGVb-bToO8BEFW2AOp8iek1n4VuQY,1246
80
80
  entari_plugin_hyw/card-ui/src/assets/vue.svg,sha256=VTLbNPHFKEGIG6uK7KbD6NCSvSGmiaZfTY-M-AQe750,496
81
81
  entari_plugin_hyw/card-ui/src/components/HelloWorld.vue,sha256=yvBIzJua9BfikUOR1I7GYytlnBbgB6xyposxslAoRLU,856
82
- entari_plugin_hyw/card-ui/src/components/MarkdownContent.vue,sha256=w6XbsYa0UQVnb_E7Z6CR2HwKMXWEEzAgQxJ5DQbL9yg,13068
82
+ entari_plugin_hyw/card-ui/src/components/MarkdownContent.vue,sha256=JO1sKThQB42bVl5382BkxpDEyRpAK7oGcmdj1LY5P64,13190
83
83
  entari_plugin_hyw/card-ui/src/components/SectionCard.vue,sha256=owcDNx2JYVmF2J5SYCroR2gvg_cPApQsNunjK1WJpVI,1433
84
- entari_plugin_hyw/card-ui/src/components/StageCard.vue,sha256=4nIZXKDtvsAKwc3-lkeONrpdT2Fbw_fgn16SdJOOku4,9066
85
- entari_plugin_hyw-3.5.0rc3.dist-info/METADATA,sha256=Nopw7PlNNtprZzKvvI_IcdmwZ8LswdeUjf-DpxhAnls,3740
86
- entari_plugin_hyw-3.5.0rc3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
87
- entari_plugin_hyw-3.5.0rc3.dist-info/top_level.txt,sha256=TIDsn6XPs6KA5e3ezsE65JoXsy03ejDdrB41I4SPjmo,18
88
- entari_plugin_hyw-3.5.0rc3.dist-info/RECORD,,
84
+ entari_plugin_hyw/card-ui/src/components/StageCard.vue,sha256=cC0TeJ4AhtN3sJEXenVpl2Eq5901GKC59qpDUNH2SZE,10964
85
+ entari_plugin_hyw-3.5.0rc4.dist-info/METADATA,sha256=Z6VKrmaffE2teCvfN2fbRZ576I8aKc_wfMPcruAi5wI,3740
86
+ entari_plugin_hyw-3.5.0rc4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
87
+ entari_plugin_hyw-3.5.0rc4.dist-info/top_level.txt,sha256=TIDsn6XPs6KA5e3ezsE65JoXsy03ejDdrB41I4SPjmo,18
88
+ entari_plugin_hyw-3.5.0rc4.dist-info/RECORD,,