pygpt-net 2.6.8__py3-none-any.whl → 2.6.10__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 (39) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +4 -0
  4. pygpt_net/controller/ctx/common.py +9 -3
  5. pygpt_net/controller/ctx/ctx.py +19 -17
  6. pygpt_net/controller/kernel/kernel.py +1 -2
  7. pygpt_net/core/agents/runner.py +19 -0
  8. pygpt_net/core/agents/tools.py +93 -52
  9. pygpt_net/core/render/web/body.py +11 -33
  10. pygpt_net/core/render/web/renderer.py +52 -79
  11. pygpt_net/data/config/config.json +4 -3
  12. pygpt_net/data/config/models.json +3 -3
  13. pygpt_net/data/config/presets/agent_openai_supervisor.json +54 -0
  14. pygpt_net/data/config/presets/agent_supervisor.json +52 -0
  15. pygpt_net/data/config/settings.json +14 -0
  16. pygpt_net/data/locale/locale.de.ini +2 -0
  17. pygpt_net/data/locale/locale.en.ini +2 -0
  18. pygpt_net/data/locale/locale.es.ini +2 -0
  19. pygpt_net/data/locale/locale.fr.ini +2 -0
  20. pygpt_net/data/locale/locale.it.ini +2 -0
  21. pygpt_net/data/locale/locale.pl.ini +3 -1
  22. pygpt_net/data/locale/locale.uk.ini +2 -0
  23. pygpt_net/data/locale/locale.zh.ini +2 -0
  24. pygpt_net/plugin/google/config.py +306 -1
  25. pygpt_net/plugin/google/plugin.py +22 -0
  26. pygpt_net/plugin/google/worker.py +579 -3
  27. pygpt_net/provider/agents/llama_index/supervisor_workflow.py +116 -0
  28. pygpt_net/provider/agents/llama_index/workflow/supervisor.py +303 -0
  29. pygpt_net/provider/agents/openai/supervisor.py +361 -0
  30. pygpt_net/provider/core/config/patch.py +11 -0
  31. pygpt_net/provider/core/preset/patch.py +18 -0
  32. pygpt_net/ui/main.py +1 -1
  33. pygpt_net/ui/widget/lists/context.py +10 -1
  34. pygpt_net/ui/widget/textarea/web.py +47 -4
  35. {pygpt_net-2.6.8.dist-info → pygpt_net-2.6.10.dist-info}/METADATA +93 -29
  36. {pygpt_net-2.6.8.dist-info → pygpt_net-2.6.10.dist-info}/RECORD +39 -34
  37. {pygpt_net-2.6.8.dist-info → pygpt_net-2.6.10.dist-info}/LICENSE +0 -0
  38. {pygpt_net-2.6.8.dist-info → pygpt_net-2.6.10.dist-info}/WHEEL +0 -0
  39. {pygpt_net-2.6.8.dist-info → pygpt_net-2.6.10.dist-info}/entry_points.txt +0 -0
pygpt_net/CHANGELOG.txt CHANGED
@@ -1,3 +1,15 @@
1
+ 2.6.10 (2025-08-17)
2
+
3
+ - Enhanced the handling of the context list.
4
+ - Integrated RAG into OpenAI Agents.
5
+ - Enhanced RAG management in Agents.
6
+ - Added an option: Config -> Agents -> General -> Auto-retrieve additional context from RAG.
7
+ - Included Google Docs, Maps, and Colab in the Google plugin.
8
+
9
+ 2.6.9 (2025-08-17)
10
+
11
+ - Added two new agents for LlamaIndex and OpenAI: Supervisor and Worker (beta).
12
+
1
13
  2.6.8 (2025-08-16)
2
14
 
3
15
  - Fixed: updated paragraph color on theme switch.
pygpt_net/__init__.py CHANGED
@@ -6,15 +6,15 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.15 00:00:00 #
9
+ # Updated Date: 2025.08.17 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  __author__ = "Marcin Szczygliński"
13
13
  __copyright__ = "Copyright 2025, Marcin Szczygliński"
14
14
  __credits__ = ["Marcin Szczygliński"]
15
15
  __license__ = "MIT"
16
- __version__ = "2.6.8"
17
- __build__ = "2025-08-16"
16
+ __version__ = "2.6.10"
17
+ __build__ = "2025-08-17"
18
18
  __maintainer__ = "Marcin Szczygliński"
