交易平台AI代理
一个AI驱动的投资组合事件分类账API构建 Microsoft代理框架, 快速API,以及 Azure PostgreSQL --部署到 Azure容器应用 并作为一个 MCP服务器 通过 Azure API管理.
______________________________________________________________________
目录
______________________________________________________________________
概述
交易平台AI代理提供了两个与投资组合事件分类账交互的界面:
| 表面 | 协议 | URL |
|---|---|---|
| REST API | HTTP/JSON | https:/// |
| MCP服务器(AI网关) | MCP/流式HTTP | https://ai-learning-apim.azure-api.net/trading-platform-mcp-server/mcp |
REST API公开结构化查询端点和自然语言 聊天 由Azure OpenAI支持的代理支持的端点。MCP服务器(托管在Azure API管理中)使所有11个端点都可以被任何MCP兼容客户端发现和调用,这些客户端包括Claude Desktop、VS Code Copilot、自定义代理框架,而无需订阅密钥。
______________________________________________________________________
建筑
┌──────────────────────────────────────────────────────────┐
│ MCP Clients │
│ (Claude Desktop · VS Code Copilot · Custom Agents) │
└────────────────┬─────────────────────────────────────────┘
│ MCP protocol (Streamable HTTP / SSE)
▼
┌──────────────────────────────────────────────────────────┐
│ Azure API Management (BasicV2+) │
│ ai-learning-apim.azure-api.net │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ MCP Server: trading-platform-mcp-server │ │
│ │ path: /trading-platform-mcp-server │ │
│ │ type: mcp subscriptionRequired: false │ │
│ │ 11 tools: health, agentStatus, portfolioSummary │ │
│ │ latestPrice, tradeHistory, accountEvents, │ │
│ │ tickerEvents, chat, clearSession, insertEvent │ │
│ │ root │ │
│ └──────────────────────┬──────────────────────────┘ │
│ │ REST proxy │
│ ┌────────────────────┐ └──────────────────────────────┐ │
│ │ REST API │ │ │
│ │ trading-platform-api │ │
│ │ path: / (root) │ │
│ │ subscriptionRequired: false │ │
│ └───────────────────────────────────────────────────┘ │
└────────────────┬─────────────────────────────────────────┘
│ HTTPS
▼
┌──────────────────────────────────────────────────────────┐
│ Azure Container Apps │
│ ai-learning-aca (East US) │
│ min-replicas: 1 | ingress: external | port: 8989 │
│ │
│ Trading Platform FastAPI (Python 3.11) │
│ ├── GET /health │
│ ├── GET / │
│ ├── POST /chat ─┐ │
│ ├── POST /clear_session ├─ Agent endpoints │
│ ├── GET /portfolio/{account_id} ─┘ │
│ ├── GET /portfolio/{account_id}/trades │
│ ├── GET /portfolio/{account_id}/events │
│ ├── GET /ticker/{ticker_symbol}/price │
│ ├── GET /ticker/{ticker_symbol}/events │
│ ├── POST /events │
│ └── GET /agent/status │
└──────┬───────────────────┬────────────────────────────────┘
│ │
│ asyncpg │ azure-identity (DefaultAzureCredential)
▼ ▼
┌─────────────┐ ┌────────────────────────────────────────┐
│ Azure │ │ Azure OpenAI / AI Foundry │
│ PostgreSQL │ │ (direct model endpoint) │
│ Flexible │ │ Model: gpt-4.1 (or configured) │
│ Server │ │ Auth: Managed Identity │
│ (SSL) │ └────────────────────────────────────────┘
└─────────────┘请求流——MCP工具调用(例如。 portfolioSummary)
Client --[MCP call_tool "portfolioSummary" {account_id: "A100"}]--> APIM
APIM --[GET /portfolio/A100]--> Container Apps
Container Apps --[asyncpg SELECT]--> PostgreSQL
PostgreSQL --[rows]--> Container Apps
Container Apps --[JSON {account_id, positions: [...]}]--> APIM
APIM --[MCP tool result]--> Client请求流——通过MCP进行NL聊天
Client --[MCP call_tool "chat" {session_id, message}]--> APIM
↓ inbound policy reconstructs JSON body
APIM --[POST /chat {session_id, message}]--> Container Apps
Container Apps --[agent.run(message, thread)]--> Azure OpenAI
Azure OpenAI --[tool_calls: get_portfolio_summary, ...]-->
Container Apps executes tools against PostgreSQL
Container Apps --[final text response]--> APIM
APIM --[MCP tool result {response: "..."}]--> Client______________________________________________________________________
项目结构
.
├── main.py # Uvicorn entry point (port 8989)
├── chat.py # Interactive CLI chat client
├── requirements.txt # Python dependencies
├── dockerfile # Production container image (python:3.11-slim)
├── docker-compose.yaml # Local stack: API + PostgreSQL + Adminer
├── env.sample # Template — copy to .env
│
├── api/
│ ├── main.py # FastAPI app — all REST endpoints
│ └── main_with_proxy.py # Alternative with APIM proxy headers
│
├── agents/
│ └── trading_platform_agent.py # ChatAgent definition + system prompt
│
├── operations/
│ └── trading_platform_operations.py # asyncpg queries (connection pool, retry)
│
├── tools/
│ └── trading_platform_tool.py # ai_function wrappers (agent-callable tools)
│
├── models/
│ └── chat_models.py # Pydantic request/response models
│
├── data/
│ ├── ddl.sql # PostgreSQL schema + indexes
│ ├── portfolio_event_ledger_500.csv # Synthetic seed data (500 rows)
│ └── portfolio_event_ledger_schema.json
│
├── tests/
│ └── test_mcp.py → (root) # (see test_mcp.py below)
│
├── test_mcp.py # End-to-end MCP server test (8 tools)
│
└── infra/
└── apim-mcp-body.json # APIM MCP ARM body reference______________________________________________________________________
Azure基础架构
所需资源
| 资源 | SKU/Tier | 备注 |
|---|---|---|
| 资源组 | — | ai-learning-rg |
| Azure容器注册表 | 基础+ | 商店 drewl/ai-agent-starter-portfolio-manager |
| Azure容器应用程序环境 | 消费 | ai-learning-aca,美国东部 |
| Azure容器应用 (app) | -- | 最小副本数:0,端口8989,外部入口 |
| Azure PostgreSQL灵活服务器 | 需要可爆B1ms+ | SSL, portfolio_event_ledger 桌子 |
| Azure OpenAI 或 AI铸造厂 | gpt-4.1(或gpt-4o) | 直接模型端点 |
| Azure API管理 | 基础V2+ | BasicV2最低要求——MCP服务器功能所需 |
重要提示: APIM MCP服务器支持(type: "mcp")要求 Basic V2或更高级别。开发人员和消费层不支持此功能。APIM配置
APIM中配置了两个API:
| APIM对象 | 类型 | 路径 | 身份验证 |
|---|---|---|---|
trading-platform-api | 休息 | / (root) | 匿名 |
trading-platform-mcp-server | MCP | /trading-platform-mcp-server | 匿名 |
MCP API创建为 type: mcp 通过Azure门户网站(API管理→ APIs → + 添加API→ MCP服务器)。它自动发现REST后端OpenAPI规范中的11个工具。
管理身份
容器应用程序使用 系统分配的管理身份 具有以下角色分配:
| 角色 | 范围 | 目的 |
|---|---|---|
Cognitive Services OpenAI User | Azure OpenAI资源 | 调用模型端点 |
身份验证使用 DefaultAzureCredential --Azure中的托管身份, az login 当地。
______________________________________________________________________
环境变量
复制 env.sample 到 .env 并填写:
# ── Azure OpenAI (direct model endpoint) ──────────────────
AZURE_OPENAI_API_ENDPOINT=https://your-resource.openai.azure.com/
MODEL_DEPLOYMENT_NAME=gpt-4.1
# ── Azure AI Foundry (alternative — if using Foundry project endpoint) ──
AZURE_PROJECT_ENDPOINT=https://your-resource.services.ai.azure.com/api/projects/your-project
# ── Azure PostgreSQL Flexible Server ──────────────────────
POSTGRES_HOST=your-server.postgres.database.azure.com
POSTGRES_PORT=5432
POSTGRES_DB=postgres
POSTGRES_USER=your-admin-user
POSTGRES_PASSWORD=your-password
POSTGRES_SSL_MODE=require
# ── API Server ─────────────────────────────────────────────
SERVER_HOST=0.0.0.0
SERVER_PORT=8989
SERVER_RELOAD=false # true = uvicorn --reload (dev only)
SERVICE_NAME=trading-platform-api
SERVICE_VERSION=1.0.0
SERVER_URL= # Public base URL for Swagger UI (e.g. https://your-aca-host)
# ── Docker ─────────────────────────────────────────────────
DOCKER_REPO_NAME=drewl/ai-agent-starter-portfolio-manager
# ── APIM MCP ───────────────────────────────────────────────
APIM_MCP_SERVER_URL=https://ai-learning-apim.azure-api.net/trading-platform-mcp-server/mcp
# ── Azure Credentials (optional — for local dev without az login) ──
AZURE_CLIENT_ID=
AZURE_CLIENT_SECRET=
AZURE_TENANT_ID=______________________________________________________________________
API终点
基本URL(生产): https://ai-learning-aca.ashycliff-5cba4403.eastus.azurecontainerapps.io\ 交互式文档: /docs
获取 /
服务信息和端点图。
答复:
{
"service": "trading-platform-api",
"version": "1.0.0",
"docs": "/docs",
"endpoints": { ... }
}______________________________________________________________________
获取 /health
服务健康检查,包括数据库连接。
答复:
{
"status": "healthy",
"service": "trading-platform-api",
"version": "1.0.0",
"agent": "ready",
"database": "connected",
"framework": "Microsoft Agent Framework"
}status 是 "healthy" 只有当代理和数据库都准备就绪时。降级为 "degraded" 否则。
______________________________________________________________________
发布 /chat
与交易平台代理进行自然语言聊天。保持完整的会话上下文 session_id.
请求:
{ "session_id": "user-1", "message": "Give me a portfolio summary for account A100" }答复:
{
"session_id": "user-1",
"response": "Account A100 holds 175 shares of MSFT ...",
"agent": "TradingPlatformAgent"
}代理可以访问7个工具,并将自动调用它们来回答问题。对话历史记录保存在内存中 session_id.
______________________________________________________________________
发布 /clear_session
清除会话的对话历史记录。
请求:
{ "session_id": "user-1" }答复:
{ "status": "cleared", "session_id": "user-1" }______________________________________________________________________
获取 /portfolio/{account_id}
账户的净股票头寸、净成本基础和每个股票代码的最后观察价格。
例子: GET /portfolio/A100
答复:
{
"account_id": "A100",
"positions": [
{
"account_id": "A100",
"ticker_symbol": "MSFT",
"net_shares": 175.0,
"net_cost": 52830.0,
"last_price": 416.10,
"last_event_ts": "2026-02-20T15:00:00+00:00"
}
]
}计算: net_shares = SUM(BUY shares) - SUM(SELL shares), net_cost = SUM(BUY value) - SUM(SELL value).
______________________________________________________________________
获取 /portfolio/{account_id}/trades
买卖账户的交易历史记录。
查询参数:
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
event_type | string | -- | 筛选条件 BUY 或 SELL |
limit | int | 100 | 最大行数 |
例子: GET /portfolio/A100/trades?event_type=BUY&limit=5
答复:
{
"account_id": "A100",
"event_type": "BUY",
"trades": [
{
"id": 499,
"account_id": "A100",
"ticker_symbol": "MSFT",
"event_ts": "2026-02-03T07:15:00+00:00",
"event_type": "BUY",
"shares": 10.0,
"price_per_share": 349.5,
"currency": "USD",
"source": "synthetic"
}
]
}______________________________________________________________________
获取 /portfolio/{account_id}/events
账户的所有分类账事件(买入、卖出、价格),最新优先。
查询参数:
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
limit | int | 100 | 最大行数 |
答复:
{
"account_id": "A100",
"count": 100,
"events": [ { ... }, ... ]
}______________________________________________________________________
获取 /ticker/{ticker_symbol}/price
最近观察到的股票市场价格(最新 PRICE 事件)。
例子: GET /ticker/MSFT/price
答复:
{
"ticker_symbol": "MSFT",
"price_per_share": 416.10,
"currency": "USD",
"event_ts": "2026-02-20T15:00:00+00:00"
}退货 404 如果自动收报机不存在价格事件。
______________________________________________________________________
获取 /ticker/{ticker_symbol}/events
所有账户的自动收报机的所有分类账事件,最新事件优先。
查询参数:
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
limit | int | 100 | 最大行数 |
答复:
{
"ticker_symbol": "MSFT",
"count": 100,
"events": [ { ... }, ... ]
}______________________________________________________________________
发布 /events
在分类账中插入新的投资组合事件。
请求:
{
"account_id": "A100",
"ticker_symbol": "MSFT",
"event_ts": "2026-02-22T10:00:00Z",
"event_type": "BUY",
"shares": 5.0,
"price_per_share": 420.0,
"currency": "USD",
"source": "api"
}event_type 必须 BUY, SELL,或 PRICE.使用 shares: 0 价格事件。
答复(201):
{ "status": "created", "id": 501, "account_id": "A100", ... }______________________________________________________________________
获取 /agent/status
代理功能和已注册的工具列表。
答复:
{
"agent": "TradingPlatformAgent",
"status": "ready",
"tools": [
{ "name": "get_events_by_account", "description": "All events for an account" },
{ "name": "get_events_by_ticker", "description": "All events for a ticker" },
{ "name": "get_portfolio_summary", "description": "Net position + cost basis per ticker" },
{ "name": "get_latest_price", "description": "Most recent PRICE observation" },
{ "name": "get_trade_history", "description": "BUY/SELL history, filterable by type" },
{ "name": "insert_trade_event", "description": "Insert a new ledger event" },
{ "name": "check_database_health", "description": "DB connectivity probe" }
]
}______________________________________________________________________
数据库模式
表: portfolio_event_ledger
CREATE TABLE portfolio_event_ledger (
id BIGSERIAL PRIMARY KEY,
account_id VARCHAR(64) NOT NULL,
ticker_symbol VARCHAR(16) NOT NULL,
event_ts TIMESTAMPTZ NOT NULL,
event_type VARCHAR(8) NOT NULL CHECK (event_type IN ('BUY', 'SELL', 'PRICE')),
shares NUMERIC(18, 6) NOT NULL DEFAULT 0,
price_per_share NUMERIC(18, 6) NOT NULL,
currency VARCHAR(8) NOT NULL,
source VARCHAR(128) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);列
| 列 | 类型 | 注释 |
|---|---|---|
id | 双周期 | 自动递增PK |
account_id | VARCHAR(64) | 例如。 A100, ACC-001 |
ticker_symbol | VARCHAR(16) | 例如。 MSFT, AAPL |
event_ts | 时间戳 | 事件发生的时间 |
event_type | VARCHAR(8) | BUY / SELL / PRICE |
shares | NUMERIC(18,6) | 股份数量; 0 价格活动 |
price_per_share | NUMERIC(18,6) | 交易价格或市场观察 |
currency | VARCHAR(8) | ISO代码,例如。 USD |
source | VARCHAR(128) | broker, market-feed, api, synthetic |
created_at | TIMESTAMPTZ | 行插入时间戳 |
索引
-- Primary query pattern: account + time
CREATE INDEX idx_pel_account_ts ON portfolio_event_ledger (account_id, event_ts DESC);
-- Ticker market queries
CREATE INDEX idx_pel_ticker_ts ON portfolio_event_ledger (ticker_symbol, event_ts DESC);
-- Event type filtering (P&L)
CREATE INDEX idx_pel_event_type ON portfolio_event_ledger (event_type);
-- Position roll-up
CREATE INDEX idx_pel_account_ticker ON portfolio_event_ledger (account_id, ticker_symbol, event_ts DESC);应用架构: psql -h -U -d postgres -f data/ddl.sql\ 加载种子数据: psql -h -U -d postgres -c "\COPY portfolio_event_ledger FROM 'data/portfolio_event_ledger_500.csv' CSV HEADER"
______________________________________________________________________
APIM MCP网关
端点
https://ai-learning-apim.azure-api.net/trading-platform-mcp-server/mcp不需要API密钥(subscriptionRequired: false).
MCP工具(11)
| 工具 | 映射到 | 描述 |
|---|---|---|
health | GET /health | 服务+数据库健康状况 |
agentStatus | GET /agent/status | 注册工具 |
portfolioSummary | GET /portfolio/{account_id} | 净头寸 |
latestPrice | GET /ticker/{ticker_symbol}/price | 最新市场价格 |
tradeHistory | GET /portfolio/{account_id}/trades | 买卖历史 |
accountEvents | GET /portfolio/{account_id}/events | 所有帐户事件 |
tickerEvents | GET /ticker/{ticker_symbol}/events | 所有股票交易事件 |
chat | POST /chat | NL与客服聊天 |
clearSession | POST /clear_session | 清除会话历史记录 |
insertEvent | POST /events | 插入新事件 |
root | GET / | 服务信息 |
使用Python
import asyncio, httpx, os
from dotenv import load_dotenv
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
load_dotenv()
MCP_URL = os.getenv("APIM_MCP_SERVER_URL")
async def main():
async with httpx.AsyncClient(timeout=120.0) as http:
async with streamable_http_client(MCP_URL, http_client=http) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
# List tools
tools = await session.list_tools()
# Call a tool
result = await session.call_tool("latestPrice", {"ticker_symbol": "MSFT"})
print(result.content[0].text)
asyncio.run(main())APIM MCP预览——POST正文解决方法
问题: APIM-MCP(2025-03-01-preview)正确代理GET端点的路径/查询参数,但 不 将MCP工具参数序列化为POST端点的JSON请求体。FastAPI接收一个空正文并返回HTTP 422。
受影响的工具: chat, clearSession, insertEvent
修复: 每个POST操作上的APIM入站策略都会根据APIM转发的查询参数重建JSON正文:
@{
try {
var body = context.Request.Body?.As<JObject>(true);
if (body != null && body.ContainsKey("session_id")) { return body.ToString(); }
} catch {}
var s = context.Request.Url.Query.GetValueOrDefault("session_id", "mcp-session");
var m = context.Request.Url.Query.GetValueOrDefault("message", "");
return new JObject(
new JProperty("session_id", s),
new JProperty("message", m)
).ToString();
}
application/json
通过以下方式部署:
# Build policy_body.json: { "properties": { "value": "
...
", "format": "xml" } }
az rest --method PUT \
--uri "https://management.azure.com/subscriptions/{subId}/resourceGroups/ai-learning-rg/providers/Microsoft.ApiManagement/service/ai-learning-apim/apis/trading-platform-api/operations/chat_chat_post/policies/policy?api-version=2022-08-01" \
--body "@policy_body.json" \
--headers "Content-Type=application/json"______________________________________________________________________
本地开发
先决条件
- Python 3.11+
- Azure命令行界面(
az login) - 访问Azure OpenAI或AI Foundry(
AZURE_OPENAI_API_ENDPOINT或AZURE_PROJECT_ENDPOINT) - PostgreSQL连接(Azure或本地Docker)
设置
# 1. Create and activate virtual environment
_env_create.bat
_env_activate.bat
# 2. Install dependencies
_install.bat
# 3. Copy and fill in environment variables
copy env.sample .env
# Edit .env with your values
# 4. Start the API server locally
python main.py
# → http://localhost:8989/docs交互式CLI聊天
python chat.py呈现一个REPL,用于向以下对象发送消息 POST /chat 并打印响应。
______________________________________________________________________
码头工人
构建与运行
# Build image
_build.bat
# Start full stack (API + PostgreSQL + Adminer)
_up.bat
# View logs
_logs.bat
# Stop
_down.bat服务开始于 _up.bat:
| 服务 | 端口 | 描述 |
|---|---|---|
ai-agent-starter-api | 8989 | 交易平台API |
ai-agent-starter-portfolio-manager-postgres | 5000→5432 | PostgreSQL 17 |
ai-agent-starter-portfolio-manager-adminer | 8888 | 管理员数据库用户界面 |
推送到注册表
_push.bat
# Pushes image to DOCKER_REPO_NAME defined in .env______________________________________________________________________
助手脚本
| 脚本 | 动作 |
|---|---|
_env_create.bat | python -m venv .venv |
_env_activate.bat 激活 .venv | |
_env_deactivate.bat | 停用 .venv |
_install.bat | pip install -r requirements.txt |
_build.bat | docker build |
_up.bat | docker compose up -d |
_down.bat | docker compose down |
_logs.bat | docker compose logs -f |
_push.bat | docker push 到注册表 |
______________________________________________________________________
测试
MCP端到端测试
针对实时APIM端点测试所有8个核心MCP工具:
python test_mcp.py预期产量:
============================================================
Trading Platform MCP Server Test
URL: https://ai-learning-apim.azure-api.net/trading-platform-mcp-server/mcp
============================================================
Connected — MCP session initialized
MCP tools advertised (11): health, agentStatus, chat, ...
[health] ✅ healthy
[agentStatus] ✅ 7 tools
[portfolioSummary] ✅ 1 ticker(s)
[latestPrice] ✅ MSFT @ 416.10
[tradeHistory] ✅ 67 trades
[accountEvents] ✅ 100 events
[tickerEvents] ✅ 100 events
[chat] ✅ Agent replied (212 chars)
------------------------------------------------------------
8/8 passed需要 APIM_MCP_SERVER_URL 在 .env.
REST API(手动)
# Health
curl https://ai-learning-aca.ashycliff-5cba4403.eastus.azurecontainerapps.io/health
# Portfolio summary
curl https://ai-learning-aca.ashycliff-5cba4403.eastus.azurecontainerapps.io/portfolio/A100
# Latest price
curl https://ai-learning-aca.ashycliff-5cba4403.eastus.azurecontainerapps.io/ticker/MSFT/price
# NL chat
curl -X POST https://ai-learning-aca.ashycliff-5cba4403.eastus.azurecontainerapps.io/chat \
-H "Content-Type: application/json" \
-d '{"session_id": "test", "message": "Summarize portfolio A100"}'______________________________________________________________________
许可证
本项目根据 许可证 文件。
