Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f23780a
fix: orchestrator agent citations
mishraomp Dec 8, 2025
6f72d3d
some more tweaks
mishraomp Dec 8, 2025
5ca1613
tweak to handle unknown in api call
mishraomp Dec 8, 2025
63d9957
some more tweaks
mishraomp Dec 9, 2025
2b49490
mandatory source citations
mishraomp Dec 9, 2025
d4d56be
feat: citation fixes and llm response fixes and ui tweaks
mishraomp Dec 9, 2025
780d9c7
feat: adding speech capabilities
mishraomp Dec 9, 2025
a3ad8a9
update caddy for websocket for speech
mishraomp Dec 9, 2025
8d857fc
forward ws headers
mishraomp Dec 9, 2025
601b63c
feat: speech service
mishraomp Dec 10, 2025
6167b17
fix: auth token
mishraomp Dec 10, 2025
830a2e2
speech key
mishraomp Dec 10, 2025
3ec3f36
speech fixes
mishraomp Dec 10, 2025
39944f8
speech
mishraomp Dec 10, 2025
ec48fca
fix caddy csp to allow media playback
mishraomp Dec 10, 2025
c888240
fix frontend caddy csp
mishraomp Dec 10, 2025
6964b81
more speech
mishraomp Dec 10, 2025
403de0d
streaming service
mishraomp Dec 10, 2025
41f6428
fix validation
mishraomp Dec 10, 2025
fa5330a
more speech
mishraomp Dec 10, 2025
419edde
fix: streaming UI
mishraomp Dec 10, 2025
f816780
consistent logging format
mishraomp Dec 10, 2025
b65a816
fix: gh workflow
mishraomp Dec 10, 2025
ec6b622
python access logging
mishraomp Dec 10, 2025
4e0867e
logging
mishraomp Dec 10, 2025
d8cf2f3
codeql fix
mishraomp Dec 11, 2025
7090627
fine tuning
mishraomp Dec 11, 2025
c141faf
codeql
mishraomp Dec 11, 2025
85d6bee
trying change
mishraomp Dec 11, 2025
a8e2e94
file size and et..
mishraomp Dec 11, 2025
ef6c7a7
document delete
mishraomp Dec 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/.builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
strategy:
matrix:
# Only building frontend containers to run PR based e2e tests
package: [frontend, api, api-ms-agent]
package: [frontend, api, api-ms-agent, proxy]
timeout-minutes: 10
steps:
- uses: bcgov/action-builder-ghcr@v3.0.1
Expand Down
158 changes: 80 additions & 78 deletions .github/workflows/.deployer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,104 +5,106 @@ on:
inputs:
### Required
environment_name:
description: 'The name of the environment to deploy to'
description: "The name of the environment to deploy to"
required: true
default: 'dev'
default: "dev"
type: string
command:
description: 'The terragrunt command to run'
description: "The terragrunt command to run"
required: true
default: 'apply'
default: "apply"
type: string
tag:
description: 'The tag of the containers to deploy'
default: 'latest'
description: "The tag of the containers to deploy"
default: "latest"
type: string
required: false
app_env:
required: false
type: string
description: 'The APP env separates between Azure ENV and Actual APP, since Azure dev is where PR, and TEST is deployed'
description: "The APP env separates between Azure ENV and Actual APP, since Azure dev is where PR, and TEST is deployed"
stack_prefix:
required: true
type: string
description: 'The stack prefix to use for the resources'


description: "The stack prefix to use for the resources"

env:
TF_VERSION: 1.12.2
TF_LOG: ERROR
AZURE_REGION: Canada Central
TF_VERSION: 1.12.2
TF_LOG: ERROR
AZURE_REGION: Canada Central
permissions:
id-token: write # This is required for requesting the JWT
contents: write # This is required for actions/checkout
jobs:
infra:
environment: ${{ inputs.environment_name }}
name: Terraform ${{inputs.command}} ${{ inputs.environment_name }}
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Azure CLI Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3
with:
terraform_version: ${{ env.TF_VERSION }}

