@aj-archipelago/cortex 1.3.67 โ†’ 1.4.0

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 (64) hide show
  1. package/config.js +27 -0
  2. package/helper-apps/cortex-doc-to-pdf/DocToPdfFunction/__init__.py +3 -0
  3. package/helper-apps/cortex-doc-to-pdf/DocToPdfFunction/function.json +20 -0
  4. package/helper-apps/cortex-doc-to-pdf/Dockerfile +46 -0
  5. package/helper-apps/cortex-doc-to-pdf/README.md +408 -0
  6. package/helper-apps/cortex-doc-to-pdf/converter.py +157 -0
  7. package/helper-apps/cortex-doc-to-pdf/docker-compose.yml +23 -0
  8. package/helper-apps/cortex-doc-to-pdf/document_converter.py +181 -0
  9. package/helper-apps/cortex-doc-to-pdf/examples/README.md +252 -0
  10. package/helper-apps/cortex-doc-to-pdf/examples/nodejs-client.js +266 -0
  11. package/helper-apps/cortex-doc-to-pdf/examples/package-lock.json +297 -0
  12. package/helper-apps/cortex-doc-to-pdf/examples/package.json +23 -0
  13. package/helper-apps/cortex-doc-to-pdf/function_app.py +85 -0
  14. package/helper-apps/cortex-doc-to-pdf/host.json +16 -0
  15. package/helper-apps/cortex-doc-to-pdf/request_handlers.py +193 -0
  16. package/helper-apps/cortex-doc-to-pdf/requirements.txt +3 -0
  17. package/helper-apps/cortex-doc-to-pdf/tests/run_tests.sh +26 -0
  18. package/helper-apps/cortex-doc-to-pdf/tests/test_conversion.py +320 -0
  19. package/helper-apps/cortex-doc-to-pdf/tests/test_streaming.py +419 -0
  20. package/helper-apps/cortex-file-handler/package-lock.json +1 -0
  21. package/helper-apps/cortex-file-handler/package.json +1 -0
  22. package/helper-apps/cortex-file-handler/src/services/ConversionService.js +81 -8
  23. package/helper-apps/cortex-file-handler/tests/FileConversionService.test.js +54 -7
  24. package/helper-apps/cortex-file-handler/tests/getOperations.test.js +19 -7
  25. package/lib/encodeCache.js +5 -0
  26. package/lib/keyValueStorageClient.js +5 -0
  27. package/lib/logger.js +1 -1
  28. package/lib/pathwayTools.js +8 -1
  29. package/lib/redisSubscription.js +6 -0
  30. package/lib/requestExecutor.js +4 -0
  31. package/lib/util.js +88 -0
  32. package/package.json +1 -1
  33. package/pathways/basePathway.js +3 -3
  34. package/pathways/bing_afagent.js +1 -0
  35. package/pathways/gemini_15_vision.js +1 -1
  36. package/pathways/google_cse.js +2 -2
  37. package/pathways/image_gemini_25.js +85 -0
  38. package/pathways/image_prompt_optimizer_gemini_25.js +149 -0
  39. package/pathways/image_qwen.js +28 -0
  40. package/pathways/image_seedream4.js +26 -0
  41. package/pathways/rag.js +1 -1
  42. package/pathways/rag_jarvis.js +1 -1
  43. package/pathways/system/entity/sys_entity_continue.js +1 -1
  44. package/pathways/system/entity/sys_generator_results.js +1 -1
  45. package/pathways/system/entity/tools/sys_tool_google_search.js +15 -2
  46. package/pathways/system/entity/tools/sys_tool_grok_x_search.js +3 -3
  47. package/pathways/system/entity/tools/sys_tool_image.js +28 -23
  48. package/pathways/system/entity/tools/sys_tool_image_gemini.js +135 -0
  49. package/server/graphql.js +9 -2
  50. package/server/modelExecutor.js +4 -0
  51. package/server/pathwayResolver.js +19 -18
  52. package/server/plugins/claude3VertexPlugin.js +13 -8
  53. package/server/plugins/gemini15ChatPlugin.js +15 -10
  54. package/server/plugins/gemini15VisionPlugin.js +2 -23
  55. package/server/plugins/gemini25ImagePlugin.js +155 -0
  56. package/server/plugins/modelPlugin.js +3 -2
  57. package/server/plugins/openAiChatPlugin.js +6 -6
  58. package/server/plugins/replicateApiPlugin.js +268 -12
  59. package/server/plugins/veoVideoPlugin.js +15 -1
  60. package/server/rest.js +2 -0
  61. package/server/typeDef.js +96 -10
  62. package/tests/integration/apptekTranslatePlugin.integration.test.js +1 -1
  63. package/tests/unit/core/pathwayManager.test.js +2 -4
  64. package/tests/unit/plugins/gemini25ImagePlugin.test.js +294 -0
