Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
1 change: 0 additions & 1 deletion api-ms-agent/app/auth/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,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
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"

# 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
2 changes: 1 addition & 1 deletion api-ms-agent/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def create_app() -> FastAPI:
app.add_middleware(AuthMiddleware)

# Root endpoints (excluded from auth)
@app.get("/")
@app.get("/api")
async def root():
"""Root endpoint - service status."""
return {
Expand Down
4 changes: 1 addition & 3 deletions api-ms-agent/app/middleware/security_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ async def dispatch(self, request: Request, call_next: Callable) -> Response:
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Strict-Transport-Security"] = (
"max-age=31536000; includeSubDomains"
)
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"

# More permissive CSP for documentation pages
Expand Down
2 changes: 2 additions & 0 deletions api-ms-agent/app/routers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
from app.routers.documents import router as documents_router
from app.routers.orchestrator import router as orchestrator_router
from app.routers.research import router as research_router
from app.routers.speech import router as speech_router
from app.routers.workflow_research import router as workflow_research_router

api_router = APIRouter()
api_router.include_router(chat_router, prefix="/chat", tags=["chat"])
api_router.include_router(documents_router, prefix="/documents", tags=["documents"])
api_router.include_router(orchestrator_router, prefix="/orchestrator", tags=["orchestrator"])
api_router.include_router(research_router, prefix="/research", tags=["research"])
api_router.include_router(speech_router, prefix="/speech", tags=["speech"])
api_router.include_router(
workflow_research_router, prefix="/workflow-research", tags=["workflow_research"]
)
33 changes: 32 additions & 1 deletion api-ms-agent/app/routers/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@

from app.auth.dependencies import get_current_user_from_request
from app.auth.models import KeycloakUser
from app.logger import get_logger
from app.services.azure_search_service import (
AzureSearchService,
get_azure_search_service,
)
from app.services.chat_agent import ChatAgentService, get_chat_agent_service
from app.services.cosmos_db_service import CosmosDbService, get_cosmos_db_service
from app.services.embedding_service import EmbeddingService, get_embedding_service
from app.utils import sort_source_dicts_by_confidence

logger = get_logger(__name__)

router = APIRouter()