- name: Image Tags
id: image-tags
shell: bash
run: |
API_IMAGE="ghcr.io/${{ github.repository }}/api:${{ inputs.tag }}"
FRONTEND_IMAGE="ghcr.io/${{ github.repository }}/frontend:${{ inputs.tag }}"

echo "api-image=$API_IMAGE" >> $GITHUB_OUTPUT
echo "frontend-image=$FRONTEND_IMAGE" >> $GITHUB_OUTPUT

- name: Terraform Init
working-directory: infra
run: |
# Initialize the Terraform backend
terraform init -upgrade -reconfigure \
-backend-config="resource_group_name=${{ secrets.VNET_RESOURCE_GROUP_NAME }}" \
-backend-config="storage_account_name=${{ vars.STORAGE_ACCOUNT_NAME }}" \
-backend-config="container_name=tfstate" \
-backend-config="key=${{ inputs.stack_prefix }}/${{ inputs.app_env }}/terraform.tfstate" \
-backend-config="subscription_id=${{ secrets.AZURE_SUBSCRIPTION_ID }}" \
-backend-config="tenant_id=${{ secrets.AZURE_TENANT_ID }}" \
-backend-config="client_id=${{ secrets.AZURE_CLIENT_ID }}" \
-backend-config="use_oidc=true"
infra:
environment: ${{ inputs.environment_name }}
name: Terraform ${{inputs.command}} ${{ inputs.environment_name }}
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Azure CLI Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3
with:
terraform_version: ${{ env.TF_VERSION }}

- name: Image Tags
id: image-tags
shell: bash
run: |
API_IMAGE="ghcr.io/${{ github.repository }}/api:${{ inputs.tag }}"
FRONTEND_IMAGE="ghcr.io/${{ github.repository }}/frontend:${{ inputs.tag }}"
PROXY_IMAGE="ghcr.io/${{ github.repository }}/proxy:${{ inputs.tag }}"

echo "api-image=$API_IMAGE" >> $GITHUB_OUTPUT
echo "frontend-image=$FRONTEND_IMAGE" >> $GITHUB_OUTPUT
echo "proxy-image=$PROXY_IMAGE" >> $GITHUB_OUTPUT

- name: Terraform Init
working-directory: infra
run: |
# Initialize the Terraform backend
terraform init -upgrade -reconfigure \
-backend-config="resource_group_name=${{ secrets.VNET_RESOURCE_GROUP_NAME }}" \
-backend-config="storage_account_name=${{ vars.STORAGE_ACCOUNT_NAME }}" \
-backend-config="container_name=tfstate" \
-backend-config="key=${{ inputs.stack_prefix }}/${{ inputs.app_env }}/terraform.tfstate" \
-backend-config="subscription_id=${{ secrets.AZURE_SUBSCRIPTION_ID }}" \
-backend-config="tenant_id=${{ secrets.AZURE_TENANT_ID }}" \
-backend-config="client_id=${{ secrets.AZURE_CLIENT_ID }}" \
-backend-config="use_oidc=true"