@@ -0,0 +1,297 @@
1
+ {
2
+ "name": "doc-to-pdf-client-examples",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "doc-to-pdf-client-examples",
9
+ "version": "1.0.0",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "axios": "^1.6.0",
13
+ "form-data": "^4.0.0"
14
+ },
15
+ "devDependencies": {}
16
+ },
17
+ "node_modules/asynckit": {
18
+ "version": "0.4.0",
19
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
20
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
21
+ "license": "MIT"
22
+ },
23
+ "node_modules/axios": {
24
+ "version": "1.12.2",
25
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
26
+ "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "follow-redirects": "^1.15.6",
30
+ "form-data": "^4.0.4",
31
+ "proxy-from-env": "^1.1.0"
32
+ }
33
+ },
34
+ "node_modules/call-bind-apply-helpers": {
35
+ "version": "1.0.2",
36
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
37
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
38
+ "license": "MIT",
39
+ "dependencies": {
40
+ "es-errors": "^1.3.0",
41
+ "function-bind": "^1.1.2"
42
+ },
43
+ "engines": {
44
+ "node": ">= 0.4"
45
+ }
46
+ },
47
+ "node_modules/combined-stream": {
48
+ "version": "1.0.8",
49
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
50
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
51
+ "license": "MIT",
52
+ "dependencies": {
53
+ "delayed-stream": "~1.0.0"
54
+ },
55
+ "engines": {
56
+ "node": ">= 0.8"
57
+ }
58
+ },
59
+ "node_modules/delayed-stream": {
60
+ "version": "1.0.0",
61
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
62
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
63
+ "license": "MIT",
64
+ "engines": {
65
+ "node": ">=0.4.0"
66
+ }
67
+ },
68
+ "node_modules/dunder-proto": {
69
+ "version": "1.0.1",
70
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
71
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
72
+ "license": "MIT",
73
+ "dependencies": {
74
+ "call-bind-apply-helpers": "^1.0.1",
75
+ "es-errors": "^1.3.0",
76
+ "gopd": "^1.2.0"
77
+ },
78
+ "engines": {
79
+ "node": ">= 0.4"
80
+ }
81
+ },
82
+ "node_modules/es-define-property": {
83
+ "version": "1.0.1",
84
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
85
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
86
+ "license": "MIT",
87
+ "engines": {
88
+ "node": ">= 0.4"
89
+ }
90
+ },
91
+ "node_modules/es-errors": {
92
+ "version": "1.3.0",
93
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
94
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
95
+ "license": "MIT",
96
+ "engines": {
97
+ "node": ">= 0.4"
98
+ }
99
+ },
100
+ "node_modules/es-object-atoms": {
101
+ "version": "1.1.1",
102
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
103
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
104
+ "license": "MIT",
105
+ "dependencies": {
106
+ "es-errors": "^1.3.0"
107
+ },
108
+ "engines": {
109
+ "node": ">= 0.4"
110
+ }
111
+ },
112
+ "node_modules/es-set-tostringtag": {
113
+ "version": "2.1.0",
114
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
115
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
116
+ "license": "MIT",
117
+ "dependencies": {
118
+ "es-errors": "^1.3.0",
119
+ "get-intrinsic": "^1.2.6",
120
+ "has-tostringtag": "^1.0.2",
121
+ "hasown": "^2.0.2"
122
+ },
123
+ "engines": {
124
+ "node": ">= 0.4"
125
+ }
126
+ },
127
+ "node_modules/follow-redirects": {
128
+ "version": "1.15.11",
129
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
130
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
131
+ "funding": [
132
+ {
133
+ "type": "individual",
134
+ "url": "https://github.com/sponsors/RubenVerborgh"
135
+ }
136
+ ],
137
+ "license": "MIT",
138
+ "engines": {
139
+ "node": ">=4.0"
140
+ },
141
+ "peerDependenciesMeta": {
142
+ "debug": {
143
+ "optional": true
144
+ }
145
+ }
146
+ },
147
+ "node_modules/form-data": {
148
+ "version": "4.0.4",
149
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
150
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
151
+ "license": "MIT",
152
+ "dependencies": {
153
+ "asynckit": "^0.4.0",
154
+ "combined-stream": "^1.0.8",
155
+ "es-set-tostringtag": "^2.1.0",
156
+ "hasown": "^2.0.2",
157
+ "mime-types": "^2.1.12"
158
+ },
159
+ "engines": {
160
+ "node": ">= 6"
161
+ }
162
+ },
163
+ "node_modules/function-bind": {
164
+ "version": "1.1.2",
165
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
166
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
167
+ "license": "MIT",
168
+ "funding": {
169
+ "url": "https://github.com/sponsors/ljharb"
170
+ }
171
+ },
172
+ "node_modules/get-intrinsic": {
173
+ "version": "1.3.0",
174
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
175
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
176
+ "license": "MIT",
177
+ "dependencies": {
178
+ "call-bind-apply-helpers": "^1.0.2",
179
+ "es-define-property": "^1.0.1",
180
+ "es-errors": "^1.3.0",
181
+ "es-object-atoms": "^1.1.1",
182
+ "function-bind": "^1.1.2",
183
+ "get-proto": "^1.0.1",
184
+ "gopd": "^1.2.0",
185
+ "has-symbols": "^1.1.0",
186
+ "hasown": "^2.0.2",
187
+ "math-intrinsics": "^1.1.0"
188
+ },
189
+ "engines": {
190
+ "node": ">= 0.4"
191
+ },
192
+ "funding": {
193
+ "url": "https://github.com/sponsors/ljharb"
194
+ }
195
+ },
196
+ "node_modules/get-proto": {
197
+ "version": "1.0.1",
198
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
199
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
200
+ "license": "MIT",
201
+ "dependencies": {
202
+ "dunder-proto": "^1.0.1",
203
+ "es-object-atoms": "^1.0.0"
204
+ },
205
+ "engines": {
206
+ "node": ">= 0.4"
207
+ }
208
+ },
209
+ "node_modules/gopd": {
210
+ "version": "1.2.0",
211
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
212
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
213
+ "license": "MIT",
214
+ "engines": {
215
+ "node": ">= 0.4"
216
+ },
217
+ "funding": {
218
+ "url": "https://github.com/sponsors/ljharb"
219
+ }
220
+ },
221
+ "node_modules/has-symbols": {
222
+ "version": "1.1.0",
223
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
224
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
225
+ "license": "MIT",
226
+ "engines": {
227
+ "node": ">= 0.4"
228
+ },
229
+ "funding": {
230
+ "url": "https://github.com/sponsors/ljharb"
231
+ }
232
+ },
233
+ "node_modules/has-tostringtag": {
234
+ "version": "1.0.2",
235
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
236
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
237
+ "license": "MIT",
238
+ "dependencies": {
239
+ "has-symbols": "^1.0.3"
240
+ },
241
+ "engines": {
242
+ "node": ">= 0.4"
243
+ },
244
+ "funding": {
245
+ "url": "https://github.com/sponsors/ljharb"
246
+ }
247
+ },
248
+ "node_modules/hasown": {
249
+ "version": "2.0.2",
250
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
251
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
252
+ "license": "MIT",
253
+ "dependencies": {
254
+ "function-bind": "^1.1.2"
255
+ },
256
+ "engines": {
257
+ "node": ">= 0.4"
258
+ }
259
+ },
260
+ "node_modules/math-intrinsics": {
261
+ "version": "1.1.0",
262
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
263
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
264
+ "license": "MIT",
265
+ "engines": {
266
+ "node": ">= 0.4"
267
+ }
268
+ },
269
+ "node_modules/mime-db": {
270
+ "version": "1.52.0",
271
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
272
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
273
+ "license": "MIT",
274
+ "engines": {
275
+ "node": ">= 0.6"
276
+ }
277
+ },
278
+ "node_modules/mime-types": {
279
+ "version": "2.1.35",
280
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
281
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
282
+ "license": "MIT",
283
+ "dependencies": {
284
+ "mime-db": "1.52.0"
285
+ },
286
+ "engines": {
287
+ "node": ">= 0.6"
288
+ }
289
+ },
290
+ "node_modules/proxy-from-env": {
291
+ "version": "1.1.0",
292
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
293
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
294
+ "license": "MIT"
295
+ }
296
+ }
297
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "doc-to-pdf-client-examples",
3
+ "version": "1.0.0",
4
+ "description": "Client examples for Document to PDF Converter service",
5
+ "main": "nodejs-client.js",
6
+ "scripts": {
7
+ "example": "node nodejs-client.js",
8
+ "test": "node nodejs-client.js"
9
+ },
10
+ "dependencies": {
11
+ "axios": "^1.6.0",
12
+ "form-data": "^4.0.0"
13
+ },
14
+ "devDependencies": {},
15
+ "keywords": [
16
+ "pdf",
17
+ "converter",
18
+ "document",
19
+ "streaming"
20
+ ],
21
+ "author": "",
22
+ "license": "ISC"
23
+ }
@@ -0,0 +1,85 @@
1
+ """Main application entry point for Azure Functions and standalone server."""
2
+
3
+ import azure.functions as func
4
+ import logging
5
+ import asyncio
6
+ from aiohttp import web
7
+ import os
8
+ from request_handlers import handle_azure_function_request, handle_aiohttp_request
9
+
10
+ # Azure Functions app
11
+ app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
12
+
13
+
14
+ @app.route(route="convert")
15
+ async def http_convert_trigger(req: func.HttpRequest) -> func.HttpResponse:
16
+ """
17
+ Azure Function HTTP trigger for document to PDF conversion.
18
+
19
+ Note: For GET requests with URLs containing special characters (like Azure SAS tokens),
20
+ the URI should be URL-encoded, or use POST with JSON body instead.
21
+ """
22
+ return await handle_azure_function_request(req)
23
+
24
+
25
+ # ============================================================================
26
+ # Standalone aiohttp server for container deployment
27
+ # ============================================================================
28
+
29
+ async def main_server():
30
+ """Main function to run the standalone aiohttp server."""
31
+ logging.basicConfig(
32
+ level=logging.INFO,
33
+ format='%(asctime)s - %(levelname)s - %(message)s'
34
+ )
35
+
36
+ port_str = os.environ.get("PORT", "8080")
37
+ try:
38
+ port = int(port_str)
39
+ except ValueError:
40
+ logging.warning(f"Invalid PORT environment variable '{port_str}'. Defaulting to 8080.")
41
+ port = 8080
42
+
43
+ aiohttp_app = web.Application()
44
+
45
+ # Conversion endpoints (both /convert and / root)
46
+ aiohttp_app.router.add_post('/convert', handle_aiohttp_request)
47
+ aiohttp_app.router.add_get('/convert', handle_aiohttp_request)
48
+ aiohttp_app.router.add_post('/', handle_aiohttp_request) # Root path also works
49
+
50
+ # Health check endpoint
51
+ async def health_check(request):
52
+ return web.json_response({"status": "healthy", "service": "doc-to-pdf-converter"})
53
+
54
+ aiohttp_app.router.add_get('/health', health_check)
55
+ aiohttp_app.router.add_get('/', health_check) # GET / returns health check
56
+
57
+ runner = web.AppRunner(aiohttp_app)
58
+ await runner.setup()
59
+ site = web.TCPSite(runner, '0.0.0.0', port)
60
+ await site.start()
61
+
62
+ logging.info(f"๐Ÿš€ Document to PDF Converter server started on http://0.0.0.0:{port}")
63
+ print(f"======== Running on http://0.0.0.0:{port} ========")
64
+ print(f"======== Convert: POST http://0.0.0.0:{port}/ or /convert ========")
65
+ print(f"======== Health: GET http://0.0.0.0:{port}/health ========")
66
+ print("(Press CTRL+C to quit)")
67
+
68
+ try:
69
+ while True:
70
+ await asyncio.sleep(3600)
71
+ except KeyboardInterrupt:
72
+ logging.info("Server shutting down...")
73
+ finally:
74
+ await runner.cleanup()
75
+ logging.info("Server stopped.")
76
+
77
+
78
+ if __name__ == "__main__":
79
+ """Run as standalone server when executed directly."""
80
+ try:
81
+ asyncio.run(main_server())
82
+ except KeyboardInterrupt:
83
+ logging.info("Application shut down by user.")
84
+ except Exception as e:
85
+ logging.critical(f"Application failed to start or crashed: {e}")
@@ -0,0 +1,16 @@
1
+ {
2
+ "version": "2.0",
3
+ "logging": {
4
+ "applicationInsights": {
5
+ "samplingSettings": {
6
+ "isEnabled": true,
7
+ "maxTelemetryItemsPerSecond": 20
8
+ }
9
+ }
10
+ },
11
+ "extensionBundle": {
12
+ "id": "Microsoft.Azure.Functions.ExtensionBundle",
13
+ "version": "[4.*, 5.0.0)"
14
+ },
15
+ "functionTimeout": "00:10:00"
16
+ }
@@ -0,0 +1,193 @@
1
+ """HTTP request handlers for both Azure Functions and standalone server."""
2
+
3
+ import logging
4
+ import json
5
+ import urllib.parse
6
+ from aiohttp import web
7
+ import azure.functions as func
8
+ from document_converter import convert_from_uri, convert_from_stream
9
+
10
+
11
+ async def handle_azure_function_request(req: func.HttpRequest) -> func.HttpResponse:
12
+ """
13
+ Azure Function HTTP trigger handler.
14
+
15
+ Supports GET and POST methods.
16
+ Accepts 'uri' parameter (query string or JSON body) or file upload.
17
+ Returns PDF as binary data with appropriate headers.
18
+ """
19
+ logging.info('Azure Function triggered')
20
+
21
+ # Get URI parameter from query string or request body
22
+ uri = req.params.get('uri')
23
+ if not uri:
24
+ try:
25
+ req_body = await req.get_json()
26
+ uri = req_body.get('uri')
27
+ except ValueError:
28
+ pass
29
+
30
+ if not uri:
31
+ return func.HttpResponse(
32
+ json.dumps({
33
+ "error": "Missing 'uri' parameter",
34
+ "message": "Please provide a 'uri' parameter with the document URL to convert. For URLs with special characters (like SAS tokens), use POST with JSON body or URL-encode the URI parameter."
35
+ }),
36
+ status_code=400,
37
+ mimetype="application/json"
38
+ )
39
+
40
+ # URL decode if it looks encoded (contains %XX patterns)
41
+ if '%' in uri:
42
+ try:
43
+ uri = urllib.parse.unquote(uri)
44
+ logging.info(f"URL-decoded URI to: {uri[:100]}...")
45
+ except Exception as e:
46
+ logging.warning(f"Failed to URL-decode URI: {e}")
47
+
48
+ # Process the conversion
49
+ result = await convert_from_uri(uri)
50
+
51
+ if result.get("success"):
52
+ # Return PDF as binary data
53
+ return func.HttpResponse(
54
+ body=result["data"],
55
+ status_code=200,
56
+ mimetype="application/pdf",
57
+ headers={
58
+ "Content-Disposition": f'attachment; filename="{result["filename"]}"',
59
+ "Content-Type": "application/pdf"
60
+ }
61
+ )
62
+ else:
63
+ # Return error as JSON
64
+ error_response = {k: v for k, v in result.items() if k != "success"}
65
+ return func.HttpResponse(
66
+ json.dumps(error_response),
67
+ status_code=400 if "Unsupported" in str(result.get("error")) or "download" in str(result.get("error")) else 500,
68
+ mimetype="application/json"
69
+ )
70
+
71
+
72
+ async def handle_aiohttp_request(request: web.Request) -> web.Response:
73
+ """
74
+ aiohttp standalone server request handler.
75
+
76
+ Supports file upload (multipart/form-data) and URI-based conversion.
77
+ Returns streaming PDF response to avoid RAM bloat.
78
+ """
79
+ logging.info(f'Request received. Content-Type: {request.content_type}')
80
+
81
+ result = None
82
+
83
+ # Check if this is a multipart file upload (streaming upload)
84
+ if request.content_type and 'multipart/form-data' in request.content_type:
85
+ logging.info("Processing multipart file upload")
86
+ try:
87
+ reader = await request.multipart()
88
+ file_data = None
89
+ filename = None
90
+
91
+ # Stream the file data
92
+ async for part in reader:
93
+ if part.name == 'file':
94
+ filename = part.filename or 'document'
95
+ # Read file in chunks to avoid RAM bloat
96
+ chunks = []
97
+ chunk_count = 0
98
+ while True:
99
+ chunk = await part.read_chunk(8192) # 8KB chunks
100
+ if not chunk:
101
+ break
102
+ chunks.append(chunk)
103
+ chunk_count += 1
104
+
105
+ file_data = b''.join(chunks)
106
+ logging.info(f"Received file: {filename}, size: {len(file_data)} bytes ({chunk_count} chunks)")
107
+ break
108
+
109
+ if not file_data:
110
+ return web.json_response(
111
+ {"error": "No file provided in multipart upload"},
112
+ status=400
113
+ )
114
+
115
+ # Convert from uploaded file
116
+ result = await convert_from_stream(file_data, filename)
117
+
118
+ except Exception as e:
119
+ logging.error(f"Error processing multipart upload: {e}", exc_info=True)
120
+ return web.json_response(
121
+ {"error": "Failed to process file upload", "details": str(e)},
122
+ status=400
123
+ )
124
+ else:
125
+ # Try URI-based conversion
126
+ uri = None
127
+
128
+ # Try to get URI from query parameters
129
+ uri = request.query.get('uri')
130
+ if uri:
131
+ logging.info(f"Found URI in query parameters: {uri[:100]}...")
132
+ # URL decode if it looks encoded
133
+ if '%' in uri:
134
+ try:
135
+ decoded_uri = urllib.parse.unquote(uri)
136
+ logging.info(f"URL-decoded URI to: {decoded_uri[:100]}...")
137
+ uri = decoded_uri
138
+ except Exception as e:
139
+ logging.warning(f"Failed to URL-decode URI: {e}")
140
+ else:
141
+ # Try to get from JSON body
142
+ logging.info("URI not found in query parameters. Checking JSON body.")
143
+ try:
144
+ req_body = await request.json()
145
+ if req_body:
146
+ uri = req_body.get('uri')
147
+ if uri:
148
+ logging.info(f"Found URI in JSON body: {uri[:100]}...")
149
+ except json.JSONDecodeError:
150
+ logging.info("Request body is not valid JSON.")
151
+ except Exception as e:
152
+ logging.warning(f"Error reading JSON body: {e}")
153
+
154
+ if not uri:
155
+ logging.warning("Neither file upload nor URI provided")
156
+ return web.json_response(
157
+ {
158
+ "error": "Please provide either a file upload or a 'uri' parameter",
159
+ "supported_methods": {
160
+ "file_upload": "POST with multipart/form-data and 'file' field",
161
+ "uri": "POST with JSON body containing 'uri' field, or GET with 'uri' query parameter"
162
+ }
163
+ },
164
+ status=400
165
+ )
166
+
167
+ # Validate URI format
168
+ if not uri.startswith(('http://', 'https://')):
169
+ logging.error(f"Invalid URI format: {uri}")
170
+ return web.json_response(
171
+ {"error": "Invalid URI format. Must start with http:// or https://"},
172
+ status=400
173
+ )
174
+
175
+ # Process the conversion from URI
176
+ result = await convert_from_uri(uri)
177
+
178
+ # Return streaming response
179
+ if result.get("success"):
180
+ # Stream PDF response to avoid loading entire file in RAM
181
+ return web.Response(
182
+ body=result["data"],
183
+ status=200,
184
+ content_type="application/pdf",
185
+ headers={
186
+ "Content-Disposition": f'attachment; filename="{result["filename"]}"'
187
+ }
188
+ )
189
+ else:
190
+ # Return error as JSON
191
+ error_response = {k: v for k, v in result.items() if k != "success"}
192
+ status_code = 400 if "Unsupported" in str(result.get("error")) or "download" in str(result.get("error")) else 500
193
+ return web.json_response(error_response, status=status_code)
@@ -0,0 +1,3 @@
1
+ azure-functions==1.18.0
2
+ aiohttp==3.9.1
3
+ PyPDF2==3.0.1
@@ -0,0 +1,26 @@
1
+ #!/bin/bash
2
+ # Run end-to-end conversion tests
3
+
4
+ echo "๐Ÿงช Running Document to PDF Conversion Tests"
5
+ echo "============================================="
6
+ echo ""
7
+
8
+ # Check if LibreOffice is installed
9
+ if ! command -v soffice &> /dev/null && ! command -v libreoffice &> /dev/null; then
10
+ echo "โŒ Error: LibreOffice not found!"
11
+ echo "Please install LibreOffice:"
12
+ echo " - macOS: brew install --cask libreoffice"
13
+ echo " - Ubuntu/Debian: sudo apt-get install libreoffice"
14
+ exit 1
15
+ fi
16
+
17
+ # Check if Python requirements are installed
18
+ if ! python3 -c "import PyPDF2" 2>/dev/null; then
19
+ echo "๐Ÿ“ฆ Installing Python dependencies..."
20
+ pip3 install -r requirements.txt
21
+ fi
22
+
23
+ # Run tests
24
+ python3 test_conversion.py
25
+
26
+ exit $?