iatoolkit 1.9.0__py3-none-any.whl → 1.15.3__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 (38) hide show
  1. iatoolkit/__init__.py +1 -1
  2. iatoolkit/common/routes.py +1 -1
  3. iatoolkit/common/util.py +8 -123
  4. iatoolkit/core.py +1 -0
  5. iatoolkit/infra/connectors/file_connector.py +10 -2
  6. iatoolkit/infra/connectors/google_drive_connector.py +3 -0
  7. iatoolkit/infra/connectors/local_file_connector.py +3 -0
  8. iatoolkit/infra/connectors/s3_connector.py +24 -1
  9. iatoolkit/infra/llm_providers/deepseek_adapter.py +17 -1
  10. iatoolkit/infra/llm_providers/gemini_adapter.py +117 -18
  11. iatoolkit/infra/llm_providers/openai_adapter.py +175 -18
  12. iatoolkit/infra/llm_response.py +13 -0
  13. iatoolkit/locales/en.yaml +47 -2
  14. iatoolkit/locales/es.yaml +45 -1
  15. iatoolkit/repositories/llm_query_repo.py +44 -33
  16. iatoolkit/services/company_context_service.py +294 -133
  17. iatoolkit/services/dispatcher_service.py +1 -1
  18. iatoolkit/services/knowledge_base_service.py +26 -4
  19. iatoolkit/services/llm_client_service.py +58 -2
  20. iatoolkit/services/prompt_service.py +236 -330
  21. iatoolkit/services/query_service.py +37 -18
  22. iatoolkit/services/storage_service.py +92 -0
  23. iatoolkit/static/js/chat_filepond.js +188 -63
  24. iatoolkit/static/js/chat_main.js +105 -52
  25. iatoolkit/static/styles/chat_iatoolkit.css +96 -0
  26. iatoolkit/system_prompts/query_main.prompt +24 -41
  27. iatoolkit/templates/chat.html +15 -6
  28. iatoolkit/views/base_login_view.py +1 -1
  29. iatoolkit/views/categories_api_view.py +43 -3
  30. iatoolkit/views/chat_view.py +1 -1
  31. iatoolkit/views/login_view.py +1 -1
  32. iatoolkit/views/prompt_api_view.py +1 -1
  33. {iatoolkit-1.9.0.dist-info → iatoolkit-1.15.3.dist-info}/METADATA +1 -1
  34. {iatoolkit-1.9.0.dist-info → iatoolkit-1.15.3.dist-info}/RECORD +38 -37
  35. {iatoolkit-1.9.0.dist-info → iatoolkit-1.15.3.dist-info}/WHEEL +0 -0
  36. {iatoolkit-1.9.0.dist-info → iatoolkit-1.15.3.dist-info}/licenses/LICENSE +0 -0
  37. {iatoolkit-1.9.0.dist-info → iatoolkit-1.15.3.dist-info}/licenses/LICENSE_COMMUNITY.md +0 -0
  38. {iatoolkit-1.9.0.dist-info → iatoolkit-1.15.3.dist-info}/top_level.txt +0 -0
@@ -7,8 +7,10 @@ import logging
7
7
  from typing import Dict, List, Optional
8
8
  from iatoolkit.infra.llm_response import LLMResponse, ToolCall, Usage
9
9
  from iatoolkit.common.exceptions import IAToolkitException
10
- import html
11
10
  from typing import List
11
+ import mimetypes
12
+ import re
13
+
12
14
 
13
15
  class OpenAIAdapter:
14
16
  """Adaptador para la API de OpenAI"""
@@ -24,9 +26,14 @@ class OpenAIAdapter:
24
26
  tools: Optional[List[Dict]] = None,
25
27
  text: Optional[Dict] = None,
26
28
  reasoning: Optional[Dict] = None,
27
- tool_choice: str = "auto") -> LLMResponse:
29
+ tool_choice: str = "auto",
30
+ images: Optional[List[Dict]] = None) -> LLMResponse:
28
31
  """Llamada a la API de OpenAI y mapeo a estructura común"""
29
32
  try:
33
+ # Handle multimodal input if images are present
34
+ if images:
35
+ input = self._prepare_multimodal_input(input, images)
36
+
30
37
  # Preparar parámetros para OpenAI
