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.
Files changed (96) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/chat/handler/anthropic_stream.py +164 -0
  4. pygpt_net/controller/chat/handler/google_stream.py +181 -0
  5. pygpt_net/controller/chat/handler/langchain_stream.py +24 -0
  6. pygpt_net/controller/chat/handler/llamaindex_stream.py +47 -0
  7. pygpt_net/controller/chat/handler/openai_stream.py +260 -0
  8. pygpt_net/controller/chat/handler/utils.py +210 -0
  9. pygpt_net/controller/chat/handler/worker.py +570 -0
  10. pygpt_net/controller/chat/handler/xai_stream.py +135 -0
  11. pygpt_net/controller/chat/stream.py +1 -1
  12. pygpt_net/controller/ctx/ctx.py +1 -1
  13. pygpt_net/controller/debug/debug.py +6 -6
  14. pygpt_net/controller/model/editor.py +3 -0
  15. pygpt_net/controller/model/importer.py +9 -2
  16. pygpt_net/controller/plugins/plugins.py +11 -3
  17. pygpt_net/controller/presets/presets.py +2 -2
  18. pygpt_net/core/bridge/context.py +35 -35
  19. pygpt_net/core/bridge/worker.py +40 -16
  20. pygpt_net/core/ctx/bag.py +7 -2
  21. pygpt_net/core/ctx/reply.py +17 -2
  22. pygpt_net/core/db/viewer.py +19 -34
  23. pygpt_net/core/render/plain/pid.py +12 -1
  24. pygpt_net/core/render/web/body.py +30 -39
  25. pygpt_net/core/tabs/tab.py +24 -1
  26. pygpt_net/data/config/config.json +10 -3
  27. pygpt_net/data/config/models.json +3 -3
  28. pygpt_net/data/config/settings.json +105 -0
  29. pygpt_net/data/css/style.dark.css +2 -3
  30. pygpt_net/data/css/style.light.css +2 -3
  31. pygpt_net/data/locale/locale.de.ini +3 -1
  32. pygpt_net/data/locale/locale.en.ini +19 -1
  33. pygpt_net/data/locale/locale.es.ini +3 -1
  34. pygpt_net/data/locale/locale.fr.ini +3 -1
  35. pygpt_net/data/locale/locale.it.ini +3 -1
  36. pygpt_net/data/locale/locale.pl.ini +4 -2
  37. pygpt_net/data/locale/locale.uk.ini +3 -1
  38. pygpt_net/data/locale/locale.zh.ini +3 -1
  39. pygpt_net/item/assistant.py +51 -2
  40. pygpt_net/item/attachment.py +21 -20
  41. pygpt_net/item/calendar_note.py +19 -2
  42. pygpt_net/item/ctx.py +115 -2
  43. pygpt_net/item/index.py +9 -2
  44. pygpt_net/item/mode.py +9 -6
  45. pygpt_net/item/model.py +20 -3
  46. pygpt_net/item/notepad.py +14 -2
  47. pygpt_net/item/preset.py +42 -2
  48. pygpt_net/item/prompt.py +8 -2
  49. pygpt_net/plugin/cmd_files/plugin.py +2 -2
  50. pygpt_net/provider/api/__init__.py +5 -3
  51. pygpt_net/provider/api/anthropic/__init__.py +190 -29
  52. pygpt_net/provider/api/anthropic/audio.py +30 -0
  53. pygpt_net/provider/api/anthropic/chat.py +341 -0
  54. pygpt_net/provider/api/anthropic/image.py +25 -0
  55. pygpt_net/provider/api/anthropic/tools.py +266 -0
  56. pygpt_net/provider/api/anthropic/vision.py +142 -0
  57. pygpt_net/provider/api/google/chat.py +2 -2
  58. pygpt_net/provider/api/google/realtime/client.py +2 -2
  59. pygpt_net/provider/api/google/tools.py +58 -48
  60. pygpt_net/provider/api/google/vision.py +7 -1
  61. pygpt_net/provider/api/openai/chat.py +1 -0
  62. pygpt_net/provider/api/openai/vision.py +6 -0
  63. pygpt_net/provider/api/x_ai/__init__.py +247 -0
  64. pygpt_net/provider/api/x_ai/audio.py +32 -0
  65. pygpt_net/provider/api/x_ai/chat.py +968 -0
  66. pygpt_net/provider/api/x_ai/image.py +208 -0
  67. pygpt_net/provider/api/x_ai/remote.py +262 -0
  68. pygpt_net/provider/api/x_ai/tools.py +120 -0
  69. pygpt_net/provider/api/x_ai/vision.py +119 -0
  70. pygpt_net/provider/core/attachment/json_file.py +2 -2
  71. pygpt_net/provider/core/config/patch.py +28 -0
  72. pygpt_net/provider/llms/anthropic.py +4 -2
  73. pygpt_net/tools/text_editor/tool.py +4 -1
  74. pygpt_net/tools/text_editor/ui/dialogs.py +1 -1
  75. pygpt_net/ui/base/config_dialog.py +5 -11
  76. pygpt_net/ui/dialog/db.py +177 -59
  77. pygpt_net/ui/dialog/dictionary.py +57 -59
  78. pygpt_net/ui/dialog/editor.py +3 -2
  79. pygpt_net/ui/dialog/image.py +1 -1
  80. pygpt_net/ui/dialog/logger.py +3 -2
  81. pygpt_net/ui/dialog/models.py +16 -16
  82. pygpt_net/ui/dialog/plugins.py +63 -60
  83. pygpt_net/ui/layout/ctx/ctx_list.py +3 -4
  84. pygpt_net/ui/layout/toolbox/__init__.py +2 -2
  85. pygpt_net/ui/layout/toolbox/assistants.py +8 -9
  86. pygpt_net/ui/layout/toolbox/presets.py +2 -2
  87. pygpt_net/ui/main.py +9 -4
  88. pygpt_net/ui/widget/element/labels.py +20 -4
  89. pygpt_net/ui/widget/textarea/editor.py +0 -4
  90. pygpt_net/ui/widget/textarea/web.py +1 -1
  91. {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/METADATA +18 -6
  92. {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/RECORD +95 -76
  93. pygpt_net/controller/chat/handler/stream_worker.py +0 -1136
  94. {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/LICENSE +0 -0
  95. {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/WHEEL +0 -0
  96. {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.08.28 09:00:00 #
9
+ # Updated Date: 2025.09.05 01:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from anthropic import Anthropic
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
- class ApiAnthropic:
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 core
36
+ Anthropic Messages API SDK wrapper
24
37
 
25
38
  :param window: Window instance
26
39
  """
27
40
  self.window = window
28
- self.client = None
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
- Return Anthropic client
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
- :param mode: Mode
40
- :param model: Model
41
- :return: Anthropic client
76
+ def call(
77
+ self,
78
+ context: BridgeContext,
79
+ extra: dict = None,
80
+ rt_signals=None, # unused for Anthropic
81
+ ) -> bool:
42
82
  """
43
- if self.client is not None:
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
- self.client.close() # close previous client if exists
46
- except Exception as e:
47
- self.window.core.debug.log(e)
48
- print("Error closing previous Anthropic client:", e)
49
- self.client = Anthropic(
50
- api_key=self.window.core.config.get('api_key_anthropic', "")
51
- )
52
- return self.client
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 Anthropic client"""
220
+ """Close client (no persistent resources to close)"""
60
221
  if self.locked:
61
222
  return
62
- if self.client is not None:
63
- try:
64
- pass
65
- # self.client.close()
66
- except Exception as e:
67
- self.window.core.debug.log(e)
68
- print("Error closing Anthropic client:", e)
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