19
19
  __github__ = "https://github.com/szczyglis-dev/py-gpt"
20
20
  __report__ = "https://github.com/szczyglis-dev/py-gpt/issues"
pygpt_net/app.py CHANGED
@@ -88,6 +88,7 @@ from pygpt_net.provider.agents.llama_index.openai_workflow import OpenAIAgent as
88
88
  # from pygpt_net.provider.agents.llama_index.legacy.react import ReactAgent
89
89
  from pygpt_net.provider.agents.llama_index.react_workflow import ReactWorkflowAgent
90
90
  from pygpt_net.provider.agents.llama_index.codeact_workflow import CodeActAgent
91
+ from pygpt_net.provider.agents.llama_index.supervisor_workflow import SupervisorAgent as LlamaSupervisorAgent
91
92
  from pygpt_net.provider.agents.openai.agent import Agent as OpenAIAgentsBase
92
93
  from pygpt_net.provider.agents.openai.agent_with_experts import Agent as OpenAIAgentsExperts
93
94
  from pygpt_net.provider.agents.openai.agent_with_experts_feedback import Agent as OpenAIAgentsExpertsFeedback
@@ -96,6 +97,7 @@ from pygpt_net.provider.agents.openai.bot_researcher import Agent as OpenAIAgent
96
97
  from pygpt_net.provider.agents.openai.agent_planner import Agent as OpenAIAgentPlanner
97
98
  from pygpt_net.provider.agents.openai.evolve import Agent as OpenAIAgentsEvolve
98
99
  from pygpt_net.provider.agents.openai.agent_b2b import Agent as OpenAIAgentsB2B
100
+ from pygpt_net.provider.agents.openai.supervisor import Agent as OpenAIAgentSupervisor
99
101
 
100
102
  # LLM wrapper providers (langchain, llama-index, embeddings)
101
103
  from pygpt_net.provider.llms.anthropic import AnthropicLLM
@@ -445,6 +447,7 @@ def run(**kwargs):
445
447
  # launcher.add_agent(ReactAgent()) # llama-index
446
448
  launcher.add_agent(ReactWorkflowAgent()) # llama-index
447
449
  launcher.add_agent(CodeActAgent()) # llama-index
450
+ launcher.add_agent(LlamaSupervisorAgent()) # llama-index
448
451
  launcher.add_agent(OpenAIAgentsBase()) # openai-agents
449
452
  launcher.add_agent(OpenAIAgentsExperts()) # openai-agents
450
453
  launcher.add_agent(OpenAIAgentFeedback()) # openai-agents
@@ -453,6 +456,7 @@ def run(**kwargs):
453
456
  launcher.add_agent(OpenAIAgentsExpertsFeedback()) # openai-agents
454
457
  launcher.add_agent(OpenAIAgentsEvolve()) # openai-agents
455
458
  launcher.add_agent(OpenAIAgentsB2B()) # openai-agents
459
+ launcher.add_agent(OpenAIAgentSupervisor()) # openai-agents
456
460
 
457
461
  # register custom agents
458
462
  agents = kwargs.get('agents', None)
@@ -31,7 +31,8 @@ class Common:
31
31
  self.summarizer = Summarizer(window)
32
32
 
33
33
  def _update_ctx_no_scroll(self):
34
- self.window.controller.ctx.update(no_scroll=True)
34
+ """Update ctx list without scroll"""
35
+ self.window.controller.ctx.update_and_restore()
35
36
 
36
37
  def update_label_by_current(self):
37
38
  """Update ctx label from current ctx"""
@@ -83,7 +84,7 @@ class Common:
83
84
  if new_id is not None:
84
85
  self.window.core.attachments.context.duplicate(meta_id, new_id)
85
86
  self.window.update_status(f"Context duplicated, new ctx id: {new_id}")
86
- QTimer.singleShot(100, self._update_ctx_no_scroll)
87
+ QTimer.singleShot(10, self._update_ctx_no_scroll)
87
88
 
88
89
  def dismiss_rename(self):
89
90
  """Dismiss rename dialog"""
