pygpt-net 2.5.14__py3-none-any.whl → 2.5.16__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 (43) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/chat/input.py +9 -2
  4. pygpt_net/controller/chat/stream.py +65 -17
  5. pygpt_net/controller/lang/mapping.py +4 -2
  6. pygpt_net/controller/model/__init__.py +3 -1
  7. pygpt_net/controller/model/importer.py +337 -0
  8. pygpt_net/controller/settings/editor.py +3 -0
  9. pygpt_net/core/bridge/worker.py +4 -2
  10. pygpt_net/core/command/__init__.py +33 -2
  11. pygpt_net/core/models/__init__.py +6 -3
  12. pygpt_net/core/models/ollama.py +7 -2
  13. pygpt_net/data/config/config.json +9 -4
  14. pygpt_net/data/config/models.json +22 -22
  15. pygpt_net/data/locale/locale.de.ini +18 -0
  16. pygpt_net/data/locale/locale.en.ini +19 -2
  17. pygpt_net/data/locale/locale.es.ini +18 -0
  18. pygpt_net/data/locale/locale.fr.ini +18 -0
  19. pygpt_net/data/locale/locale.it.ini +18 -0
  20. pygpt_net/data/locale/locale.pl.ini +19 -1
  21. pygpt_net/data/locale/locale.uk.ini +18 -0
  22. pygpt_net/data/locale/locale.zh.ini +17 -0
  23. pygpt_net/item/ctx.py +2 -1
  24. pygpt_net/item/model.py +5 -1
  25. pygpt_net/plugin/cmd_files/__init__.py +2 -2
  26. pygpt_net/plugin/cmd_files/worker.py +2 -2
  27. pygpt_net/provider/core/model/json_file.py +3 -0
  28. pygpt_net/provider/core/model/patch.py +24 -1
  29. pygpt_net/provider/gpt/__init__.py +54 -21
  30. pygpt_net/provider/gpt/responses.py +279 -0
  31. pygpt_net/provider/gpt/vision.py +40 -16
  32. pygpt_net/provider/llms/ollama.py +7 -2
  33. pygpt_net/provider/llms/ollama_custom.py +693 -0
  34. pygpt_net/ui/dialog/models_importer.py +82 -0
  35. pygpt_net/ui/dialogs.py +3 -1
  36. pygpt_net/ui/menu/config.py +18 -7
  37. pygpt_net/ui/widget/dialog/model_importer.py +55 -0
  38. pygpt_net/ui/widget/lists/model_importer.py +151 -0
  39. {pygpt_net-2.5.14.dist-info → pygpt_net-2.5.16.dist-info}/METADATA +75 -9
  40. {pygpt_net-2.5.14.dist-info → pygpt_net-2.5.16.dist-info}/RECORD +43 -37
  41. {pygpt_net-2.5.14.dist-info → pygpt_net-2.5.16.dist-info}/LICENSE +0 -0
  42. {pygpt_net-2.5.14.dist-info → pygpt_net-2.5.16.dist-info}/WHEEL +0 -0
  43. {pygpt_net-2.5.14.dist-info → pygpt_net-2.5.16.dist-info}/entry_points.txt +0 -0
@@ -6,7 +6,7 @@
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.03.02 19:00:00 #
9
+ # Updated Date: 2025.06.25 02:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from httpx_socks import SyncProxyTransport
@@ -29,6 +29,7 @@ from .assistants import Assistants
29
29
  from .chat import Chat
30
30
  from .completion import Completion
31
31
  from .image import Image
32
+ from .responses import Responses
32
33
  from .store import Store
33
34
  from .summarizer import Summarizer
34
35
  from .vision import Vision
@@ -47,6 +48,7 @@ class Gpt:
47
48
  self.chat = Chat(window)
48
49
  self.completion = Completion(window)
49
50
  self.image = Image(window)
51
+ self.responses = Responses(window)
50
52
  self.store = Store(window)
51
53
  self.summarizer = Summarizer(window)
52
54
  self.vision = Vision(window)
@@ -108,6 +110,12 @@ class Gpt:
108
110
  ai_name = ctx.output_name
109
111
  thread_id = ctx.thread # from ctx
110
112
 
113
+ # --- Responses API ---- /beta/
114
+ use_responses_api = False
115
+ if mode == MODE_CHAT:
116
+ use_responses_api = True # use responses API for chat, audio, research modes
117
+ ctx.use_responses_api = use_responses_api # set in context
118
+
111
119
  # get model id
