Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ Thumbs.db
**/coverage
**/.pytest_cache
**/.mypy_cache

# Large documentation files
*.pdf
foundry*.md
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# LLM Configuration (Required)
HINDSIGHT_API_LLM_PROVIDER=openai
HINDSIGHT_API_LLM_API_KEY=your-api-key-here
HINDSIGHT_API_LLM_MODEL=o3-mini
HINDSIGHT_API_LLM_MODEL=gpt-5.2-chat
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5.2 is very slow and we don't want poor first-user experience

HINDSIGHT_API_LLM_BASE_URL=https://api.openai.com/v1

# API Configuration (Optional)
Expand Down
644 changes: 543 additions & 101 deletions AGENTS.md

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions Dockerfile.agent-api
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Hindsight Agent API - Azure Container Apps Deployment
# Multi-stage build for smaller image

FROM python:3.12-slim as builder

WORKDIR /app

# Install build dependencies
RUN pip install --no-cache-dir --upgrade pip

# Copy requirements and install
COPY requirements-agent-api.txt .
RUN pip install --no-cache-dir -r requirements-agent-api.txt

# Production stage
FROM python:3.12-slim

WORKDIR /app

# Copy installed packages from builder
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin

# Copy application code
COPY hindsight_agent_api.py .
COPY hindsight_client.py .
COPY config.py .

# Create non-root user for security
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser

# Azure Container Apps expects port 8080 by default
ENV PORT=8080
ENV HOST=0.0.0.0

EXPOSE 8080

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')" || exit 1

CMD ["python", "hindsight_agent_api.py"]
146 changes: 146 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""
Centralized configuration module for Hindsight Foundry Agent.

Loads configuration from Azure App Configuration with fallback to environment variables.
"""
import os
from dataclasses import dataclass
from typing import Optional

# Try to import Azure App Configuration provider
try:
from azure.appconfiguration.provider import load
# DefaultAzureCredential unused but keeping if needed for future AAD auth
HAS_APP_CONFIG = True
except ImportError:
HAS_APP_CONFIG = False


@dataclass
class HindsightConfig:
"""Configuration for the Hindsight Foundry Agent."""
project_endpoint: str
model_deployment_name: str
mcp_base_url: str
default_bank_id: str
agent_name: str

@property
def mcp_url(self) -> str:
"""Full MCP URL including bank_id path."""
base = self.mcp_base_url.rstrip('/')
return f"{base}/mcp/{self.default_bank_id}/"


# Azure App Configuration endpoint and connection string
APP_CONFIG_ENDPOINT = "https://hindsightapp.azconfig.io"
# Use environment variable for secrets - never hardcode credentials
APP_CONFIG_CONNECTION_STRING = os.environ.get(
"AZURE_APP_CONFIG_CONNECTION_STRING",
"" # Must be set in environment for App Config to work
)

# Prefix for configuration keys in App Configuration
CONFIG_PREFIX = "Hindsight:"


def _get_from_app_config() -> Optional[dict]:
"""Load configuration from Azure App Configuration."""
if not HAS_APP_CONFIG:
print("Azure App Configuration SDK not installed")
return None

try:
from azure.appconfiguration.provider import SettingSelector

# Load using connection string (access key auth)
if not APP_CONFIG_CONNECTION_STRING:
print("WARNING: AZURE_APP_CONFIG_CONNECTION_STRING is empty. App Config will fail.")

config = load(
connection_string=APP_CONFIG_CONNECTION_STRING,
selects=[SettingSelector(key_filter=f"{CONFIG_PREFIX}*")],
trim_prefixes=[CONFIG_PREFIX],
)

if config:
print("Loaded configuration from Azure App Configuration")
return dict(config)

except Exception as e:
print(f"WARNING: Could not load from App Configuration: {e}")

return None


def _get_from_env() -> dict:
"""Load configuration from environment variables."""
return {
"ProjectEndpoint": os.environ.get(
"HINDSIGHT_PROJECT_ENDPOINT",
"https://jacob-1216-resource.services.ai.azure.com/api/projects/jacob-1216"
),
# Use gpt-4o as default - verified working deployment
# Set HINDSIGHT_MODEL_DEPLOYMENT_NAME to override
"ModelDeploymentName": os.environ.get(
"HINDSIGHT_MODEL_DEPLOYMENT_NAME",
"gpt-5.2-chat"
),
"McpBaseUrl": os.environ.get(
"HINDSIGHT_MCP_BASE_URL",
"https://hindsight-api.politebay-1635b4f9.centralus.azurecontainerapps.io"
),
"DefaultBankId": os.environ.get(
"HINDSIGHT_DEFAULT_BANK_ID",
"hindsight_agent_bank"
),
"AgentName": os.environ.get(
"HINDSIGHT_AGENT_NAME",
"Hindsight"
),
}


def get_config() -> HindsightConfig:
"""
Get configuration from Azure App Configuration or environment variables.