31
38
  params = {
32
39
  'model': model,
@@ -56,28 +63,177 @@ class OpenAIAdapter:
56
63
 
57
64
  raise IAToolkitException(IAToolkitException.ErrorType.LLM_ERROR, error_message)
58
65
 
66
+ def _prepare_multimodal_input(self, messages: List[Dict], images: List[Dict]) -> List[Dict]:
67
+ """
68
+ Transforma el mensaje del usuario de texto simple a contenido multimodal (texto + imágenes)
69
+ usando el formato de Responses API (input_text/input_image).
70
+ """
71
+ # Encontrar el último mensaje del usuario
72
+ target_message = None
73
+ for msg in reversed(messages):
74
+ if msg.get('role') == 'user':
75
+ target_message = msg
76
+ break
77
+
78
+ if not target_message:
79
+ return messages
80
+
81
+ text_content = target_message.get('content', '')
82
+ content_parts = []
83
+
84
+ # Agregar parte de texto (Responses API)
85
+ if text_content:
86
+ content_parts.append({"type": "input_text", "text": text_content})
87
+
88
+ # Agregar partes de imagen (Responses API)
89
+ for img in images:
90
+ filename = img.get('name', '')
91
+ mime_type, _ = mimetypes.guess_type(filename)
92
+ if not mime_type:
93
+ mime_type = 'image/jpeg'
94
+
95
+ base64_data = img.get('base64', '')
96
+ url = f"data:{mime_type};base64,{base64_data}"
97
+
98
+ content_parts.append({
99
+ "type": "input_image",
100
+ "image_url": url
101
+ })
102
+
103
+ # Construir nueva lista de mensajes con el contenido actualizado
104
+ final_messages = []
105
+ for msg in messages:
106
+ if msg is target_message:
107
+ new_msg = msg.copy()
108
+ new_msg['content'] = content_parts
109
+ final_messages.append(new_msg)
110
+ else:
111
+ final_messages.append(msg)
112
+
113
+ return final_messages
114
+
59
115
  def _map_openai_response(self, openai_response) -> LLMResponse:
60
116
  """Mapear respuesta de OpenAI a estructura común"""
61
- # Mapear tool calls
62
- tool_calls = []
63
- if hasattr(openai_response, 'output') and openai_response.output:
64
- for tool_call in openai_response.output:
65
- if hasattr(tool_call, 'type') and tool_call.type == "function_call":
66
- tool_calls.append(ToolCall(
67
- call_id=getattr(tool_call, 'call_id', ''),
68
- type=tool_call.type,
69
- name=getattr(tool_call, 'name', ''),
70
- arguments=getattr(tool_call, 'arguments', '{}')
71
- ))
72
-
73
- # Mapear usage
117
+ tool_calls: List[ToolCall] = []
118
+ content_parts: List[Dict] = []
119
+ output_text = ""
120
+
121
+ print(f'openai_response.output: {openai_response.output}')
122
+ output_items = getattr(openai_response, 'output', []) or []
123
+
124
+ def _extract_markdown_images(text: str) -> None:
125
+ # Pattern: ![Alt](https://...)
126
+ markdown_images = re.findall(r'!\[([^\]]*)\]\((https?://[^)]+)\)', text or "")
127
+ for _alt_text, url in markdown_images:
128
+ content_parts.append({
129
+ "type": "image",
130
+ "source": {
131
+ "type": "url",
132
+ "media_type": "image/webp",
133
+ "url": url
134
+ }
135
+ })
136
+
137
+ for item in output_items:
138
+ item_type = getattr(item, 'type', '')
139
+
140
+ # 1) Tool calls (Responses API)
141
+ if item_type == "function_call":
142
+ tool_calls.append(ToolCall(
143
+ call_id=getattr(item, 'call_id', ''),
144
+ type=item_type,
145
+ name=getattr(item, 'name', ''),
146
+ arguments=getattr(item, 'arguments', '{}')
147
+ ))
148
+ continue
149
+
150
+ # 2) Mensajes (lo más común en Responses API)
151
+ if item_type == "message":
152
+ msg_content = getattr(item, "content", None) or []
153
+ for part in msg_content:
154
+ part_type = getattr(part, "type", "") or ""
155
+
156
+ # 2.A) Texto
157
+ if part_type in ("output_text", "text"):
158
+ text_content = getattr(part, "text", "") or ""
159
+ if text_content:
160
+ _extract_markdown_images(text_content)
161
+ output_text += text_content
162
+ content_parts.append({"type": "text", "text": text_content})
163
+
164
+ # 2.B) Imagen (puede venir como URL o base64 según el SDK/endpoint)
165
+ elif part_type in ("output_image", "image"):
166
+ # Algunas variantes comunes:
167
+ # - part.image_url (string URL)
168
+ # - part.url
169
+ # - part.b64_json (base64)
170
+ image_url = getattr(part, "image_url", None) or getattr(part, "url", None)
171
+ b64 = getattr(part, "b64_json", None) or getattr(part, "image", None) or getattr(part, "data", None)
172
+
173
+ # mime_type a veces viene, a veces no
174
+ mime_type = getattr(part, "media_type", None) or getattr(part, "mime_type", None) or "image/png"
175
+
176
+ if image_url:
177
+ content_parts.append({
178
+ "type": "image",
179
+ "source": {
180
+ "type": "url",
181
+ "media_type": mime_type,
182
+ "url": image_url
183
+ }
184
+ })
185
+ output_text += "\n[Imagen Generada]\n"
186
+ elif b64:
187
+ content_parts.append({
188
+ "type": "image",
189
+ "source": {
190
+ "type": "base64",
191
+ "media_type": mime_type,
192
+ "data": b64
193
+ }
194
+ })
195
+ output_text += "\n[Imagen Generada]\n"
196
+
197
+ continue
198
+
199
+ # 3) Compatibilidad hacia atrás: ítems planos "text"
200
+ if item_type == "text":
201
+ text_content = getattr(item, 'text', '') or ""
202
+ if text_content:
203
+ _extract_markdown_images(text_content)
204
+ output_text += text_content
205
+ content_parts.append({"type": "text", "text": text_content})
206
+ continue
207
+
208
+ # 4) Compatibilidad hacia atrás: ítems planos "image"
209
+ if item_type == "image":
210
+ base64_data = getattr(item, 'image', '') or getattr(item, 'data', '')
211
+ mime_type = getattr(item, 'media_type', 'image/png')
212
+ if base64_data:
213
+ content_parts.append({
214
+ "type": "image",
215
+ "source": {
216
+ "type": "base64",
217
+ "media_type": mime_type,
218
+ "data": base64_data
219
+ }
220
+ })
221
+ output_text += "\n[Imagen Generada]\n"
222
+ continue
223
+
224
+ # Fallback: Si no se extrajo texto, probamos output_text directo
225
+ if not output_text:
226
+ output_text = getattr(openai_response, 'output_text', '') or ""
227
+ if output_text and not content_parts:
228
+ _extract_markdown_images(output_text)
229
+ content_parts.append({"type": "text", "text": output_text})
230
+
74
231
  usage = Usage(
75
232
  input_tokens=openai_response.usage.input_tokens if openai_response.usage else 0,
76
233
  output_tokens=openai_response.usage.output_tokens if openai_response.usage else 0,
77
234
  total_tokens=openai_response.usage.total_tokens if openai_response.usage else 0
78
235
  )
79
236
 
80
- # Reasoning content extracted from Responses output items (type="reasoning")
81
237
  reasoning_list = self._extract_reasoning_content(openai_response)
82
238
  reasoning_str = "\n".join(reasoning_list)
83
239
 
@@ -85,10 +241,11 @@ class OpenAIAdapter:
85
241
  id=openai_response.id,
86
242
  model=openai_response.model,
87
243
  status=openai_response.status,
88
- output_text=getattr(openai_response, 'output_text', ''),
244
+ output_text=output_text,
89
245
  output=tool_calls,
90
246
  usage=usage,
91
- reasoning_content=reasoning_str
247
+ reasoning_content=reasoning_str,
248
+ content_parts=content_parts
92
249
  )