112
120
  model_id = None
113
121
  if model is not None:
@@ -128,20 +136,30 @@ class Gpt:
128
136
  )
129
137
  used_tokens = self.completion.get_used_tokens()
130
138
 
131
- # chat (OpenAI) | research (Perplexity)
139
+ # chat, audio (OpenAI) | research (Perplexity)
132
140
  elif mode in [
133
141
  MODE_CHAT,
134
142
  MODE_AUDIO,
135
143
  MODE_RESEARCH
136
144
  ]:
137
- response = self.chat.send(
138
- context=context,
139
- extra=extra,
140
- )
141
- if hasattr(response, "citations"):
142
- if response.citations:
143
- ctx.urls = response.citations
144
- used_tokens = self.chat.get_used_tokens()
145
+ # responses API
146
+ if use_responses_api:
147
+ response = self.responses.send(
148
+ context=context,
149
+ extra=extra,
150
+ )
151
+ used_tokens = self.responses.get_used_tokens()
152
+ else:
153
+ # chat completion API
154
+ response = self.chat.send(
155
+ context=context,
156
+ extra=extra,
157
+ )
158
+ if hasattr(response, "citations"):
159
+ if response.citations:
160
+ ctx.urls = response.citations
161
+ used_tokens = self.chat.get_used_tokens()
162
+
145
163
  self.vision.append_images(ctx) # append images to ctx if provided
146
164
 
147
165
  # image
@@ -184,7 +202,7 @@ class Gpt:
184
202
 
185
203
  # if stream
186
204
  if stream:
187
- ctx.stream = response
205
+ ctx.stream = response # generator
188
206
  ctx.set_output("", ai_name) # set empty output
189
207
  ctx.input_tokens = used_tokens # get from input tokens calculation
190
208
  return True
@@ -206,13 +224,21 @@ class Gpt:
206
224
  MODE_VISION,
207
225
  MODE_RESEARCH
208
226
  ]:
209
- if response.choices[0]:
210
- if response.choices[0].message.content:
211
- output = response.choices[0].message.content.strip()
212
- elif response.choices[0].message.tool_calls:
213
- ctx.tool_calls = self.window.core.command.unpack_tool_calls(
214
- response.choices[0].message.tool_calls,
227
+ if use_responses_api:
228
+ if response.output_text:
229
+ output = response.output_text.strip()
230
+ if response.output:
231
+ ctx.tool_calls = self.window.core.command.unpack_tool_calls_responses(
232
+ response.output,
215
233
  )
234
+ else:
235
+ if response.choices[0]:
236
+ if response.choices[0].message.content:
237
+ output = response.choices[0].message.content.strip()
238
+ elif response.choices[0].message.tool_calls:
239
+ ctx.tool_calls = self.window.core.command.unpack_tool_calls(
240
+ response.choices[0].message.tool_calls,
241
+ )
216
242
  # audio
217
243
  elif mode in [MODE_AUDIO]:
218
244
  if response.choices[0]:
@@ -234,10 +260,17 @@ class Gpt:
234
260
  )
235
261
 
236
262
  ctx.set_output(output, ai_name)
237
- ctx.set_tokens(
238
- response.usage.prompt_tokens,
239
- response.usage.completion_tokens,
240
- )
263
+
264
+ if not use_responses_api:
265
+ ctx.set_tokens(
266
+ response.usage.prompt_tokens,
267
+ response.usage.completion_tokens,
268
+ )
269
+ else:
270
+ ctx.set_tokens(
271
+ response.usage.input_tokens,
272
+ response.usage.output_tokens,
273
+ )
241
274
  return True
242
275
 
243
276
  def quick_call(self, context: BridgeContext, extra: dict = None) -> str:
