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
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2025.08.17 02:00:00 #
10
+ # ================================================== #
11
+
12
+ from typing import Dict, Any, List
13
+
14
+ from pygpt_net.core.bridge import BridgeContext
15
+ from pygpt_net.core.types import (
16
+ AGENT_TYPE_LLAMA,
17
+ AGENT_MODE_WORKFLOW,
18
+ )
19
+ from llama_index.core.llms.llm import LLM
20
+ from llama_index.core.tools.types import BaseTool
21
+
22
+ from .workflow.supervisor import (
23
+ get_workflow,
24
+ SUPERVISOR_PROMPT,
25
+ WORKER_PROMPT,
26
+ )
27
+ from ..base import BaseAgent
28
+
29
+ class SupervisorAgent(BaseAgent):
30
+ def __init__(self, *args, **kwargs):
31
+ super(SupervisorAgent, self).__init__(*args, **kwargs)
32
+ self.id = "supervisor"
33
+ self.type = AGENT_TYPE_LLAMA
34
+ self.mode = AGENT_MODE_WORKFLOW
35
+ self.name = "Supervisor + worker"
36
+
37
+ def get_agent(self, window, kwargs: Dict[str, Any]):
38
+ """
39
+ Get agent instance
40
+
41
+ :param window: Window instance
42
+ :param kwargs: Agent parameters
43
+ :return: PlannerWorkflow instance
44
+ """
45
+ context = kwargs.get("context", BridgeContext())
46
+ preset = context.preset
47
+ tools: List[BaseTool] = kwargs.get("tools", []) or []
48
+ llm_supervisor: LLM = kwargs.get("llm", None)
49
+ verbose: bool = kwargs.get("verbose", False)
50
+ max_steps: int = kwargs.get("max_steps", 12)
51
+
52
+ # get prompts from options or use defaults
53
+ prompt_supervisor = self.get_option(preset, "supervisor", "prompt")
54
+ prompt_worker = self.get_option(preset, "worker", "prompt")
55
+ if not prompt_supervisor:
56
+ prompt_supervisor = SUPERVISOR_PROMPT
57
+ if not prompt_worker:
58
+ prompt_worker = WORKER_PROMPT
59
+
60
+ # get worker LLM from options
61
+ model_worker = window.core.models.get(
62
+ self.get_option(preset, "worker", "model")
63
+ )
64
+ llm_worker = window.core.idx.llm.get(model_worker, stream=False)
65
+ worker_memory_session_id = ""
66
+ if context.ctx and context.ctx.meta:
67
+ worker_memory_session_id = "llama_worker_session_" + str(context.ctx.meta.id)
68
+
69
+ # create workflow
70
+ return get_workflow(
71
+ tools,
72
+ llm_supervisor=llm_supervisor,
73
+ llm_worker=llm_worker,
74
+ verbose=verbose,
75
+ max_steps=max_steps,
76
+ prompt_supervisor=prompt_supervisor,
77
+ prompt_worker=prompt_worker,
78
+ worker_memory_session_id=worker_memory_session_id,
79
+ )
80
+
81
+ def get_options(self) -> Dict[str, Any]:
82
+ """
83
+ Return Agent options
84
+
85
+ :return: dict of options
86
+ """
87
+ return {
88
+ "supervisor": {
89
+ "label": "Supervisor",
90
+ "options": {
91
+ "prompt": {
92
+ "type": "textarea",
93
+ "label": "Prompt",
94
+ "description": "Prompt for supervisor",
95
+ "default": SUPERVISOR_PROMPT,
96
+ },
97
+ }
98
+ },
99
+ "worker": {
100
+ "label": "Worker",
101
+ "options": {
102
+ "model": {
103
+ "label": "Model",
104
+ "type": "combo",
105
+ "use": "models",
106
+ "default": "gpt-4o",
107
+ },
108
+ "prompt": {
109
+ "type": "textarea",
110
+ "label": "Prompt",
111
+ "description": "Prompt for worker",
112
+ "default": WORKER_PROMPT,
113
+ },
114
+ }
115
+ },
116
+ }
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2025.08.17 02:00:00 #
10
+ # ================================================== #
11
+
12
+ import re
13
+ from typing import Optional, Literal, List
14
+ from pydantic import BaseModel, ValidationError
15
+ from llama_index.core.workflow import Workflow, Context, StartEvent, StopEvent, Event, step
16
+ from llama_index.core.agent.workflow import FunctionAgent, AgentStream
17
+ from llama_index.core.memory import Memory
18
+
19
+ # ==== Prompts ====
20
+ SUPERVISOR_PROMPT = """
21
+ You are the “Supervisor” – the main orchestrator. Do not use tools directly.
22
+ Your tasks:
23
+ - Break down the user's task into steps and create precise instructions for the “Worker” agent.
24
+ - Do not pass your history/memory to the Worker. Only pass minimal, self-sufficient instructions.
25
+ - After each Worker response, assess progress towards the Definition of Done (DoD). If not met – generate a better instruction.
26
+ - Ask the user only when absolutely necessary. Then stop and return the question.
27
+ - When the task is complete – return the final answer to the user.
28
+ Always return only ONE JSON object:
29
+ {
30
+ "action": "task" | "final" | "ask_user",
31
+ "instruction": "<Worker's instruction or ''>",
32
+ "final_answer": "<final answer or ''>",
33
+ "question": "<user question or ''>",
34
+ "reasoning": "<brief reasoning and quality control>",
35
+ "done_criteria": "<list/text of DoD criteria>"
36
+ }
37
+ Ensure proper JSON (no comments, no trailing commas). Respond in the user's language.
38
+ """
39
+
40
+ WORKER_PROMPT = """
41
+ You are the “Worker” – executor of the Supervisor's instructions. You have your own memory and tools.
42
+ - Execute the Supervisor's instructions precisely and concisely.
43
+ - Use the available tools and return a brief result + relevant data/reasoning.
44
+ - Maintain the working context in your memory (only Worker).
45
+ - Return plain text (not JSON) unless instructed otherwise by the Supervisor.
46
+ - Respond in the user's language.
47
+ """
48
+
49
+
50
+ # ==== Supervisor's JSON Structures ====
51
+ class SupervisorDirective(BaseModel):
52
+ action: Literal["task", "final", "ask_user"]
53
+ instruction: str = ""
54
+ final_answer: str = ""
55
+ question: str = ""
56
+ reasoning: str = ""
57
+ done_criteria: str = ""
58
+
59
+ JSON_RE = re.compile(r"\{[\s\S]*\}$", re.MULTILINE)
60
+
61
+ def parse_supervisor_json(text: str) -> SupervisorDirective:
62
+ """
63
+ Parse the Supervisor's JSON response from text.
64
+
65
+ :param text: The text response from the Supervisor.
66
+ :return: SupervisorDirective: Parsed directive from the Supervisor.
67
+ """
68
+ try:
69
+ return SupervisorDirective.model_validate_json(text)
70
+ except Exception:
71
+ pass
72
+ fence = re.search(r"```json\s*([\s\S]*?)\s*```", text, re.IGNORECASE)
73
+ if fence:
74
+ try:
75
+ return SupervisorDirective.model_validate_json(fence.group(1).strip())
76
+ except Exception:
77
+ pass
78
+ tail = JSON_RE.findall(text)
79
+ for candidate in tail[::-1]:
80
+ try:
81
+ return SupervisorDirective.model_validate_json(candidate.strip())
82
+ except Exception:
83
+ continue
84
+ first = text.find("{")
85
+ if first != -1:
86
+ try:
87
+ return SupervisorDirective.model_validate_json(text[first:])
88
+ except Exception:
89
+ pass
90
+ raise ValueError("Failed to parse a valid JSON from the Supervisor's response.")
91
+
92
+ # ==== Workflow Events ====
93
+ class InputEvent(StartEvent):
94
+ user_msg: str
95
+ external_context: Optional[str] = ""
96
+ round_idx: int = 0
97
+ max_rounds: int = 8
98
+ stop_on_ask_user: bool = True
99
+ last_worker_output: Optional[str] = None
100
+
101
+ class ExecuteEvent(Event):
102
+ instruction: str
103
+ round_idx: int
104
+ max_rounds: int
105
+ external_context: str = ""
106
+ stop_on_ask_user: bool = True
107
+
108
+ class OutputEvent(StopEvent):
109
+ status: Literal["final", "ask_user", "max_rounds"]
110
+ final_answer: str
111
+ rounds_used: int
112
+
113
+ # ==== Main Workflow ====
114
+ class SupervisorWorkflow(Workflow):
115
+ _supervisor: FunctionAgent
116
+ _worker: FunctionAgent
117
+ _supervisor_memory: Memory
118
+ _worker_memory: Memory
119
+ _max_steps: int = 12
120
+
121
+ def __init__(self, **kwargs):
122
+ super().__init__(timeout=kwargs.get("timeout", 120), verbose=kwargs.get("verbose", True))
123
+ self._supervisor = kwargs["supervisor"]
124
+ self._worker = kwargs["worker"]
125
+ self._worker_memory = kwargs.get("worker_memory")
126
+ self._max_steps = kwargs.get("max_steps", 12)
127
+
128
+ def run(
129
+ self,
130
+ query: str,
131
+ ctx: Optional[Context] = None,
132
+ memory: Optional[Memory] = None, # <- only for Supervisor
133
+ verbose: bool = False,
134
+ **kwargs
135
+ ):
136
+ """
137
+ Run the SupervisorWorkflow with the given query and context.
138
+
139
+ :param query: The user's query to start the workflow.
140
+ :param ctx: Context for the workflow, used to write events.
141
+ :param memory: Optional memory for the Supervisor agent. If not provided, it uses the default memory.
142
+ :param verbose: If True, enables verbose output for the workflow.
143
+ :param kwargs: Additional keyword arguments for the workflow, such as `external_context`, `stop_on_ask_user`, etc.
144
+ :return: OutputEvent or ExecuteEvent based on the workflow's progress.
145
+ """
146
+ if verbose:
147
+ self._verbose = True
148
+
149
+ if memory is not None:
150
+ self._supervisor_memory = memory # use external memory for Supervisor
151
+
152
+ start_event = InputEvent(
153
+ user_msg=query,
154
+ external_context=kwargs.get("external_context", ""),
155
+ round_idx=0,
156
+ max_rounds=self._max_steps,
157
+ stop_on_ask_user=kwargs.get("stop_on_ask_user", True),
158
+ last_worker_output=None,
159
+ )
160
+ return super().run(ctx=ctx, start_event=start_event)
161
+
162
+ async def _emit_text(
163
+ self,
164
+ ctx: Context,
165
+ text: str,
166
+ agent_name: str = "SupervisorWorkflow"
167
+ ):
168
+ """
169
+ Emit a text message to the context stream.
170
+
171
+ :param ctx: The context to write the event to
172
+ :param text: The text message to emit.
173
+ :param agent_name: The name of the agent emitting the text (default: "PlannerWorkflow").
174
+ """
175
+ try:
176
+ ctx.write_event_to_stream(AgentStream(delta=text))
177
+ except ValidationError:
178
+ ctx.write_event_to_stream(
179
+ AgentStream(
180
+ delta=text,
181
+ response=text,
182
+ current_agent_name=agent_name,
183
+ tool_calls=[],
184
+ raw={},
185
+ )
186
+ )
187
+
188
+ @step
189
+ async def supervisor_step(self, ctx: Context, ev: InputEvent) -> ExecuteEvent | OutputEvent:
190
+ """
191
+ Supervisor step to process the user's input and generate an instruction for the Worker.
192
+
193
+ :param ctx: Context for the workflow
194
+ :param ev: InputEvent containing the user's message and context.
195
+ :return: ExecuteEvent for the Worker or OutputEvent if final answer is reached.
196
+ """
197
+ parts: List[str] = []
198
+ if ev.external_context:
199
+ parts.append(f"<external_context>\n{ev.external_context}\n</external_context>")
200
+ if ev.user_msg and ev.round_idx == 0:
201
+ parts.append(f"<task_from_user>\n{ev.user_msg}\n</task_from_user>")
202
+ if ev.last_worker_output:
203
+ parts.append(f"<last_worker_output>\n{ev.last_worker_output}\n</last_worker_output>")
204
+ parts.append(
205
+ f"<control>\nround={ev.round_idx} max_rounds={ev.max_rounds}\n"
206
+ "Return ONE JSON following the schema.\n</control>"
207
+ )
208
+ sup_input = "\n".join(parts)
209
+ sup_resp = await self._supervisor.run(user_msg=sup_input, memory=self._supervisor_memory)
210
+ directive = parse_supervisor_json(str(sup_resp))
211
+
212
+ if directive.action == "final":
213
+ await self._emit_text(ctx, f"\n\n{directive.final_answer or str(sup_resp)}", agent_name=self._supervisor.name)
214
+ return OutputEvent(status="final", final_answer=directive.final_answer or str(sup_resp), rounds_used=ev.round_idx)
215
+ if directive.action == "ask_user" and ev.stop_on_ask_user:
216
+ q = directive.question or "I need more information, please clarify."
217
+ await self._emit_text(ctx, f"\n\n{q}", agent_name=self._supervisor.name)
218
+ return OutputEvent(status="ask_user", final_answer=q, rounds_used=ev.round_idx)
219
+ if ev.round_idx >= ev.max_rounds:
220
+ await self._emit_text(ctx, "\n\nMax rounds exceeded.", agent_name=self._supervisor.name)
221
+ return OutputEvent(status="max_rounds", final_answer="Exceeded maximum number of iterations.", rounds_used=ev.round_idx)
222
+
223
+ instruction = (directive.instruction or "").strip() or "Perform a step that gets closest to fulfilling the DoD."
224
+ return ExecuteEvent(
225
+ instruction=instruction,
226
+ round_idx=ev.round_idx,
227
+ max_rounds=ev.max_rounds,
228
+ external_context=ev.external_context or "",
229
+ stop_on_ask_user=ev.stop_on_ask_user,
230
+ )
231
+
232
+ @step
233
+ async def worker_step(self, ctx: Context, ev: ExecuteEvent) -> InputEvent:
234
+ """
235
+ Worker step to execute the Supervisor's instruction.
236
+
237
+ :param ctx: Context for the workflow
238
+ :param ev: ExecuteEvent containing the instruction and context.
239
+ :return: InputEvent for the next round or final output.
240
+ """
241
+ worker_input = f"Instruction from Supervisor:\n{ev.instruction}\n"
242
+ await self._emit_text(ctx, f"\n\n**Supervisor:** {ev.instruction}", agent_name=self._worker.name)
243
+
244
+ worker_resp = await self._worker.run(user_msg=worker_input, memory=self._worker_memory)
245
+ await self._emit_text(ctx, f"\n\n**Worker:** {worker_resp}", agent_name=self._worker.name)
246
+
247
+ return InputEvent(
248
+ user_msg="",
249
+ last_worker_output=str(worker_resp),
250
+ round_idx=ev.round_idx + 1,
251
+ max_rounds=ev.max_rounds,
252
+ external_context=ev.external_context,
253
+ stop_on_ask_user=ev.stop_on_ask_user,
254
+ )
255
+
256
+ # ==== Factory ====
257
+ def get_workflow(
258
+ tools,
259
+ llm_supervisor,
260
+ llm_worker,
261
+ verbose: bool = False,
262
+ prompt_supervisor: str = SUPERVISOR_PROMPT,
263
+ prompt_worker: str = WORKER_PROMPT,
264
+ max_steps: int = 12,
265
+ worker_memory_session_id: str = "llama_worker_session" # session ID for worker memory
266
+ ):
267
+ """
268
+ Create a SupervisorWorkflow instance.
269
+
270
+ :param tools: List of tools for the Worker agent.
271
+ :param llm_supervisor: LLM instance for the Supervisor agent.
272
+ :param llm_worker: LLM instance for the Worker agent.
273
+ :param verbose: Verbose output flag.
274
+ :param prompt_supervisor: Prompt for the Supervisor agent.
275
+ :param prompt_worker: Prompt for the Worker agent.
276
+ :param max_steps: Maximum number of steps for the workflow.
277
+ :param worker_memory_session_id: Session ID for the Worker agent's memory.
278
+ :return: SupervisorWorkflow instance
279
+ """
280
+ supervisor = FunctionAgent(
281
+ name="Supervisor",
282
+ llm=llm_supervisor,
283
+ system_prompt=prompt_supervisor,
284
+ tools=[],
285
+ )
286
+ worker = FunctionAgent(
287
+ name="Worker",
288
+ llm=llm_worker,
289
+ system_prompt=prompt_worker,
290
+ tools=tools,
291
+ )
292
+
293
+ # separate memory for the worker
294
+ worker_memory = Memory.from_defaults(session_id=worker_memory_session_id, token_limit=40000)
295
+
296
+ return SupervisorWorkflow(
297
+ supervisor=supervisor,
298
+ worker=worker,
299
+ worker_memory=worker_memory,
300
+ verbose=verbose,
301
+ timeout=120,
302
+ max_steps=max_steps,
303
+ )