Priority:
1. Azure App Configuration (if available)
2. Environment variables
3. Hardcoded defaults
"""
# Try App Configuration first
config_dict = _get_from_app_config()

# Allow environment variable overrides
if config_dict:
if "HINDSIGHT_MODEL_DEPLOYMENT_NAME" in os.environ:
print(f"ℹ Overriding model with: {os.environ['HINDSIGHT_MODEL_DEPLOYMENT_NAME']}")
config_dict["ModelDeploymentName"] = os.environ["HINDSIGHT_MODEL_DEPLOYMENT_NAME"]

# Fall back to environment variables
if not config_dict:
print("Using environment/default configuration")
config_dict = _get_from_env()

return HindsightConfig(
project_endpoint=config_dict.get("ProjectEndpoint", ""),
model_deployment_name=config_dict.get("ModelDeploymentName", "gpt-5.2-chat"),
mcp_base_url=config_dict.get("McpBaseUrl", ""),
default_bank_id=config_dict.get("DefaultBankId", "hindsight_agent_bank"),
agent_name=config_dict.get("AgentName", "Hindsight"),
)


if __name__ == "__main__":
# Test configuration loading
print("Testing configuration loading...")
config = get_config()
print(f"\nConfiguration loaded:")
print(f" Project Endpoint: {config.project_endpoint}")
print(f" Model: {config.model_deployment_name}")
print(f" MCP Base URL: {config.mcp_base_url}")
print(f" Default Bank ID: {config.default_bank_id}")
print(f" Agent Name: {config.agent_name}")
print(f" Full MCP URL: {config.mcp_url}")
84 changes: 84 additions & 0 deletions create_agent_full_spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Create Hindsight agent with complete OpenAPI spec."""
import json
from azure.identity import AzureCliCredential
from azure.ai.projects import AIProjectClient
from azure.ai.projects.models import (
PromptAgentDefinition,
OpenApiAgentTool,
OpenApiFunctionDefinition,
OpenApiManagedAuthDetails,
OpenApiManagedSecurityScheme
)

# Load full spec
with open('hindsight-tools-openapi-full.json', 'r') as f:
openapi_spec = json.load(f)

print(f"Loaded spec with {len(openapi_spec['paths'])} endpoints")

# Connect
credential = AzureCliCredential()
import os
project_endpoint = os.environ.get(
'HINDSIGHT_PROJECT_ENDPOINT',
'https://jacob-1216-resource.services.ai.azure.com/api/projects/jacob-1216'
)
client = AIProjectClient(credential=credential, endpoint=project_endpoint)

# Create OpenAPI function definition
openapi_func_def = OpenApiFunctionDefinition(
name='hindsight_memory_api',
description='Complete Hindsight Memory API - memory storage, retrieval, reflection, bank management, entity management, documents, operations, and system monitoring',
spec=openapi_spec,
auth=OpenApiManagedAuthDetails(
security_scheme=OpenApiManagedSecurityScheme(audience='https://cognitiveservices.azure.com')
)
)

# Create OpenAPI tool
openapi_tool = OpenApiAgentTool(openapi=openapi_func_def)

# Agent instructions
INSTRUCTIONS = '''You are Hindsight, an AI with persistent memory capabilities.

## Core Memory Operations
- **retain**: Store new memories (facts, experiences, opinions)
- **recall**: Search and retrieve relevant memories using semantic search
- **reflect**: Synthesize memories into coherent understanding

## Memory Types
- **world**: Facts about the external world
- **experience**: Personal experiences and events
- **opinion**: Beliefs, preferences, and judgments

## Admin Capabilities
You can also manage the memory system:
- List, create, update, and delete memory banks
- View bank profiles and statistics
- List and manage memories within banks
- View entities and their observations
- Manage documents and chunks
- Monitor async operations
- Check system health and metrics

## Behavior
1. Before answering questions about past conversations or user preferences, use recall
2. Store important information the user shares using retain
3. Use reflect to build comprehensive understanding of topics
4. When asked about system status, use health/metrics endpoints
5. Help users manage their memory banks when requested

Always be helpful and use your memory capabilities proactively.'''

# Create agent definition (name goes in create_version, not definition)
agent_def = PromptAgentDefinition(
model='gpt-5.2-chat',
instructions=INSTRUCTIONS,
tools=[openapi_tool]
)

# Create new version
result = client.agents.create_version('Hindsight-v3', definition=agent_def)
print(f"Created agent: {result.name}:{result.version}")
print(f"Model: {result.definition.model}")
print(f"Tools: {len(result.definition.tools)}")
95 changes: 95 additions & 0 deletions deploy-bicep.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Deploy Hindsight Agent API to Azure using Bicep

