pygpt-net 2.6.36__py3-none-any.whl → 2.6.38__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 +12 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/controller/chat/handler/anthropic_stream.py +164 -0
- pygpt_net/controller/chat/handler/google_stream.py +181 -0
- pygpt_net/controller/chat/handler/langchain_stream.py +24 -0
- pygpt_net/controller/chat/handler/llamaindex_stream.py +47 -0
- pygpt_net/controller/chat/handler/openai_stream.py +260 -0
- pygpt_net/controller/chat/handler/utils.py +210 -0
- pygpt_net/controller/chat/handler/worker.py +570 -0
- pygpt_net/controller/chat/handler/xai_stream.py +135 -0
- pygpt_net/controller/chat/stream.py +1 -1
- pygpt_net/controller/ctx/ctx.py +1 -1
- pygpt_net/controller/debug/debug.py +6 -6
- pygpt_net/controller/model/editor.py +3 -0
- pygpt_net/controller/model/importer.py +9 -2
- pygpt_net/controller/plugins/plugins.py +11 -3
- pygpt_net/controller/presets/presets.py +2 -2
- pygpt_net/core/bridge/context.py +35 -35
- pygpt_net/core/bridge/worker.py +40 -16
- pygpt_net/core/ctx/bag.py +7 -2
- pygpt_net/core/ctx/reply.py +17 -2
- pygpt_net/core/db/viewer.py +19 -34
- pygpt_net/core/render/plain/pid.py +12 -1
- pygpt_net/core/render/web/body.py +30 -39
- pygpt_net/core/tabs/tab.py +24 -1
- pygpt_net/data/config/config.json +10 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +105 -0
- pygpt_net/data/css/style.dark.css +2 -3
- pygpt_net/data/css/style.light.css +2 -3
- pygpt_net/data/locale/locale.de.ini +3 -1
- pygpt_net/data/locale/locale.en.ini +19 -1
- pygpt_net/data/locale/locale.es.ini +3 -1
- pygpt_net/data/locale/locale.fr.ini +3 -1
- pygpt_net/data/locale/locale.it.ini +3 -1
- pygpt_net/data/locale/locale.pl.ini +4 -2
- pygpt_net/data/locale/locale.uk.ini +3 -1
- pygpt_net/data/locale/locale.zh.ini +3 -1
- pygpt_net/item/assistant.py +51 -2
- pygpt_net/item/attachment.py +21 -20
- pygpt_net/item/calendar_note.py +19 -2
- pygpt_net/item/ctx.py +115 -2
- pygpt_net/item/index.py +9 -2
- pygpt_net/item/mode.py +9 -6
- pygpt_net/item/model.py +20 -3
- pygpt_net/item/notepad.py +14 -2
- pygpt_net/item/preset.py +42 -2
- pygpt_net/item/prompt.py +8 -2
- pygpt_net/plugin/cmd_files/plugin.py +2 -2
- pygpt_net/provider/api/__init__.py +5 -3
- pygpt_net/provider/api/anthropic/__init__.py +190 -29
- pygpt_net/provider/api/anthropic/audio.py +30 -0
- pygpt_net/provider/api/anthropic/chat.py +341 -0
- pygpt_net/provider/api/anthropic/image.py +25 -0
- pygpt_net/provider/api/anthropic/tools.py +266 -0
- pygpt_net/provider/api/anthropic/vision.py +142 -0
- pygpt_net/provider/api/google/chat.py +2 -2
- pygpt_net/provider/api/google/realtime/client.py +2 -2
- pygpt_net/provider/api/google/tools.py +58 -48
- pygpt_net/provider/api/google/vision.py +7 -1
- pygpt_net/provider/api/openai/chat.py +1 -0
- pygpt_net/provider/api/openai/vision.py +6 -0
- pygpt_net/provider/api/x_ai/__init__.py +247 -0
- pygpt_net/provider/api/x_ai/audio.py +32 -0
- pygpt_net/provider/api/x_ai/chat.py +968 -0
- pygpt_net/provider/api/x_ai/image.py +208 -0
- pygpt_net/provider/api/x_ai/remote.py +262 -0
- pygpt_net/provider/api/x_ai/tools.py +120 -0
- pygpt_net/provider/api/x_ai/vision.py +119 -0
- pygpt_net/provider/core/attachment/json_file.py +2 -2
- pygpt_net/provider/core/config/patch.py +28 -0
- pygpt_net/provider/llms/anthropic.py +4 -2
- pygpt_net/tools/text_editor/tool.py +4 -1
- pygpt_net/tools/text_editor/ui/dialogs.py +1 -1
- pygpt_net/ui/base/config_dialog.py +5 -11
- pygpt_net/ui/dialog/db.py +177 -59
- pygpt_net/ui/dialog/dictionary.py +57 -59
- pygpt_net/ui/dialog/editor.py +3 -2
- pygpt_net/ui/dialog/image.py +1 -1
- pygpt_net/ui/dialog/logger.py +3 -2
- pygpt_net/ui/dialog/models.py +16 -16
- pygpt_net/ui/dialog/plugins.py +63 -60
- pygpt_net/ui/layout/ctx/ctx_list.py +3 -4
- pygpt_net/ui/layout/toolbox/__init__.py +2 -2
- pygpt_net/ui/layout/toolbox/assistants.py +8 -9
- pygpt_net/ui/layout/toolbox/presets.py +2 -2
- pygpt_net/ui/main.py +9 -4
- pygpt_net/ui/widget/element/labels.py +20 -4
- pygpt_net/ui/widget/textarea/editor.py +0 -4
- pygpt_net/ui/widget/textarea/web.py +1 -1
- {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/METADATA +18 -6
- {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/RECORD +95 -76
- pygpt_net/controller/chat/handler/stream_worker.py +0 -1136
- {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/entry_points.txt +0 -0
|
@@ -6,63 +6,224 @@
|
|
|
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.
|
|
9
|
+
# Updated Date: 2025.09.05 01:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
-
from
|
|
12
|
+
from typing import Optional, Dict, Any
|
|
13
13
|
|
|
14
|
+
import anthropic
|
|
14
15
|
from pygpt_net.core.types import (
|
|
16
|
+
MODE_ASSISTANT,
|
|
17
|
+
MODE_AUDIO,
|
|
15
18
|
MODE_CHAT,
|
|
19
|
+
MODE_COMPLETION,
|
|
20
|
+
MODE_IMAGE,
|
|
21
|
+
MODE_RESEARCH,
|
|
16
22
|
)
|
|
23
|
+
from pygpt_net.core.bridge.context import BridgeContext
|
|
17
24
|
from pygpt_net.item.model import ModelItem
|
|
18
25
|
|
|
19
|
-
|
|
26
|
+
from .chat import Chat
|
|
27
|
+
from .tools import Tools
|
|
28
|
+
from .vision import Vision
|
|
29
|
+
from .audio import Audio
|
|
30
|
+
from .image import Image
|
|
31
|
+
|
|
20
32
|
|
|
33
|
+
class ApiAnthropic:
|
|
21
34
|
def __init__(self, window=None):
|
|
22
35
|
"""
|
|
23
|
-
Anthropic API wrapper
|
|
36
|
+
Anthropic Messages API SDK wrapper
|
|
24
37
|
|
|
25
38
|
:param window: Window instance
|
|
26
39
|
"""
|
|
27
40
|
self.window = window
|
|
28
|
-
self.
|
|
41
|
+
self.chat = Chat(window)
|
|
42
|
+
self.tools = Tools(window)
|
|
43
|
+
self.vision = Vision(window)
|
|
44
|
+
self.audio = Audio(window) # stub helpers (no official audio out/in in SDK as of now)
|
|
45
|
+
self.image = Image(window) # stub: no image generation in Anthropic
|
|
46
|
+
self.client: Optional[anthropic.Anthropic] = None
|
|
29
47
|
self.locked = False
|
|
48
|
+
self.last_client_args: Optional[Dict[str, Any]] = None
|
|
30
49
|
|
|
31
50
|
def get_client(
|
|
32
51
|
self,
|
|
33
52
|
mode: str = MODE_CHAT,
|
|
34
|
-
model: ModelItem = None
|
|
35
|
-
) -> Anthropic:
|
|
53
|
+
model: ModelItem = None,
|
|
54
|
+
) -> anthropic.Anthropic:
|
|
55
|
+
"""
|
|
56
|
+
Get or create Anthropic client
|
|
57
|
+
|
|
58
|
+
:param mode: Mode (chat, completion, image, etc.)
|
|
59
|
+
:param model: ModelItem
|
|
60
|
+
:return: anthropic.Anthropic instance
|
|
36
61
|
"""
|
|
37
|
-
|
|
62
|
+
# Build minimal args from app config
|
|
63
|
+
args = self.window.core.models.prepare_client_args(mode, model)
|
|
64
|
+
filtered = {}
|
|
65
|
+
if args.get("api_key"):
|
|
66
|
+
filtered["api_key"] = args["api_key"]
|
|
67
|
+
|
|
68
|
+
# Optionally honor custom base_url if present in config (advanced)
|
|
69
|
+
# base_url = self.window.core.config.get("api_native_anthropic.base_url", "").strip()
|
|
70
|
+
# if base_url:
|
|
71
|
+
# filtered["base_url"] = base_url
|
|
72
|
+
|
|
73
|
+
# Keep a fresh client per call; Anthropic client is lightweight
|
|
74
|
+
return anthropic.Anthropic(**filtered)
|
|
38
75
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
76
|
+
def call(
|
|
77
|
+
self,
|
|
78
|
+
context: BridgeContext,
|
|
79
|
+
extra: dict = None,
|
|
80
|
+
rt_signals=None, # unused for Anthropic
|
|
81
|
+
) -> bool:
|
|
42
82
|
"""
|
|
43
|
-
|
|
83
|
+
Make an API call to Anthropic Messages API
|
|
84
|
+
|
|
85
|
+
:param context: BridgeContext
|
|
86
|
+
:param extra: Extra parameters
|
|
87
|
+
:param rt_signals: Not used (no realtime Voice API)
|
|
88
|
+
:return: True if successful, False otherwise
|
|
89
|
+
"""
|
|
90
|
+
mode = context.mode
|
|
91
|
+
model = context.model
|
|
92
|
+
stream = context.stream
|
|
93
|
+
ctx = context.ctx
|
|
94
|
+
ai_name = ctx.output_name if ctx else "assistant"
|
|
95
|
+
|
|
96
|
+
# Anthropic: no Responses API; stream events are custom to Anthropic
|
|
97
|
+
if ctx:
|
|
98
|
+
ctx.use_responses_api = False
|
|
99
|
+
|
|
100
|
+
used_tokens = 0
|
|
101
|
+
response = None
|
|
102
|
+
|
|
103
|
+
if mode in (MODE_COMPLETION, MODE_CHAT, MODE_AUDIO, MODE_RESEARCH):
|
|
104
|
+
# MODE_AUDIO fallback: treat as normal chat (no native audio API)
|
|
105
|
+
response = self.chat.send(context=context, extra=extra)
|
|
106
|
+
used_tokens = self.chat.get_used_tokens()
|
|
107
|
+
if ctx:
|
|
108
|
+
self.vision.append_images(ctx)
|
|
109
|
+
|
|
110
|
+
elif mode == MODE_IMAGE:
|
|
111
|
+
# Anthropic does not support image generation – only vision (image input in chat)
|
|
112
|
+
return self.image.generate(context=context, extra=extra) # always returns False
|
|
113
|
+
|
|
114
|
+
elif mode == MODE_ASSISTANT:
|
|
115
|
+
return False # not implemented for Anthropic
|
|
116
|
+
|
|
117
|
+
if stream:
|
|
118
|
+
if ctx:
|
|
119
|
+
ctx.stream = response
|
|
120
|
+
ctx.set_output("", ai_name)
|
|
121
|
+
ctx.input_tokens = used_tokens
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
if response is None:
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
if isinstance(response, dict) and "error" in response:
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
if ctx:
|
|
131
|
+
ctx.ai_name = ai_name
|
|
132
|
+
self.chat.unpack_response(mode, response, ctx)
|
|
44
133
|
try:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
134
|
+
import json
|
|
135
|
+
for tc in getattr(ctx, "tool_calls", []) or []:
|
|
136
|
+
fn = tc.get("function") or {}
|
|
137
|
+
args = fn.get("arguments")
|
|
138
|
+
if isinstance(args, str):
|
|
139
|
+
try:
|
|
140
|
+
fn["arguments"] = json.loads(args)
|
|
141
|
+
except Exception:
|
|
142
|
+
fn["arguments"] = {}
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
def quick_call(
|
|
148
|
+
self,
|
|
149
|
+
context: BridgeContext,
|
|
150
|
+
extra: dict = None
|
|
151
|
+
) -> str:
|
|
152
|
+
"""
|
|
153
|
+
Make a quick API call to Anthropic and return the output text
|
|
154
|
+
|
|
155
|
+
:param context: BridgeContext
|
|
156
|
+
:param extra: Extra parameters
|
|
157
|
+
:return: Output text
|
|
158
|
+
"""
|
|
159
|
+
if context.request:
|
|
160
|
+
context.stream = False
|
|
161
|
+
context.mode = MODE_CHAT
|
|
162
|
+
self.locked = True
|
|
163
|
+
self.call(context, extra)
|
|
164
|
+
self.locked = False
|
|
165
|
+
return context.ctx.output
|
|
166
|
+
|
|
167
|
+
self.locked = True
|
|
168
|
+
try:
|
|
169
|
+
ctx = context.ctx
|
|
170
|
+
prompt = context.prompt
|
|
171
|
+
system_prompt = context.system_prompt
|
|
172
|
+
temperature = context.temperature
|
|
173
|
+
history = context.history
|
|
174
|
+
functions = context.external_functions
|
|
175
|
+
model = context.model or self.window.core.models.from_defaults()
|
|
176
|
+
|
|
177
|
+
client = self.get_client(MODE_CHAT, model)
|
|
178
|
+
tools = self.tools.get_all_tools(model, functions)
|
|
179
|
+
|
|
180
|
+
inputs = self.chat.build_input(
|
|
181
|
+
prompt=prompt,
|
|
182
|
+
system_prompt=system_prompt,
|
|
183
|
+
model=model,
|
|
184
|
+
history=history,
|
|
185
|
+
attachments=context.attachments,
|
|
186
|
+
multimodal_ctx=context.multimodal_ctx,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Anthropic params
|
|
190
|
+
params: Dict[str, Any] = {
|
|
191
|
+
"model": model.id,
|
|
192
|
+
"max_tokens": context.max_tokens if context.max_tokens else 1024,
|
|
193
|
+
"messages": inputs,
|
|
194
|
+
}
|
|
195
|
+
if system_prompt:
|
|
196
|
+
params["system"] = system_prompt
|
|
197
|
+
if temperature is not None:
|
|
198
|
+
params["temperature"] = temperature
|
|
199
|
+
if tools: # only include when non-empty list
|
|
200
|
+
params["tools"] = tools
|
|
201
|
+
|
|
202
|
+
resp = client.messages.create(**params)
|
|
203
|
+
|
|
204
|
+
if ctx:
|
|
205
|
+
calls = self.chat.extract_tool_calls(resp)
|
|
206
|
+
if calls:
|
|
207
|
+
ctx.tool_calls = calls
|
|
208
|
+
return self.chat.extract_text(resp)
|
|
209
|
+
except Exception as e:
|
|
210
|
+
self.window.core.debug.log(e)
|
|
211
|
+
return ""
|
|
212
|
+
finally:
|
|
213
|
+
self.locked = False
|
|
53
214
|
|
|
54
215
|
def stop(self):
|
|
55
|
-
"""On global event stop"""
|
|
216
|
+
"""On global event stop (no-op for Anthropic)"""
|
|
56
217
|
pass
|
|
57
218
|
|
|
58
219
|
def close(self):
|
|
59
|
-
"""Close
|
|
220
|
+
"""Close client (no persistent resources to close)"""
|
|
60
221
|
if self.locked:
|
|
61
222
|
return
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
223
|
+
self.client = None
|
|
224
|
+
|
|
225
|
+
def safe_close(self):
|
|
226
|
+
"""Close client (safe)"""
|
|
227
|
+
if self.locked:
|
|
228
|
+
return
|
|
229
|
+
self.client = None
|
|
@@ -0,0 +1,30 @@
|
|
|
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.05 01:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from typing import Optional
|
|
13
|
+
from pygpt_net.core.bridge.context import MultimodalContext
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Audio:
|
|
17
|
+
def __init__(self, window=None):
|
|
18
|
+
"""
|
|
19
|
+
Audio helpers for Anthropic (currently no official input/output audio in Python SDK).
|
|
20
|
+
|
|
21
|
+
:param window: Window instance
|
|
22
|
+
"""
|
|
23
|
+
self.window = window
|
|
24
|
+
|
|
25
|
+
def build_input_block(self, multimodal_ctx: Optional[MultimodalContext]) -> Optional[dict]:
|
|
26
|
+
"""
|
|
27
|
+
Future hook: build input_audio block if Anthropic exposes it publicly.
|
|
28
|
+
Currently returns None to avoid 400 errors.
|
|
29
|
+
"""
|
|
30
|
+
return None
|
|
@@ -0,0 +1,341 @@
|
|
|
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.05 01:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from typing import Optional, Dict, Any, List
|
|
13
|
+
|
|
14
|
+
from pygpt_net.core.types import MODE_CHAT, MODE_AUDIO
|
|
15
|
+
from pygpt_net.core.bridge.context import BridgeContext, MultimodalContext
|
|
16
|
+
from pygpt_net.item.attachment import AttachmentItem
|
|
17
|
+
from pygpt_net.item.ctx import CtxItem
|
|
18
|
+
from pygpt_net.item.model import ModelItem
|
|
19
|
+
|
|
20
|
+
import anthropic
|
|
21
|
+
from anthropic.types import Message
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Chat:
|
|
25
|
+
def __init__(self, window=None):
|
|
26
|
+
"""
|
|
27
|
+
Anthropic chat / multimodal API wrapper.
|
|
28
|
+
|
|
29
|
+
:param window: Window instance
|
|
30
|
+
"""
|
|
31
|
+
self.window = window
|
|
32
|
+
self.input_tokens = 0
|
|
33
|
+
|
|
34
|
+
def send(self, context: BridgeContext, extra: Optional[Dict[str, Any]] = None):
|
|
35
|
+
"""
|
|
36
|
+
Call Anthropic Messages API for chat / multimodal.
|
|
37
|
+
|
|
38
|
+
:param context: BridgeContext
|
|
39
|
+
:param extra: Extra parameters (not used)
|
|
40
|
+
:return: Message or generator of Message (if streaming)
|
|
41
|
+
"""
|
|
42
|
+
prompt = context.prompt
|
|
43
|
+
stream = context.stream
|
|
44
|
+
system_prompt = context.system_prompt
|
|
45
|
+
model = context.model
|
|
46
|
+
functions = context.external_functions
|
|
47
|
+
attachments = context.attachments
|
|
48
|
+
multimodal_ctx = context.multimodal_ctx
|
|
49
|
+
mode = context.mode
|
|
50
|
+
ctx = context.ctx or CtxItem()
|
|
51
|
+
api = self.window.core.api.anthropic
|
|
52
|
+
client: anthropic.Anthropic = api.get_client(context.mode, model)
|
|
53
|
+
|
|
54
|
+
msgs = self.build_input(
|
|
55
|
+
prompt=prompt,
|
|
56
|
+
system_prompt=system_prompt,
|
|
57
|
+
model=model,
|
|
58
|
+
history=context.history,
|
|
59
|
+
attachments=attachments,
|
|
60
|
+
multimodal_ctx=multimodal_ctx,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
self.reset_tokens()
|
|
64
|
+
count_msgs = self._build_count_messages(prompt, system_prompt, model, context.history)
|
|
65
|
+
self.input_tokens += self.window.core.tokens.from_messages(count_msgs, model.id)
|
|
66
|
+
|
|
67
|
+
tools = api.tools.get_all_tools(model, functions)
|
|
68
|
+
max_tokens = context.max_tokens if context.max_tokens else 1024
|
|
69
|
+
temperature = self.window.core.config.get('temperature')
|
|
70
|
+
top_p = self.window.core.config.get('top_p')
|
|
71
|
+
|
|
72
|
+
params: Dict[str, Any] = {
|
|
73
|
+
"model": model.id,
|
|
74
|
+
"messages": msgs,
|
|
75
|
+
"max_tokens": max_tokens,
|
|
76
|
+
}
|
|
77
|
+
# Add optional fields only if provided
|
|
78
|
+
if system_prompt:
|
|
79
|
+
params["system"] = system_prompt # SDK expects string or blocks, not None
|
|
80
|
+
if temperature is not None:
|
|
81
|
+
params["temperature"] = temperature # keep as-is; upstream config controls the type
|
|
82
|
+
if top_p is not None:
|
|
83
|
+
params["top_p"] = top_p
|
|
84
|
+
if tools: # only include when non-empty list
|
|
85
|
+
params["tools"] = tools # must be a valid list per API
|
|
86
|
+
|
|
87
|
+
if mode == MODE_AUDIO:
|
|
88
|
+
stream = False # no native TTS
|
|
89
|
+
|
|
90
|
+
if stream:
|
|
91
|
+
return client.messages.create(stream=True, **params)
|
|
92
|
+
else:
|
|
93
|
+
return client.messages.create(**params)
|
|
94
|
+
|
|
95
|
+
def unpack_response(self, mode: str, response: Message, ctx: CtxItem):
|
|
96
|
+
"""
|
|
97
|
+
Unpack non-streaming response and set context.
|
|
98
|
+
|
|
99
|
+
:param mode: Mode (chat/audio)
|
|
100
|
+
:param response: Message response from API
|
|
101
|
+
:param ctx: CtxItem to update
|
|
102
|
+
"""
|
|
103
|
+
ctx.output = self.extract_text(response)
|
|
104
|
+
|
|
105
|
+
calls = self.extract_tool_calls(response)
|
|
106
|
+
if calls:
|
|
107
|
+
ctx.tool_calls = calls
|
|
108
|
+
|
|
109
|
+
# Usage
|
|
110
|
+
try:
|
|
111
|
+
usage = getattr(response, "usage", None)
|
|
112
|
+
if usage:
|
|
113
|
+
p = getattr(usage, "input_tokens", 0) or 0
|
|
114
|
+
c = getattr(usage, "output_tokens", 0) or 0
|
|
115
|
+
ctx.set_tokens(p, c)
|
|
116
|
+
if not isinstance(ctx.extra, dict):
|
|
117
|
+
ctx.extra = {}
|
|
118
|
+
ctx.extra["usage"] = {"vendor": "anthropic", "input_tokens": p, "output_tokens": c}
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
# Collect web search citations (web_search_tool_result blocks)
|
|
123
|
+
try:
|
|
124
|
+
self._collect_web_search_urls(response, ctx)
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
def extract_text(self, response: Message) -> str:
|
|
129
|
+
"""
|
|
130
|
+
Extract text from response content blocks.
|
|
131
|
+
|
|
132
|
+
Join all text blocks into a single string.
|
|
133
|
+
|
|
134
|
+
:param response: Message response from API
|
|
135
|
+
:return: Extracted text
|
|
136
|
+
"""
|
|
137
|
+
out: List[str] = []
|
|
138
|
+
try:
|
|
139
|
+
for blk in getattr(response, "content", []) or []:
|
|
140
|
+
if getattr(blk, "type", "") == "text" and getattr(blk, "text", None):
|
|
141
|
+
out.append(str(blk.text))
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
return "".join(out).strip()
|
|
145
|
+
|
|
146
|
+
def extract_tool_calls(self, response: Message) -> List[dict]:
|
|
147
|
+
"""
|
|
148
|
+
Extract tool_use blocks as app tool calls.
|
|
149
|
+
|
|
150
|
+
Each tool call is a dict with keys: id (str), type="function", function (dict with name and arguments).
|
|
151
|
+
|
|
152
|
+
:param response: Message response from API
|
|
153
|
+
:return: List of tool calls
|
|
154
|
+
"""
|
|
155
|
+
out: List[dict] = []
|
|
156
|
+
|
|
157
|
+
def to_plain(obj):
|
|
158
|
+
try:
|
|
159
|
+
if hasattr(obj, "model_dump"):
|
|
160
|
+
return obj.model_dump()
|
|
161
|
+
if hasattr(obj, "to_dict"):
|
|
162
|
+
return obj.to_dict()
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
if isinstance(obj, dict):
|
|
166
|
+
return {k: to_plain(v) for k, v in obj.items()}
|
|
167
|
+
if isinstance(obj, (list, tuple)):
|
|
168
|
+
return [to_plain(x) for x in obj]
|
|
169
|
+
return obj
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
for blk in getattr(response, "content", []) or []:
|
|
173
|
+
if getattr(blk, "type", "") == "tool_use":
|
|
174
|
+
out.append({
|
|
175
|
+
"id": getattr(blk, "id", "") or "",
|
|
176
|
+
"type": "function",
|
|
177
|
+
"function": {
|
|
178
|
+
"name": getattr(blk, "name", "") or "",
|
|
179
|
+
"arguments": to_plain(getattr(blk, "input", {}) or {}),
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
return out
|
|
185
|
+
|
|
186
|
+
def _collect_web_search_urls(self, response: Message, ctx: CtxItem):
|
|
187
|
+
"""
|
|
188
|
+
Collect URLs from web_search_tool_result blocks and attach to ctx.urls.
|
|
189
|
+
|
|
190
|
+
:param response: Message response from API
|
|
191
|
+
:param ctx: CtxItem to update
|
|
192
|
+
"""
|
|
193
|
+
urls: List[str] = []
|
|
194
|
+
try:
|
|
195
|
+
for blk in getattr(response, "content", []) or []:
|
|
196
|
+
if getattr(blk, "type", "") == "web_search_tool_result":
|
|
197
|
+
content = getattr(blk, "content", None) or []
|
|
198
|
+
for item in content:
|
|
199
|
+
if isinstance(item, dict) and item.get("type") == "web_search_result":
|
|
200
|
+
u = (item.get("url") or "").strip()
|
|
201
|
+
if u.startswith("http://") or u.startswith("https://"):
|
|
202
|
+
urls.append(u)
|
|
203
|
+
except Exception:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
if urls:
|
|
207
|
+
if ctx.urls is None:
|
|
208
|
+
ctx.urls = []
|
|
209
|
+
for u in urls:
|
|
210
|
+
if u not in ctx.urls:
|
|
211
|
+
ctx.urls.append(u)
|
|
212
|
+
|
|
213
|
+
def build_input(
|
|
214
|
+
self,
|
|
215
|
+
prompt: str,
|
|
216
|
+
system_prompt: str,
|
|
217
|
+
model: ModelItem,
|
|
218
|
+
history: Optional[List[CtxItem]] = None,
|
|
219
|
+
attachments: Optional[Dict[str, AttachmentItem]] = None,
|
|
220
|
+
multimodal_ctx: Optional[MultimodalContext] = None) -> List[dict]:
|
|
221
|
+
"""
|
|
222
|
+
Build Anthropic messages list.
|
|
223
|
+
|
|
224
|
+
:param prompt: User prompt
|
|
225
|
+
:param system_prompt: System prompt
|
|
226
|
+
:param model: ModelItem
|
|
227
|
+
:param history: Optional list of CtxItem for context
|
|
228
|
+
:param attachments: Optional dict of attachments (id -> AttachmentItem)
|
|
229
|
+
:param multimodal_ctx: Optional MultimodalContext
|
|
230
|
+
:return: List of messages for API
|
|
231
|
+
"""
|
|
232
|
+
messages: List[dict] = []
|
|
233
|
+
|
|
234
|
+
if self.window.core.config.get('use_context'):
|
|
235
|
+
items = self.window.core.ctx.get_history(
|
|
236
|
+
history,
|
|
237
|
+
model.id,
|
|
238
|
+
MODE_CHAT,
|
|
239
|
+
self.window.core.tokens.from_user(prompt, system_prompt),
|
|
240
|
+
self._fit_ctx(model),
|
|
241
|
+
)
|
|
242
|
+
for item in items:
|
|
243
|
+
if item.final_input:
|
|
244
|
+
messages.append({"role": "user", "content": str(item.final_input)})
|
|
245
|
+
if item.final_output:
|
|
246
|
+
messages.append({"role": "assistant", "content": str(item.final_output)})
|
|
247
|
+
|
|
248
|
+
parts = self._build_user_parts(
|
|
249
|
+
content=str(prompt or ""),
|
|
250
|
+
attachments=attachments,
|
|
251
|
+
multimodal_ctx=multimodal_ctx,
|
|
252
|
+
)
|
|
253
|
+
messages.append({"role": "user", "content": parts if parts else [{"type": "text", "text": str(prompt or "")}]})
|
|
254
|
+
return messages
|
|
255
|
+
|
|
256
|
+
def _build_user_parts(
|
|
257
|
+
self,
|
|
258
|
+
content: str,
|
|
259
|
+
attachments: Optional[Dict[str, AttachmentItem]] = None,
|
|
260
|
+
multimodal_ctx: Optional[MultimodalContext] = None) -> List[dict]:
|
|
261
|
+
"""
|
|
262
|
+
Build user content blocks (image + text).
|
|
263
|
+
|
|
264
|
+
:param content: Text content
|
|
265
|
+
:param attachments: Optional dict of attachments (id -> AttachmentItem)
|
|
266
|
+
:param multimodal_ctx: Optional MultimodalContext
|
|
267
|
+
:return: List of content blocks
|
|
268
|
+
"""
|
|
269
|
+
parts: List[dict] = []
|
|
270
|
+
self.window.core.api.anthropic.vision.reset()
|
|
271
|
+
if attachments:
|
|
272
|
+
img_parts = self.window.core.api.anthropic.vision.build_blocks(content, attachments)
|
|
273
|
+
parts.extend(img_parts)
|
|
274
|
+
if content:
|
|
275
|
+
parts.append({"type": "text", "text": str(content)})
|
|
276
|
+
|
|
277
|
+
# No input_audio supported in SDK at the time of writing
|
|
278
|
+
if multimodal_ctx and getattr(multimodal_ctx, "is_audio_input", False):
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
return parts
|
|
282
|
+
|
|
283
|
+
def _fit_ctx(self, model: ModelItem) -> int:
|
|
284
|
+
"""
|
|
285
|
+
Fit context length to model limits.
|
|
286
|
+
|
|
287
|
+
:param model: ModelItem
|
|
288
|
+
:return: Max context tokens
|
|
289
|
+
"""
|
|
290
|
+
max_ctx_tokens = self.window.core.config.get('max_total_tokens')
|
|
291
|
+
if model and model.ctx and 0 < model.ctx < max_ctx_tokens:
|
|
292
|
+
max_ctx_tokens = model.ctx
|
|
293
|
+
return max_ctx_tokens
|
|
294
|
+
|
|
295
|
+
def _build_count_messages(
|
|
296
|
+
self,
|
|
297
|
+
prompt: str,
|
|
298
|
+
system_prompt: str,
|
|
299
|
+
model: ModelItem,
|
|
300
|
+
history: Optional[List[CtxItem]] = None) -> List[dict]:
|
|
301
|
+
"""
|
|
302
|
+
Build messages for token counting (without attachments).
|
|
303
|
+
|
|
304
|
+
:param prompt: User prompt
|
|
305
|
+
:param system_prompt: System prompt
|
|
306
|
+
:param model: ModelItem
|
|
307
|
+
:param history: Optional list of CtxItem for context
|
|
308
|
+
:return: List of messages for token counting
|
|
309
|
+
"""
|
|
310
|
+
messages = []
|
|
311
|
+
if system_prompt:
|
|
312
|
+
messages.append({"role": "system", "content": system_prompt})
|
|
313
|
+
if self.window.core.config.get('use_context'):
|
|
314
|
+
used_tokens = self.window.core.tokens.from_user(prompt, system_prompt)
|
|
315
|
+
items = self.window.core.ctx.get_history(
|
|
316
|
+
history,
|
|
317
|
+
model.id,
|
|
318
|
+
MODE_CHAT,
|
|
319
|
+
used_tokens,
|
|
320
|
+
self._fit_ctx(model),
|
|
321
|
+
)
|
|
322
|
+
for item in items:
|
|
323
|
+
if item.final_input:
|
|
324
|
+
messages.append({"role": "user", "content": str(item.final_input)})
|
|
325
|
+
if item.final_output:
|
|
326
|
+
messages.append({"role": "assistant", "content": str(item.final_output)})
|
|
327
|
+
|
|
328
|
+
messages.append({"role": "user", "content": str(prompt or "")})
|
|
329
|
+
return messages
|
|
330
|
+
|
|
331
|
+
def reset_tokens(self):
|
|
332
|
+
"""Reset input tokens counter."""
|
|
333
|
+
self.input_tokens = 0
|
|
334
|
+
|
|
335
|
+
def get_used_tokens(self) -> int:
|
|
336
|
+
"""
|
|
337
|
+
Get used input tokens count.
|
|
338
|
+
|
|
339
|
+
:return: used input tokens
|
|
340
|
+
"""
|
|
341
|
+
return self.input_tokens
|
|
@@ -0,0 +1,25 @@
|
|
|
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.05 01:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from typing import Optional, Dict
|
|
13
|
+
from pygpt_net.core.bridge.context import BridgeContext
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Image:
|
|
17
|
+
def __init__(self, window=None):
|
|
18
|
+
self.window = window
|
|
19
|
+
|
|
20
|
+
def generate(self, context: BridgeContext, extra: Optional[Dict] = None, sync: bool = True) -> bool:
|
|
21
|
+
"""
|
|
22
|
+
Anthropic does not support image generation; only vision input.
|
|
23
|
+
"""
|
|
24
|
+
# Inform handlers that nothing was generated
|
|
25
|
+
return False
|