@@ -97,7 +98,12 @@ class Common:
97
98
  """
98
99
  data_id = meta.id if meta else None
99
100
  title = meta.name if meta else None
100
- self.window.controller.ui.tabs.focus_by_type(Tab.TAB_CHAT, data_id=data_id, title=title, meta=meta)
101
+ self.window.controller.ui.tabs.focus_by_type(
102
+ Tab.TAB_CHAT,
103
+ data_id=data_id,
104
+ title=title,
105
+ meta=meta
106
+ )
101
107
 
102
108
  def restore_display_filter(self):
103
109
  """Restore display filter"""
@@ -334,17 +334,20 @@ class Ctx:
334
334
  if id is not None:
335
335
  self.select(id)
336
336
 
337
- def update_list(self, reload: bool = False):
337
+ def update_list(self, reload: bool = False, restore_scroll: bool = False):
338
338
  """
339
339
  Reload current ctx list
340
340
 
341
341
  :param reload: reload ctx list items
342
+ :param restore_scroll: restore scroll position
342
343
  """
343
344
  self.window.ui.contexts.ctx_list.update(
344
345
  'ctx.list',
345
346
  self.window.core.ctx.get_meta(reload),
346
347
  expand=False,
347
348
  )
349
+ if restore_scroll:
350
+ self.window.ui.nodes['ctx.list'].restore_scroll_position()
348
351
 
349
352
  def refresh(self, restore_model: bool = True):
350
353
  """
@@ -520,7 +523,7 @@ class Ctx:
520
523
  self.window.core.ctx.clear_current()
521
524
  event = RenderEvent(RenderEvent.CLEAR_OUTPUT)
522
525
  self.window.dispatch(event)
523
- self.update(no_scroll=True)
526
+ self.update_and_restore()
524
527
 
525
528
  self.window.controller.ui.tabs.update_title_current("...")
526
529
 
@@ -653,8 +656,7 @@ class Ctx:
653
656
  if meta is not None:
654
657
  meta.important = value
655
658
  self.window.core.ctx.save(id)
656
- self.update(no_scroll=True)
657
- self.select_by_current()
659
+ self.update_and_restore()
658
660
 
659
661
  def is_important(self, idx: int) -> bool:
660
662
  """
@@ -686,9 +688,15 @@ class Ctx:
686
688
  self.window.core.ctx.save(id)
687
689
  QTimer.singleShot(
688
690
  10,
689
- lambda: self.update(no_scroll=True)
691
+ lambda: self.update_and_restore()
690
692
  )
691
693
 
694
+ def update_and_restore(self):
695
+ """Update ctx and restore scroll position"""
696
+ self.window.ui.nodes['ctx.list'].store_scroll_position()
697
+ self.update()
698
+ self.window.ui.nodes['ctx.list'].restore_scroll_position()
699
+
692
700
  def update_name(
693
701
  self,
694
702
  id: int,
@@ -713,7 +721,7 @@ class Ctx:
713
721
  self.window.ui.dialog['rename'].close()
714
722
 
715
723
  if refresh:
716
- self.update(no_scroll=True)
724
+ self.update_and_restore()
717
725
  else:
718
726
  self.update(reload=True, all=False, no_scroll=True)
719
727
 
@@ -964,7 +972,7 @@ class Ctx:
964
972
  if update:
965
973
  QTimer.singleShot(
966
974
  10,
967
- lambda: self.update()
975
+ lambda: self.update_and_restore()
968
976
  )
969
977
 
970
978
  def remove_from_group(self, meta_id):
@@ -977,7 +985,7 @@ class Ctx:
977
985
  self.group_id = None
978
986
  QTimer.singleShot(
979
987
  10,
980
- lambda: self.update(no_scroll=True)
988
+ lambda: self.update_and_restore()
981
989
  )
982
990
 
983
991
  def new_group(
@@ -1063,13 +1071,7 @@ class Ctx:
1063
1071
  self.window.core.ctx.update_group(group)
1064
1072
  if close:
1065
1073
  self.window.ui.dialog['rename'].close()
1066
- self.update(
1067
- reload=True,
1068
- all=False,
1069
- select=False,
1070
- no_scroll=True
1071
- )
1072
- self.select_group(id)
1074
+ self.update_and_restore()
1073
1075
 
1074
1076
  def get_group_name(self, id: int) -> str:
1075
1077
  """
@@ -1119,7 +1121,7 @@ class Ctx:
1119
1121
  self.window.core.ctx.remove_group(group, all=False)
