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.
- {io4it-4.0.26 → io4it-4.0.28}/PKG-INFO +1 -1
- {io4it-4.0.26 → io4it-4.0.28}/io4it.egg-info/PKG-INFO +1 -1
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/custom_api.py +93 -14
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWCustomLLMApi.py +93 -9
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWDoclingToMarkdown.py +52 -1
- io4it-4.0.28/orangecontrib/IO4IT/widgets/OWJsonToDataTable.py +384 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWMarkdownLoader.py +3 -3
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWProcessPoolExecutor.py +1 -1
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/ow_json_to_data_table.ui +22 -3
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owcustomllmapi.ui +89 -12
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owfile2images.ui +4 -4
- {io4it-4.0.26 → io4it-4.0.28}/setup.py +1 -1
- io4it-4.0.26/orangecontrib/IO4IT/widgets/OWJsonToDataTable.py +0 -167
- {io4it-4.0.26 → io4it-4.0.28}/io4it.egg-info/SOURCES.txt +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/io4it.egg-info/dependency_links.txt +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/io4it.egg-info/entry_points.txt +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/io4it.egg-info/namespace_packages.txt +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/io4it.egg-info/requires.txt +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/io4it.egg-info/top_level.txt +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/__init__.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/ocr_function/__init__.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/ocr_function/word_converter.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/toolViews/designer/key_manager_ui.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/toolViews/designer/link_workflow_manager_ui.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/toolViews/key_manager_ui.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/toolViews/linkHTLMWorkflow_manager_ui.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/__init__.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/environment_collector.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/file2images.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/keys_manager.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/mail.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/pool_exec_utils.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/pst_extractor/TigerODMPST.dll +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/pst_extractor/__init__.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/pst_extractor/export_pst.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/pst_extractor/libpff.dll +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/pst_extractor/z.dll +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/secret_manager.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/utils/utils_md.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWChatGpt.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWDeep_Search.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWExportMarkdown.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWExtractTablesDocxToXlsx.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWInboxMailMonitoring.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWMD2HTML.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWMarkdownizer.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWOfficeNormalizer.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWPPTX2JPG.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWParserHTML.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWPdfType.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWPipAuditor.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWS3Uploader.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWS3downloader.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWS3list.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWWebSearch.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWXLSX_insert.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWmailLoader.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWmailSender.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/OWwordpdf2docx.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/__init__.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/__init__.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/nogui.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/ow_extract_tables_docx_to_xlsx.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/ow_file_ext_selector.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owchatgpt.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owdeepsearch.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owdoclingtomarkdown.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owexportmarkdown.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owinboxmailmonitoring.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owinsertdatatoexcel.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owmailloader.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owmailsender.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owmarkdownizer.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owmarkdownloader.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owmd2html.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owofficenormalizer.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owparserhtml.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owpdftype.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owpipauditor.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owprocesspoolexecutor.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owspeechtotext.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owvisualizationer.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/owwebsearch.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/designer/wordpdf2docx.ui +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/__init__.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/chatgpt.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/check_pdf.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/deepsearch.svg +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/dep_md_old.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/download.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/export_md.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/extract_table.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/file2images.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/file_extensor.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/html.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/json-file.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/list_aws.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/load_md.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/mail_loader.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/mail_writer.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/md.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/monitor-email.svg +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/office_normalizer.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/owcustomllmapi.svg +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/owmd2html.svg +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/pip_auditor.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/process_pool_executor.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/speech_to_text.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/upload.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/visualizationer.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/websearch.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/wordpdf2docx.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons/xlsx_insert.png +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/IO4IT/widgets/icons_dev/__init__.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/orangecontrib/__init__.py +0 -0
- {io4it-4.0.26 → io4it-4.0.28}/setup.cfg +0 -0
|
@@ -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"
|
|
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
|
-
|
|
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
|
|
36
|
-
|
|
76
|
+
return None, ""
|
|
77
|
+
|
|
37
78
|
json_data = json.loads(match.group())
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 = "
|
|
169
|
+
kind, token = None, ""
|
|
94
170
|
print("An error occured when trying to get token from stream response:", e)
|
|
95
|
-
answer
|
|
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.
|
|
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.
|
|
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("/
|
|
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(
|
|
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 "/
|
|
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
|
|
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.
|
|
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
|
-
|
|
214
|
-
|
|
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:
|