@@ -0,0 +1,279 @@
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.06.25 02:00:00 #
10
+ # ================================================== #
11
+
12
+ import json
13
+ import time
14
+ from typing import Optional, Dict, Any, List
15
+
16
+ from pygpt_net.core.types import (
17
+ MODE_CHAT,
18
+ MODE_VISION,
19
+ MODE_AUDIO,
20
+ MODE_RESEARCH,
21
+ )
22
+ from pygpt_net.core.bridge.context import BridgeContext, MultimodalContext
23
+ from pygpt_net.item.ctx import CtxItem
24
+ from pygpt_net.item.model import ModelItem
25
+
26
+ from .utils import sanitize_name
27
+ from pygpt_net.item.attachment import AttachmentItem
28
+
29
+
30
+ class Responses:
31
+ def __init__(self, window=None):
32
+ """
33
+ Responses API wrapper
34
+
35
+ :param window: Window instance
36
+ """
37
+ self.window = window
38
+ self.input_tokens = 0
39
+ self.audio_prev_id = None
40
+ self.audio_prev_expires_ts = None
41
+
42
+ def send(
43
+ self,
44
+ context: BridgeContext,
45
+ extra: Optional[Dict[str, Any]] = None
46
+ ):
47
+ """
48
+ Call OpenAI API for chat
49
+
50
+ :param context: Bridge context
51
+ :param extra: Extra arguments
52
+ :return: response or stream chunks
53
+ """
54
+ prompt = context.prompt
55
+ stream = context.stream
56
+ max_tokens = int(context.max_tokens or 0)
57
+ system_prompt = context.system_prompt
58
+ mode = context.mode
59
+ model = context.model
60
+ functions = context.external_functions
61
+ attachments = context.attachments
62
+ multimodal_ctx = context.multimodal_ctx
63
+
64
+ ctx = context.ctx
65
+ if ctx is None:
66
+ ctx = CtxItem() # create empty context
67
+ user_name = ctx.input_name # from ctx
68
+ ai_name = ctx.output_name # from ctx
69
+
70
+ client = self.window.core.gpt.get_client(mode)
71
+
72
+ # build chat messages
73
+ messages = self.build(
74
+ prompt=prompt,
75
+ system_prompt=system_prompt,
76
+ model=model,
77
+ history=context.history,
78
+ attachments=attachments,
79
+ ai_name=ai_name,
80
+ user_name=user_name,
81
+ multimodal_ctx=multimodal_ctx,
82
+ )
83
+ msg_tokens = self.window.core.tokens.from_messages(
84
+ messages,
85
+ model.id,
86
+ )
87
+ # check if max tokens not exceeded
88
+ if max_tokens > 0:
89
+ if msg_tokens + int(max_tokens) > model.ctx:
90
+ max_tokens = model.ctx - msg_tokens - 1
91
+ if max_tokens < 0:
92
+ max_tokens = 0
93
+
94
+ # extra API kwargs
95
+ response_kwargs = {}
96
+
97
+ # tools / functions
98
+ tools = []
99
+ if functions is not None and isinstance(functions, list):
100
+ for function in functions:
101
+ if str(function['name']).strip() == '' or function['name'] is None:
102
+ continue
103
+ params = {}
104
+ if function['params'] is not None and function['params'] != "":
105
+ params = json.loads(function['params']) # unpack JSON from string
106
+ tools.append({
107
+ "type": "function",
108
+ "name": function['name'],
109
+ "parameters": params,
110
+ "description": function['desc'],
111
+ })
112
+
113
+ # extra arguments, o3 only
114
+ if model.extra and "reasoning_effort" in model.extra:
115
+ response_kwargs['reasoning'] = {}
116
+ response_kwargs['reasoning']['effort'] = model.extra["reasoning_effort"]
117
+
118
+ # extend tools with external tools
119
+ if not model.id.startswith("o1") and not model.id.startswith("o3"):
120
+ tools.append({"type": "web_search_preview"})
121
+
122
+ # tool calls are not supported for o1-mini and o1-preview
123
+ if (model.id is not None
124
+ and model.id not in ["o1-mini", "o1-preview"]):
125
+ if len(tools) > 0:
126
+ response_kwargs['tools'] = tools
127
+
128
+ # audio mode
129
+ if mode in [MODE_AUDIO]:
130
+ stream = False
131
+ voice_id = "alloy"
132
+ tmp_voice = self.window.core.plugins.get_option("audio_output", "openai_voice")
133
+ if tmp_voice:
134
+ voice_id = tmp_voice
135
+ response_kwargs["modalities"] = ["text", "audio"]
136
+ response_kwargs["audio"] = {
137
+ "voice": voice_id,
138
+ "format": "wav"
139
+ }
140
+
141
+ response = client.responses.create(
142
+ input=messages,
143
+ model=model.id,
144
+ stream=stream,
145
+ **response_kwargs,
146
+ )
147
+ return response
148
+
149
+ def build(
150
+ self,
151
+ prompt: str,
152
+ system_prompt: str,
153
+ model: ModelItem,
154
+ history: Optional[List[CtxItem]] = None,
155
+ attachments: Optional[Dict[str, AttachmentItem]] = None,
156
+ ai_name: Optional[str] = None,
157
+ user_name: Optional[str] = None,
158
+ multimodal_ctx: Optional[MultimodalContext] = None,
159
+ ) -> list:
160
+ """
161
+ Build list of chat messages
162
+
163
+ :param prompt: user prompt
164
+ :param system_prompt: system prompt
165
+ :param history: history
166
+ :param model: model item
167
+ :param attachments: attachments
168
+ :param ai_name: AI name
169
+ :param user_name: username
170
+ :param multimodal_ctx: Multimodal context
171
+ :return: messages list
172
+ """
173
+ messages = []
174
+
175
+ # tokens config
176
+ mode = MODE_CHAT
177
+ allowed_system = True
178
+ if (model.id is not None
179
+ and model.id in ["o1-mini", "o1-preview"]):
180
+ allowed_system = False
181
+
182
+ used_tokens = self.window.core.tokens.from_user(
183
+ prompt,
184
+ system_prompt,
185
+ ) # threshold and extra included
186
+ max_ctx_tokens = self.window.core.config.get('max_total_tokens') # max context window
187
+
188
+ # fit to max model tokens
189
+ if max_ctx_tokens > model.ctx:
190
+ max_ctx_tokens = model.ctx
191
+
192
+ # input tokens: reset
193
+ self.reset_tokens()
194
+
195
+ # append system prompt
196
+ if allowed_system:
197
+ if system_prompt is not None and system_prompt != "":
198
+ messages.append({"role": "developer", "content": system_prompt})
199
+
200
+ # append messages from context (memory)
201
+ if self.window.core.config.get('use_context'):
202
+ items = self.window.core.ctx.get_history(
203
+ history,
204
+ model.id,
205
+ mode,
206
+ used_tokens,
207
+ max_ctx_tokens,
208
+ )
209
+ for item in items:
210
+ # input
211
+ if item.final_input is not None and item.final_input != "":
212
+ messages.append({
213
+ "role": "user",
214
+ "content": item.final_input,
215
+ })
216
+
217
+ # output
218
+ if item.final_output is not None and item.final_output != "":
219
+ msg = {
220
+ "role": "assistant",
221
+ "content": item.final_output,
222
+ }
223
+ # append previous audio ID
224
+ if MODE_AUDIO in model.mode:
225
+ if item.audio_id:
226
+ # at first check expires_at - expired audio throws error in API
227
+ current_timestamp = time.time()
228
+ audio_timestamp = int(item.audio_expires_ts) if item.audio_expires_ts else 0
229
+ if audio_timestamp and audio_timestamp > current_timestamp:
230
+ msg["audio"] = {
231
+ "id": item.audio_id
232
+ }
233
+ elif self.audio_prev_id:
234
+ current_timestamp = time.time()
235
+ audio_timestamp = int(self.audio_prev_expires_ts) if self.audio_prev_expires_ts else 0
236
+ if audio_timestamp and audio_timestamp > current_timestamp:
237
+ msg["audio"] = {
238
+ "id": self.audio_prev_id
239
+ }
240
+ messages.append(msg)
241
+
242
+ # use vision and audio if available in current model
243
+ content = str(prompt)
244
+ if MODE_VISION in model.mode:
245
+ content = self.window.core.gpt.vision.build_content(
246
+ content=content,
247
+ attachments=attachments,
248
+ responses_api=True,
249
+ )
250
+ if MODE_AUDIO in model.mode:
251
+ content = self.window.core.gpt.audio.build_content(
252
+ content=content,
253
+ multimodal_ctx=multimodal_ctx,
254
+ )
255
+
256
+ # append current prompt
257
+ messages.append({
258
+ "role": "user",
259
+ "content": content,
260
+ })
261
+
262
+ # input tokens: update
263
+ self.input_tokens += self.window.core.tokens.from_messages(
264
+ messages,
265
+ model.id,
266
+ )
267
+ return messages
268
+
269
+ def reset_tokens(self):
270
+ """Reset input tokens counter"""
271
+ self.input_tokens = 0
272
+
273
+ def get_used_tokens(self) -> int:
274
+ """
275
+ Get input tokens counter
276
+
277
+ :return: input tokens
278
+ """
279
+ return self.input_tokens
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2024.12.14 22:00:00 #
9
+ # Updated Date: 2025.06.25 02:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import base64
@@ -168,18 +168,26 @@ class Vision:
168
168
  self,