1120
1122
  if self.group_id == id:
1121
1123
  self.group_id = None
1122
- self.update(no_scroll=True)
1124
+ self.update_and_restore()
1123
1125
 
1124
1126
  def delete_group_all(
1125
1127
  self,
@@ -1144,7 +1146,7 @@ class Ctx:
1144
1146
  self.window.core.ctx.remove_group(group, all=True)
1145
1147
  if self.group_id == id:
1146
1148
  self.group_id = None
1147
- self.update()
1149
+ self.update_and_restore()
1148
1150
 
1149
1151
  def reload(self):
1150
1152
  """Reload ctx"""
@@ -31,7 +31,7 @@ from .reply import Reply
31
31
  from .stack import Stack
32
32
 
33
33
 
34
- class Kernel(QObject):
34
+ class Kernel:
35
35
 
36
36
  STATE_IDLE = "idle"
37
37
  STATE_BUSY = "busy"
@@ -81,7 +81,6 @@ class Kernel(QObject):
81
81
 
82
82
  :param window: The window context to which the kernel will be bound.
83
83
  """
84
- super(Kernel, self).__init__(window)
85
84
  self.window = window
86
85
  self.replies = Reply(window)
87
86
  self.stack = Stack(window)
@@ -105,6 +105,25 @@ class Runner:
105
105
  self.window.core.agents.tools.context = context
106
106
  self.window.core.agents.tools.agent_idx = vector_store_idx
107
107
 
108
+ # --- ADDITIONAL CONTEXT ---
109
+ # append additional context from RAG if available
110
+ if vector_store_idx and self.window.core.config.get("agent.idx.auto_retrieve", True):
111
+ ad_context = self.window.core.idx.chat.query_retrieval(
112
+ query=prompt,
113
+ idx=vector_store_idx,
114
+ model=context.model,
115
+ )
116
+ if ad_context:
117
+ to_append = ""
118
+ if ctx.hidden_input is None:
119
+ ctx.hidden_input = ""
120
+ if not ctx.hidden_input: # may be not empty (appended before from attachments)
121
+ to_append = "ADDITIONAL CONTEXT:"
122
+ ctx.hidden_input += to_append
123
+ to_append += "\n" + ad_context
124
+ ctx.hidden_input += to_append
125
+ prompt += "\n\n" + to_append
126
+
108
127
  tools = self.window.core.agents.tools.prepare(context, extra, force=True)
109
128
  function_tools = self.window.core.agents.tools.get_function_tools(ctx, extra, force=True)
110
129
  plugin_tools = self.window.core.agents.tools.get_plugin_tools(context, extra, force=True)
@@ -25,6 +25,13 @@ from pygpt_net.item.ctx import CtxItem
25
25
 
26
26
 
27
27
  class Tools:
28
+
29
+ QUERY_ENGINE_TOOL_NAME = "rag_get_context"
30
+ QUERY_ENGINE_TOOL_DESCRIPTION = "Get additional context for provided question. Use this whenever you need additional context to provide an answer. "
31
+ QUERY_ENGINE_TOOL_SPEC = ("**"+QUERY_ENGINE_TOOL_NAME+"**: "
32
+ + QUERY_ENGINE_TOOL_DESCRIPTION +
33
+ "available params: {'query': {'type': 'string', 'description': 'query string'}}, required: [query]")
34
+
28
35
  def __init__(self, window=None):
29
36
  """
30
37
  Agent tools
@@ -62,6 +69,32 @@ class Tools:
62
69
  plugin_functions = self.get_plugin_functions(context.ctx, verbose=verbose, force=force)
63
70
  tools.extend(plugin_functions)
64
71
 
72
+ # add query engine tool if idx is provided
73
+ query_engine_tools = self.get_retriever_tool(
74
+ context=context,
75
+ extra=extra,
76
+ verbose=verbose,
77
+ )
78
+ if query_engine_tools:
79
+ tools.extend(query_engine_tools)
80
+ return tools
81
+
82
+ def get_retriever_tool(
83
+ self,
84
+ context: BridgeContext,
85
+ extra: Dict[str, Any],
86
+ verbose: bool = False
87
+ ) -> List[BaseTool]:
88
+ """
89
+ Prepare tools for agent
90
+
91
+ :param context: BridgeContext
92
+ :param extra: extra data
93
+ :param verbose: verbose mode
94
+ :return: list of tools
95
+ """
96
+ tool = None
97
+
65
98
  # add query engine tool if idx is provided
66
99
  idx = extra.get("agent_idx", None)
67
100
  if idx is not None and idx != "_":
@@ -69,19 +102,51 @@ class Tools:
69
102
  index = self.window.core.idx.storage.get(idx, llm, embed_model) # get index
70
103
  if index is not None:
71
104
  query_engine = index.as_query_engine(similarity_top_k=3)
72
- query_engine_tools = [
105
+ tool = [
73
106
  QueryEngineTool(
74
107
  query_engine=query_engine,
75
108
  metadata=ToolMetadata(
76
- name="query_engine",
77
- description=(
78
- "Provides additional context and access to the indexed documents."
79
- ),
109
+ name=self.QUERY_ENGINE_TOOL_NAME,
110
+ description=self.QUERY_ENGINE_TOOL_DESCRIPTION,
80
111
  ),
81
112
  ),
82
113
  ]
83
- tools.extend(query_engine_tools)
84
- return tools
114
+ return tool
115
+
116
+ def get_openai_retriever_tool(
117
+ self,
118
+ idx: str,
119
+ verbose: bool = False
120
+ ) -> OpenAIFunctionTool:
121
+ """
122
+ Prepare OpenAI retriever tool for agent
123
+
124
+ :param idx: index name
125
+ :param verbose: verbose mode
126
+ :return: OpenAIFunctionTool instance
127
+ """
128
+ async def run_function(run_ctx: RunContextWrapper[Any], args: str) -> str:
129
+ name = run_ctx.tool_name
130
+ print("[Plugin] Tool call: " + name + " with args: " + str(args))
131
+ cmd = {
132
+ "cmd": name,
133
+ "params": json.loads(args) # args should be a JSON string
134
+ }
135
+ return self.tool_exec(name, cmd["params"])
136
+
137
+ schema = {"type": "object", "properties": {
138
+ "query": {
139
+ "type": "string",
140
+ "description": "The query string to search in the index."
141
+ }
142
+ }, "additionalProperties": False}
143
+ description = self.QUERY_ENGINE_TOOL_DESCRIPTION + f" Index: {idx}"
144
+ return OpenAIFunctionTool(
145
+ name=self.QUERY_ENGINE_TOOL_NAME,
146
+ description=description,
147
+ params_json_schema=schema,
148
+ on_invoke_tool=run_function,
149
+ )
85
150
 
86
151
  def get_plugin_functions(
87
152
  self,
@@ -204,6 +269,11 @@ class Tools:
204
269
  tools.append(tool)
205
270
  except Exception as e:
206
271
  print(e)
272
+
273
+ # append query engine tool if idx is provided
274
+ if self.agent_idx is not None and self.agent_idx != "_":
275
+ tools.append(self.get_openai_retriever_tool(self.agent_idx))
276
+
207
277
  return tools
208
278
 
209
279
  def get_plugin_tools(
@@ -230,7 +300,6 @@ class Tools:
230
300
  continue # skip blacklisted commands
231
301
 
232
302
  description = item['desc']
233
- schema = json.loads(item['params']) # from JSON to dict
234
303
 
235
304
  def make_func(name, description):
236
305
  def func(**kwargs):
@@ -255,9 +324,17 @@ class Tools:
255
324
  print(e)
256
325
 
257
326
  # add query engine tool if idx is provided
258
- if self.agent_idx is not None and self.agent_idx != "_":
259
- tools["query_engine"] = None # placeholder for query engine tool
260
-
327
+ if self.agent_idx is not None and self.agent_idx != "_":
328
+ extra = {
329
+ "agent_idx": self.agent_idx, # agent index for query engine tool
330
+ }
331
+ query_engine_tools = self.get_retriever_tool(
332
+ context=context,
333
+ extra=extra,
334
+ verbose=verbose,
335
+ )
336
+ if query_engine_tools:
337
+ tools["query_engine"] = query_engine_tools[0] # add query engine tool
261
338
  return tools
262
339
 
263
340
 
@@ -281,9 +358,7 @@ class Tools:
281
358
 
282
359
  # add query engine tool spec if idx is provided
283
360
  if self.agent_idx is not None and self.agent_idx != "_":
284
- specs.append("**query_engine**: "
285
- "Provides additional context and access to the indexed documents, "
286
- "available params: {'query': {'type': 'string', 'description': 'query string'}}, required: [query]")
361
+ specs.append(self.QUERY_ENGINE_TOOL_SPEC)
287
362
 
288
363
  for func in functions:
289
364
  try:
@@ -292,8 +367,9 @@ class Tools:
292
367
  continue # skip blacklisted commands
293
368
  description = func['desc']
294
369
  schema = json.loads(func['params']) # from JSON to dict
295
- spec = "**{}**: {}, available params: {}, required: {}\n".format(name, description, schema.get("properties", {}), schema.get("required", []))
296
- specs.append(spec)
370
+ specs.append(
371
+ f"**{name}**: {description}, available params: {schema.get('properties', {})}, required: {schema.get('required', [])}\n"
372
+ )
297
373
  except Exception as e:
298
374
  print(e)
299
375
  return specs
@@ -308,7 +384,7 @@ class Tools:
308
384
  """
309
385
  print("[Plugin] Tool call: " + cmd + " " + str(params))
310
386
  # special case for query engine tool
311
- if cmd == "query_engine":
387
+ if cmd == self.QUERY_ENGINE_TOOL_NAME:
312
388
  if "query" not in params:
313
389
  return "Query parameter is required for query_engine tool."
314
390
  if self.context is None:
@@ -340,41 +416,6 @@ class Tools:
340
416
  )
341
417
  return response
342
418
 
343
- def get_retriever_tool(
344
- self,
345
- context: BridgeContext,
346
- extra: Dict[str, Any],
347
- verbose: bool = False
348
- ) -> List[BaseTool]:
349
- """
350
- Prepare tools for agent
351
-
352
- :param context: BridgeContext
353
- :param extra: extra data
354
- :param verbose: verbose mode
355
- :return: list of tools
356
- """
357
- tool = None
358
- # add query engine tool if idx is provided
359
- idx = extra.get("agent_idx", None)
360
- if idx is not None and idx != "_":
361
- llm, embed_model = self.window.core.idx.llm.get_service_context(model=context.model)
362
- index = self.window.core.idx.storage.get(idx, llm, embed_model) # get index
363
- if index is not None:
364
- query_engine = index.as_query_engine(similarity_top_k=3)
365
- tool = [
366
- QueryEngineTool(
367
- query_engine=query_engine,
368
- metadata=ToolMetadata(
369
- name="query_engine",
370
- description=(
371
- "Provides additional context and access to the indexed documents."
372
- ),
373
- ),
374
- ),
375
- ]
376
- return tool
377
-
378
419
  def export_sources(
379
420
  self,
380
421
  response: AgentChatResponse
@@ -985,9 +985,9 @@ class Body:
985
985
  syntax_style = self.window.core.config.get("render.code_syntax") or "default"
986
986
 
987
987
  theme_css = self.window.controller.theme.markdown.get_web_css().replace('%fonts%', fonts_path)
988
- parts = [self._SPINNER, theme_css]
989
- parts.append("pre { color: #fff; }" if syntax_style in self._syntax_dark else "pre { color: #000; }")
990
- parts.append(self.highlight.get_style_defs())
988
+ parts = [self._SPINNER, theme_css,
989
+ "pre { color: #fff; }" if syntax_style in self._syntax_dark else "pre { color: #000; }",
990
+ self.highlight.get_style_defs()]
991
991
  return "\n".join(parts)
992
992
 
993
993
  def prepare_action_icons(self, ctx: CtxItem) -> str:
@@ -1014,32 +1014,19 @@ class Body:
1014
1014
  if ctx.output:
1015
1015
  cid = ctx.id
1016
1016
  t = trans
1017
-
1018
1017
  icons.append(
1019
- f'<a href="extra-audio-read:{cid}" class="action-icon" data-id="{cid}" role="button">'
1020
- f'<span class="cmd">{self.get_icon("volume", t("ctx.extra.audio"), ctx)}</span></a>'
1021
- )
1018
+ f'<a href="extra-audio-read:{cid}" class="action-icon" data-id="{cid}" role="button"><span class="cmd">{self.get_icon("volume", t("ctx.extra.audio"), ctx)}</span></a>')
1022
1019
  icons.append(
1023
- f'<a href="extra-copy:{cid}" class="action-icon" data-id="{cid}" role="button">'
1024
- f'<span class="cmd">{self.get_icon("copy", t("ctx.extra.copy"), ctx)}</span></a>'
1025
- )
1020
+ f'<a href="extra-copy:{cid}" class="action-icon" data-id="{cid}" role="button"><span class="cmd">{self.get_icon("copy", t("ctx.extra.copy"), ctx)}</span></a>')
1026
1021
  icons.append(
1027
- f'<a href="extra-replay:{cid}" class="action-icon" data-id="{cid}" role="button">'
1028
- f'<span class="cmd">{self.get_icon("reload", t("ctx.extra.reply"), ctx)}</span></a>'
1029
- )
1022
+ f'<a href="extra-replay:{cid}" class="action-icon" data-id="{cid}" role="button"><span class="cmd">{self.get_icon("reload", t("ctx.extra.reply"), ctx)}</span></a>')
1030
1023
  icons.append(
1031
- f'<a href="extra-edit:{cid}" class="action-icon edit-icon" data-id="{cid}" role="button">'
1032
- f'<span class="cmd">{self.get_icon("edit", t("ctx.extra.edit"), ctx)}</span></a>'
1033
- )
1024
+ f'<a href="extra-edit:{cid}" class="action-icon edit-icon" data-id="{cid}" role="button"><span class="cmd">{self.get_icon("edit", t("ctx.extra.edit"), ctx)}</span></a>')
1034
1025
  icons.append(
1035
- f'<a href="extra-delete:{cid}" class="action-icon edit-icon" data-id="{cid}" role="button">'
1036
- f'<span class="cmd">{self.get_icon("delete", t("ctx.extra.delete"), ctx)}</span></a>'
1037
- )
1026
+ f'<a href="extra-delete:{cid}" class="action-icon edit-icon" data-id="{cid}" role="button"><span class="cmd">{self.get_icon("delete", t("ctx.extra.delete"), ctx)}</span></a>')
1038
1027
  if not self.window.core.ctx.is_first_item(cid):
1039
1028
  icons.append(
1040
- f'<a href="extra-join:{cid}" class="action-icon edit-icon" data-id="{cid}" role="button">'
1041
- f'<span class="cmd">{self.get_icon("playlist_add", t("ctx.extra.join"), ctx)}</span></a>'
1042
- )
1029
+ f'<a href="extra-join:{cid}" class="action-icon edit-icon" data-id="{cid}" role="button"><span class="cmd">{self.get_icon("playlist_add", t("ctx.extra.join"), ctx)}</span></a>')
1043
1030
  return icons
1044
1031
 
1045
1032
  def get_icon(
@@ -1058,9 +1045,7 @@ class Body:
1058
1045
  """
1059
1046
  app_path = self.window.core.config.get_app_path()
1060
1047
  icon_path = os.path.join(app_path, "data", "icons", f"{icon}.svg")
1061
- return (
1062
- f'<img src="file://{icon_path}" class="action-img" title="{title}" alt="{title}" data-id="{item.id}">'
1063
- )
1048
+ return f'<img src="file://{icon_path}" class="action-img" title="{title}" alt="{title}" data-id="{item.id}">'
1064
1049
 
1065
1050
  def get_image_html(
1066
1051
  self,
@@ -1078,14 +1063,7 @@ class Body:
1078
1063
  """
1079
1064
  url, path = self.window.core.filesystem.extract_local_url(url)
1080
1065
  basename = os.path.basename(path)
1081
- return (
1082
- f'<div class="extra-src-img-box" title="{url}">'
1083
- f'<div class="img-outer"><div class="img-wrapper">'
1084
- f'<a href="{url}"><img src="{path}" class="image"></a>'
1085
- f'</div>'
1086
- f'<a href="{url}" class="title">{basename}</a>'
1087
- f'</div></div><br/>'
1088
- )
1066
+ return f'<div class="extra-src-img-box" title="{url}"><div class="img-outer"><div class="img-wrapper"><a href="{url}"><img src="{path}" class="image"></a></div><a href="{url}" class="title">{basename}</a></div></div><br/>'
1089
1067
 
1090
1068
  def get_url_html(
1091
1069
  self,