- name: Terraform ${{inputs.command}}
working-directory: infra
env:
TF_VAR_target_env: ${{ inputs.environment_name }}
TF_VAR_azure_subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
TF_VAR_azure_tenant_id: ${{ secrets.AZURE_TENANT_ID }}
TF_VAR_vnet_resource_group_name: ${{ secrets.VNET_RESOURCE_GROUP_NAME }}
TF_VAR_vnet_name: ${{ secrets.VNET_NAME }}
TF_VAR_api_image: ${{ steps.image-tags.outputs.api-image }}
TF_VAR_stack_prefix: ${{ inputs.stack_prefix }}
TF_VAR_azure_client_id: ${{ secrets.AZURE_CLIENT_ID }}
TF_VAR_repo_name: ${{ github.event.repository.name }}
TF_VAR_frontend_image: ${{ steps.image-tags.outputs.frontend-image }}
TF_VAR_app_env: ${{ inputs.app_env }}
TF_VAR_app_name: ${{ inputs.stack_prefix }}-${{ inputs.app_env }}
TF_VAR_vnet_address_space: ${{ secrets.VNET_ADDRESS_SPACE }}
TF_VAR_client_id: ${{ secrets.AZURE_CLIENT_ID }}
TF_VAR_subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
TF_VAR_tenant_id: ${{ secrets.AZURE_TENANT_ID }}
TF_VAR_resource_group_name: ${{ github.event.repository.name }}-${{ inputs.app_env }}
TF_VAR_common_tags: >-
{"environment":"${{ inputs.environment_name }}","stack_prefix":"${{ inputs.stack_prefix }}","app_env":"${{ inputs.app_env }}","repo_name":"${{ github.event.repository.name }}"}
run: |
- name: Terraform ${{inputs.command}}
working-directory: infra
env:
TF_VAR_target_env: ${{ inputs.environment_name }}
TF_VAR_azure_subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
TF_VAR_azure_tenant_id: ${{ secrets.AZURE_TENANT_ID }}
TF_VAR_vnet_resource_group_name: ${{ secrets.VNET_RESOURCE_GROUP_NAME }}
TF_VAR_vnet_name: ${{ secrets.VNET_NAME }}
TF_VAR_api_image: ${{ steps.image-tags.outputs.api-image }}
TF_VAR_stack_prefix: ${{ inputs.stack_prefix }}
TF_VAR_azure_client_id: ${{ secrets.AZURE_CLIENT_ID }}
TF_VAR_repo_name: ${{ github.event.repository.name }}
TF_VAR_frontend_image: ${{ steps.image-tags.outputs.frontend-image }}
TF_VAR_proxy_image: ${{ steps.image-tags.outputs.proxy-image }}
TF_VAR_app_env: ${{ inputs.app_env }}
TF_VAR_app_name: ${{ inputs.stack_prefix }}-${{ inputs.app_env }}
TF_VAR_vnet_address_space: ${{ secrets.VNET_ADDRESS_SPACE }}
TF_VAR_client_id: ${{ secrets.AZURE_CLIENT_ID }}
TF_VAR_subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
TF_VAR_tenant_id: ${{ secrets.AZURE_TENANT_ID }}
TF_VAR_resource_group_name: ${{ github.event.repository.name }}-${{ inputs.app_env }}
TF_VAR_common_tags: >-
{"environment":"${{ inputs.environment_name }}","stack_prefix":"${{ inputs.stack_prefix }}","app_env":"${{ inputs.app_env }}","repo_name":"${{ github.event.repository.name }}"}
run: |
# now run terraform command with conditional auto-approve
if [[ "${{ inputs.command }}" == "apply" || "${{ inputs.command }}" == "destroy" ]]; then
terraform ${{ inputs.command }} -auto-approve -input=false
Expand Down
36 changes: 28 additions & 8 deletions api-ms-agent/app/auth/service.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
"""Authentication service for JWT validation with Keycloak."""

import time
from typing import Any

import httpx
from fastapi import HTTPException, status
from jose import JWTError, jwt

from app.auth.models import KeycloakUser
from app.config import settings
from app.http_client import get_http_client
from app.logger import get_logger

logger = get_logger(__name__)

# JWKS cache TTL in seconds (10 minutes - balances key rotation detection with performance)
_JWKS_CACHE_TTL_SECONDS = 600


class AuthService:
"""JWT authentication service for Keycloak integration."""
Expand All @@ -31,6 +35,7 @@ def __init__(self):
f"{self.keycloak_url}/realms/{self.keycloak_realm}/protocol/openid-connect/certs"
)
self._jwks_cache: dict[str, Any] | None = None
self._jwks_cache_time: float = 0.0