169
169
  content: Union[str, list],
170
170
  attachments: Optional[Dict[str, AttachmentItem]] = None,
171
+ responses_api: Optional[bool] = False,
171
172
  ) -> List[dict]:
172
173
  """
173
174
  Build vision content
174
175
 
175
176
  :param content: content (str or list)
176
177
  :param attachments: attachments (dict, optional)
178
+ :param responses_api: if True, use responses API format
177
179
  :return: List of contents
178
180
  """
181
+ type_text = "text"
182
+ type_image = "image_url"
183
+ if responses_api:
184
+ type_text = "input_text"
185
+ type_image = "input_image"
186
+
179
187
  if not isinstance(content, list):
180
188
  content = [
181
189
  {
182
- "type": "text",
190
+ "type": type_text,
183
191
  "text": str(content)
184
192
  }
185
193
  ]
@@ -193,14 +201,22 @@ class Vision:
193
201
  urls = self.extract_urls(prompt)
194
202
  if len(urls) > 0:
195
203
  for url in urls:
196
- content.append(
197
- {
198
- "type": "image_url",
199
- "image_url": {
200
- "url": url,
204
+ if not responses_api:
205
+ content.append(
206
+ {
207
+ "type": type_image,
208
+ "image_url": {
209
+ "url": url,
210
+ }
211
+ }
212
+ )
213
+ else:
214
+ content.append(
215
+ {
216
+ "type": type_image,
217
+ "image_url": url,
201
218
  }
202
- }
203
- )
219
+ )
204
220
  self.urls.append(url)
205
221
 
206
222
  # local images (attachments)
@@ -211,14 +227,22 @@ class Vision:
211
227
  # check if it's an image
212
228
  if self.is_image(attachment.path):
213
229
  base64_image = self.encode_image(attachment.path)
214
- content.append(
215
- {
216
- "type": "image_url",
217
- "image_url": {
218
- "url": f"data:image/jpeg;base64,{base64_image}",
230
+ if not responses_api:
231
+ content.append(
232
+ {
233
+ "type": type_image,
234
+ "image_url": {
235
+ "url": f"data:image/jpeg;base64,{base64_image}",
236
+ }
219
237
  }
220
- }
221
- )
238
+ )
239
+ else:
240
+ content.append(
241
+ {
242
+ "type": type_image,
243
+ "image_url": f"data:image/jpeg;base64,{base64_image}",
244
+ }
245
+ )
222
246
  self.attachments[id] = attachment.path
223
247
  attachment.consumed = True
224
248
 
@@ -6,7 +6,7 @@
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.01.31 19:00:00 #
9
+ # Updated Date: 2025.06.24 16:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -14,7 +14,8 @@ from typing import Optional, List, Dict
14
14
 
15
15
  from langchain_community.chat_models import ChatOllama
16
16
 
17
- from llama_index.llms.ollama import Ollama
17
+ from .ollama_custom import Ollama
18
+
18
19
  from llama_index.core.llms.llm import BaseLLM as LlamaBaseLLM
19
20
  from llama_index.core.base.embeddings.base import BaseEmbedding
20
21
  from llama_index.embeddings.ollama import OllamaEmbedding
@@ -85,6 +86,8 @@ class OllamaLLM(BaseLLM):
85
86
  args = self.parse_args(model.llama_index)
86
87
  if "request_timeout" not in args:
87
88
  args["request_timeout"] = 120
89
+ if 'OLLAMA_API_BASE' in os.environ:
90
+ args["base_url"] = os.environ['OLLAMA_API_BASE']
88
91
  return Ollama(**args)
89
92
 
90
93
  def get_embeddings_model(
@@ -104,6 +107,8 @@ class OllamaLLM(BaseLLM):
104
107
  args = self.parse_args({
105
108
  "args": config,
106
109
  })
110
+ if 'OLLAMA_API_BASE' in os.environ:
111
+ args["base_url"] = os.environ['OLLAMA_API_BASE']
107
112
  return OllamaEmbedding(**args)
108
113
 
109
114
  def init_embeddings(