.DESCRIPTION
This script deploys the Hindsight Agent API infrastructure using Bicep
and builds/pushes the container image to ACR.

.PARAMETER ResourceGroup
The Azure resource group name (default: hindsight-rg)

.PARAMETER Location
The Azure region (default: centralus)

.EXAMPLE
.\deploy-bicep.ps1
.\deploy-bicep.ps1 -ResourceGroup my-rg -Location eastus
#>

param(
[string]$ResourceGroup = "hindsight-rg",
[string]$Location = "centralus",
[string]$ImageTag = "latest",
[string]$AiResourceGroup = "jacob-1216-resource",
[string]$AiResourceName = "jacob-1216-resource"
)

$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path

Write-Host "Deploying Hindsight Agent API with Bicep" -ForegroundColor Cyan
Write-Host " Resource Group: $ResourceGroup"
Write-Host " Location: $Location"

# Check Azure CLI login
Write-Host "`nChecking Azure CLI authentication..." -ForegroundColor Yellow
$accountJson = az account show 2>$null
if (-not $accountJson) {
Write-Host "Not logged in to Azure CLI. Run 'az login' first." -ForegroundColor Red
exit 1
}
$account = $accountJson | ConvertFrom-Json
Write-Host " Logged in as: $($account.user.name)"
Write-Host " Subscription: $($account.name)"

# Create resource group if needed
Write-Host "`nEnsuring resource group exists..." -ForegroundColor Yellow
az group create --name $ResourceGroup --location $Location --output none 2>$null

# Deploy Bicep template
Write-Host "`nDeploying infrastructure with Bicep..." -ForegroundColor Yellow
$deploymentJson = az deployment group create --resource-group $ResourceGroup --template-file "$ScriptDir/infra/agent-api.bicep" --parameters location=$Location imageTag=$ImageTag aiProjectResourceGroup=$AiResourceGroup aiResourceName=$AiResourceName --query "properties.outputs" --output json

if (-not $deploymentJson) {
Write-Host "Bicep deployment failed" -ForegroundColor Red
exit 1
}

$deploymentOutput = $deploymentJson | ConvertFrom-Json

$acrLoginServer = $deploymentOutput.acrLoginServer.value
$containerAppName = $deploymentOutput.containerAppName.value
$containerAppUrl = $deploymentOutput.containerAppUrl.value
$principalId = $deploymentOutput.principalId.value

Write-Host " ACR: $acrLoginServer"
Write-Host " Container App: $containerAppName"
Write-Host " Principal ID: $principalId"

# Build and push container image
Write-Host "`nBuilding and pushing container image..." -ForegroundColor Yellow
$acrName = $acrLoginServer.Split('.')[0]

az acr build --registry $acrName --resource-group $ResourceGroup --image "${containerAppName}:$ImageTag" --file "$ScriptDir/Dockerfile.agent-api" $ScriptDir

# Update container app to use new image (triggers deployment)
Write-Host "`nUpdating container app..." -ForegroundColor Yellow
az containerapp update --name $containerAppName --resource-group $ResourceGroup --image "$acrLoginServer/${containerAppName}:$ImageTag" --output none

# Assign RBAC to AI Project (cross-resource-group)
Write-Host "`nAssigning RBAC to AI Project..." -ForegroundColor Yellow
$subscriptionId = $account.id
$aiResourceId = "/subscriptions/$subscriptionId/resourceGroups/$AiResourceGroup/providers/Microsoft.CognitiveServices/accounts/$AiResourceName"

az role assignment create --assignee $principalId --role "Cognitive Services User" --scope $aiResourceId --output none 2>$null
Comment on lines 81 to 86
Copy link

Copilot AI Dec 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded Azure resource URLs and IDs in deployment script reduce reusability. The AI resource ID at line 82 is hardcoded with a specific subscription ID and resource group 'jacob-1216-resource'. This couples the deployment script to a specific environment. Consider making these values parameters or loading them from configuration, which would allow the script to be used across different subscriptions and resource groups.

Copilot uses AI. Check for mistakes.

Write-Host "`nDeployment complete!" -ForegroundColor Green
Write-Host ""
Write-Host " API URL: $containerAppUrl" -ForegroundColor Cyan
Write-Host " API Docs: $containerAppUrl/docs"
Write-Host " Health: $containerAppUrl/health"
Write-Host ""
Write-Host "Test with:" -ForegroundColor Yellow
Write-Host "Invoke-RestMethod -Uri '$containerAppUrl/chat' -Method Post -ContentType 'application/json' -Body '{""message"":""Hello!""}'"
Loading