feat(ultraplan-local): v1.6.0 — /ultraresearch-local deep research command
Add /ultraresearch-local for structured research combining local codebase analysis with external knowledge via parallel agent swarms. Produces research briefs with triangulation, confidence ratings, and source quality assessment. New command: /ultraresearch-local with modes --quick, --local, --external, --fg. New agents: research-orchestrator (opus), docs-researcher, community-researcher, security-researcher, contrarian-researcher, gemini-bridge (all sonnet). New template: research-brief-template.md. Integration: --research flag in /ultraplan-local accepts pre-built research briefs (up to 3), enriches the interview and exploration phases. Planning orchestrator cross-references brief findings during synthesis. Design principle: Context Engineering — right information to right agent at right time. Research briefs are structured artifacts in the pipeline: ultraresearch → brief → ultraplan --research → plan → ultraexecute. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
baa2d0220b
488 changed files with 213221 additions and 0 deletions
|
|
@ -0,0 +1,464 @@
|
|||
# Asynchronous Processing Patterns
|
||||
|
||||
**Last updated:** 2026-02
|
||||
**Status:** GA
|
||||
**Category:** Performance & Scalability
|
||||
|
||||
---
|
||||
|
||||
## Introduksjon
|
||||
|
||||
Asynkron prosessering er en arkitekturstrategi der AI-forespørsler behandles uavhengig av den opprinnelige klientforbindelsen. I stedet for at klienten venter synkront på et svar fra Azure OpenAI (som kan ta fra 500ms til flere minutter for reasoning-modeller), plasseres forespørselen i en kø, behandles i bakgrunnen, og resultatet leveres via polling, webhook eller push-notifikasjon.
|
||||
|
||||
For Azure OpenAI tilbyr Microsoft flere innebygde asynkrone mekanismer: Batch API for store volum, Background Tasks i Responses API for langvarige oppgaver, og Webhooks for hendelsesbasert leveranse. I tillegg kan organisasjoner bygge egne asynkrone arkitekturer med Azure Service Bus, Azure Queue Storage eller Azure Event Hubs som mellomlag.
|
||||
|
||||
I norsk offentlig sektor er asynkron prosessering spesielt relevant for dokumentanalyse, saksbehandlingsstøtte og rapportgenerering — oppgaver der brukeren ikke trenger umiddelbart svar, men der volumet kan være svært høyt i perioder (f.eks. ved frister for høringssvar eller klagebehandling).
|
||||
|
||||
## Kjernekomponenter
|
||||
|
||||
| Komponent | Formål | Teknologi |
|
||||
|-----------|--------|-----------|
|
||||
| Azure Service Bus | Enterprise message broker med køer og topics | Azure Service Bus |
|
||||
| Azure Queue Storage | Enkel, kostnadseffektiv meldingskø | Azure Storage |
|
||||
| Azure Event Hubs | Høy-throughput event streaming | Azure Event Hubs |
|
||||
| Azure Functions | Serverless compute for kø-triggered prosessering | Azure Functions |
|
||||
| Batch API | Innebygd asynkron batch-prosessering | Azure OpenAI |
|
||||
| Background Tasks | Langvarige oppgaver i Responses API | Azure OpenAI |
|
||||
| Webhooks | Hendelsesbasert notifikasjon | Azure OpenAI |
|
||||
|
||||
## Queue-based Architectures
|
||||
|
||||
### Service Bus-basert AI-prosessering
|
||||
|
||||
```python
|
||||
# Producer: Legg forespørsler i kø
|
||||
from azure.servicebus import ServiceBusClient, ServiceBusMessage
|
||||
import json
|
||||
|
||||
class AIRequestProducer:
|
||||
"""Queue AI requests via Azure Service Bus."""
|
||||
|
||||
def __init__(self, connection_string: str, queue_name: str = "ai-requests"):
|
||||
self.client = ServiceBusClient.from_connection_string(connection_string)
|
||||
self.sender = self.client.get_queue_sender(queue_name)
|
||||
|
||||
async def submit_request(
|
||||
self,
|
||||
request_id: str,
|
||||
messages: list[dict],
|
||||
priority: str = "normal",
|
||||
callback_url: str = None
|
||||
) -> str:
|
||||
"""Submit AI request to queue. Returns request ID for polling."""
|
||||
payload = {
|
||||
"request_id": request_id,
|
||||
"messages": messages,
|
||||
"priority": priority,
|
||||
"callback_url": callback_url,
|
||||
"submitted_at": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
message = ServiceBusMessage(
|
||||
body=json.dumps(payload),
|
||||
message_id=request_id,
|
||||
subject=priority,
|
||||
session_id=request_id if priority == "urgent" else None,
|
||||
time_to_live=timedelta(hours=24)
|
||||
)
|
||||
|
||||
await self.sender.send_messages(message)
|
||||
return request_id
|
||||
|
||||
|
||||
# Consumer: Prosesser forespørsler fra kø
|
||||
from azure.servicebus.aio import ServiceBusClient as AsyncServiceBusClient
|
||||
from openai import AsyncAzureOpenAI
|
||||
|
||||
class AIRequestConsumer:
|
||||
"""Process AI requests from Service Bus queue."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sb_connection: str,
|
||||
queue_name: str,
|
||||
openai_client: AsyncAzureOpenAI,
|
||||
max_concurrent: int = 10
|
||||
):
|
||||
self.sb_client = AsyncServiceBusClient.from_connection_string(
|
||||
sb_connection)
|
||||
self.queue_name = queue_name
|
||||
self.openai = openai_client
|
||||
self.semaphore = asyncio.Semaphore(max_concurrent)
|
||||
|
||||
async def process_messages(self):
|
||||
"""Continuously process messages from queue."""
|
||||
async with self.sb_client.get_queue_receiver(
|
||||
self.queue_name,
|
||||
max_wait_time=30
|
||||
) as receiver:
|
||||
async for message in receiver:
|
||||
asyncio.create_task(
|
||||
self._handle_message(receiver, message))
|
||||
|
||||
async def _handle_message(self, receiver, message):
|
||||
async with self.semaphore:
|
||||
try:
|
||||
payload = json.loads(str(message))
|
||||
|
||||
# Prosesser med Azure OpenAI
|
||||
response = await self.openai.chat.completions.create(
|
||||
model="gpt-4o",
|
||||
messages=payload["messages"],
|
||||
max_tokens=2000
|
||||
)
|
||||
|
||||
# Lagre resultat
|
||||
await self._store_result(
|
||||
payload["request_id"],
|
||||
response.choices[0].message.content
|
||||
)
|
||||
|
||||
# Callback hvis konfigurert
|
||||
if payload.get("callback_url"):
|
||||
await self._send_callback(
|
||||
payload["callback_url"],
|
||||
payload["request_id"],
|
||||
response.choices[0].message.content
|
||||
)
|
||||
|
||||
await receiver.complete_message(message)
|
||||
|
||||
except Exception as e:
|
||||
if message.delivery_count < 3:
|
||||
await receiver.abandon_message(message)
|
||||
else:
|
||||
await receiver.dead_letter_message(
|
||||
message,
|
||||
reason=str(e))
|
||||
```
|
||||
|
||||
### Azure Functions Queue Trigger
|
||||
|
||||
```csharp
|
||||
// Azure Function: Prosesser AI-forespørsler fra Storage Queue
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Messaging.ServiceBus;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
|
||||
public class AIRequestProcessor
|
||||
{
|
||||
private readonly AzureOpenAIClient _openAIClient;
|
||||
|
||||
public AIRequestProcessor(AzureOpenAIClient openAIClient)
|
||||
{
|
||||
_openAIClient = openAIClient;
|
||||
}
|
||||
|
||||
[Function("ProcessAIRequest")]
|
||||
[ServiceBusOutput("ai-results", Connection = "ServiceBusConnection")]
|
||||
public async Task<ServiceBusMessage> Run(
|
||||
[ServiceBusTrigger("ai-requests",
|
||||
Connection = "ServiceBusConnection")]
|
||||
ServiceBusReceivedMessage message,
|
||||
FunctionContext context)
|
||||
{
|
||||
var logger = context.GetLogger("ProcessAIRequest");
|
||||
var request = JsonSerializer.Deserialize<AIRequest>(
|
||||
message.Body.ToString());
|
||||
|
||||
logger.LogInformation(
|
||||
"Processing request {RequestId}", request!.RequestId);
|
||||
|
||||
var chatClient = _openAIClient.GetChatClient("gpt-4o");
|
||||
var response = await chatClient.CompleteChatAsync(
|
||||
request.Messages.Select(m =>
|
||||
new UserChatMessage(m.Content)).ToList());
|
||||
|
||||
var result = new AIResult
|
||||
{
|
||||
RequestId = request.RequestId,
|
||||
Output = response.Value.Content[0].Text,
|
||||
CompletedAt = DateTime.UtcNow,
|
||||
TokensUsed = response.Value.Usage.TotalTokenCount
|
||||
};
|
||||
|
||||
return new ServiceBusMessage(
|
||||
JsonSerializer.Serialize(result))
|
||||
{
|
||||
MessageId = request.RequestId,
|
||||
Subject = "completed"
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event-Driven Design
|
||||
|
||||
### Azure OpenAI med Event Grid
|
||||
|
||||
```python
|
||||
# Event-driven pattern: Trigger AI-prosessering fra dokumenter
|
||||
# Ny blob → Event Grid → Function → OpenAI → Result store
|
||||
|
||||
from azure.functions import Blueprint, EventGridEvent
|
||||
from openai import AzureOpenAI
|
||||
import json
|
||||
|
||||
bp = Blueprint()
|
||||
|
||||
@bp.event_grid_trigger(arg_name="event")
|
||||
@bp.cosmos_db_output(
|
||||
arg_name="resultDoc",
|
||||
database_name="ai-results",
|
||||
container_name="completions",
|
||||
connection="CosmosConnection"
|
||||
)
|
||||
async def process_document_event(
|
||||
event: EventGridEvent,
|
||||
resultDoc: func.Out[str]
|
||||
):
|
||||
"""Process document when uploaded to Blob Storage."""
|
||||
data = event.get_json()
|
||||
blob_url = data["url"]
|
||||
|
||||
# Hent dokumentinnhold
|
||||
document_text = await download_and_extract(blob_url)
|
||||
|
||||
# Prosesser med Azure OpenAI
|
||||
client = AzureOpenAI(
|
||||
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
|
||||
api_key=os.environ["AZURE_OPENAI_KEY"],
|
||||
api_version="2024-10-21"
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4o",
|
||||
messages=[
|
||||
{"role": "system", "content": "Analyser dette dokumentet..."},
|
||||
{"role": "user", "content": document_text[:128000]}
|
||||
],
|
||||
max_tokens=2000
|
||||
)
|
||||
|
||||
result = {
|
||||
"id": event.id,
|
||||
"source_blob": blob_url,
|
||||
"analysis": response.choices[0].message.content,
|
||||
"tokens_used": response.usage.total_tokens,
|
||||
"processed_at": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
resultDoc.set(json.dumps(result))
|
||||
```
|
||||
|
||||
## Request-Response Decoupling
|
||||
|
||||
### Background Tasks med Azure OpenAI Responses API
|
||||
|
||||
```python
|
||||
from openai import AzureOpenAI
|
||||
import time
|
||||
|
||||
def submit_background_task(client: AzureOpenAI, prompt: str) -> str:
|
||||
"""Submit long-running task using background mode."""
|
||||
response = client.responses.create(
|
||||
model="o3", # Reasoning modell — kan ta minutter
|
||||
input=prompt,
|
||||
background=True # Kjør asynkront
|
||||
)
|
||||
return response.id
|
||||
|
||||
def poll_for_result(
|
||||
client: AzureOpenAI,
|
||||
response_id: str,
|
||||
max_wait_seconds: int = 600,
|
||||
poll_interval: int = 5
|
||||
) -> dict:
|
||||
"""Poll for background task completion."""
|
||||
start = time.time()
|
||||
|
||||
while time.time() - start < max_wait_seconds:
|
||||
result = client.responses.retrieve(response_id)
|
||||
|
||||
if result.status == "completed":
|
||||
return {
|
||||
"status": "completed",
|
||||
"output": result.output,
|
||||
"duration_seconds": round(time.time() - start, 1)
|
||||
}
|
||||
elif result.status == "failed":
|
||||
return {"status": "failed", "error": result.error}
|
||||
|
||||
time.sleep(poll_interval)
|
||||
|
||||
return {"status": "timeout"}
|
||||
|
||||
# Bruk: Kompleks analyse som kan ta flere minutter
|
||||
response_id = submit_background_task(
|
||||
client,
|
||||
"Analyser dette reguleringsverket og identifiser alle krav..."
|
||||
)
|
||||
|
||||
# Klienten kan gjøre andre ting mens vi venter
|
||||
result = poll_for_result(client, response_id)
|
||||
```
|
||||
|
||||
## Status Polling and Webhooks
|
||||
|
||||
### Webhook-basert notifikasjon
|
||||
|
||||
```python
|
||||
# Webhook handler for Azure OpenAI events
|
||||
from flask import Flask, request, Response
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
WEBHOOK_SECRET = os.environ["OPENAI_WEBHOOK_SECRET"]
|
||||
|
||||
@app.route("/webhooks/openai", methods=["POST"])
|
||||
def handle_openai_webhook():
|
||||
"""Handle Azure OpenAI webhook events."""
|
||||
# Verifiser signatur
|
||||
signature = request.headers.get("Webhook-Signature")
|
||||
webhook_id = request.headers.get("Webhook-ID")
|
||||
|
||||
if not verify_signature(request.data, signature):
|
||||
return Response("Invalid signature", status=400)
|
||||
|
||||
# Idempotency check
|
||||
if is_already_processed(webhook_id):
|
||||
return Response(status=200)
|
||||
|
||||
event = request.get_json()
|
||||
|
||||
# Prosesser event
|
||||
if event.get("type") == "batch.completed":
|
||||
handle_batch_complete(event["data"])
|
||||
elif event.get("type") == "fine_tuning.job.succeeded":
|
||||
handle_finetuning_complete(event["data"])
|
||||
|
||||
mark_as_processed(webhook_id)
|
||||
return Response(status=200)
|
||||
|
||||
def verify_signature(payload: bytes, signature: str) -> bool:
|
||||
"""Verify webhook signature."""
|
||||
expected = hmac.new(
|
||||
WEBHOOK_SECRET.encode(),
|
||||
payload,
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
return hmac.compare_digest(expected, signature)
|
||||
|
||||
|
||||
# Polling-basert status-sjekk med exponential backoff
|
||||
import asyncio
|
||||
|
||||
async def poll_with_backoff(
|
||||
check_fn,
|
||||
initial_interval: float = 2.0,
|
||||
max_interval: float = 60.0,
|
||||
backoff_factor: float = 1.5,
|
||||
timeout: float = 3600.0
|
||||
) -> dict:
|
||||
"""Poll with exponential backoff until completion or timeout."""
|
||||
interval = initial_interval
|
||||
elapsed = 0.0
|
||||
|
||||
while elapsed < timeout:
|
||||
result = await check_fn()
|
||||
|
||||
if result.get("status") in ("completed", "failed"):
|
||||
return result
|
||||
|
||||
await asyncio.sleep(interval)
|
||||
elapsed += interval
|
||||
interval = min(interval * backoff_factor, max_interval)
|
||||
|
||||
return {"status": "timeout", "elapsed": elapsed}
|
||||
```
|
||||
|
||||
### REST API for Status Polling
|
||||
|
||||
```csharp
|
||||
// ASP.NET Core: Status polling endpoint for async AI requests
|
||||
[ApiController]
|
||||
[Route("api/ai")]
|
||||
public class AIRequestController : ControllerBase
|
||||
{
|
||||
private readonly ICosmosDbService _cosmosDb;
|
||||
private readonly IServiceBusSender _sender;
|
||||
|
||||
[HttpPost("requests")]
|
||||
public async Task<IActionResult> SubmitRequest(
|
||||
[FromBody] AIRequestDto request)
|
||||
{
|
||||
var requestId = Guid.NewGuid().ToString();
|
||||
|
||||
// Legg i kø for asynkron prosessering
|
||||
await _sender.SendAsync(new ServiceBusMessage(
|
||||
JsonSerializer.Serialize(request))
|
||||
{
|
||||
MessageId = requestId
|
||||
});
|
||||
|
||||
// Returner 202 Accepted med Location header
|
||||
return AcceptedAtAction(
|
||||
nameof(GetStatus),
|
||||
new { requestId },
|
||||
new { requestId, status = "queued" });
|
||||
}
|
||||
|
||||
[HttpGet("requests/{requestId}/status")]
|
||||
public async Task<IActionResult> GetStatus(string requestId)
|
||||
{
|
||||
var result = await _cosmosDb.GetRequestStatus(requestId);
|
||||
|
||||
if (result == null)
|
||||
return NotFound();
|
||||
|
||||
if (result.Status == "completed")
|
||||
return Ok(result);
|
||||
|
||||
// Returnér 200 med status og Retry-After header
|
||||
Response.Headers.Append("Retry-After", "5");
|
||||
return Ok(new { requestId, status = result.Status });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Norsk offentlig sektor
|
||||
|
||||
- **Saksbehandlingssystemer**: Asynkron prosessering er ideelt for AI-assistert saksbehandling der analyse kan ta tid. Saksbehandler sender inn dokument, fortsetter med annet arbeid, og mottar notifikasjon når analysen er ferdig.
|
||||
- **Arkivloven**: Sørg for at alle mellomliggende meldinger i køer (Service Bus, Queue Storage) krypteres og at sensitive data ikke lagres utover nødvendig prosesseringstid.
|
||||
- **Personvern**: Dead letter queues kan inneholde personopplysninger — konfigurer automatisk sletting og monitorering av DLQ-dybde.
|
||||
- **Tilgjengelighet**: Asynkrone mønstre forbedrer brukeropplevelsen for tjenester med krav om universell utforming — brukere slipper å vente på skjermen.
|
||||
- **Batch-prosessering**: Bruk Azure OpenAI Batch API for periodiske oppgaver (nattlige rapporter, ukentlige analyser) med 50% kostnadsreduksjon.
|
||||
|
||||
## Beslutningsrammeverk
|
||||
|
||||
| Scenario | Anbefaling | Begrunnelse |
|
||||
|----------|------------|-------------|
|
||||
| Bruker venter på svar (<3s) | Synkron + streaming | Best brukeropplevelse for korte svar |
|
||||
| Dokumentanalyse (minutter) | Service Bus kø + polling | Bruker kan gjøre annet arbeid |
|
||||
| Reasoning-modell (o3/o1) | Background Tasks API | Innebygd asynkron prosessering |
|
||||
| Stort batch-volum (1000+) | Azure OpenAI Batch API | 50% kostnadsreduksjon |
|
||||
| Event-drevet pipeline | Event Grid + Functions | Automatisk trigger ved nye data |
|
||||
| Kritisk pålitelighet | Service Bus + DLQ | Garantert leveranse og feilhåndtering |
|
||||
|
||||
## Referanser
|
||||
|
||||
- [Azure OpenAI Batch API](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/batch) — Batch processing
|
||||
- [Azure OpenAI Responses API — Background tasks](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/responses) — Background mode
|
||||
- [Azure OpenAI Webhooks](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/webhooks) — Event notifications
|
||||
- [Event-driven architecture style](https://learn.microsoft.com/azure/architecture/guide/architecture-styles/event-driven) — Architecture patterns
|
||||
- [Azure Functions on Container Apps](https://learn.microsoft.com/azure/container-apps/functions-unified-platform) — Event-driven compute
|
||||
|
||||
## For Cosmo
|
||||
|
||||
- **Bruk denne referansen** når kunden har AI-workloads som ikke krever umiddelbart svar, eller når de opplever timeout-problemer med langvarige AI-forespørsler.
|
||||
- Azure OpenAI Background Tasks er den enkleste løsningen for reasoning-modeller (o3, o1) som kan ta minutter — sett `background: true`.
|
||||
- For enterprise-arkitekturer, anbefal Service Bus fremfor Queue Storage — gir sessions, dead letter queues og transaksjonsstøtte.
|
||||
- Implementer alltid idempotency i webhook-handlere og consumers — meldinger kan leveres mer enn én gang.
|
||||
- Batch API bør være standard for alle ikke-sanntids workloads — 50% kostnadsreduksjon er en enkel gevinst.
|
||||
Loading…
Add table
Add a link
Reference in a new issue