async def validate_token(self, token: str) -> KeycloakUser:
"""Validate JWT token and return user information."""
Expand Down Expand Up @@ -64,7 +69,6 @@ async def validate_token(self, token: str) -> KeycloakUser:
audience=self.keycloak_client_id,
options={"verify_exp": True},
)
logger.info("Token decoded successfully", sub=payload.get("sub"))
except JWTError as e:
logger.error("JWT verification failed", error=str(e))
raise HTTPException(
Expand Down Expand Up @@ -96,13 +100,23 @@ async def validate_token(self, token: str) -> KeycloakUser:
) from e

async def _get_signing_key(self, kid: str) -> str:
"""Get the signing key from Keycloak JWKS endpoint."""
"""Get the signing key from Keycloak JWKS endpoint with TTL-based caching."""
try:
if not self._jwks_cache:
async with httpx.AsyncClient() as client:
response = await client.get(self.jwks_uri, timeout=30.0)
response.raise_for_status()
self._jwks_cache = response.json()
# Check if cache is expired or empty
cache_age = time.time() - self._jwks_cache_time
cache_expired = cache_age > _JWKS_CACHE_TTL_SECONDS

if not self._jwks_cache or cache_expired:
client = await get_http_client()
response = await client.get(self.jwks_uri)
response.raise_for_status()
self._jwks_cache = response.json()
self._jwks_cache_time = time.time()
logger.debug(
"jwks_cache_refreshed",
cache_was_expired=cache_expired,
keys_count=len(self._jwks_cache.get("keys", [])),
)

# Find the key with matching kid
for key_data in self._jwks_cache.get("keys", []):
Expand All @@ -128,6 +142,12 @@ async def _get_signing_key(self, kid: str) -> str:
)
raise

# Key not found - force refresh cache once in case of key rotation
if not cache_expired:
logger.info("jwks_key_not_found_refreshing", kid=kid)
self._jwks_cache = None # Force refresh on next call
return await self._get_signing_key(kid) # Retry with fresh cache

raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Unable to find signing key",
Expand Down
10 changes: 7 additions & 3 deletions api-ms-agent/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ class Settings(BaseSettings):
azure_openai_embedding_deployment: str = "text-embedding-3-large"

# LLM Configuration - Low temperature for high confidence responses
llm_temperature: float = 0.1 # Low temperature for consistent, high-confidence responses
llm_max_output_tokens: int = 900 # Cap responses to control cost/token usage
llm_temperature: float = 0.0 # Low temperature for consistent, high-confidence responses
llm_max_output_tokens: int = 5000 # Cap responses to control cost/token usage

# Dev UI (Agent Framework DevUI) settings
devui_enabled: bool = True
devui_host: str = "localhost"
devui_port: int = 8000
devui_auto_open: bool = True
devui_auto_open: bool = False
devui_mode: str = "developer" # developer | user

# Cosmos DB settings - for chat history, metadata, and workflow persistence
Expand All @@ -53,6 +53,10 @@ class Settings(BaseSettings):
azure_document_intelligence_endpoint: str = ""
azure_document_intelligence_key: str = "" # Optional if using managed identity

# Azure Speech Services settings (for TTS)
azure_speech_key: str = ""
azure_speech_region: str = "canadacentral"
azure_speech_endpoint: str = ""
# MCP base URLs for BC APIs (override defaults if needed)
geocoder_base_url: str = ""
orgbook_base_url: str = ""
Expand Down
4 changes: 1 addition & 3 deletions api-ms-agent/app/devui.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ def _collect_entities() -> list[object]:
try:
research_agent = get_deep_research_service()._create_research_agent() # noqa: SLF001
entities.append(research_agent)
logger.info(
"devui_entity_added", entity=getattr(research_agent, "name", "research_agent")
)
logger.info("devui_entity_added", entity=getattr(research_agent, "name", "research_agent"))
except Exception as exc: # noqa: BLE001
logger.warning("devui_research_agent_init_failed", error=str(exc))
return entities
Expand Down
Loading