Expand Down Expand Up @@ -95,6 +99,15 @@ async def chat(
session_id = request.session_id or str(uuid4())
user_id = current_user.sub

logger.info(
"chat_request_received",
user_id=user_id,
session_id=session_id,
message_preview=request.message[:100],
has_history=request.history is not None,
document_id=request.document_id,
)

# If no history provided but session_id exists, try to load from Cosmos DB
history = None
if request.history:
Expand Down Expand Up @@ -175,6 +188,7 @@ async def chat(
message=request.message,
history=history,
session_id=session_id,
user_id=user_id,
document_context=document_context,
)

Expand All @@ -185,7 +199,9 @@ async def chat(

if document_sources:
# Use document sources from RAG search (more accurate citations)
for doc_src in document_sources:
# Sort by confidence (highest first)
sorted_doc_sources = sort_source_dicts_by_confidence(document_sources)
for doc_src in sorted_doc_sources:
sources.append(
SourceInfoResponse(
source_type=doc_src["source_type"],
Expand All @@ -196,6 +212,7 @@ async def chat(
)
else:
# No document context - use agent's LLM knowledge sources
# Sources are already sorted in the chat_agent service
for src in result.sources:
sources.append(
SourceInfoResponse(
Expand Down Expand Up @@ -235,6 +252,14 @@ async def chat(
sources=all_sources,
)

logger.info(
"chat_request_completed",
user_id=user_id,
session_id=session_id,
source_count=len(sources),
has_sufficient_info=result.has_sufficient_info,
)

return ChatResponse(
response=result.response,
session_id=session_id,
Expand All @@ -243,6 +268,12 @@ async def chat(
)

except Exception as e:
logger.error(
"chat_request_failed",
user_id=user_id,
session_id=session_id,
error=str(e),
)
raise HTTPException(status_code=500, detail=f"Chat error: {str(e)}") from e


Expand Down
72 changes: 69 additions & 3 deletions api-ms-agent/app/routers/documents.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Documents router - API endpoints for document indexing and vector search."""

import logging
from datetime import datetime, UTC
from typing import Annotated

Expand All @@ -9,6 +8,7 @@

from app.auth.dependencies import get_current_user_from_request
from app.auth.models import KeycloakUser
from app.logger import get_logger
from app.services.azure_search_service import AzureSearchService, get_azure_search_service
from app.services.cosmos_db_service import CosmosDbService, get_cosmos_db_service
from app.services.document_intelligence_service import (
Expand All @@ -20,7 +20,7 @@

router = APIRouter()

logger = logging.getLogger(__name__)
logger = get_logger(__name__)


class IndexDocumentRequest(BaseModel):
Expand Down Expand Up @@ -135,6 +135,12 @@ async def upload_document(
)

user_id = current_user.sub if current_user else "anonymous"
logger.info(
"document_upload_requested",
filename=file.filename,
user_id=user_id,
content_type=file.content_type,
)

try:
content_bytes = await file.read()
Expand Down Expand Up @@ -173,6 +179,15 @@ async def upload_document(
chunk_overlap=200,
)

logger.info(
"document_upload_completed",
document_id=document.id,
filename=file.filename,
user_id=user_id,
total_chunks=len(document.chunks),
total_pages=total_pages,
)

return UploadDocumentResponse(
id=document.id,
filename=file.filename or "unknown",
Expand All @@ -187,7 +202,12 @@ async def upload_document(
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
except Exception as e:
logger.error(f"Error processing document upload: {e}")
logger.error(
"document_upload_failed",
filename=file.filename,
user_id=user_id,
error=str(e),
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Upload error: {str(e)}",
Expand Down Expand Up @@ -229,6 +249,7 @@ async def list_documents(
) -> DocumentListResponse:
"""List all indexed documents for the current user."""
user_id = current_user.sub
logger.debug("list_documents_requested", user_id=user_id, limit=limit)

try:
documents = await embedding_service.list_documents(user_id, limit)
Expand All @@ -246,6 +267,7 @@ async def list_documents(
total=len(documents),
)
except Exception as e:
logger.error("list_documents_failed", user_id=user_id, error=str(e))
raise HTTPException(status_code=500, detail=f"List error: {str(e)}") from e


Expand All @@ -261,6 +283,12 @@ async def index_document(
The document is chunked and each chunk is embedded and stored in Cosmos DB.
"""
user_id = current_user.sub
logger.info(
"document_index_requested",
user_id=user_id,
document_id=request.document_id,
title=request.title,
)

try:
document = await embedding_service.index_document(
Expand All @@ -275,6 +303,13 @@ async def index_document(
chunk_overlap=request.chunk_overlap,
)

logger.info(
"document_index_completed",
user_id=user_id,
document_id=document.id,
chunks_created=len(document.chunks),
)

return IndexDocumentResponse(
document_id=document.id,
chunks_created=len(document.chunks),
Expand All @@ -297,6 +332,13 @@ async def search_documents(
Perform vector similarity search across indexed documents.
"""
user_id = current_user.sub
logger.info(
"document_search_requested",
user_id=user_id,
query_length=len(request.query),
document_id=request.document_id,
top_k=request.top_k,
)

try:
results = await embedding_service.search(
Expand All @@ -307,6 +349,12 @@ async def search_documents(
min_similarity=request.min_similarity,
)

logger.info(
"document_search_completed",
user_id=user_id,
results_count=len(results),
)

return SearchResponse(
query=request.query,
results=[
Expand All @@ -323,6 +371,7 @@ async def search_documents(
)

except Exception as e:
logger.error("document_search_failed", user_id=user_id, error=str(e))
raise HTTPException(status_code=500, detail=f"Search error: {str(e)}") from e


Expand All @@ -334,15 +383,32 @@ async def delete_document(
) -> dict:
"""Delete a document and all its chunks."""
user_id = current_user.sub
logger.info(
"document_delete_requested",
user_id=user_id,
document_id=document_id,
)

try:
count = await embedding_service.delete_document(document_id, user_id)
logger.info(
"document_delete_completed",
user_id=user_id,
document_id=document_id,
chunks_deleted=count,
)
return {
"status": "deleted",
"document_id": document_id,
"chunks_deleted": count,
}
except Exception as e:
logger.error(
"document_delete_failed",
user_id=user_id,
document_id=document_id,
error=str(e),
)
raise HTTPException(status_code=500, detail=f"Delete error: {str(e)}") from e


Expand Down
1 change: 1 addition & 0 deletions api-ms-agent/app/routers/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ async def query_orchestrator(
result = await orchestrator.process_query(
query=request.query,
session_id=request.session_id,
user_id=current_user.sub,
)

# Map sources to the response model
Expand Down
Loading