licos-dev-sdk 0.4.4__tar.gz → 0.4.6__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.
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/DOCX.md +1 -1
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/PKG-INFO +1 -1
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/pyproject.toml +1 -1
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_inspect.py +1 -1
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/model.py +13 -3
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_docx_inspect.py +20 -0
- licos_dev_sdk-0.4.6/tests/test_langchain_protocol.py +154 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/.gitignore +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/__init__.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/_utils.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/archive.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/chart.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/data.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/diagram.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_common.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_edit.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_expectations.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_layout.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_package.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_preservation.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_structure.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_table.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_table_actions.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_read.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_table_export.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/image.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/observability.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/pdf_inspect.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/ppt_master_runtime.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/presentation.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/presentation_canvas.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/presentation_inspect.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/spreadsheet.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/spreadsheet_edit.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/spreadsheet_html.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/spreadsheet_inspect.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/web.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_document_read.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_document_spreadsheet.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_document_table_export.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_docx_structure.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_docx_table_edit.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_html_docx_features.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_model.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_observability.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_output_paths.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_ppt_master_runtime.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_presentation.py +0 -0
- {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_spreadsheet_html_edit.py +0 -0
|
@@ -62,7 +62,7 @@ def inspect_docx(source: str | Path, *, scope: str = "all", offset: int = 1, lim
|
|
|
62
62
|
if element.tag == W + "tbl":
|
|
63
63
|
grid = element.find(W + "tblGrid")
|
|
64
64
|
tables.append({"part": part, "row_count": len(element.findall(W + "tr")),
|
|
65
|
-
"column_count": len(grid) if grid is not None else None})
|
|
65
|
+
"column_count": len(grid.findall(W + "gridCol")) if grid is not None else None})
|
|
66
66
|
if element.tag not in {A + "blip", V + "imagedata"}:
|
|
67
67
|
continue
|
|
68
68
|
rid = element.get(R + "embed") or element.get(R + "link") or element.get(R + "id")
|
|
@@ -482,6 +482,7 @@ def create_chat_openai(
|
|
|
482
482
|
max_completion_tokens: int | None = None,
|
|
483
483
|
timeout: int | None = None,
|
|
484
484
|
streaming: bool = True,
|
|
485
|
+
use_responses_api: bool | None = None,
|
|
485
486
|
default_headers: dict[str, str] | None = None,
|
|
486
487
|
**kwargs: Any,
|
|
487
488
|
) -> Any:
|
|
@@ -490,6 +491,8 @@ def create_chat_openai(
|
|
|
490
491
|
`model=None`, an empty string, or `model="auto"` uses the catalog default. The
|
|
491
492
|
returned ChatOpenAI receives the current user token as its OpenAI-compatible API
|
|
492
493
|
key, so deployed projects do not need LLM_MODEL/OPENAI_MODEL env injection.
|
|
494
|
+
A catalog `response_url` selects Responses by default. Set
|
|
495
|
+
`use_responses_api=False` to use Chat Completions explicitly.
|
|
493
496
|
"""
|
|
494
497
|
try:
|
|
495
498
|
from langchain_core.callbacks import BaseCallbackHandler
|
|
@@ -500,6 +503,11 @@ def create_chat_openai(
|
|
|
500
503
|
|
|
501
504
|
runtime = _model_runtime(base_url=base_url, user_token=user_token, user_id=user_id, workspace_id=workspace_id)
|
|
502
505
|
endpoint = _resolve_chat_endpoint(runtime, model_group=model_group, requested_model=model)
|
|
506
|
+
if use_responses_api is None:
|
|
507
|
+
use_responses_api = bool(endpoint.response_url)
|
|
508
|
+
if use_responses_api and not endpoint.response_url:
|
|
509
|
+
raise ConfigurationError(f"Model {endpoint.model!r} has no response_url configured")
|
|
510
|
+
endpoint_url = endpoint.response_url if use_responses_api else endpoint.base_url
|
|
503
511
|
headers = _without_authorization(
|
|
504
512
|
_merge_headers(
|
|
505
513
|
endpoint.request_headers or endpoint.required_headers,
|
|
@@ -540,7 +548,8 @@ def create_chat_openai(
|
|
|
540
548
|
)
|
|
541
549
|
chat_kwargs: dict[str, Any] = {
|
|
542
550
|
"model": endpoint.model,
|
|
543
|
-
"base_url":
|
|
551
|
+
"base_url": _openai_sdk_base_url(endpoint_url, use_responses_api=use_responses_api),
|
|
552
|
+
"use_responses_api": use_responses_api,
|
|
544
553
|
"api_key": api_key or runtime.token,
|
|
545
554
|
"streaming": streaming,
|
|
546
555
|
**_not_none(
|
|
@@ -622,9 +631,10 @@ def _LangChainUsageHandler(
|
|
|
622
631
|
return UsageHandler()
|
|
623
632
|
|
|
624
633
|
|
|
625
|
-
def
|
|
634
|
+
def _openai_sdk_base_url(url: str, *, use_responses_api: bool) -> str:
|
|
626
635
|
trimmed = str(url or "").strip().rstrip("/")
|
|
627
|
-
|
|
636
|
+
suffix = "/responses" if use_responses_api else "/chat/completions"
|
|
637
|
+
return trimmed.removesuffix(suffix).rstrip("/")
|
|
628
638
|
|
|
629
639
|
|
|
630
640
|
def resolve_vision_endpoint(
|
|
@@ -82,6 +82,26 @@ def test_incomplete_toc_and_external_or_missing_image_relationships(tmp_path):
|
|
|
82
82
|
assert [image['status'] for image in report['images']['items']] == ['external', 'missing_relationship']
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def test_table_grid_revisions_do_not_count_as_current_columns(tmp_path):
|
|
86
|
+
path = tmp_path / 'tracked-table.docx'
|
|
87
|
+
doc = Document()
|
|
88
|
+
table = doc.add_table(rows=3, cols=2)
|
|
89
|
+
change = OxmlElement('w:tblGridChange')
|
|
90
|
+
change.set(qn('w:id'), '1')
|
|
91
|
+
previous = OxmlElement('w:tblGrid')
|
|
92
|
+
previous.append(OxmlElement('w:gridCol'))
|
|
93
|
+
change.append(previous)
|
|
94
|
+
table._tbl.tblGrid.append(change)
|
|
95
|
+
doc.save(path)
|
|
96
|
+
original = path.read_bytes()
|
|
97
|
+
assert len(Document(path).tables[0].columns) == 2
|
|
98
|
+
report = inspect_docx(path, scope='tables')
|
|
99
|
+
assert report['tables']['items'] == [
|
|
100
|
+
{'part': 'word/document.xml', 'row_count': 3, 'column_count': 2},
|
|
101
|
+
]
|
|
102
|
+
assert path.read_bytes() == original
|
|
103
|
+
|
|
104
|
+
|
|
85
105
|
def test_batch_file_assertions_resolve_from_explicit_base_and_keep_exact_text(tmp_path, monkeypatch):
|
|
86
106
|
path = tmp_path / 'report.docx'
|
|
87
107
|
text = ' 中文🙂\tA\n' * 90 + ' '
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import sys
|
|
7
|
+
import unittest
|
|
8
|
+
from unittest.mock import patch
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "licos-platform-sdk" / "src"))
|
|
14
|
+
|
|
15
|
+
from licos_dev_sdk import model
|
|
16
|
+
from licos_platform_sdk._runtime import ConfigurationError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
CHAT_URL = "https://chat.example/compatible/v1/chat/completions"
|
|
20
|
+
RESPONSES_URL = "https://responses.example/api/v1/responses"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LangChainProtocolTests(unittest.TestCase):
|
|
24
|
+
def setUp(self):
|
|
25
|
+
runtime = model.ModelRuntime(base_url="https://platform.example", token="user-token")
|
|
26
|
+
self.runtime_patch = patch.object(model, "_model_runtime", return_value=runtime)
|
|
27
|
+
self.usage_patch = patch.object(model, "_enqueue_model_usage")
|
|
28
|
+
self.runtime_patch.start()
|
|
29
|
+
self.usage = self.usage_patch.start()
|
|
30
|
+
self.addCleanup(self.runtime_patch.stop)
|
|
31
|
+
self.addCleanup(self.usage_patch.stop)
|
|
32
|
+
|
|
33
|
+
def endpoint(self, response_url):
|
|
34
|
+
return model.ModelEndpoint(
|
|
35
|
+
provider="test", capability="chat", model="test-model", base_url=CHAT_URL,
|
|
36
|
+
response_url=response_url, request_headers={"X-Provider-Route": "route-1"},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def response(self, request):
|
|
40
|
+
if str(request.url) == RESPONSES_URL:
|
|
41
|
+
body = {
|
|
42
|
+
"id": "resp_1", "object": "response", "created_at": 1,
|
|
43
|
+
"model": "test-model", "status": "completed", "error": None,
|
|
44
|
+
"output": [{"id": "msg_1", "type": "message", "role": "assistant", "status": "completed",
|
|
45
|
+
"content": [{"type": "output_text", "text": "answer", "annotations": []}]}],
|
|
46
|
+
"usage": {"input_tokens": 3, "output_tokens": 2, "total_tokens": 5},
|
|
47
|
+
}
|
|
48
|
+
else:
|
|
49
|
+
self.assertEqual(str(request.url), CHAT_URL)
|
|
50
|
+
body = {
|
|
51
|
+
"id": "chat_1", "object": "chat.completion", "created": 1, "model": "test-model",
|
|
52
|
+
"choices": [{"index": 0, "message": {"role": "assistant", "content": "answer"}, "finish_reason": "stop"}],
|
|
53
|
+
"usage": {"prompt_tokens": 3, "completion_tokens": 2, "total_tokens": 5},
|
|
54
|
+
}
|
|
55
|
+
return httpx.Response(200, json=body)
|
|
56
|
+
|
|
57
|
+
def test_catalog_selects_actual_protocol_and_address(self):
|
|
58
|
+
for response_url, override, expected in [
|
|
59
|
+
(RESPONSES_URL, None, RESPONSES_URL),
|
|
60
|
+
(None, None, CHAT_URL),
|
|
61
|
+
(RESPONSES_URL, False, CHAT_URL),
|
|
62
|
+
(RESPONSES_URL, True, RESPONSES_URL),
|
|
63
|
+
]:
|
|
64
|
+
for asynchronous in (False, True):
|
|
65
|
+
with self.subTest(response_url=response_url, override=override, asynchronous=asynchronous):
|
|
66
|
+
requests = []
|
|
67
|
+
|
|
68
|
+
def handle(request):
|
|
69
|
+
requests.append(request)
|
|
70
|
+
return self.response(request)
|
|
71
|
+
|
|
72
|
+
transport = httpx.MockTransport(handle)
|
|
73
|
+
sync_client = httpx.Client(transport=transport)
|
|
74
|
+
async_client = httpx.AsyncClient(transport=transport)
|
|
75
|
+
try:
|
|
76
|
+
with patch.object(model, "_resolve_chat_endpoint", return_value=self.endpoint(response_url)):
|
|
77
|
+
options = {} if override is None else {"use_responses_api": override}
|
|
78
|
+
chat = model.create_chat_openai(streaming=False, http_client=sync_client,
|
|
79
|
+
http_async_client=async_client, **options)
|
|
80
|
+
result = asyncio.run(chat.ainvoke("question")) if asynchronous else chat.invoke("question")
|
|
81
|
+
self.assertEqual(len(requests), 1)
|
|
82
|
+
self.assertEqual(str(requests[0].url), expected)
|
|
83
|
+
payload = json.loads(requests[0].content)
|
|
84
|
+
self.assertIn("input" if expected == RESPONSES_URL else "messages", payload)
|
|
85
|
+
self.assertEqual(requests[0].headers["Authorization"], "Bearer user-token")
|
|
86
|
+
self.assertEqual(requests[0].headers["X-Provider-Route"], "route-1")
|
|
87
|
+
self.assertTrue(requests[0].headers[model.MODEL_REQUEST_ID_HEADER])
|
|
88
|
+
self.assertEqual(result.text, "answer")
|
|
89
|
+
self.assertEqual(result.usage_metadata["total_tokens"], 5)
|
|
90
|
+
self.assertEqual(chat.use_responses_api, expected == RESPONSES_URL)
|
|
91
|
+
finally:
|
|
92
|
+
sync_client.close()
|
|
93
|
+
asyncio.run(async_client.aclose())
|
|
94
|
+
|
|
95
|
+
def test_explicit_responses_requires_catalog_address(self):
|
|
96
|
+
with patch.object(model, "_resolve_chat_endpoint", return_value=self.endpoint(None)):
|
|
97
|
+
with self.assertRaisesRegex(ConfigurationError, "response_url"):
|
|
98
|
+
model.create_chat_openai(use_responses_api=True)
|
|
99
|
+
|
|
100
|
+
def test_streaming_uses_selected_endpoint_and_keeps_usage(self):
|
|
101
|
+
for response_url in (RESPONSES_URL, None):
|
|
102
|
+
for asynchronous in (False, True):
|
|
103
|
+
with self.subTest(response_url=response_url, asynchronous=asynchronous):
|
|
104
|
+
requests = []
|
|
105
|
+
|
|
106
|
+
def handle(request):
|
|
107
|
+
requests.append(request)
|
|
108
|
+
body = self.response(request).json()
|
|
109
|
+
if response_url:
|
|
110
|
+
events = [
|
|
111
|
+
{"type": "response.created", "response": {**body, "status": "in_progress", "output": []}},
|
|
112
|
+
{"type": "response.output_text.delta", "output_index": 0, "content_index": 0,
|
|
113
|
+
"item_id": "msg_1", "delta": "answer"},
|
|
114
|
+
{"type": "response.output_text.done", "output_index": 0, "content_index": 0,
|
|
115
|
+
"item_id": "msg_1", "text": "answer"},
|
|
116
|
+
{"type": "response.completed", "response": body},
|
|
117
|
+
]
|
|
118
|
+
text = "".join(f"event: {event['type']}\ndata: {json.dumps({**event, 'sequence_number': i})}\n\n"
|
|
119
|
+
for i, event in enumerate(events))
|
|
120
|
+
else:
|
|
121
|
+
base = {"id": "chat_1", "object": "chat.completion.chunk", "created": 1, "model": "test-model"}
|
|
122
|
+
events = [
|
|
123
|
+
{**base, "choices": [{"index": 0, "delta": {"role": "assistant", "content": "answer"}, "finish_reason": None}]},
|
|
124
|
+
{**base, "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]},
|
|
125
|
+
{**base, "choices": [], "usage": body["usage"]},
|
|
126
|
+
]
|
|
127
|
+
text = "".join(f"data: {json.dumps(event)}\n\n" for event in events) + "data: [DONE]\n\n"
|
|
128
|
+
return httpx.Response(200, headers={"Content-Type": "text/event-stream"}, content=text)
|
|
129
|
+
|
|
130
|
+
transport = httpx.MockTransport(handle)
|
|
131
|
+
sync_client = httpx.Client(transport=transport)
|
|
132
|
+
async_client = httpx.AsyncClient(transport=transport)
|
|
133
|
+
try:
|
|
134
|
+
with patch.object(model, "_resolve_chat_endpoint", return_value=self.endpoint(response_url)):
|
|
135
|
+
chat = model.create_chat_openai(http_client=sync_client, http_async_client=async_client)
|
|
136
|
+
|
|
137
|
+
async def collect():
|
|
138
|
+
return [chunk async for chunk in chat.astream("question")]
|
|
139
|
+
|
|
140
|
+
chunks = asyncio.run(collect()) if asynchronous else list(chat.stream("question"))
|
|
141
|
+
combined = chunks[0]
|
|
142
|
+
for chunk in chunks[1:]:
|
|
143
|
+
combined += chunk
|
|
144
|
+
self.assertEqual(str(requests[0].url), response_url or CHAT_URL)
|
|
145
|
+
self.assertTrue(json.loads(requests[0].content)["stream"])
|
|
146
|
+
self.assertEqual(combined.text, "answer")
|
|
147
|
+
self.assertEqual(combined.usage_metadata["total_tokens"], 5)
|
|
148
|
+
finally:
|
|
149
|
+
sync_client.close()
|
|
150
|
+
asyncio.run(async_client.aclose())
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_table_actions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|