93
250
 
94
251
  def _extract_reasoning_content(self, openai_response) -> List[str]:
@@ -34,6 +34,9 @@ class LLMResponse:
34
34
  usage: Usage
35
35
  reasoning_content: str = None # campo opcional para Chain of Thought
36
36
 
37
+ # ordered list of content blocks (text and image mixed)
38
+ # Example: [{"type": "text", "text": "..."}, {"type": "image", "source": {"type": "base64", "data": "..."}}]
39
+ content_parts: List[Dict] = None
37
40
 
38
41
  def __post_init__(self):
39
42
  """Asegura que output sea una lista"""
@@ -43,3 +46,13 @@ class LLMResponse:
43
46
  if self.reasoning_content is None:
44
47
  self.reasoning_content = ""
45
48
 
49
+ if self.content_parts is None:
50
+ self.content_parts = []
51
+
52
+ # if the response has legacy text and no content parts, create a default text part
53
+ if self.output_text:
54
+ self.content_parts.append({
55
+ "type": "text",
56
+ "text": self.output_text
57
+ })
58
+
iatoolkit/locales/en.yaml CHANGED
@@ -1,5 +1,14 @@
1
1
  # Language: English
2
2
  ui:
3
+ common:
4
+ actions: "Actions"
5
+ save: "Save"
6
+ cancel: "Cancel"
7
+ apply: "Apply Changes"
8
+ discard_changes: "Discard Changes"
9
+ order: "Order"
10
+
11
+
3
12
  login_widget:
