@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.
- package/.github/workflows/cortex-file-handler-test.yml +61 -0
- package/README.md +31 -7
- package/config/default.example.json +15 -0
- package/config.js +133 -12
- package/helper-apps/cortex-autogen2/DigiCertGlobalRootCA.crt.pem +22 -0
- package/helper-apps/cortex-autogen2/Dockerfile +31 -0
- package/helper-apps/cortex-autogen2/Dockerfile.worker +41 -0
- package/helper-apps/cortex-autogen2/README.md +183 -0
- package/helper-apps/cortex-autogen2/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/agents.py +131 -0
- package/helper-apps/cortex-autogen2/docker-compose.yml +20 -0
- package/helper-apps/cortex-autogen2/function_app.py +55 -0
- package/helper-apps/cortex-autogen2/host.json +15 -0
- package/helper-apps/cortex-autogen2/main.py +126 -0
- package/helper-apps/cortex-autogen2/poetry.lock +3652 -0
- package/helper-apps/cortex-autogen2/pyproject.toml +36 -0
- package/helper-apps/cortex-autogen2/requirements.txt +20 -0
- package/helper-apps/cortex-autogen2/send_task.py +105 -0
- package/helper-apps/cortex-autogen2/services/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/services/azure_queue.py +85 -0
- package/helper-apps/cortex-autogen2/services/redis_publisher.py +153 -0
- package/helper-apps/cortex-autogen2/task_processor.py +488 -0
- package/helper-apps/cortex-autogen2/tools/__init__.py +24 -0
- package/helper-apps/cortex-autogen2/tools/azure_blob_tools.py +175 -0
- package/helper-apps/cortex-autogen2/tools/azure_foundry_agents.py +601 -0
- package/helper-apps/cortex-autogen2/tools/coding_tools.py +72 -0
- package/helper-apps/cortex-autogen2/tools/download_tools.py +48 -0
- package/helper-apps/cortex-autogen2/tools/file_tools.py +545 -0
- package/helper-apps/cortex-autogen2/tools/search_tools.py +646 -0
- package/helper-apps/cortex-azure-cleaner/README.md +36 -0
- package/helper-apps/cortex-file-converter/README.md +93 -0
- package/helper-apps/cortex-file-converter/key_to_pdf.py +104 -0
- package/helper-apps/cortex-file-converter/list_blob_extensions.py +89 -0
- package/helper-apps/cortex-file-converter/process_azure_keynotes.py +181 -0
- package/helper-apps/cortex-file-converter/requirements.txt +1 -0
- package/helper-apps/cortex-file-handler/.env.test.azure.ci +7 -0
- package/helper-apps/cortex-file-handler/.env.test.azure.sample +1 -1
- package/helper-apps/cortex-file-handler/.env.test.gcs.ci +10 -0
- package/helper-apps/cortex-file-handler/.env.test.gcs.sample +2 -2
- package/helper-apps/cortex-file-handler/INTERFACE.md +41 -0
- package/helper-apps/cortex-file-handler/package.json +1 -1
- package/helper-apps/cortex-file-handler/scripts/setup-azure-container.js +41 -17
- package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +30 -15
- package/helper-apps/cortex-file-handler/scripts/test-azure.sh +32 -6
- package/helper-apps/cortex-file-handler/scripts/test-gcs.sh +24 -2
- package/helper-apps/cortex-file-handler/scripts/validate-env.js +128 -0
- package/helper-apps/cortex-file-handler/src/blobHandler.js +161 -51
- package/helper-apps/cortex-file-handler/src/constants.js +3 -0
- package/helper-apps/cortex-file-handler/src/fileChunker.js +10 -8
- package/helper-apps/cortex-file-handler/src/index.js +116 -9
- package/helper-apps/cortex-file-handler/src/redis.js +61 -1
- package/helper-apps/cortex-file-handler/src/services/ConversionService.js +11 -8
- package/helper-apps/cortex-file-handler/src/services/FileConversionService.js +2 -2
- package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +88 -6
- package/helper-apps/cortex-file-handler/src/services/storage/GCSStorageProvider.js +58 -0
- package/helper-apps/cortex-file-handler/src/services/storage/StorageFactory.js +25 -5
- package/helper-apps/cortex-file-handler/src/services/storage/StorageProvider.js +9 -0
- package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +120 -16
- package/helper-apps/cortex-file-handler/src/start.js +27 -17
- package/helper-apps/cortex-file-handler/tests/FileConversionService.test.js +52 -1
- package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +40 -0
- package/helper-apps/cortex-file-handler/tests/checkHashShortLived.test.js +553 -0
- package/helper-apps/cortex-file-handler/tests/cleanup.test.js +46 -52
- package/helper-apps/cortex-file-handler/tests/containerConversionFlow.test.js +451 -0
- package/helper-apps/cortex-file-handler/tests/containerNameParsing.test.js +229 -0
- package/helper-apps/cortex-file-handler/tests/containerParameterFlow.test.js +392 -0
- package/helper-apps/cortex-file-handler/tests/conversionResilience.test.js +7 -2
- package/helper-apps/cortex-file-handler/tests/deleteOperations.test.js +348 -0
- package/helper-apps/cortex-file-handler/tests/fileChunker.test.js +23 -2
- package/helper-apps/cortex-file-handler/tests/fileUpload.test.js +11 -5
- package/helper-apps/cortex-file-handler/tests/getOperations.test.js +58 -24
- package/helper-apps/cortex-file-handler/tests/postOperations.test.js +11 -4
- package/helper-apps/cortex-file-handler/tests/shortLivedUrlConversion.test.js +225 -0
- package/helper-apps/cortex-file-handler/tests/start.test.js +8 -12
- package/helper-apps/cortex-file-handler/tests/storage/StorageFactory.test.js +80 -0
- package/helper-apps/cortex-file-handler/tests/storage/StorageService.test.js +388 -22
- package/helper-apps/cortex-file-handler/tests/testUtils.helper.js +74 -0
- package/lib/cortexResponse.js +153 -0
- package/lib/entityConstants.js +21 -3
- package/lib/logger.js +21 -4
- package/lib/pathwayTools.js +28 -9
- package/lib/util.js +49 -0
- package/package.json +1 -1
- package/pathways/basePathway.js +1 -0
- package/pathways/bing_afagent.js +54 -1
- package/pathways/call_tools.js +2 -3
- package/pathways/chat_jarvis.js +1 -1
- package/pathways/google_cse.js +27 -0
- package/pathways/grok_live_search.js +18 -0
- package/pathways/system/entity/memory/sys_memory_lookup_required.js +1 -0
- package/pathways/system/entity/memory/sys_memory_required.js +1 -0
- package/pathways/system/entity/memory/sys_search_memory.js +1 -0
- package/pathways/system/entity/sys_entity_agent.js +56 -4
- package/pathways/system/entity/sys_generator_quick.js +1 -0
- package/pathways/system/entity/tools/sys_tool_bing_search_afagent.js +26 -0
- package/pathways/system/entity/tools/sys_tool_google_search.js +141 -0
- package/pathways/system/entity/tools/sys_tool_grok_x_search.js +237 -0
- package/pathways/system/entity/tools/sys_tool_image.js +1 -1
- package/pathways/system/rest_streaming/sys_claude_37_sonnet.js +21 -0
- package/pathways/system/rest_streaming/sys_claude_41_opus.js +21 -0
- package/pathways/system/rest_streaming/sys_claude_4_sonnet.js +21 -0
- package/pathways/system/rest_streaming/sys_google_gemini_25_flash.js +25 -0
- package/pathways/system/rest_streaming/{sys_google_gemini_chat.js → sys_google_gemini_25_pro.js} +6 -4
- package/pathways/system/rest_streaming/sys_grok_4.js +23 -0
- package/pathways/system/rest_streaming/sys_grok_4_fast_non_reasoning.js +23 -0
- package/pathways/system/rest_streaming/sys_grok_4_fast_reasoning.js +23 -0
- package/pathways/system/rest_streaming/sys_openai_chat.js +3 -0
- package/pathways/system/rest_streaming/sys_openai_chat_gpt41.js +22 -0
- package/pathways/system/rest_streaming/sys_openai_chat_gpt41_mini.js +21 -0
- package/pathways/system/rest_streaming/sys_openai_chat_gpt41_nano.js +21 -0
- package/pathways/system/rest_streaming/{sys_claude_35_sonnet.js → sys_openai_chat_gpt4_omni.js} +6 -4
- package/pathways/system/rest_streaming/sys_openai_chat_gpt4_omni_mini.js +21 -0
- package/pathways/system/rest_streaming/{sys_claude_3_haiku.js → sys_openai_chat_gpt5.js} +7 -5
- package/pathways/system/rest_streaming/sys_openai_chat_gpt5_chat.js +21 -0
- package/pathways/system/rest_streaming/sys_openai_chat_gpt5_mini.js +21 -0
- package/pathways/system/rest_streaming/sys_openai_chat_gpt5_nano.js +21 -0
- package/pathways/system/rest_streaming/{sys_openai_chat_o1.js → sys_openai_chat_o3.js} +6 -3
- package/pathways/system/rest_streaming/sys_openai_chat_o3_mini.js +3 -0
- package/pathways/system/workspaces/run_workspace_prompt.js +99 -0
- package/pathways/vision.js +1 -1
- package/server/graphql.js +1 -1
- package/server/modelExecutor.js +8 -0
- package/server/pathwayResolver.js +166 -16
- package/server/pathwayResponseParser.js +16 -8
- package/server/plugins/azureFoundryAgentsPlugin.js +1 -1
- package/server/plugins/claude3VertexPlugin.js +193 -45
- package/server/plugins/gemini15ChatPlugin.js +21 -0
- package/server/plugins/gemini15VisionPlugin.js +360 -0
- package/server/plugins/googleCsePlugin.js +94 -0
- package/server/plugins/grokVisionPlugin.js +365 -0
- package/server/plugins/modelPlugin.js +3 -1
- package/server/plugins/openAiChatPlugin.js +106 -13
- package/server/plugins/openAiVisionPlugin.js +42 -30
- package/server/resolver.js +28 -4
- package/server/rest.js +270 -53
- package/server/typeDef.js +1 -0
- package/tests/{mocks.js → helpers/mocks.js} +5 -2
- package/tests/{server.js → helpers/server.js} +2 -2
- package/tests/helpers/sseAssert.js +23 -0
- package/tests/helpers/sseClient.js +73 -0
- package/tests/helpers/subscriptionAssert.js +11 -0
- package/tests/helpers/subscriptions.js +113 -0
- package/tests/{sublong.srt → integration/features/translate/sublong.srt} +4543 -4543
- package/tests/integration/features/translate/translate_chunking_stream.test.js +100 -0
- package/tests/{translate_srt.test.js → integration/features/translate/translate_srt.test.js} +2 -2
- package/tests/integration/graphql/async/stream/agentic.test.js +477 -0
- package/tests/integration/graphql/async/stream/subscription_streaming.test.js +62 -0
- package/tests/integration/graphql/async/stream/sys_entity_start_streaming.test.js +71 -0
- package/tests/integration/graphql/async/stream/vendors/claude_streaming.test.js +56 -0
- package/tests/integration/graphql/async/stream/vendors/gemini_streaming.test.js +66 -0
- package/tests/integration/graphql/async/stream/vendors/grok_streaming.test.js +56 -0
- package/tests/integration/graphql/async/stream/vendors/openai_streaming.test.js +72 -0
- package/tests/integration/graphql/features/google/sysToolGoogleSearch.test.js +96 -0
- package/tests/integration/graphql/features/grok/grok.test.js +688 -0
- package/tests/integration/graphql/features/grok/grok_x_search_tool.test.js +354 -0
- package/tests/{main.test.js → integration/graphql/features/main.test.js} +1 -1
- package/tests/{call_tools.test.js → integration/graphql/features/tools/call_tools.test.js} +2 -2
- package/tests/{vision.test.js → integration/graphql/features/vision/vision.test.js} +1 -1
- package/tests/integration/graphql/subscriptions/connection.test.js +26 -0
- package/tests/{openai_api.test.js → integration/rest/oai/openai_api.test.js} +63 -238
- package/tests/integration/rest/oai/tool_calling_api.test.js +343 -0
- package/tests/integration/rest/oai/tool_calling_streaming.test.js +85 -0
- package/tests/integration/rest/vendors/claude_streaming.test.js +47 -0
- package/tests/integration/rest/vendors/claude_tool_calling_streaming.test.js +75 -0
- package/tests/integration/rest/vendors/gemini_streaming.test.js +47 -0
- package/tests/integration/rest/vendors/gemini_tool_calling_streaming.test.js +75 -0
- package/tests/integration/rest/vendors/grok_streaming.test.js +55 -0
- package/tests/integration/rest/vendors/grok_tool_calling_streaming.test.js +75 -0
- package/tests/{azureAuthTokenHelper.test.js → unit/core/azureAuthTokenHelper.test.js} +1 -1
- package/tests/{chunkfunction.test.js → unit/core/chunkfunction.test.js} +2 -2
- package/tests/{config.test.js → unit/core/config.test.js} +3 -3
- package/tests/{encodeCache.test.js → unit/core/encodeCache.test.js} +1 -1
- package/tests/{fastLruCache.test.js → unit/core/fastLruCache.test.js} +1 -1
- package/tests/{handleBars.test.js → unit/core/handleBars.test.js} +1 -1
- package/tests/{memoryfunction.test.js → unit/core/memoryfunction.test.js} +2 -2
- package/tests/unit/core/mergeResolver.test.js +952 -0
- package/tests/{parser.test.js → unit/core/parser.test.js} +3 -3
- package/tests/unit/core/pathwayResolver.test.js +187 -0
- package/tests/{requestMonitor.test.js → unit/core/requestMonitor.test.js} +1 -1
- package/tests/{requestMonitorDurationEstimator.test.js → unit/core/requestMonitorDurationEstimator.test.js} +1 -1
- package/tests/{truncateMessages.test.js → unit/core/truncateMessages.test.js} +3 -3
- package/tests/{util.test.js → unit/core/util.test.js} +1 -1
- package/tests/{apptekTranslatePlugin.test.js → unit/plugins/apptekTranslatePlugin.test.js} +3 -3
- package/tests/{azureFoundryAgents.test.js → unit/plugins/azureFoundryAgents.test.js} +136 -1
- package/tests/{claude3VertexPlugin.test.js → unit/plugins/claude3VertexPlugin.test.js} +32 -10
- package/tests/{claude3VertexToolConversion.test.js → unit/plugins/claude3VertexToolConversion.test.js} +3 -3
- package/tests/unit/plugins/googleCsePlugin.test.js +111 -0
- package/tests/unit/plugins/grokVisionPlugin.test.js +1392 -0
- package/tests/{modelPlugin.test.js → unit/plugins/modelPlugin.test.js} +3 -3
- package/tests/{multimodal_conversion.test.js → unit/plugins/multimodal_conversion.test.js} +4 -4
- package/tests/{openAiChatPlugin.test.js → unit/plugins/openAiChatPlugin.test.js} +13 -4
- package/tests/{openAiToolPlugin.test.js → unit/plugins/openAiToolPlugin.test.js} +35 -27
- package/tests/{tokenHandlingTests.test.js → unit/plugins/tokenHandlingTests.test.js} +5 -5
- package/tests/{translate_apptek.test.js → unit/plugins/translate_apptek.test.js} +3 -3
- package/tests/{streaming.test.js → unit/plugins.streaming/plugin_stream_events.test.js} +19 -58
- package/helper-apps/mogrt-handler/tests/test-files/test.gif +0 -1
- package/helper-apps/mogrt-handler/tests/test-files/test.mogrt +0 -1
- package/helper-apps/mogrt-handler/tests/test-files/test.mp4 +0 -1
- package/pathways/system/rest_streaming/sys_openai_chat_gpt4.js +0 -19
- package/pathways/system/rest_streaming/sys_openai_chat_gpt4_32.js +0 -19
- package/pathways/system/rest_streaming/sys_openai_chat_gpt4_turbo.js +0 -19
- package/pathways/system/workspaces/run_claude35_sonnet.js +0 -21
- package/pathways/system/workspaces/run_claude3_haiku.js +0 -20
- package/pathways/system/workspaces/run_gpt35turbo.js +0 -20
- package/pathways/system/workspaces/run_gpt4.js +0 -20
- package/pathways/system/workspaces/run_gpt4_32.js +0 -20
- package/tests/agentic.test.js +0 -256
- package/tests/pathwayResolver.test.js +0 -78
- package/tests/subscription.test.js +0 -387
- /package/tests/{subchunk.srt → integration/features/translate/subchunk.srt} +0 -0
- /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
|
+
|