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.
Files changed (51) hide show
  1. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/DOCX.md +1 -1
  2. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/PKG-INFO +1 -1
  3. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/pyproject.toml +1 -1
  4. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_inspect.py +1 -1
  5. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/model.py +13 -3
  6. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_docx_inspect.py +20 -0
  7. licos_dev_sdk-0.4.6/tests/test_langchain_protocol.py +154 -0
  8. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/.gitignore +0 -0
  9. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/__init__.py +0 -0
  10. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/_utils.py +0 -0
  11. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/archive.py +0 -0
  12. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/chart.py +0 -0
  13. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/data.py +0 -0
  14. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/diagram.py +0 -0
  15. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document.py +0 -0
  16. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_common.py +0 -0
  17. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx.py +0 -0
  18. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_edit.py +0 -0
  19. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_expectations.py +0 -0
  20. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_layout.py +0 -0
  21. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_package.py +0 -0
  22. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_preservation.py +0 -0
  23. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_structure.py +0 -0
  24. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_table.py +0 -0
  25. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_table_actions.py +0 -0
  26. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_read.py +0 -0
  27. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_table_export.py +0 -0
  28. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/image.py +0 -0
  29. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/observability.py +0 -0
  30. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/pdf_inspect.py +0 -0
  31. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/ppt_master_runtime.py +0 -0
  32. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/presentation.py +0 -0
  33. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/presentation_canvas.py +0 -0
  34. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/presentation_inspect.py +0 -0
  35. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/spreadsheet.py +0 -0
  36. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/spreadsheet_edit.py +0 -0
  37. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/spreadsheet_html.py +0 -0
  38. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/spreadsheet_inspect.py +0 -0
  39. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/web.py +0 -0
  40. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_document_read.py +0 -0
  41. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_document_spreadsheet.py +0 -0
  42. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_document_table_export.py +0 -0
  43. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_docx_structure.py +0 -0
  44. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_docx_table_edit.py +0 -0
  45. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_html_docx_features.py +0 -0
  46. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_model.py +0 -0
  47. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_observability.py +0 -0
  48. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_output_paths.py +0 -0
  49. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_ppt_master_runtime.py +0 -0
  50. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_presentation.py +0 -0
  51. {licos_dev_sdk-0.4.4 → licos_dev_sdk-0.4.6}/tests/test_spreadsheet_html_edit.py +0 -0
@@ -1,4 +1,4 @@
1
- # Word SDK / CLI 0.4.4
1
+ # Word SDK / CLI 0.4.5
2
2
 
3
3
  Cloud document execution stays in `licos-dev-sdk` and `licos-dev`. CLI commands
4
4
  only parse arguments and call the SDK; no additional Agent-native Word tools are
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: licos-dev-sdk
3
- Version: 0.4.4
3
+ Version: 0.4.6
4
4
  Summary: LICOS Dev SDK - file generation and model capability clients
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: beautifulsoup4>=4.12
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "licos-dev-sdk"
7
- version = "0.4.4"
7
+ version = "0.4.6"
8
8
  description = "LICOS Dev SDK - file generation and model capability clients"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -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": _chat_completions_sdk_base_url(endpoint.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 _chat_completions_sdk_base_url(url: str) -> str:
634
+ def _openai_sdk_base_url(url: str, *, use_responses_api: bool) -> str:
626
635
  trimmed = str(url or "").strip().rstrip("/")
627
- return trimmed.removesuffix("/chat/completions").rstrip("/")
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