4
13
  title: "Sign In"
5
14
  welcome_message: "Enter your credentials or register to access this platform."
@@ -88,7 +97,15 @@ ui:
88
97
  add: "Add"
89
98
  create: "Create"
90
99
  delete: "Delete"
91
-
100
+ add_new: "Add new"
101
+ category_name_placeholder: "Category name"
102
+ items: "Items"
103
+ drag_drop_hint: "Use arrows to reorder items."
104
+ current_items: "Current items"
105
+ manage: "Manage"
106
+ unsaved_changes_title: "Unsaved Changes"
107
+ unsaved_changes_message: "You have unsaved changes. Do you want to save them before proceeding?"
108
+ discard_changes: "Discard Changes"
92
109
 
93
110
  db_explorer:
94
111
  data_explorer: "Data Explorer"
@@ -181,10 +198,32 @@ ui:
181
198
  delete_cancel: "Cancel"
182
199
  target_collection: "Collection"
183
200
  select_collection_placeholder: "Select a collection"
201
+ select_collection: "Select a collection"
184
202
  collection_required: "Collection is required"
185
203
  collection: "Collection"
204
+ manage_collections: "Manage collections"
186
205
  all_collections: "All collections"
187
206
 
207
+ json_editor:
208
+ title: "JSON Schema Editor"
209
+ subtitle: "Define the structure and metadata for this JSON field"
210
+ field_name: "Field Name"
211
+ type: "Data Type"
212
+ description: "Description"
213
+ empty_schema: "No fields defined yet."
214
+ add_root: "Add Root Field"
215
+ add_field: "Add Field"
216
+ types:
217
+ string: "String"
218
+ integer: "Integer"
219
+ number: "Number"
220
+ boolean: "Boolean"
221
+ object: "Object (Map)"
222
+ array: "Array (List)"
223
+ jsonb: "JSON Raw"
224
+
225
+
226
+
188
227
  tooltips:
189
228
  history: "History of my queries"
190
229
  reload_context: "Force Context Reload"
@@ -350,7 +389,6 @@ js_messages:
350
389
  db_created: "Created"
351
390
  db_last_access: "Last access"
352
391
  db_filename: "Filename"
353
- db_user: "User"
354
392
  db_status: "Status"
355
393
  db_collection: "Collection"
356
394
  editor_no_file_selected: "No file selected"
@@ -358,6 +396,13 @@ js_messages:
358
396
  cant_load_company: "Could not load company.yaml"
359
397
  config_saved: "Configuration saved successfully."
360
398
  config_error: "Error saving configuration."
399
+ all_collections: "All collections"
400
+ no_variables_found: "No variables found in prompt template."
401
+ manage_collections: "Manage collections"
402
+ edit: "Edit"
403
+
404
+
405
+
361
406
 
362
407
 
363
408
 
iatoolkit/locales/es.yaml CHANGED
@@ -1,5 +1,13 @@
1
1
  # locales/es.yaml
2
2
  ui:
3
+ common:
4
+ actions: "Acciones"
5
+ save: "Guardar"
6
+ cancel: "Cancelar"
7
+ apply: "Aplicar Cambios"
8
+ discard_changes: "Descartar Cambios"
9
+ order: "Orden"
10
+
3
11
  login_widget:
4
12
  title: "Iniciar Sesión"
5
13
  welcome_message: "Ingresa tus credenciales o registrate para acceder a la plataforma."
@@ -85,6 +93,16 @@ ui:
85
93
  add: "Agregar"
86
94
  create: "Crear"
87
95
  delete: "Borrar"
96
+ add_new: "Agregar nuevo"
97
+ category_name_placeholder: "nombre de categoria"
98
+ items: "Items"
99
+ drag_drop_hint: "Utiliza las flechas para ordenar items.."
100
+ current_items: "items actuales"
101
+ manage: "Administrar"
102
+ unsaved_changes_title: "Cambios sin guardar"
103
+ unsaved_changes_message: "Tienes modificaciones sin guardar. Quieres grabarlos antes de salir?"
104
+ discard_changes: "Descartar cambios"
105
+
88
106
 
89
107
  db_explorer:
90
108
  data_explorer: "Explorador de datos"
@@ -136,6 +154,7 @@ ui:
136
154
  category_label: "Categoría"
137
155
  delete_confirmation: "Eliminar el prompt?"
138
156
 
157
+
139
158
  config:
140
159
  editor_description: "Editor de configuración"
141
160
  title: "Editor de configuraciones"
@@ -179,7 +198,26 @@ ui:
179
198
  collection_required: "Debe seleccionar una categoría"
180
199
  all_collections: "Todas las categorías"
181
200
  collection: "Categoría"
201
+ manage_collections: "Administra collections"
202
+ select_collection: "Selecciona una categorìa"
182
203
 
204
+ json_editor:
205
+ title: "Editor de Esquema JSON"
206
+ subtitle: "Define la estructura y metadatos para este campo JSON"
207
+ field_name: "Nombre del Campo"
208
+ type: "Tipo de Dato"
209
+ description: "Descripción"
210
+ empty_schema: "Aún no hay campos definidos."
211
+ add_root: "Agregar Campo Raíz"
212
+ add_field: "Agregar Campo"
213
+ types:
214
+ string: "Texto (String)"
215
+ integer: "Entero"
216
+ number: "Número (Decimal)"
217
+ boolean: "Booleano"
218
+ object: "Objeto (Mapa)"
219
+ array: "Lista (Array)"
220
+ jsonb: "JSON Crudo"
183
221
 
184
222
  tooltips:
185
223
  history: "Historial con mis consultas"
@@ -340,7 +378,6 @@ js_messages:
340
378
  search_placeholder: "Buscar usuarios..."
341
379
  showing: "Mostrando"
342
380
  records: "Registros"
343
- db_user: "Usuario"
344
381
  db_role: "Rol"
345
382
  db_verified: "Verificado"
346
383
  db_collection: "Colección"
@@ -354,6 +391,13 @@ js_messages:
354
391
  cant_load_company: "No puede cargarcompany.yaml"
355
392
  config_saved: "Configuración guardada correctamente."
356
393
  config_error: "Error guardando configuración"
394
+ all_collections: "Todas las colecciones"
395
+ no_variables_found: "El template no contiene variables."
396
+ manage_collections: "Administra collections"
397
+ edit: "Editar"
398
+
399
+
400
+
357
401
 
358
402
 
359
403
 
@@ -22,12 +22,20 @@ class LLMQueryRepo:
22
22
  def rollback(self):
23
23
  self.session.rollback()
24
24
 
25
+ # save new query result in the database
25
26
  def add_query(self, query: LLMQuery):
26
27
  self.session.add(query)
27
28
  self.session.commit()
28
29
  return query
29
30
 
31
+ # get user query history
32
+ def get_history(self, company: Company, user_identifier: str) -> list[LLMQuery]:
33
+ return self.session.query(LLMQuery).filter(
34
+ LLMQuery.user_identifier == user_identifier,
35
+ ).filter_by(company_id=company.id).order_by(LLMQuery.created_at.desc()).limit(100).all()
30
36
 
37
+
38
+ ## --- Tools related methods
31
39
  def get_company_tools(self, company: Company) -> list[Tool]:
32
40
  return (
33
41
  self.session.query(Tool)
@@ -63,6 +71,30 @@ class LLMQueryRepo:
63
71
  def delete_tool(self, tool: Tool):
64
72
  self.session.query(Tool).filter_by(id=tool.id).delete(synchronize_session=False)
65
73
 
74
+ # -- Prompt related methods
75
+
76
+ def get_prompt_by_name(self, company: Company, prompt_name: str):
77
+ return self.session.query(Prompt).filter_by(company_id=company.id, name=prompt_name).first()
78
+
79
+
80
+ def get_prompts(self, company: Company, include_all: bool = False) -> list[Prompt]:
81
+ if include_all:
82
+ # Include all prompts (for the prompt admin dashboard)
83
+ return self.session.query(Prompt).filter(
84
+ Prompt.company_id == company.id,
85
+ ).all()
86
+ else:
87
+ # Only active company prompts (default behavior for end users)
88
+ return self.session.query(Prompt).filter(
89
+ Prompt.company_id == company.id,
90
+ Prompt.prompt_type == PromptType.COMPANY.value,
91
+ Prompt.active == True
92
+ ).all()
93
+
94
+ def get_system_prompts(self) -> list[Prompt]:
95
+ return self.session.query(Prompt).filter_by(prompt_type=PromptType.SYSTEM.value, active=True).order_by(
96
+ Prompt.order).all()
97
+
66
98
  def create_or_update_prompt(self, new_prompt: Prompt):
67
99
  prompt = self.session.query(Prompt).filter_by(company_id=new_prompt.company_id,
68
100
  name=new_prompt.name).first()
@@ -80,6 +112,18 @@ class LLMQueryRepo:
80
112
  self.session.commit()
81
113
  return prompt
82
114
 
115
+ def delete_prompt(self, prompt: Prompt):
116
+ self.session.delete(prompt)
117
+ self.session.commit()
118
+
119
+ # -- Prompt category methods
120
+
121
+ def get_category_by_name(self, company_id: int, name: str) -> PromptCategory:
122
+ return self.session.query(PromptCategory).filter_by(company_id=company_id, name=name).first()
123
+
124
+ def get_all_categories(self, company_id: int) -> List[PromptCategory]:
125
+ return self.session.query(PromptCategory).filter_by(company_id=company_id).order_by(PromptCategory.order).all()
126
+
83
127
  def create_or_update_prompt_category(self, new_category: PromptCategory):
84
128
  category = self.session.query(PromptCategory).filter_by(company_id=new_category.company_id,
85
129
  name=new_category.name).first()
@@ -92,36 +136,3 @@ class LLMQueryRepo:
92
136
  self.session.flush()
93
137
  return category
94
138
 
95
- def get_history(self, company: Company, user_identifier: str) -> list[LLMQuery]:
96
- return self.session.query(LLMQuery).filter(
97
- LLMQuery.user_identifier == user_identifier,
98
- ).filter_by(company_id=company.id).order_by(LLMQuery.created_at.desc()).limit(100).all()
99
-
100
- def get_prompts(self, company: Company, include_all: bool = False) -> list[Prompt]:
101
- if include_all:
102
- # Include all prompts: company, system, agent
103
- return self.session.query(Prompt).filter(
104
- Prompt.company_id == company.id,
105
- ).all()
106
- else:
107
- # Only company prompts, excluding system (default behavior for end users)
108
- return self.session.query(Prompt).filter(
109
- Prompt.company_id == company.id,
110
- Prompt.prompt_type == PromptType.COMPANY.value
111
- ).all()
112
-
113
- def get_prompt_by_name(self, company: Company, prompt_name: str):
114
- return self.session.query(Prompt).filter_by(company_id=company.id, name=prompt_name).first()
115
-
116
- def get_category_by_name(self, company_id: int, name: str) -> PromptCategory:
117
- return self.session.query(PromptCategory).filter_by(company_id=company_id, name=name).first()
118
-
119
- def get_all_categories(self, company_id: int) -> List[PromptCategory]:
120
- return self.session.query(PromptCategory).filter_by(company_id=company_id).order_by(PromptCategory.order).all()
121
-
122
- def get_system_prompts(self) -> list[Prompt]:
123
- return self.session.query(Prompt).filter_by(prompt_type=PromptType.SYSTEM.value, active=True).order_by(Prompt.order).all()
124
-
125
- def delete_prompt(self, prompt: Prompt):
126
- self.session.delete(prompt)
127
- self.session.commit()