pygpt-net 2.6.59__py3-none-any.whl → 2.6.61__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.
- pygpt_net/CHANGELOG.txt +11 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +9 -5
- pygpt_net/controller/__init__.py +1 -0
- pygpt_net/controller/chat/common.py +115 -6
- pygpt_net/controller/chat/input.py +4 -1
- pygpt_net/controller/presets/editor.py +442 -39
- pygpt_net/controller/presets/presets.py +121 -6
- pygpt_net/controller/settings/editor.py +0 -15
- pygpt_net/controller/theme/markdown.py +2 -5
- pygpt_net/controller/ui/ui.py +4 -7
- pygpt_net/core/agents/custom/__init__.py +281 -0
- pygpt_net/core/agents/custom/debug.py +64 -0
- pygpt_net/core/agents/custom/factory.py +109 -0
- pygpt_net/core/agents/custom/graph.py +71 -0
- pygpt_net/core/agents/custom/llama_index/__init__.py +10 -0
- pygpt_net/core/agents/custom/llama_index/factory.py +100 -0
- pygpt_net/core/agents/custom/llama_index/router_streamer.py +106 -0
- pygpt_net/core/agents/custom/llama_index/runner.py +562 -0
- pygpt_net/core/agents/custom/llama_index/stream.py +56 -0
- pygpt_net/core/agents/custom/llama_index/utils.py +253 -0
- pygpt_net/core/agents/custom/logging.py +50 -0
- pygpt_net/core/agents/custom/memory.py +51 -0
- pygpt_net/core/agents/custom/router.py +155 -0
- pygpt_net/core/agents/custom/router_streamer.py +187 -0
- pygpt_net/core/agents/custom/runner.py +455 -0
- pygpt_net/core/agents/custom/schema.py +127 -0
- pygpt_net/core/agents/custom/utils.py +193 -0
- pygpt_net/core/agents/provider.py +72 -7
- pygpt_net/core/agents/runner.py +7 -4
- pygpt_net/core/agents/runners/helpers.py +1 -1
- pygpt_net/core/agents/runners/llama_workflow.py +3 -0
- pygpt_net/core/agents/runners/openai_workflow.py +8 -1
- pygpt_net/core/db/viewer.py +11 -5
- pygpt_net/{ui/widget/builder → core/node_editor}/__init__.py +2 -2
- pygpt_net/core/{builder → node_editor}/graph.py +28 -226
- pygpt_net/core/node_editor/models.py +118 -0
- pygpt_net/core/node_editor/types.py +78 -0
- pygpt_net/core/node_editor/utils.py +17 -0
- pygpt_net/core/presets/presets.py +216 -29
- pygpt_net/core/render/markdown/parser.py +0 -2
- pygpt_net/core/render/web/renderer.py +10 -8
- pygpt_net/data/config/config.json +5 -6
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +2 -38
- pygpt_net/data/locale/locale.de.ini +64 -1
- pygpt_net/data/locale/locale.en.ini +63 -4
- pygpt_net/data/locale/locale.es.ini +64 -1
- pygpt_net/data/locale/locale.fr.ini +64 -1
- pygpt_net/data/locale/locale.it.ini +64 -1
- pygpt_net/data/locale/locale.pl.ini +65 -2
- pygpt_net/data/locale/locale.uk.ini +64 -1
- pygpt_net/data/locale/locale.zh.ini +64 -1
- pygpt_net/data/locale/plugin.cmd_system.en.ini +62 -66
- pygpt_net/item/agent.py +5 -1
- pygpt_net/item/preset.py +19 -1
- pygpt_net/provider/agents/base.py +33 -2
- pygpt_net/provider/agents/llama_index/flow_from_schema.py +92 -0
- pygpt_net/provider/agents/openai/flow_from_schema.py +96 -0
- pygpt_net/provider/core/agent/json_file.py +11 -5
- pygpt_net/provider/core/config/patch.py +10 -1
- pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +0 -6
- pygpt_net/tools/agent_builder/tool.py +233 -52
- pygpt_net/tools/agent_builder/ui/dialogs.py +172 -28
- pygpt_net/tools/agent_builder/ui/list.py +37 -10
- pygpt_net/ui/__init__.py +2 -4
- pygpt_net/ui/dialog/about.py +58 -38
- pygpt_net/ui/dialog/db.py +142 -3
- pygpt_net/ui/dialog/preset.py +62 -8
- pygpt_net/ui/layout/toolbox/presets.py +52 -16
- pygpt_net/ui/main.py +1 -1
- pygpt_net/ui/widget/dialog/db.py +0 -0
- pygpt_net/ui/widget/lists/preset.py +644 -60
- pygpt_net/{core/builder → ui/widget/node_editor}/__init__.py +2 -2
- pygpt_net/ui/widget/node_editor/command.py +373 -0
- pygpt_net/ui/widget/node_editor/config.py +157 -0
- pygpt_net/ui/widget/node_editor/editor.py +2070 -0
- pygpt_net/ui/widget/node_editor/item.py +493 -0
- pygpt_net/ui/widget/node_editor/node.py +1460 -0
- pygpt_net/ui/widget/node_editor/utils.py +17 -0
- pygpt_net/ui/widget/node_editor/view.py +364 -0
- pygpt_net/ui/widget/tabs/output.py +1 -1
- pygpt_net/ui/widget/textarea/input.py +2 -2
- pygpt_net/utils.py +114 -2
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/METADATA +80 -93
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/RECORD +88 -61
- pygpt_net/core/agents/custom.py +0 -150
- pygpt_net/ui/widget/builder/editor.py +0 -2001
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
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.09.24 23:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from agents import Agent as OpenAIAgent
|
|
17
|
+
from pygpt_net.item.preset import PresetItem
|
|
18
|
+
from pygpt_net.core.bridge import BridgeContext
|
|
19
|
+
from pygpt_net.provider.api.openai.agents.remote_tools import append_tools
|
|
20
|
+
from pygpt_net.provider.api.openai.agents.experts import get_experts
|
|
21
|
+
|
|
22
|
+
from .schema import AgentNode
|
|
23
|
+
from .router import build_router_instruction
|
|
24
|
+
from .utils import NodeRuntime
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class BuiltAgent:
|
|
29
|
+
instance: OpenAIAgent
|
|
30
|
+
name: str
|
|
31
|
+
instructions: str
|
|
32
|
+
multi_output: bool
|
|
33
|
+
allowed_routes: List[str]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AgentFactory:
|
|
37
|
+
"""
|
|
38
|
+
Builds OpenAIAgent instances from AgentNode + NodeRuntime.
|
|
39
|
+
"""
|
|
40
|
+
def __init__(self, window, logger) -> None:
|
|
41
|
+
self.window = window
|
|
42
|
+
self.logger = logger
|
|
43
|
+
|
|
44
|
+
def build(
|
|
45
|
+
self,
|
|
46
|
+
node: AgentNode,
|
|
47
|
+
node_runtime: NodeRuntime,
|
|
48
|
+
preset: Optional[PresetItem],
|
|
49
|
+
function_tools: List[dict],
|
|
50
|
+
force_router: bool,
|
|
51
|
+
friendly_map: Dict[str, str],
|
|
52
|
+
handoffs_enabled: bool = True,
|
|
53
|
+
context: Optional[BridgeContext] = None,
|
|
54
|
+
) -> BuiltAgent:
|
|
55
|
+
# Agent name
|
|
56
|
+
agent_name = (node.name or "").strip() or (preset.name if preset else f"Agent {node.id}")
|
|
57
|
+
|
|
58
|
+
# Multi-output routing instruction injection
|
|
59
|
+
multi_output = force_router or (len(node.outputs or []) > 1)
|
|
60
|
+
allowed_routes = list(node.outputs or [])
|
|
61
|
+
|
|
62
|
+
instr = node_runtime.instructions
|
|
63
|
+
if multi_output and allowed_routes:
|
|
64
|
+
router_instr = build_router_instruction(agent_name, node.id, allowed_routes, friendly_map)
|
|
65
|
+
instr = router_instr + "\n\n" + instr if instr else router_instr
|
|
66
|
+
|
|
67
|
+
# Base kwargs
|
|
68
|
+
kwargs: Dict[str, Any] = {
|
|
69
|
+
"name": agent_name,
|
|
70
|
+
"instructions": instr,
|
|
71
|
+
"model": self.window.core.agents.provider.get_openai_model(node_runtime.model),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Tools
|
|
75
|
+
tool_kwargs = append_tools(
|
|
76
|
+
tools=function_tools or [],
|
|
77
|
+
window=self.window,
|
|
78
|
+
model=node_runtime.model,
|
|
79
|
+
preset=preset,
|
|
80
|
+
allow_local_tools=node_runtime.allow_local_tools,
|
|
81
|
+
allow_remote_tools=node_runtime.allow_remote_tools,
|
|
82
|
+
)
|
|
83
|
+
kwargs.update(tool_kwargs)
|
|
84
|
+
|
|
85
|
+
# Experts/handoffs if any
|
|
86
|
+
if handoffs_enabled:
|
|
87
|
+
experts = get_experts(
|
|
88
|
+
window=self.window,
|
|
89
|
+
preset=preset,
|
|
90
|
+
verbose=False,
|
|
91
|
+
tools=function_tools or [],
|
|
92
|
+
)
|
|
93
|
+
if experts:
|
|
94
|
+
kwargs["handoffs"] = experts
|
|
95
|
+
|
|
96
|
+
# Build instance
|
|
97
|
+
instance = OpenAIAgent(**kwargs)
|
|
98
|
+
self.logger.debug(
|
|
99
|
+
f"Built agent {node.id} ({agent_name}), "
|
|
100
|
+
f"multi_output={multi_output}, routes={allowed_routes}, model={node_runtime.model.name}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return BuiltAgent(
|
|
104
|
+
instance=instance,
|
|
105
|
+
name=agent_name,
|
|
106
|
+
instructions=instr,
|
|
107
|
+
multi_output=multi_output,
|
|
108
|
+
allowed_routes=allowed_routes,
|
|
109
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
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.09.24 23:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Dict, List, Optional, Tuple
|
|
16
|
+
|
|
17
|
+
from .schema import FlowSchema, AgentNode, StartNode, EndNode, MemoryNode
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class FlowGraph:
|
|
22
|
+
schema: FlowSchema
|
|
23
|
+
adjacency: Dict[str, List[str]] = field(default_factory=dict) # node_id -> list of node_ids
|
|
24
|
+
agent_to_memory: Dict[str, Optional[str]] = field(default_factory=dict) # agent_id -> mem_id or None
|
|
25
|
+
start_targets: List[str] = field(default_factory=list) # immediate next nodes from start
|
|
26
|
+
end_nodes: List[str] = field(default_factory=list) # ids of end nodes
|
|
27
|
+
|
|
28
|
+
def get_next(self, node_id: str) -> List[str]:
|
|
29
|
+
return self.adjacency.get(node_id, [])
|
|
30
|
+
|
|
31
|
+
def first_connected_end(self, node_id: str) -> Optional[str]:
|
|
32
|
+
outs = self.get_next(node_id)
|
|
33
|
+
for out in outs:
|
|
34
|
+
if out in self.schema.ends:
|
|
35
|
+
return out
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
def pick_default_start_agent(self) -> Optional[str]:
|
|
39
|
+
"""Pick lowest numeric agent id if no start is present."""
|
|
40
|
+
if not self.schema.agents:
|
|
41
|
+
return None
|
|
42
|
+
# Prefer numeric suffix; fallback to lexicographic
|
|
43
|
+
def key_fn(aid: str) -> Tuple[int, str]:
|
|
44
|
+
m = re.search(r"(\d+)$", aid)
|
|
45
|
+
if m:
|
|
46
|
+
try:
|
|
47
|
+
return (int(m.group(1)), aid)
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
return (10**9, aid)
|
|
51
|
+
|
|
52
|
+
return sorted(self.schema.agents.keys(), key=key_fn)[0]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_graph(fs: FlowSchema) -> FlowGraph:
|
|
56
|
+
g = FlowGraph(schema=fs)
|
|
57
|
+
# adjacency from agents
|
|
58
|
+
for aid, anode in fs.agents.items():
|
|
59
|
+
g.adjacency[aid] = list(anode.outputs or [])
|
|
60
|
+
g.agent_to_memory[aid] = anode.memory_out
|
|
61
|
+
|
|
62
|
+
# adjacency from start nodes
|
|
63
|
+
g.end_nodes = list(fs.ends.keys())
|
|
64
|
+
g.start_targets = []
|
|
65
|
+
if fs.starts:
|
|
66
|
+
# By spec there can be multiple start nodes; we concatenate their outputs in order found
|
|
67
|
+
for sid, snode in fs.starts.items():
|
|
68
|
+
g.adjacency[sid] = list(snode.outputs or [])
|
|
69
|
+
g.start_targets.extend(snode.outputs or [])
|
|
70
|
+
|
|
71
|
+
return g
|
|
@@ -0,0 +1,10 @@
|
|
|
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.09.24 23:00:00 #
|
|
10
|
+
# ================================================== #
|
|
@@ -0,0 +1,100 @@
|
|
|
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.09.25 14:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, Dict, List
|
|
15
|
+
|
|
16
|
+
from llama_index.core.agent.workflow import ReActAgent, FunctionAgent
|
|
17
|
+
|
|
18
|
+
from ..schema import AgentNode
|
|
19
|
+
from ..router import build_router_instruction
|
|
20
|
+
from .utils import NodeRuntime, coerce_li_tools
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class BuiltAgentLI:
|
|
25
|
+
instance: Any
|
|
26
|
+
name: str
|
|
27
|
+
instructions: str
|
|
28
|
+
multi_output: bool
|
|
29
|
+
allowed_routes: List[str]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AgentFactoryLI:
|
|
33
|
+
"""
|
|
34
|
+
Build LlamaIndex ReActAgent/FunctionAgent from AgentNode + NodeRuntime and explicit LLM/tools.
|
|
35
|
+
"""
|
|
36
|
+
def __init__(self, window, logger) -> None:
|
|
37
|
+
self.window = window
|
|
38
|
+
self.logger = logger
|
|
39
|
+
|
|
40
|
+
def build(
|
|
41
|
+
self,
|
|
42
|
+
*,
|
|
43
|
+
node: AgentNode,
|
|
44
|
+
node_runtime: NodeRuntime,
|
|
45
|
+
llm: Any, # LLM instance (z appki lub resolve_llm)
|
|
46
|
+
tools: List[Any], # BaseTool list
|
|
47
|
+
friendly_map: Dict[str, Any],
|
|
48
|
+
force_router: bool = False,
|
|
49
|
+
chat_history: List[Any] = None,
|
|
50
|
+
max_iterations: int = 10,
|
|
51
|
+
) -> BuiltAgentLI:
|
|
52
|
+
agent_name = (node.name or "").strip() or f"Agent {node.id}"
|
|
53
|
+
|
|
54
|
+
multi_output = force_router or (len(node.outputs or []) > 1)
|
|
55
|
+
allowed_routes = list(node.outputs or [])
|
|
56
|
+
|
|
57
|
+
instr = node_runtime.instructions
|
|
58
|
+
if multi_output and allowed_routes:
|
|
59
|
+
router_instr = build_router_instruction(agent_name, node.id, allowed_routes, friendly_map)
|
|
60
|
+
instr = router_instr + "\n\n" + instr if instr else router_instr
|
|
61
|
+
|
|
62
|
+
node_tools = tools if (node_runtime.allow_local_tools or node_runtime.allow_remote_tools) else []
|
|
63
|
+
|
|
64
|
+
# Prefer FunctionAgent if the underlying LLM supports function-calling (recommended by LI).
|
|
65
|
+
# This yields more direct compliance with system_prompt for simple single-output tasks.
|
|
66
|
+
is_fc_model = False
|
|
67
|
+
try:
|
|
68
|
+
is_fc_model = bool(getattr(getattr(llm, "metadata", None), "is_function_calling_model", False))
|
|
69
|
+
except Exception:
|
|
70
|
+
is_fc_model = False
|
|
71
|
+
|
|
72
|
+
if multi_output:
|
|
73
|
+
agent_cls = FunctionAgent # routers: keep JSON compliance
|
|
74
|
+
else:
|
|
75
|
+
agent_cls = FunctionAgent if is_fc_model else ReActAgent
|
|
76
|
+
|
|
77
|
+
kwargs: Dict[str, Any] = {
|
|
78
|
+
"name": agent_name,
|
|
79
|
+
"system_prompt": instr,
|
|
80
|
+
"llm": llm,
|
|
81
|
+
"chat_history": chat_history or [],
|
|
82
|
+
"max_iterations": int(max_iterations),
|
|
83
|
+
# Provide a short description to reinforce the agent's purpose (if role present).
|
|
84
|
+
"description": (node_runtime.role or agent_name),
|
|
85
|
+
}
|
|
86
|
+
if node_tools:
|
|
87
|
+
kwargs["tools"] = coerce_li_tools(node_tools)
|
|
88
|
+
|
|
89
|
+
instance = agent_cls(**kwargs)
|
|
90
|
+
self.logger.debug(
|
|
91
|
+
f"[li] Built agent {node.id} ({agent_name}), multi_output={multi_output}, "
|
|
92
|
+
f"routes={allowed_routes}, agent_cls={agent_cls.__name__}"
|
|
93
|
+
)
|
|
94
|
+
return BuiltAgentLI(
|
|
95
|
+
instance=instance,
|
|
96
|
+
name=agent_name,
|
|
97
|
+
instructions=instr,
|
|
98
|
+
multi_output=multi_output,
|
|
99
|
+
allowed_routes=allowed_routes,
|
|
100
|
+
)
|
|
@@ -0,0 +1,106 @@
|
|
|
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.09.24 23:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DelayedRouterStreamerLI:
|
|
19
|
+
"""Collect raw JSON stream (no UI)."""
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self._raw = ""
|
|
22
|
+
def reset(self):
|
|
23
|
+
self._raw = ""
|
|
24
|
+
def handle_delta(self, delta: str):
|
|
25
|
+
self._raw += delta or ""
|
|
26
|
+
@property
|
|
27
|
+
def buffer(self) -> str:
|
|
28
|
+
return self._raw
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RealtimeRouterStreamerLI:
|
|
32
|
+
"""
|
|
33
|
+
Stream only JSON 'content' string incrementally.
|
|
34
|
+
handle_delta(delta) -> returns decoded content suffix to emit (may be '').
|
|
35
|
+
"""
|
|
36
|
+
CONTENT_PATTERN = re.compile(r'"content"\s*:\s*"')
|
|
37
|
+
|
|
38
|
+
def __init__(self):
|
|
39
|
+
self._raw = ""
|
|
40
|
+
self._content_started = False
|
|
41
|
+
self._content_closed = False
|
|
42
|
+
self._content_start_idx = -1
|
|
43
|
+
self._content_raw = ""
|
|
44
|
+
self._content_decoded = ""
|
|
45
|
+
|
|
46
|
+
def reset(self):
|
|
47
|
+
self._raw = ""
|
|
48
|
+
self._content_started = False
|
|
49
|
+
self._content_closed = False
|
|
50
|
+
self._content_start_idx = -1
|
|
51
|
+
self._content_raw = ""
|
|
52
|
+
self._content_decoded = ""
|
|
53
|
+
|
|
54
|
+
def handle_delta(self, delta: str) -> str:
|
|
55
|
+
if not delta:
|
|
56
|
+
return ""
|
|
57
|
+
self._raw += delta
|
|
58
|
+
if not self._content_started:
|
|
59
|
+
m = self.CONTENT_PATTERN.search(self._raw)
|
|
60
|
+
if m:
|
|
61
|
+
self._content_started = True
|
|
62
|
+
self._content_start_idx = m.end()
|
|
63
|
+
else:
|
|
64
|
+
return ""
|
|
65
|
+
if self._content_started and not self._content_closed:
|
|
66
|
+
return self._process()
|
|
67
|
+
return ""
|
|
68
|
+
|
|
69
|
+
def _process(self) -> str:
|
|
70
|
+
sub = self._raw[self._content_start_idx:]
|
|
71
|
+
close_idx = self._find_unescaped_quote(sub)
|
|
72
|
+
if close_idx is not None:
|
|
73
|
+
portion = sub[:close_idx]
|
|
74
|
+
self._content_closed = True
|
|
75
|
+
else:
|
|
76
|
+
portion = sub
|
|
77
|
+
new_raw_piece = portion[len(self._content_raw):]
|
|
78
|
+
if not new_raw_piece:
|
|
79
|
+
return ""
|
|
80
|
+
self._content_raw += new_raw_piece
|
|
81
|
+
try:
|
|
82
|
+
decoded_full = json.loads(f'"{self._content_raw}"')
|
|
83
|
+
new_suffix = decoded_full[len(self._content_decoded):]
|
|
84
|
+
self._content_decoded = decoded_full
|
|
85
|
+
return new_suffix
|
|
86
|
+
except Exception:
|
|
87
|
+
return ""
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _find_unescaped_quote(s: str) -> Optional[int]:
|
|
91
|
+
i = 0
|
|
92
|
+
while i < len(s):
|
|
93
|
+
if s[i] == '"':
|
|
94
|
+
j = i - 1
|
|
95
|
+
bs = 0
|
|
96
|
+
while j >= 0 and s[j] == '\\':
|
|
97
|
+
bs += 1
|
|
98
|
+
j -= 1
|
|
99
|
+
if bs % 2 == 0:
|
|
100
|
+
return i
|
|
101
|
+
i += 1
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def buffer(self) -> str:
|
|
106
|
+
return self._raw
|