IO4IT 4.0.26__tar.gz → 4.0.28__tar.gz

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 (116) hide show
  1. {io4it-4.0.26 → io4it-4.0.28}/PKG-INFO +1 -1
  2. {io4it-4.0.26 → io4it-4.0.28}/io4it.egg-info/PKG-INFO +1 -1
  3. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/custom_api.py +93 -14
  4. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWCustomLLMApi.py +93 -9
  5. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWDoclingToMarkdown.py +52 -1
  6. io4it-4.0.28/orangecontrib/IO4IT/widgets/OWJsonToDataTable.py +384 -0
  7. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWMarkdownLoader.py +3 -3
  8. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWProcessPoolExecutor.py +1 -1
  9. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/ow_json_to_data_table.ui +22 -3
  10. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owcustomllmapi.ui +89 -12
  11. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owfile2images.ui +4 -4
  12. {io4it-4.0.26 → io4it-4.0.28}/setup.py +1 -1
  13. io4it-4.0.26/orangecontrib/IO4IT/widgets/OWJsonToDataTable.py +0 -167
  14. {io4it-4.0.26 → io4it-4.0.28}/io4it.egg-info/SOURCES.txt +0 -0
  15. {io4it-4.0.26 → io4it-4.0.28}/io4it.egg-info/dependency_links.txt +0 -0
  16. {io4it-4.0.26 → io4it-4.0.28}/io4it.egg-info/entry_points.txt +0 -0
  17. {io4it-4.0.26 → io4it-4.0.28}/io4it.egg-info/namespace_packages.txt +0 -0
  18. {io4it-4.0.26 → io4it-4.0.28}/io4it.egg-info/requires.txt +0 -0
  19. {io4it-4.0.26 → io4it-4.0.28}/io4it.egg-info/top_level.txt +0 -0
  20. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/__init__.py +0 -0
  21. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/ocr_function/__init__.py +0 -0
  22. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/ocr_function/word_converter.py +0 -0
  23. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/toolViews/designer/key_manager_ui.ui +0 -0
  24. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/toolViews/designer/link_workflow_manager_ui.ui +0 -0
  25. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/toolViews/key_manager_ui.py +0 -0
  26. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/toolViews/linkHTLMWorkflow_manager_ui.py +0 -0
  27. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/__init__.py +0 -0
  28. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/environment_collector.py +0 -0
  29. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/file2images.py +0 -0
  30. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/keys_manager.py +0 -0
  31. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/mail.py +0 -0
  32. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/pool_exec_utils.py +0 -0
  33. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/pst_extractor/TigerODMPST.dll +0 -0
  34. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/pst_extractor/__init__.py +0 -0
  35. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/pst_extractor/export_pst.py +0 -0
  36. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/pst_extractor/libpff.dll +0 -0
  37. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/pst_extractor/z.dll +0 -0
  38. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/secret_manager.py +0 -0
  39. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/utils_md.py +0 -0
  40. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWChatGpt.py +0 -0
  41. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWDeep_Search.py +0 -0
  42. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWExportMarkdown.py +0 -0
  43. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWExtractTablesDocxToXlsx.py +0 -0
  44. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWInboxMailMonitoring.py +0 -0
  45. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWMD2HTML.py +0 -0
  46. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWMarkdownizer.py +0 -0
  47. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWOfficeNormalizer.py +0 -0
  48. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWPPTX2JPG.py +0 -0
  49. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWParserHTML.py +0 -0
  50. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWPdfType.py +0 -0
  51. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWPipAuditor.py +0 -0
  52. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWS3Uploader.py +0 -0
  53. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWS3downloader.py +0 -0
  54. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWS3list.py +0 -0
  55. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWWebSearch.py +0 -0
  56. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWXLSX_insert.py +0 -0
  57. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWmailLoader.py +0 -0
  58. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWmailSender.py +0 -0
  59. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWwordpdf2docx.py +0 -0
  60. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/__init__.py +0 -0
  61. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/__init__.py +0 -0
  62. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/nogui.ui +0 -0
  63. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/ow_extract_tables_docx_to_xlsx.ui +0 -0
  64. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/ow_file_ext_selector.ui +0 -0
  65. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owchatgpt.ui +0 -0
  66. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owdeepsearch.ui +0 -0
  67. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owdoclingtomarkdown.ui +0 -0
  68. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owexportmarkdown.ui +0 -0
  69. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owinboxmailmonitoring.ui +0 -0
  70. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owinsertdatatoexcel.ui +0 -0
  71. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owmailloader.ui +0 -0
  72. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owmailsender.ui +0 -0
  73. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owmarkdownizer.ui +0 -0
  74. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owmarkdownloader.ui +0 -0
  75. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owmd2html.ui +0 -0
  76. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owofficenormalizer.ui +0 -0
  77. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owparserhtml.ui +0 -0
  78. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owpdftype.ui +0 -0
  79. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owpipauditor.ui +0 -0
  80. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owprocesspoolexecutor.ui +0 -0
  81. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owspeechtotext.ui +0 -0
  82. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owvisualizationer.ui +0 -0
  83. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owwebsearch.ui +0 -0
  84. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/wordpdf2docx.ui +0 -0
  85. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/__init__.py +0 -0
  86. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/chatgpt.png +0 -0
  87. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/check_pdf.png +0 -0
  88. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/deepsearch.svg +0 -0
  89. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/dep_md_old.png +0 -0
  90. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/download.png +0 -0
  91. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/export_md.png +0 -0
  92. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/extract_table.png +0 -0
  93. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/file2images.png +0 -0
  94. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/file_extensor.png +0 -0
  95. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/html.png +0 -0
  96. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/json-file.png +0 -0
  97. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/list_aws.png +0 -0
  98. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/load_md.png +0 -0
  99. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/mail_loader.png +0 -0
  100. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/mail_writer.png +0 -0
  101. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/md.png +0 -0
  102. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/monitor-email.svg +0 -0
  103. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/office_normalizer.png +0 -0
  104. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/owcustomllmapi.svg +0 -0
  105. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/owmd2html.svg +0 -0
  106. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/pip_auditor.png +0 -0
  107. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/process_pool_executor.png +0 -0
  108. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/speech_to_text.png +0 -0
  109. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/upload.png +0 -0
  110. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/visualizationer.png +0 -0
  111. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/websearch.png +0 -0
  112. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/wordpdf2docx.png +0 -0
  113. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/xlsx_insert.png +0 -0
  114. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons_dev/__init__.py +0 -0
  115. {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/__init__.py +0 -0
  116. {io4it-4.0.26 → io4it-4.0.28}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: io4it
3
- Version: 4.0.26
3
+ Version: 4.0.28
4
4
  Home-page:
5
5
  Author:
6
6
  Author-email:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: io4it
3
- Version: 4.0.26
3
+ Version: 4.0.28
4
4
  Home-page:
5
5
  Author:
6
6
  Author-email:
@@ -22,22 +22,97 @@ def build_headers(header_name, auth_scheme, api_key):
22
22
  return {"Content-Type": "application/json", header_name: value}
23
23
 
24
24
 
25
+ def list_models(config):
26
+ """
27
+ GET Base URL + /models and return the list of model ids exposed by an
28
+ OpenAI-compatible endpoint (llama-server, vLLM, OpenAI, ...).
29
+
30
+ Expects Base URL to already include the API version prefix if the
31
+ server uses one, e.g. "http://127.0.0.1:8080/v1".
32
+
33
+ config keys: base_url, header_name, auth_scheme, api_key, cert_path,
34
+ cert_is_ca, use_proxy, proxy_url
35
+ """
36
+ url = config["base_url"].rstrip("/") + "/models"
37
+ api_key = config.get("api_key")
38
+ if api_key:
39
+ headers = build_headers(config["header_name"], config["auth_scheme"], api_key)
40
+ else:
41
+ headers = {"Content-Type": "application/json"}
42
+
43
+ kwargs = {"timeout": 30}
44
+ cert_path = config.get("cert_path")
45
+ if cert_path:
46
+ if config.get("cert_is_ca"):
47
+ kwargs["verify"] = cert_path
48
+ else:
49
+ kwargs["cert"] = cert_path
50
+
51
+ if config.get("use_proxy"):
52
+ proxy_url = config.get("proxy_url") or ""
53
+ kwargs["proxies"] = {"http": proxy_url, "https": proxy_url}
54
+
55
+ response = requests.get(url, headers=headers, **kwargs)
56
+ response.raise_for_status()
57
+ data = response.json()
58
+ return [m.get("id", "?") for m in data.get("data", [])]
59
+
60
+
25
61
  def search_for_streamed_token(decoded_line):
26
62
  r"""
27
63
  /!\ This function is meant to be updated whenever a new response format is identified /!\
28
64
 
29
- Parse a decoded stream response "line" to find the token generated by a LLM.
65
+ Parse a decoded stream response "line" and return (kind, token), where
66
+ kind is "content" for actual answer text, "reasoning" for a model's
67
+ internal thinking trace (some reasoning models, e.g. Qwen3 through
68
+ llama-server, stream it in a separate delta field: "reasoning" or
69
+ "reasoning_content" depending on server version), or None if the line
70
+ carries no usable token.
30
71
  """
31
- known_keys = ["content", "reasoning"]
72
+ reasoning_keys = ("reasoning", "reasoning_content")
32
73
  dict_regex = r"\{[\s\S]*\}"
33
74
  match = re.search(dict_regex, decoded_line)
34
75
  if not match:
35
- return "Token not found"
36
-
76
+ return None, ""
77
+
37
78
  json_data = json.loads(match.group())
38
- delta = json_data["choices"][0]["delta"]
39
- token = next((delta[key] for key in known_keys if key in delta), "Token not found")
40
- return token
79
+ # "choices" can be an empty list on the last chunk of a stream, e.g.
80
+ # llama-server sends {"choices": [], "usage": {...}, ...} right before
81
+ # [DONE] once "usage" reporting is involved.
82
+ choices = json_data.get("choices") or []
83
+ if not choices:
84
+ return None, ""
85
+ delta = choices[0].get("delta", {})
86
+ # .get(key) (not "key in delta") also skips a key whose value is
87
+ # JSON null, e.g. {"delta": {"role": "assistant", "content": null}},
88
+ # which some OpenAI-compatible servers (llama-server included) send.
89
+ if delta.get("content"):
90
+ return "content", delta["content"]
91
+ for key in reasoning_keys:
92
+ if delta.get(key):
93
+ return "reasoning", delta[key]
94
+ # Diagnostic: if the delta carries something we don't recognize (e.g. a
95
+ # model's thinking trace under a key that isn't "reasoning" or
96
+ # "reasoning_content" on this particular server), log it so the actual
97
+ # key name shows up in the console instead of silently dropping it.
98
+ ignored_keys = {"role", "tool_calls", "function_call", "refusal"}
99
+ unknown = {k: v for k, v in delta.items() if k not in ignored_keys and v}
100
+ if unknown:
101
+ print("search_for_streamed_token: unrecognized delta keys, please report ->", unknown)
102
+ return None, ""
103
+
104
+
105
+ _THINK_BLOCK_RE = re.compile(r"<think>[\s\S]*?</think>", re.IGNORECASE)
106
+
107
+
108
+ def strip_thinking(text):
109
+ """
110
+ Some reasoning models (e.g. Qwen3 served through llama-server) emit
111
+ their internal <think>...</think> trace as regular streamed content,
112
+ since it isn't split into a separate delta field. Strip it out so it
113
+ doesn't end up in the final answer.
114
+ """
115
+ return _THINK_BLOCK_RE.sub("", text).strip()
41
116
 
42
117
 
43
118
  def call_chat_completion(messages, config, stream=False, workflow_id="Request_RAG"):
@@ -58,6 +133,7 @@ def call_chat_completion(messages, config, stream=False, workflow_id="Request_RA
58
133
  "messages": messages,
59
134
  "temperature": config["temperature"],
60
135
  "max_tokens": config["max_tokens"],
136
+ "stream": stream,
61
137
  }
62
138
 
63
139
  kwargs = {"timeout": 600}
@@ -81,20 +157,23 @@ def call_chat_completion(messages, config, stream=False, workflow_id="Request_RA
81
157
 
82
158
  if not stream:
83
159
  data = response.json()
84
- return data["choices"][0]["message"]["content"]
160
+ return strip_thinking(data["choices"][0]["message"]["content"])
85
161
  else:
86
162
  answer = ""
87
163
  for line in response.iter_lines():
88
164
  if line:
89
165
  decoded_line = line.decode("utf-8")
90
166
  try:
91
- token = search_for_streamed_token(decoded_line)
167
+ kind, token = search_for_streamed_token(decoded_line)
92
168
  except Exception as e:
93
- token = "Error"
169
+ kind, token = None, ""
94
170
  print("An error occured when trying to get token from stream response:", e)
95
- answer += token
171
+ # Only actual answer content goes into the final answer;
172
+ # a model's reasoning/thinking trace is deliberately dropped.
173
+ if kind == "content":
174
+ answer += token
96
175
  write_tokens_to_file(token, workflow_id=workflow_id)
97
- return answer
176
+ return strip_thinking(answer)
98
177
 
99
178
 
100
179
  def generate_answers(data, config, progress_callback=None):
@@ -123,7 +202,7 @@ def generate_answers(data, config, progress_callback=None):
123
202
  image_paths = image_paths_column[i].split(";") if image_paths_column is not None else None
124
203
  messages = prompt_to_messages(prompt, system_prompt, image_paths)
125
204
  try:
126
- answer = call_chat_completion(messages, config, stream=True)
205
+ answer = call_chat_completion(messages, config, stream=True, workflow_id=config.get("workflow_id") or "")
127
206
  except requests.HTTPError as e:
128
207
  body = e.response.text if e.response is not None else str(e)
129
208
  answer = f"[ERROR] HTTP {e.response.status_code if e.response is not None else '?'}: {body}"
@@ -149,7 +228,7 @@ def continue_conversation(table, config, progress_callback=None):
149
228
  return data
150
229
 
151
230
  try:
152
- answer = call_chat_completion(messages, config, stream=True)
231
+ answer = call_chat_completion(messages, config, stream=True, workflow_id=config.get("workflow_id") or "")
153
232
  except requests.HTTPError as e:
154
233
  body = e.response.text if e.response is not None else str(e)
155
234
  answer = f"[ERROR] HTTP {e.response.status_code if e.response is not None else '?'}: {body}"
@@ -12,13 +12,15 @@ from AnyQt.QtWidgets import (
12
12
 
13
13
  if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
14
14
  from Orange.widgets.orangecontrib.IO4IT.utils import custom_api
15
- from Orange.widgets.orangecontrib.AAIT.utils import thread_management
15
+ from Orange.widgets.orangecontrib.IO4IT.utils import keys_manager
16
+ from Orange.widgets.orangecontrib.AAIT.utils import thread_management, MetManagement
16
17
  from Orange.widgets.orangecontrib.AAIT.llm.answers_llama import identify_table_type
17
18
  from Orange.widgets.orangecontrib.AAIT.utils.import_uic import uic
18
19
  from Orange.widgets.orangecontrib.AAIT.utils.initialize_from_ini import apply_modification_from_python_file
19
20
  else:
20
21
  from orangecontrib.IO4IT.utils import custom_api
21
- from orangecontrib.AAIT.utils import thread_management
22
+ from orangecontrib.IO4IT.utils import keys_manager
23
+ from orangecontrib.AAIT.utils import thread_management, MetManagement
22
24
  from orangecontrib.AAIT.llm.answers_llama import identify_table_type
23
25
  from orangecontrib.AAIT.utils.import_uic import uic
24
26
  from orangecontrib.AAIT.utils.initialize_from_ini import apply_modification_from_python_file
@@ -47,10 +49,11 @@ class OWCustomLLMApi(widget.OWWidget):
47
49
  data = Output("Data", Orange.data.Table)
48
50
 
49
51
  base_url = Setting("")
50
- route = Setting("/v1/chat/completions")
52
+ route = Setting("/chat/completions")
51
53
  header_name = Setting("Authorization")
52
54
  auth_scheme = Setting("Bearer")
53
55
  api_key = Setting("")
56
+ key_service = Setting("")
54
57
  cert_path = Setting("")
55
58
  cert_is_ca = Setting(False)
56
59
  use_proxy = Setting(False)
@@ -58,6 +61,7 @@ class OWCustomLLMApi(widget.OWWidget):
58
61
  model = Setting("")
59
62
  max_tokens = Setting(4096)
60
63
  temperature = Setting(0.4)
64
+ workflow_id = Setting("")
61
65
 
62
66
  @Inputs.data
63
67
  def set_data(self, in_data):
@@ -79,7 +83,7 @@ class OWCustomLLMApi(widget.OWWidget):
79
83
  def __init__(self):
80
84
  super().__init__()
81
85
  self.setFixedWidth(700)
82
- self.setFixedHeight(695)
86
+ self.setFixedHeight(809)
83
87
  uic.loadUi(self.gui, self)
84
88
 
85
89
  self.label_description = self.findChild(QLabel, 'Description')
@@ -104,6 +108,10 @@ class OWCustomLLMApi(widget.OWWidget):
104
108
  self.line_api_key.setText(self.api_key)
105
109
  self.line_api_key.editingFinished.connect(self.update_parameters)
106
110
 
111
+ self.line_key_service = self.findChild(QLineEdit, 'lineKeyService')
112
+ self.line_key_service.setText(self.key_service)
113
+ self.line_key_service.editingFinished.connect(self.update_parameters)
114
+
107
115
  self.line_cert_path = self.findChild(QLineEdit, 'lineCertPath')
108
116
  self.line_cert_path.setText(self.cert_path)
109
117
  self.line_cert_path.editingFinished.connect(self.update_parameters)
@@ -135,24 +143,33 @@ class OWCustomLLMApi(widget.OWWidget):
135
143
  self.box_temperature.setValue(self.temperature)
136
144
  self.box_temperature.editingFinished.connect(self.update_parameters)
137
145
 
146
+ self.line_workflow_id = self.findChild(QLineEdit, 'lineWorkflowId')
147
+ self.line_workflow_id.setText(self.workflow_id)
148
+ self.line_workflow_id.editingFinished.connect(self.update_parameters)
149
+
138
150
  self.push_button_run = self.findChild(QPushButton, 'pushButtonRun')
139
151
  self.push_button_run.clicked.connect(self.run)
140
152
 
153
+ self.button_list_models = self.findChild(QPushButton, 'buttonListModels')
154
+ self.button_list_models.clicked.connect(self.list_models)
155
+
141
156
  self.textBrowser = self.findChild(QTextBrowser, 'textBrowser')
142
157
 
143
158
  self.data = None
144
159
  self.thread = None
145
160
  self.can_run = True
146
161
  self.result = None
162
+ self._resolved_api_key = None
147
163
 
148
164
  self.post_initialized()
149
165
 
150
166
  def update_parameters(self):
151
167
  self.base_url = self.line_base_url.text().strip()
152
- self.route = self.line_route.text().strip() or "/v1/chat/completions"
168
+ self.route = self.line_route.text().strip() or "/chat/completions"
153
169
  self.header_name = self.line_header_name.text().strip() or "Authorization"
154
170
  self.auth_scheme = self.line_auth_scheme.text().strip()
155
171
  self.api_key = self.line_api_key.text()
172
+ self.key_service = self.line_key_service.text().strip()
156
173
  self.cert_path = self.line_cert_path.text().strip()
157
174
  self.cert_is_ca = self.check_cert_is_ca.isChecked()
158
175
  self.use_proxy = self.check_use_proxy.isChecked()
@@ -160,6 +177,7 @@ class OWCustomLLMApi(widget.OWWidget):
160
177
  self.model = self.line_model.text().strip()
161
178
  self.max_tokens = self.box_max_tokens.value()
162
179
  self.temperature = self.box_temperature.value()
180
+ self.workflow_id = self.line_workflow_id.text().strip()
163
181
 
164
182
  def browse_cert(self):
165
183
  path, _ = QFileDialog.getOpenFileName(self, "Select a .pem file", "", "PEM files (*.pem);;All files (*)")
@@ -167,13 +185,53 @@ class OWCustomLLMApi(widget.OWWidget):
167
185
  self.line_cert_path.setText(path)
168
186
  self.update_parameters()
169
187
 
170
- def build_config(self):
188
+ def resolve_api_key(self):
189
+ """
190
+ Resolve the API key to actually use. A "Key manager service" name
191
+ takes priority: the real key is fetched from the secure key store
192
+ (Key Manager tool) and never persisted in the workflow file. Falls
193
+ back to the plain-text "API key" field (stored in the .ows file)
194
+ for legacy workflows that don't use the key store.
195
+ """
196
+ if self.key_service:
197
+ cfg = keys_manager.lire_config_api(self.key_service)
198
+ if not cfg or not cfg.get("api_key"):
199
+ self.error(
200
+ f"No API key found for service '{self.key_service}' in the Key Manager. "
201
+ f"Register it there (Create / overwrite api key), or clear this field to "
202
+ f"use the 'API key' field instead."
203
+ )
204
+ return None
205
+ return cfg["api_key"]
206
+ if not self.api_key:
207
+ self.error("API key is required (either via 'Key manager service' or the 'API key' field).")
208
+ return None
209
+ return self.api_key
210
+
211
+ def get_optional_api_key(self):
212
+ """
213
+ Best-effort API key resolution for diagnostic calls (e.g. List models):
214
+ unlike resolve_api_key(), does not fail when no key is configured at
215
+ all, since some local servers (llama-server without --api-key) don't
216
+ require one. Returns (api_key_or_None, error_message_or_None).
217
+ """
218
+ if self.key_service:
219
+ cfg = keys_manager.lire_config_api(self.key_service)
220
+ if not cfg or not cfg.get("api_key"):
221
+ return None, (
222
+ f"No API key found for service '{self.key_service}' in the Key Manager. "
223
+ f"Register it there, or clear this field."
224
+ )
225
+ return cfg["api_key"], None
226
+ return (self.api_key or None), None
227
+
228
+ def build_config(self, api_key=None):
171
229
  return {
172
230
  "base_url": self.base_url,
173
231
  "route": self.route,
174
232
  "header_name": self.header_name,
175
233
  "auth_scheme": self.auth_scheme,
176
- "api_key": self.api_key,
234
+ "api_key": api_key if api_key is not None else self._resolved_api_key,
177
235
  "cert_path": self.cert_path,
178
236
  "cert_is_ca": self.cert_is_ca,
179
237
  "use_proxy": self.use_proxy,
@@ -181,8 +239,30 @@ class OWCustomLLMApi(widget.OWWidget):
181
239
  "model": self.model,
182
240
  "max_tokens": self.max_tokens,
183
241
  "temperature": self.temperature,
242
+ "workflow_id": self.workflow_id,
184
243
  }
185
244
 
245
+ def list_models(self):
246
+ self.error("")
247
+ self.warning("")
248
+
249
+ if not self.base_url:
250
+ self.error("Base URL is required.")
251
+ return
252
+
253
+ api_key, err = self.get_optional_api_key()
254
+ if err:
255
+ self.error(err)
256
+ return
257
+
258
+ try:
259
+ models = custom_api.list_models(self.build_config(api_key=api_key))
260
+ except Exception as e:
261
+ self.error(f"Unable to list models: {e}")
262
+ return
263
+
264
+ self.textBrowser.setText("\n".join(models) if models else "No model returned by the server.")
265
+
186
266
  def run(self):
187
267
  self.error("")
188
268
  self.warning("")
@@ -210,8 +290,8 @@ class OWCustomLLMApi(widget.OWWidget):
210
290
  self.Outputs.data.send(None)
211
291
  return
212
292
 
213
- if not self.api_key:
214
- self.error("API key is required.")
293
+ self._resolved_api_key = self.resolve_api_key()
294
+ if self._resolved_api_key is None:
215
295
  self.Outputs.data.send(None)
216
296
  return
217
297
 
@@ -221,6 +301,10 @@ class OWCustomLLMApi(widget.OWWidget):
221
301
  self.progressBarInit()
222
302
  self.textBrowser.setText("")
223
303
 
304
+ if self.workflow_id:
305
+ chemin_dossier = MetManagement.get_api_local_folder(workflow_id=self.workflow_id)
306
+ if os.path.exists(chemin_dossier):
307
+ MetManagement.write_file_time(chemin_dossier + "time.txt")
224
308
 
225
309
  if table_type == "batch":
226
310
  self.thread = thread_management.Thread(custom_api.generate_answers, self.data, self.build_config())
@@ -1,4 +1,4 @@
1
- import os, time, sys
1
+ import os, time, sys, re
2
2
  from pathlib import Path
3
3
  from concurrent.futures import as_completed
4
4
 
@@ -14,6 +14,7 @@ from openpyxl import Workbook
14
14
  # --- Docling (unique lib utilisée pour la conversion) ---
15
15
  from docling.backend.pypdfium2_backend import PyPdfiumDocumentBackend
16
16
  from docling.datamodel.base_models import InputFormat
17
+ from docling.datamodel.pipeline_options import PdfPipelineOptions, RapidOcrOptions
17
18
  from docling.document_converter import (
18
19
  DocumentConverter,
19
20
  PdfFormatOption,
@@ -31,6 +32,45 @@ else:
31
32
  from orangecontrib.AAIT.utils.import_uic import uic
32
33
 
33
34
 
35
+ # --------- helpers Docling ----------
36
+ def _build_converter(force_ocr: bool = False):
37
+ """Construit le DocumentConverter.
38
+ force_ocr=False : configuration d'origine (options PDF par défaut).
39
+ force_ocr=True : OCR RapidOCR forcé sur toute la page (PDF image).
40
+ """
41
+ pdf_format = PdfFormatOption(
42
+ pipeline_cls=StandardPdfPipeline,
43
+ backend=PyPdfiumDocumentBackend
44
+ )
45
+ if force_ocr:
46
+ pdf_options = PdfPipelineOptions()
47
+ pdf_options.do_ocr = True
48
+ pdf_options.ocr_options = RapidOcrOptions(force_full_page_ocr=True)
49
+ pdf_format = PdfFormatOption(
50
+ pipeline_cls=StandardPdfPipeline,
51
+ backend=PyPdfiumDocumentBackend,
52
+ pipeline_options=pdf_options,
53
+ )
54
+
55
+ # Docling minimal config (inspiré du snippet)
56
+ return DocumentConverter(
57
+ allowed_formats=[InputFormat.PDF, InputFormat.DOCX, InputFormat.PPTX],
58
+ format_options={
59
+ InputFormat.PDF: pdf_format,
60
+ InputFormat.DOCX: WordFormatOption(
61
+ pipeline_cls=SimplePipeline
62
+ ),
63
+ # PPTX: pas d'option spécifique; géré par défaut
64
+ },
65
+ )
66
+
67
+
68
+ def _has_text(md: str, min_chars: int = 20) -> bool:
69
+ """True si le Markdown contient du vrai texte (hors commentaires <!-- image -->)."""
70
+ cleaned = re.sub(r"<!--.*?-->", "", md, flags=re.DOTALL)
71
+ return sum(c.isalnum() for c in cleaned) >= min_chars
72
+
73
+
34
74
  # --------- worker stateless : convertit 1 fichier avec Docling ----------
35
75
  def _convert_one_file(file_path_str: str):
36
76
  """Convertit un fichier (PDF/DOCX/PPTX) en Markdown via Docling.
@@ -67,7 +107,18 @@ def _convert_one_file(file_path_str: str):
67
107
  },
68
108
  )
69
109
  doc = doc_converter.convert(str(src)).document
110
+ md = doc.export_to_markdown(page_break_placeholder="<-!-- page-break --!->")
111
+ # 1er passage : comportement d'origine
112
+ doc = _build_converter().convert(str(src)).document
70
113
  md = doc.export_to_markdown()
114
+ message = ""
115
+
116
+ # Repli : PDF sans texte exploitable (PDF image) -> OCR pleine page
117
+ if src.suffix.lower() == ".pdf" and not _has_text(md):
118
+ doc = _build_converter(force_ocr=True).convert(str(src)).document
119
+ md = doc.export_to_markdown()
120
+ message = "ocr pleine page (pdf sans texte natif)"
121
+
71
122
  out_md.write_text(md, encoding="utf-8")
72
123
  status, message = "ok", ""
73
124
  except Exception as e: