@aj-archipelago/cortex 1.3.62 → 1.3.63

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 (211) hide show
  1. package/.github/workflows/cortex-file-handler-test.yml +61 -0
  2. package/README.md +31 -7
  3. package/config/default.example.json +15 -0
  4. package/config.js +133 -12
  5. package/helper-apps/cortex-autogen2/DigiCertGlobalRootCA.crt.pem +22 -0
  6. package/helper-apps/cortex-autogen2/Dockerfile +31 -0
  7. package/helper-apps/cortex-autogen2/Dockerfile.worker +41 -0
  8. package/helper-apps/cortex-autogen2/README.md +183 -0
  9. package/helper-apps/cortex-autogen2/__init__.py +1 -0
  10. package/helper-apps/cortex-autogen2/agents.py +131 -0
  11. package/helper-apps/cortex-autogen2/docker-compose.yml +20 -0
  12. package/helper-apps/cortex-autogen2/function_app.py +55 -0
  13. package/helper-apps/cortex-autogen2/host.json +15 -0
  14. package/helper-apps/cortex-autogen2/main.py +126 -0
  15. package/helper-apps/cortex-autogen2/poetry.lock +3652 -0
  16. package/helper-apps/cortex-autogen2/pyproject.toml +36 -0
  17. package/helper-apps/cortex-autogen2/requirements.txt +20 -0
  18. package/helper-apps/cortex-autogen2/send_task.py +105 -0
  19. package/helper-apps/cortex-autogen2/services/__init__.py +1 -0
  20. package/helper-apps/cortex-autogen2/services/azure_queue.py +85 -0
  21. package/helper-apps/cortex-autogen2/services/redis_publisher.py +153 -0
  22. package/helper-apps/cortex-autogen2/task_processor.py +488 -0
  23. package/helper-apps/cortex-autogen2/tools/__init__.py +24 -0
  24. package/helper-apps/cortex-autogen2/tools/azure_blob_tools.py +175 -0
  25. package/helper-apps/cortex-autogen2/tools/azure_foundry_agents.py +601 -0
  26. package/helper-apps/cortex-autogen2/tools/coding_tools.py +72 -0
  27. package/helper-apps/cortex-autogen2/tools/download_tools.py +48 -0
  28. package/helper-apps/cortex-autogen2/tools/file_tools.py +545 -0
  29. package/helper-apps/cortex-autogen2/tools/search_tools.py +646 -0
  30. package/helper-apps/cortex-azure-cleaner/README.md +36 -0
  31. package/helper-apps/cortex-file-converter/README.md +93 -0
  32. package/helper-apps/cortex-file-converter/key_to_pdf.py +104 -0
  33. package/helper-apps/cortex-file-converter/list_blob_extensions.py +89 -0
  34. package/helper-apps/cortex-file-converter/process_azure_keynotes.py +181 -0
  35. package/helper-apps/cortex-file-converter/requirements.txt +1 -0
  36. package/helper-apps/cortex-file-handler/.env.test.azure.ci +7 -0
  37. package/helper-apps/cortex-file-handler/.env.test.azure.sample +1 -1
  38. package/helper-apps/cortex-file-handler/.env.test.gcs.ci +10 -0
  39. package/helper-apps/cortex-file-handler/.env.test.gcs.sample +2 -2
  40. package/helper-apps/cortex-file-handler/INTERFACE.md +41 -0
  41. package/helper-apps/cortex-file-handler/package.json +1 -1
  42. package/helper-apps/cortex-file-handler/scripts/setup-azure-container.js +41 -17
  43. package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +30 -15
  44. package/helper-apps/cortex-file-handler/scripts/test-azure.sh +32 -6
  45. package/helper-apps/cortex-file-handler/scripts/test-gcs.sh +24 -2
  46. package/helper-apps/cortex-file-handler/scripts/validate-env.js +128 -0
  47. package/helper-apps/cortex-file-handler/src/blobHandler.js +161 -51
  48. package/helper-apps/cortex-file-handler/src/constants.js +3 -0
  49. package/helper-apps/cortex-file-handler/src/fileChunker.js +10 -8
  50. package/helper-apps/cortex-file-handler/src/index.js +116 -9
  51. package/helper-apps/cortex-file-handler/src/redis.js +61 -1
  52. package/helper-apps/cortex-file-handler/src/services/ConversionService.js +11 -8
  53. package/helper-apps/cortex-file-handler/src/services/FileConversionService.js +2 -2
  54. package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +88 -6
  55. package/helper-apps/cortex-file-handler/src/services/storage/GCSStorageProvider.js +58 -0
  56. package/helper-apps/cortex-file-handler/src/services/storage/StorageFactory.js +25 -5
  57. package/helper-apps/cortex-file-handler/src/services/storage/StorageProvider.js +9 -0
  58. package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +120 -16
  59. package/helper-apps/cortex-file-handler/src/start.js +27 -17
  60. package/helper-apps/cortex-file-handler/tests/FileConversionService.test.js +52 -1
  61. package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +40 -0
  62. package/helper-apps/cortex-file-handler/tests/checkHashShortLived.test.js +553 -0
  63. package/helper-apps/cortex-file-handler/tests/cleanup.test.js +46 -52
  64. package/helper-apps/cortex-file-handler/tests/containerConversionFlow.test.js +451 -0
  65. package/helper-apps/cortex-file-handler/tests/containerNameParsing.test.js +229 -0
  66. package/helper-apps/cortex-file-handler/tests/containerParameterFlow.test.js +392 -0
  67. package/helper-apps/cortex-file-handler/tests/conversionResilience.test.js +7 -2
  68. package/helper-apps/cortex-file-handler/tests/deleteOperations.test.js +348 -0
  69. package/helper-apps/cortex-file-handler/tests/fileChunker.test.js +23 -2
  70. package/helper-apps/cortex-file-handler/tests/fileUpload.test.js +11 -5
  71. package/helper-apps/cortex-file-handler/tests/getOperations.test.js +58 -24
  72. package/helper-apps/cortex-file-handler/tests/postOperations.test.js +11 -4
  73. package/helper-apps/cortex-file-handler/tests/shortLivedUrlConversion.test.js +225 -0
  74. package/helper-apps/cortex-file-handler/tests/start.test.js +8 -12
  75. package/helper-apps/cortex-file-handler/tests/storage/StorageFactory.test.js +80 -0
  76. package/helper-apps/cortex-file-handler/tests/storage/StorageService.test.js +388 -22
  77. package/helper-apps/cortex-file-handler/tests/testUtils.helper.js +74 -0
  78. package/lib/cortexResponse.js +153 -0
  79. package/lib/entityConstants.js +21 -3
  80. package/lib/logger.js +21 -4
  81. package/lib/pathwayTools.js +28 -9
  82. package/lib/util.js +49 -0
  83. package/package.json +1 -1
  84. package/pathways/basePathway.js +1 -0
  85. package/pathways/bing_afagent.js +54 -1
  86. package/pathways/call_tools.js +2 -3
  87. package/pathways/chat_jarvis.js +1 -1
  88. package/pathways/google_cse.js +27 -0
  89. package/pathways/grok_live_search.js +18 -0
  90. package/pathways/system/entity/memory/sys_memory_lookup_required.js +1 -0
  91. package/pathways/system/entity/memory/sys_memory_required.js +1 -0
  92. package/pathways/system/entity/memory/sys_search_memory.js +1 -0
  93. package/pathways/system/entity/sys_entity_agent.js +56 -4
  94. package/pathways/system/entity/sys_generator_quick.js +1 -0
  95. package/pathways/system/entity/tools/sys_tool_bing_search_afagent.js +26 -0
  96. package/pathways/system/entity/tools/sys_tool_google_search.js +141 -0
  97. package/pathways/system/entity/tools/sys_tool_grok_x_search.js +237 -0
  98. package/pathways/system/entity/tools/sys_tool_image.js +1 -1
  99. package/pathways/system/rest_streaming/sys_claude_37_sonnet.js +21 -0
  100. package/pathways/system/rest_streaming/sys_claude_41_opus.js +21 -0
  101. package/pathways/system/rest_streaming/sys_claude_4_sonnet.js +21 -0
  102. package/pathways/system/rest_streaming/sys_google_gemini_25_flash.js +25 -0
  103. package/pathways/system/rest_streaming/{sys_google_gemini_chat.js → sys_google_gemini_25_pro.js} +6 -4
  104. package/pathways/system/rest_streaming/sys_grok_4.js +23 -0
  105. package/pathways/system/rest_streaming/sys_grok_4_fast_non_reasoning.js +23 -0
  106. package/pathways/system/rest_streaming/sys_grok_4_fast_reasoning.js +23 -0
  107. package/pathways/system/rest_streaming/sys_openai_chat.js +3 -0
  108. package/pathways/system/rest_streaming/sys_openai_chat_gpt41.js +22 -0
  109. package/pathways/system/rest_streaming/sys_openai_chat_gpt41_mini.js +21 -0
  110. package/pathways/system/rest_streaming/sys_openai_chat_gpt41_nano.js +21 -0
  111. package/pathways/system/rest_streaming/{sys_claude_35_sonnet.js → sys_openai_chat_gpt4_omni.js} +6 -4
  112. package/pathways/system/rest_streaming/sys_openai_chat_gpt4_omni_mini.js +21 -0
  113. package/pathways/system/rest_streaming/{sys_claude_3_haiku.js → sys_openai_chat_gpt5.js} +7 -5
  114. package/pathways/system/rest_streaming/sys_openai_chat_gpt5_chat.js +21 -0
  115. package/pathways/system/rest_streaming/sys_openai_chat_gpt5_mini.js +21 -0
  116. package/pathways/system/rest_streaming/sys_openai_chat_gpt5_nano.js +21 -0
  117. package/pathways/system/rest_streaming/{sys_openai_chat_o1.js → sys_openai_chat_o3.js} +6 -3
  118. package/pathways/system/rest_streaming/sys_openai_chat_o3_mini.js +3 -0
  119. package/pathways/system/workspaces/run_workspace_prompt.js +99 -0
  120. package/pathways/vision.js +1 -1
  121. package/server/graphql.js +1 -1
  122. package/server/modelExecutor.js +8 -0
  123. package/server/pathwayResolver.js +166 -16
  124. package/server/pathwayResponseParser.js +16 -8
  125. package/server/plugins/azureFoundryAgentsPlugin.js +1 -1
  126. package/server/plugins/claude3VertexPlugin.js +193 -45
  127. package/server/plugins/gemini15ChatPlugin.js +21 -0
  128. package/server/plugins/gemini15VisionPlugin.js +360 -0
  129. package/server/plugins/googleCsePlugin.js +94 -0
  130. package/server/plugins/grokVisionPlugin.js +365 -0
  131. package/server/plugins/modelPlugin.js +3 -1
  132. package/server/plugins/openAiChatPlugin.js +106 -13
  133. package/server/plugins/openAiVisionPlugin.js +42 -30
  134. package/server/resolver.js +28 -4
  135. package/server/rest.js +270 -53
  136. package/server/typeDef.js +1 -0
  137. package/tests/{mocks.js → helpers/mocks.js} +5 -2
  138. package/tests/{server.js → helpers/server.js} +2 -2
  139. package/tests/helpers/sseAssert.js +23 -0
  140. package/tests/helpers/sseClient.js +73 -0
  141. package/tests/helpers/subscriptionAssert.js +11 -0
  142. package/tests/helpers/subscriptions.js +113 -0
  143. package/tests/{sublong.srt → integration/features/translate/sublong.srt} +4543 -4543
  144. package/tests/integration/features/translate/translate_chunking_stream.test.js +100 -0
  145. package/tests/{translate_srt.test.js → integration/features/translate/translate_srt.test.js} +2 -2
  146. package/tests/integration/graphql/async/stream/agentic.test.js +477 -0
  147. package/tests/integration/graphql/async/stream/subscription_streaming.test.js +62 -0
  148. package/tests/integration/graphql/async/stream/sys_entity_start_streaming.test.js +71 -0
  149. package/tests/integration/graphql/async/stream/vendors/claude_streaming.test.js +56 -0
  150. package/tests/integration/graphql/async/stream/vendors/gemini_streaming.test.js +66 -0
  151. package/tests/integration/graphql/async/stream/vendors/grok_streaming.test.js +56 -0
  152. package/tests/integration/graphql/async/stream/vendors/openai_streaming.test.js +72 -0
  153. package/tests/integration/graphql/features/google/sysToolGoogleSearch.test.js +96 -0
  154. package/tests/integration/graphql/features/grok/grok.test.js +688 -0
  155. package/tests/integration/graphql/features/grok/grok_x_search_tool.test.js +354 -0
  156. package/tests/{main.test.js → integration/graphql/features/main.test.js} +1 -1
  157. package/tests/{call_tools.test.js → integration/graphql/features/tools/call_tools.test.js} +2 -2
  158. package/tests/{vision.test.js → integration/graphql/features/vision/vision.test.js} +1 -1
  159. package/tests/integration/graphql/subscriptions/connection.test.js +26 -0
  160. package/tests/{openai_api.test.js → integration/rest/oai/openai_api.test.js} +63 -238
  161. package/tests/integration/rest/oai/tool_calling_api.test.js +343 -0
  162. package/tests/integration/rest/oai/tool_calling_streaming.test.js +85 -0
  163. package/tests/integration/rest/vendors/claude_streaming.test.js +47 -0
  164. package/tests/integration/rest/vendors/claude_tool_calling_streaming.test.js +75 -0
  165. package/tests/integration/rest/vendors/gemini_streaming.test.js +47 -0
  166. package/tests/integration/rest/vendors/gemini_tool_calling_streaming.test.js +75 -0
  167. package/tests/integration/rest/vendors/grok_streaming.test.js +55 -0
  168. package/tests/integration/rest/vendors/grok_tool_calling_streaming.test.js +75 -0
  169. package/tests/{azureAuthTokenHelper.test.js → unit/core/azureAuthTokenHelper.test.js} +1 -1
  170. package/tests/{chunkfunction.test.js → unit/core/chunkfunction.test.js} +2 -2
  171. package/tests/{config.test.js → unit/core/config.test.js} +3 -3
  172. package/tests/{encodeCache.test.js → unit/core/encodeCache.test.js} +1 -1
  173. package/tests/{fastLruCache.test.js → unit/core/fastLruCache.test.js} +1 -1
  174. package/tests/{handleBars.test.js → unit/core/handleBars.test.js} +1 -1
  175. package/tests/{memoryfunction.test.js → unit/core/memoryfunction.test.js} +2 -2
  176. package/tests/unit/core/mergeResolver.test.js +952 -0
  177. package/tests/{parser.test.js → unit/core/parser.test.js} +3 -3
  178. package/tests/unit/core/pathwayResolver.test.js +187 -0
  179. package/tests/{requestMonitor.test.js → unit/core/requestMonitor.test.js} +1 -1
  180. package/tests/{requestMonitorDurationEstimator.test.js → unit/core/requestMonitorDurationEstimator.test.js} +1 -1
  181. package/tests/{truncateMessages.test.js → unit/core/truncateMessages.test.js} +3 -3
  182. package/tests/{util.test.js → unit/core/util.test.js} +1 -1
  183. package/tests/{apptekTranslatePlugin.test.js → unit/plugins/apptekTranslatePlugin.test.js} +3 -3
  184. package/tests/{azureFoundryAgents.test.js → unit/plugins/azureFoundryAgents.test.js} +136 -1
  185. package/tests/{claude3VertexPlugin.test.js → unit/plugins/claude3VertexPlugin.test.js} +32 -10
  186. package/tests/{claude3VertexToolConversion.test.js → unit/plugins/claude3VertexToolConversion.test.js} +3 -3
  187. package/tests/unit/plugins/googleCsePlugin.test.js +111 -0
  188. package/tests/unit/plugins/grokVisionPlugin.test.js +1392 -0
  189. package/tests/{modelPlugin.test.js → unit/plugins/modelPlugin.test.js} +3 -3
  190. package/tests/{multimodal_conversion.test.js → unit/plugins/multimodal_conversion.test.js} +4 -4
  191. package/tests/{openAiChatPlugin.test.js → unit/plugins/openAiChatPlugin.test.js} +13 -4
  192. package/tests/{openAiToolPlugin.test.js → unit/plugins/openAiToolPlugin.test.js} +35 -27
  193. package/tests/{tokenHandlingTests.test.js → unit/plugins/tokenHandlingTests.test.js} +5 -5
  194. package/tests/{translate_apptek.test.js → unit/plugins/translate_apptek.test.js} +3 -3
  195. package/tests/{streaming.test.js → unit/plugins.streaming/plugin_stream_events.test.js} +19 -58
  196. package/helper-apps/mogrt-handler/tests/test-files/test.gif +0 -1
  197. package/helper-apps/mogrt-handler/tests/test-files/test.mogrt +0 -1
  198. package/helper-apps/mogrt-handler/tests/test-files/test.mp4 +0 -1
  199. package/pathways/system/rest_streaming/sys_openai_chat_gpt4.js +0 -19
  200. package/pathways/system/rest_streaming/sys_openai_chat_gpt4_32.js +0 -19
  201. package/pathways/system/rest_streaming/sys_openai_chat_gpt4_turbo.js +0 -19
  202. package/pathways/system/workspaces/run_claude35_sonnet.js +0 -21
  203. package/pathways/system/workspaces/run_claude3_haiku.js +0 -20
  204. package/pathways/system/workspaces/run_gpt35turbo.js +0 -20
  205. package/pathways/system/workspaces/run_gpt4.js +0 -20
  206. package/pathways/system/workspaces/run_gpt4_32.js +0 -20
  207. package/tests/agentic.test.js +0 -256
  208. package/tests/pathwayResolver.test.js +0 -78
  209. package/tests/subscription.test.js +0 -387
  210. /package/tests/{subchunk.srt → integration/features/translate/subchunk.srt} +0 -0
  211. /package/tests/{subhorizontal.srt → integration/features/translate/subhorizontal.srt} +0 -0
@@ -0,0 +1,601 @@
1
+ """
2
+ Utilities to call Azure Foundry Agents (Threads / Runs) API as a callable tool.
3
+
4
+ Provides a single entrypoint `call_azure_foundry_agent` which will:
5
+ - Construct the request payload expected by Azure Foundry Agents
6
+ - POST to create a run
7
+ - Poll the run status until completion (or timeout)
8
+ - Retrieve messages from the thread and return the assistant's final text
9
+
10
+ Design is intentionally lightweight and dependency-only-on-requests.
11
+ Returns JSON strings for easy use by other tools in the project.
12
+ """
13
+
14
+ from typing import Any, Dict, List, Optional, Union
15
+ import requests
16
+ import time
17
+ import json
18
+ import logging
19
+ import os
20
+ from datetime import datetime, timedelta
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def _get_service_principal_creds_from_env() -> Optional[Dict[str, Any]]:
26
+ """Load service principal credentials from AZURE_SERVICE_PRINCIPAL_CREDENTIALS env var
27
+ or from individual AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_CLIENT_SECRET vars.
28
+ Supports JSON string or path to a file containing JSON.
29
+ """
30
+ val = os.getenv("AZURE_SERVICE_PRINCIPAL_CREDENTIALS")
31
+ if val:
32
+ # If value looks like a path to a file, try to read it
33
+ try:
34
+ if os.path.exists(val):
35
+ with open(val, "r", encoding="utf-8") as f:
36
+ return json.load(f)
37
+ except Exception:
38
+ pass
39
+
40
+ # Normalize common wrappers: strip surrounding quotes and unescape
41
+ v = val.strip()
42
+ # If value is surrounded by matching single or double quotes, strip them
43
+ if len(v) >= 2 and ((v[0] == v[-1]) and v[0] in ('"', "'")):
44
+ v = v[1:-1]
45
+
46
+ # Unescape common escapes produced by some dotenv serializers
47
+ v = v.replace('\\"', '"').replace("\\'", "'").replace('\\n', '\n')
48
+
49
+ # Try parse as JSON
50
+ try:
51
+ return json.loads(v)
52
+ except Exception:
53
+ # Try Python literal eval (handles single-quoted dicts)
54
+ try:
55
+ import ast
56
+
57
+ parsed = ast.literal_eval(v)
58
+ if isinstance(parsed, dict):
59
+ return parsed
60
+ except Exception:
61
+ pass
62
+
63
+ # Try interpreting as dotenv-style key=value lines
64
+ try:
65
+ lines = [l.strip() for l in v.splitlines() if l.strip() and not l.strip().startswith("#")]
66
+ kv = {}
67
+ for line in lines:
68
+ if "=" in line:
69
+ k, vv = line.split("=", 1)
70
+ k = k.strip()
71
+ vv = vv.strip().strip('"').strip("'")
72
+ kv[k] = vv
73
+ # If keys look like tenant/client/secret, return mapped shape
74
+ if any(k.lower() in ("tenant_id", "tenantid", "tenant") for k in kv.keys()) and any(
75
+ k.lower() in ("client_id", "clientid", "client") for k in kv.keys()
76
+ ):
77
+ out = {}
78
+ out["tenant_id"] = kv.get("tenant_id") or kv.get("tenantId") or kv.get("AZURE_TENANT_ID") or kv.get("tenant")
79
+ out["client_id"] = kv.get("client_id") or kv.get("clientId") or kv.get("AZURE_CLIENT_ID") or kv.get("client")
80
+ out["client_secret"] = kv.get("client_secret") or kv.get("clientSecret") or kv.get("AZURE_CLIENT_SECRET") or kv.get("clientSecret")
81
+ if out["tenant_id"] and out["client_id"] and out["client_secret"]:
82
+ # scope optional
83
+ out_scope = kv.get("scope") or kv.get("AZURE_SERVICE_PRINCIPAL_SCOPE")
84
+ if out_scope:
85
+ out["scope"] = out_scope
86
+ return out
87
+ except Exception:
88
+ pass
89
+
90
+ # Fallback to individual env vars
91
+ tenant = os.getenv("AZURE_TENANT_ID") or os.getenv("AZURE_TENANT")
92
+ client = os.getenv("AZURE_CLIENT_ID") or os.getenv("AZURE_CLIENT")
93
+ secret = os.getenv("AZURE_CLIENT_SECRET") or os.getenv("AZURE_CLIENTKEY")
94
+ scope = os.getenv("AZURE_SERVICE_PRINCIPAL_SCOPE")
95
+ if tenant and client and secret:
96
+ out = {"tenant_id": tenant, "client_id": client, "client_secret": secret}
97
+ if scope:
98
+ out["scope"] = scope
99
+ return out
100
+
101
+ return None
102
+
103
+ # Try to import Azure SDK components if available (optional path)
104
+ try:
105
+ from azure.ai.projects import AIProjectClient # type: ignore
106
+ from azure.identity import ClientSecretCredential, DefaultAzureCredential # type: ignore
107
+ from azure.ai.agents.models import ListSortOrder # type: ignore
108
+ _AZURE_SDK_AVAILABLE = True
109
+ except Exception:
110
+ _AZURE_SDK_AVAILABLE = False
111
+
112
+
113
+ def _convert_to_azure_foundry_messages(
114
+ context: Optional[str],
115
+ examples: Optional[List[Dict[str, Any]]],
116
+ messages: List[Dict[str, Any]],
117
+ ) -> List[Dict[str, Any]]:
118
+ azure_messages: List[Dict[str, Any]] = []
119
+
120
+ if context:
121
+ azure_messages.append({"role": "system", "content": context})
122
+
123
+ if examples:
124
+ for example in examples:
125
+ try:
126
+ inp = example.get("input", {})
127
+ out = example.get("output", {})
128
+ azure_messages.append({"role": inp.get("author", "user"), "content": inp.get("content")})
129
+ azure_messages.append({"role": out.get("author", "assistant"), "content": out.get("content")})
130
+ except Exception:
131
+ # ignore malformed example
132
+ continue
133
+
134
+ for message in messages or []:
135
+ # Expect message to have 'author' and 'content' keys in Palm-like format,
136
+ # or 'role' and 'content' already in Azure format.
137
+ if "role" in message:
138
+ azure_messages.append({"role": message.get("role"), "content": message.get("content")})
139
+ else:
140
+ azure_messages.append({"role": message.get("author"), "content": message.get("content")})
141
+
142
+ return azure_messages
143
+
144
+
145
+ def _parse_assistant_text_from_messages(messages_resp: Dict[str, Any]) -> Optional[str]:
146
+ # messages_resp expected shape: {"data": [...] } or {"messages": [...]}
147
+ msgs = None
148
+ if not messages_resp:
149
+ return None
150
+
151
+ if isinstance(messages_resp, dict) and "data" in messages_resp and isinstance(messages_resp["data"], list):
152
+ msgs = messages_resp["data"]
153
+ elif isinstance(messages_resp, dict) and "messages" in messages_resp and isinstance(messages_resp["messages"], list):
154
+ msgs = messages_resp["messages"]
155
+ elif isinstance(messages_resp, list):
156
+ msgs = messages_resp
157
+ else:
158
+ return None
159
+
160
+ # Iterate from last to first to find the last assistant message
161
+ for message in reversed(msgs):
162
+ try:
163
+ role = message.get("role")
164
+ if role != "assistant":
165
+ continue
166
+
167
+ content = message.get("content")
168
+ # content may be an array of parts: [{type: 'text', text: '...'}]
169
+ if isinstance(content, list):
170
+ for part in content:
171
+ if not isinstance(part, dict):
172
+ continue
173
+ if part.get("type") == "text":
174
+ text_val = part.get("text")
175
+ if isinstance(text_val, str):
176
+ return text_val
177
+ if isinstance(text_val, dict) and isinstance(text_val.get("value"), str):
178
+ return text_val.get("value")
179
+
180
+ # If content is string, return it
181
+ if isinstance(content, str):
182
+ return content
183
+
184
+ # Some responses embed messages under message.content.text.value
185
+ if isinstance(message.get("content"), dict):
186
+ # try a few common shapes
187
+ c = message.get("content")
188
+ # content.text may be { value: '...' } or string
189
+ text_node = None
190
+ if isinstance(c.get("text"), dict):
191
+ text_node = c.get("text").get("value")
192
+ elif isinstance(c.get("text"), str):
193
+ text_node = c.get("text")
194
+ if isinstance(text_node, str):
195
+ return text_node
196
+ except Exception:
197
+ continue
198
+
199
+ return None
200
+
201
+
202
+ class AzureAuthTokenHelper:
203
+ """Helper to obtain and cache an Azure AD service principal access token.
204
+
205
+ Expects a dict with keys: tenant_id / tenantId, client_id / clientId,
206
+ client_secret / clientSecret, optional scope.
207
+ """
208
+ def __init__(self, creds: Dict[str, Any]):
209
+ if not creds or not isinstance(creds, dict):
210
+ raise ValueError("Azure credentials must be a dict parsed from AZURE_SERVICE_PRINCIPAL_CREDENTIALS")
211
+
212
+ self.tenant_id = creds.get("tenant_id") or creds.get("tenantId")
213
+ self.client_id = creds.get("client_id") or creds.get("clientId")
214
+ self.client_secret = creds.get("client_secret") or creds.get("clientSecret")
215
+ self.scope = creds.get("scope") or "https://ai.azure.com/.default"
216
+
217
+ if not (self.tenant_id and self.client_id and self.client_secret):
218
+ raise ValueError("Azure credentials must include tenant_id, client_id, and client_secret")
219
+
220
+ self.token: Optional[str] = None
221
+ self.expiry: Optional[datetime] = None
222
+ self.token_url = f"https://login.microsoftonline.com/{self.tenant_id}/oauth2/v2.0/token"
223
+
224
+ def is_token_valid(self) -> bool:
225
+ # 5 minute buffer
226
+ if not self.token or not self.expiry:
227
+ return False
228
+ return datetime.utcnow() < (self.expiry - timedelta(minutes=5))
229
+
230
+ def refresh_token(self) -> None:
231
+ data = {
232
+ "client_id": self.client_id,
233
+ "client_secret": self.client_secret,
234
+ "scope": self.scope,
235
+ "grant_type": "client_credentials",
236
+ }
237
+ headers = {"Content-Type": "application/x-www-form-urlencoded"}
238
+ resp = requests.post(self.token_url, data=data, headers=headers, timeout=10)
239
+ resp.raise_for_status()
240
+ payload = resp.json()
241
+ access_token = payload.get("access_token")
242
+ if not access_token:
243
+ raise RuntimeError("Azure token response missing access_token")
244
+ self.token = access_token
245
+ expires_in = int(payload.get("expires_in", 3600))
246
+ self.expiry = datetime.utcnow() + timedelta(seconds=expires_in)
247
+
248
+ def get_access_token(self) -> str:
249
+ if not self.is_token_valid():
250
+ self.refresh_token()
251
+ return self.token
252
+
253
+
254
+ def call_azure_foundry_agent(
255
+ project_url: str,
256
+ agent_id: str,
257
+ messages: List[Dict[str, Any]],
258
+ context: Optional[str] = None,
259
+ examples: Optional[List[Dict[str, Any]]] = None,
260
+ parameters: Optional[Dict[str, Any]] = None,
261
+ auth_token: Optional[str] = None,
262
+ api_version: str = "2025-05-15-preview",
263
+ poll_interval_s: float = 1.0,
264
+ max_poll_attempts: int = 60,
265
+ extra_headers: Optional[Dict[str, str]] = None,
266
+ ) -> str:
267
+ """
268
+ Call Azure Foundry Agents API to create a run and wait for completion.
269
+
270
+ Args:
271
+ project_url: base URL for the Foundry project (e.g. https://foundry.example.com)
272
+ agent_id: assistant/agent id to use (assistant_id)
273
+ messages: list of messages (Palm-like or Azure role format)
274
+ context: optional system context string
275
+ examples: optional examples list
276
+ parameters: optional additional parameters to forward into the request body
277
+ auth_token: optional bearer token for Authorization header
278
+ api_version: version query param
279
+ poll_interval_s: seconds between polls
280
+ max_poll_attempts: maximum number of polls before timeout
281
+ extra_headers: any additional headers to include
282
+
283
+ Returns:
284
+ JSON string with result. On success returns {"status":"success","result": <text_or_full_response>}.
285
+ On failure returns {"status":"error","error": "..."}
286
+ """
287
+ try:
288
+ # Prefer using the Azure SDK path if available - it handles auth and endpoints robustly.
289
+ if _AZURE_SDK_AVAILABLE:
290
+ try:
291
+ # Build credential: prefer explicit service principal creds in env var, else DefaultAzureCredential
292
+ cred = None
293
+ if not auth_token:
294
+ creds_env = os.getenv("AZURE_SERVICE_PRINCIPAL_CREDENTIALS")
295
+ if creds_env:
296
+ try:
297
+ creds = json.loads(creds_env)
298
+ tenant = creds.get("tenant_id") or creds.get("tenantId")
299
+ client = creds.get("client_id") or creds.get("clientId")
300
+ secret = creds.get("client_secret") or creds.get("clientSecret")
301
+ if tenant and client and secret:
302
+ cred = ClientSecretCredential(tenant, client, secret)
303
+ except Exception:
304
+ cred = None
305
+ if cred is None:
306
+ # Will try environment-based credentials (AZURE_CLIENT_ID etc.) or managed identity
307
+ cred = DefaultAzureCredential()
308
+
309
+ # Instantiate client with the provided project endpoint
310
+ project_client = AIProjectClient(endpoint=project_url, credential=cred)
311
+
312
+ # If thread_id provided, post a simple message
313
+ thread_id_param = parameters.get("thread_id") if parameters else None
314
+ if thread_id_param:
315
+ last_msg = (messages or [])[-1] if messages else None
316
+ if not last_msg:
317
+ return json.dumps({"status": "error", "error": "no_message_to_post"})
318
+ role = last_msg.get("role") or last_msg.get("author") or "user"
319
+ content_text = last_msg.get("content")
320
+ if isinstance(content_text, dict):
321
+ content_text = content_text.get("text") or content_text.get("value")
322
+ if not isinstance(content_text, str):
323
+ content_text = json.dumps(content_text)
324
+
325
+ msg = project_client.agents.messages.create(thread_id=thread_id_param, role=role, content=content_text)
326
+ return json.dumps({"status": "success", "result": json.loads(json.dumps(msg, default=lambda o: getattr(o, '__dict__', str(o))))})
327
+
328
+ # Create thread, post message, and create & process run
329
+ agent = project_client.agents.get_agent(agent_id)
330
+ thread = project_client.agents.threads.create()
331
+ # Post initial user message
332
+ if messages and len(messages) > 0:
333
+ first = messages[0]
334
+ content_text = first.get("content")
335
+ if isinstance(content_text, dict):
336
+ content_text = content_text.get("text") or content_text.get("value")
337
+ if not isinstance(content_text, str):
338
+ content_text = json.dumps(content_text)
339
+ _ = project_client.agents.messages.create(thread_id=thread.id, role=first.get("role") or first.get("author") or "user", content=content_text)
340
+
341
+ run = project_client.agents.runs.create_and_process(thread_id=thread.id, agent_id=agent.id)
342
+ # run may be synchronous; check status
343
+ if getattr(run, "status", None) == "failed":
344
+ return json.dumps({"status": "error", "error": "run_failed", "detail": getattr(run, "last_error", None)})
345
+
346
+ # Retrieve messages
347
+ msgs = project_client.agents.messages.list(thread_id=thread.id, order=ListSortOrder.ASCENDING)
348
+ extracted = []
349
+ for m in msgs:
350
+ try:
351
+ # m may have text_messages attribute; extract last text value
352
+ text_msgs = getattr(m, "text_messages", None)
353
+ if text_msgs:
354
+ last_text = text_msgs[-1]
355
+ text_val = getattr(last_text, "text", None)
356
+ if isinstance(text_val, dict):
357
+ val = text_val.get("value")
358
+ else:
359
+ val = getattr(text_val, "value", None) if text_val else None
360
+ extracted.append({"role": getattr(m, "role", None), "text": val})
361
+ else:
362
+ # fallback to simple content
363
+ extracted.append({"role": getattr(m, "role", None), "content": getattr(m, "content", None)})
364
+ except Exception:
365
+ continue
366
+
367
+ return json.dumps({"status": "success", "result": extracted})
368
+ except Exception as e:
369
+ # If SDK path fails, log and fall back to HTTP implementation below
370
+ logger.warning(f"[AzureFoundry] SDK path failed, falling back to HTTP: {e}")
371
+
372
+ # If parameters include a thread_id, prefer posting directly to that thread's messages endpoint.
373
+ # This mirrors a working call pattern: POST /threads/{thread_id}/messages
374
+ thread_id_param = parameters.get("thread_id") if parameters else None
375
+ if thread_id_param:
376
+ # Post the last message in the messages list to the thread
377
+ last_msg = (messages or [])[-1] if messages else None
378
+ if not last_msg:
379
+ return json.dumps({"status": "error", "error": "no_message_to_post"})
380
+
381
+ # Determine role and content
382
+ role = last_msg.get("role") or last_msg.get("author") or "user"
383
+ content_text = last_msg.get("content")
384
+ if isinstance(content_text, dict):
385
+ # if structure like {"text": "..."}
386
+ content_text = content_text.get("text") or content_text.get("value")
387
+
388
+ if not isinstance(content_text, str):
389
+ # fallback to JSON stringified content
390
+ content_text = json.dumps(content_text)
391
+
392
+ # API expects content[0].text to be a string when creating messages
393
+ post_body = {
394
+ "role": role,
395
+ "content": [
396
+ {"type": "text", "text": content_text}
397
+ ]
398
+ }
399
+
400
+ post_url = project_url.rstrip("/") + f"/threads/{thread_id_param}/messages"
401
+ pheaders = {"Content-Type": "application/json"}
402
+ if auth_token:
403
+ pheaders["Authorization"] = f"Bearer {auth_token}"
404
+ # try to obtain token from env if missing
405
+ if not auth_token:
406
+ creds_env = os.getenv("AZURE_SERVICE_PRINCIPAL_CREDENTIALS")
407
+ if creds_env:
408
+ try:
409
+ creds = json.loads(creds_env)
410
+ # infer scope from project_url if missing
411
+ if not creds.get("scope"):
412
+ try:
413
+ from urllib.parse import urlparse
414
+
415
+ parsed = urlparse(project_url)
416
+ base = f"{parsed.scheme}://{parsed.netloc}"
417
+ creds["scope"] = base.rstrip("/") + "/.default"
418
+ except Exception:
419
+ creds["scope"] = "https://ai.azure.com/.default"
420
+ helper = AzureAuthTokenHelper(creds)
421
+ auth_token = helper.get_access_token()
422
+ pheaders["Authorization"] = f"Bearer {auth_token}"
423
+ except Exception as e:
424
+ logger.warning(f"[AzureFoundry] Failed to obtain auth token from AZURE_SERVICE_PRINCIPAL_CREDENTIALS: {e}")
425
+
426
+ pparams = {"api-version": api_version}
427
+ logger.info(f"[AzureFoundry] Posting message to thread {thread_id_param} at {post_url}")
428
+ presp = requests.post(post_url, headers=pheaders, params=pparams, json=post_body, timeout=30)
429
+ try:
430
+ presp.raise_for_status()
431
+ except Exception as e:
432
+ logger.error(f"[AzureFoundry] Post message failed: {e} - status: {presp.status_code} - text: {presp.text}")
433
+ return json.dumps({"status": "error", "error": f"Post message failed: {presp.status_code} {presp.text}"})
434
+
435
+ return json.dumps({"status": "success", "result": presp.json()})
436
+
437
+ # If no explicit auth_token provided, try to obtain one from env AZURE_SERVICE_PRINCIPAL_CREDENTIALS
438
+ if not auth_token:
439
+ creds_env = os.getenv("AZURE_SERVICE_PRINCIPAL_CREDENTIALS")
440
+ if creds_env:
441
+ try:
442
+ creds = json.loads(creds_env)
443
+ helper = AzureAuthTokenHelper(creds)
444
+ auth_token = helper.get_access_token()
445
+ except Exception as e:
446
+ logger.warning(f"[AzureFoundry] Failed to obtain auth token from AZURE_SERVICE_PRINCIPAL_CREDENTIALS: {e}")
447
+
448
+ # Build request messages in Azure format
449
+ request_messages = _convert_to_azure_foundry_messages(context, examples, messages)
450
+
451
+ # Build payload
452
+ body: Dict[str, Any] = {
453
+ "assistant_id": agent_id,
454
+ "thread": {"messages": request_messages},
455
+ "stream": bool(parameters.get("stream") if parameters else False),
456
+ }
457
+
458
+ # Merge allowed parameter keys into body
459
+ if parameters:
460
+ allowed_keys = [
461
+ "tools",
462
+ "tool_resources",
463
+ "metadata",
464
+ "instructions",
465
+ "model",
466
+ "temperature",
467
+ "max_tokens",
468
+ "top_p",
469
+ "tool_choice",
470
+ "response_format",
471
+ "parallel_tool_calls",
472
+ "truncation_strategy",
473
+ ]
474
+ for k in allowed_keys:
475
+ if k in parameters:
476
+ body[k] = parameters[k]
477
+
478
+ url = project_url.rstrip("/") + "/threads/runs"
479
+ headers = {"Content-Type": "application/json"}
480
+ if auth_token:
481
+ headers["Authorization"] = f"Bearer {auth_token}"
482
+ if extra_headers:
483
+ headers.update(extra_headers)
484
+
485
+ params = {"api-version": api_version}
486
+
487
+ logger.info(f"[AzureFoundry] Creating run at {url} (assistant_id={agent_id})")
488
+ resp = requests.post(url, headers=headers, params=params, json=body, timeout=30)
489
+ try:
490
+ resp.raise_for_status()
491
+ except Exception as e:
492
+ logger.error(f"[AzureFoundry] Create run failed: {e} - status: {resp.status_code} - text: {resp.text}")
493
+ return json.dumps({"status": "error", "error": f"Create run failed: {resp.status_code} {resp.text}"})
494
+
495
+ run_resp = resp.json()
496
+
497
+ # If the response already contains messages, try to parse them
498
+ if isinstance(run_resp, dict) and (run_resp.get("messages") or run_resp.get("data")):
499
+ parsed = _parse_assistant_text_from_messages(run_resp)
500
+ if parsed:
501
+ return json.dumps({"status": "success", "result": parsed})
502
+ # otherwise return the raw run response
503
+ return json.dumps({"status": "success", "result": run_resp})
504
+
505
+ run_id = run_resp.get("id")
506
+ thread_id = run_resp.get("thread_id")
507
+
508
+ if not run_id or not thread_id:
509
+ # Nothing to poll; return run response
510
+ return json.dumps({"status": "success", "result": run_resp})
511
+
512
+ # Poll for completion
513
+ attempts = 0
514
+ poll_url = project_url.rstrip("/") + f"/threads/{thread_id}/runs/{run_id}"
515
+ while attempts < max_poll_attempts:
516
+ attempts += 1
517
+ time.sleep(poll_interval_s)
518
+ try:
519
+ pheaders = {"Content-Type": "application/json"}
520
+ if auth_token:
521
+ pheaders["Authorization"] = f"Bearer {auth_token}"
522
+ if extra_headers:
523
+ pheaders.update(extra_headers)
524
+
525
+ presp = requests.get(poll_url, headers=pheaders, params={"api-version": api_version}, timeout=20)
526
+ presp.raise_for_status()
527
+ status_json = presp.json()
528
+
529
+ status = status_json.get("status")
530
+ if not status:
531
+ # keep polling
532
+ continue
533
+
534
+ if status == "completed":
535
+ logger.info(f"[AzureFoundry] Run completed: {run_id}")
536
+ # retrieve messages
537
+ break
538
+
539
+ if status in ("failed", "cancelled"):
540
+ logger.error(f"[AzureFoundry] Run {status}: {run_id}")
541
+ return json.dumps({"status": "error", "error": f"Run {status}", "detail": status_json})
542
+
543
+ # otherwise continue polling
544
+ continue
545
+
546
+ except Exception as e:
547
+ logger.warning(f"[AzureFoundry] Polling attempt {attempts} failed: {e}")
548
+ continue
549
+
550
+ else:
551
+ logger.error(f"[AzureFoundry] Polling timed out after {max_poll_attempts} attempts for run {run_id}")
552
+ return json.dumps({"status": "error", "error": "polling_timeout"})
553
+
554
+ # Retrieve messages from thread
555
+ try:
556
+ messages_url = project_url.rstrip("/") + f"/threads/{thread_id}/messages"
557
+ mheaders = {"Content-Type": "application/json"}
558
+ if auth_token:
559
+ mheaders["Authorization"] = f"Bearer {auth_token}"
560
+ if extra_headers:
561
+ mheaders.update(extra_headers)
562
+
563
+ mresp = requests.get(messages_url, headers=mheaders, params={"api-version": api_version, "order": "asc"}, timeout=30)
564
+ mresp.raise_for_status()
565
+ messages_json = mresp.json()
566
+
567
+ parsed_text = _parse_assistant_text_from_messages(messages_json)
568
+ if parsed_text:
569
+ return json.dumps({"status": "success", "result": parsed_text})
570
+ # fallback: return whole messages payload
571
+ return json.dumps({"status": "success", "result": messages_json})
572
+
573
+ except Exception as e:
574
+ logger.error(f"[AzureFoundry] Failed to retrieve messages: {e}")
575
+ return json.dumps({"status": "error", "error": f"retrieve_messages_failed: {str(e)}"})
576
+
577
+ except Exception as exc:
578
+ logger.exception("[AzureFoundry] Unexpected error")
579
+ return json.dumps({"status": "error", "error": str(exc)})
580
+
581
+
582
+ def get_azure_foundry_tool(project_url: str, agent_id: str, auth_token: Optional[str] = None):
583
+ """
584
+ Return a callable suitable as a simple tool wrapper.
585
+
586
+ The returned function signature is: (messages, context=None, examples=None, parameters=None) -> str
587
+ """
588
+ def tool(messages: List[Dict[str, Any]], context: Optional[str] = None, examples: Optional[List[Dict[str, Any]]] = None, parameters: Optional[Dict[str, Any]] = None):
589
+ return call_azure_foundry_agent(
590
+ project_url=project_url,
591
+ agent_id=agent_id,
592
+ messages=messages,
593
+ context=context,
594
+ examples=examples,
595
+ parameters=parameters,
596
+ auth_token=auth_token,
597
+ )
598
+
599
+ return tool
600
+
601
+
@@ -0,0 +1,72 @@
1
+ """
2
+ Core Coding Tool for Cortex-AutoGen2
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ import subprocess
8
+ from contextlib import redirect_stdout, redirect_stderr
9
+ from io import StringIO
10
+ import traceback
11
+ from typing import Dict, Any
12
+ import json
13
+
14
+ def _execute_code_sync(code: str) -> Dict[str, Any]:
15
+ """
16
+ Execute Python code in a sandboxed environment and return structured results.
17
+
18
+ Args:
19
+ code: Python code to execute
20
+
21
+ Returns:
22
+ Dict with status, stdout, and stderr
23
+ """
24
+ try:
25
+ # Capture output
26
+ stdout_buffer = StringIO()
27
+ stderr_buffer = StringIO()
28
+
29
+ # Execute the code in a restricted environment
30
+ with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
31
+ exec(code, {'__builtins__': __builtins__, 'os': os, 'sys': sys})
32
+
33
+ stdout = stdout_buffer.getvalue()
34
+ stderr = stderr_buffer.getvalue()
35
+
36
+ if stderr:
37
+ return {
38
+ "status": "error",
39
+ "stdout": stdout,
40
+ "stderr": stderr,
41
+ "traceback": stderr,
42
+ }
43
+
44
+ return {
45
+ "status": "success",
46
+ "stdout": stdout,
47
+ "stderr": stderr,
48
+ }
49
+
50
+ except Exception:
51
+ tb = traceback.format_exc()
52
+ return {
53
+ "status": "error",
54
+ "stdout": "",
55
+ "stderr": tb,
56
+ "traceback": tb,
57
+ }
58
+
59
+ async def execute_code(code: str) -> str:
60
+ """
61
+ Executes a block of Python code and returns the output.
62
+ This tool is essential for any task that requires generating and running code.
63
+
64
+ Args:
65
+ code: A string containing the Python code to be executed.
66
+
67
+ Returns:
68
+ A JSON string containing the execution status, stdout, and stderr.
69
+ """
70
+ result = _execute_code_sync(code)
71
+ return json.dumps(result, indent=2)
72
+