WisePick API
June 9, 2026 · View on GitHub
Docs: Overview | Integration & SDK | Agent Protocol
WisePick Decision API (WPDA) is a stateless capability routing / decision layer:
POST /v1/decide → ECU → your runtime executes → POST /v1/feedback
It does not run tools or own orchestration.
Agent protocol & runtime behavior: AGENTS.md only.
Product overview: README.md.
Hosted Endpoint
Default hosted deployment: https://api.wishweaver.top
For most evaluations and pilot integrations, use the hosted endpoint before deploying your own infrastructure.
Self-hosted deployment: see Deployment & Production Requirements.
Deployment & Production Requirements | 部署与生产环境要求
Production architecture contract: Deploy WPDA as your product’s routing decision service (not a task orchestrator). In production, run WisePick in your private containers or cloud environment and ensure the agent runtime can reach it reliably (stable base URL, health checks, persistence).
生产环境架构契约: 将 WPDA 作为产品的路由决策服务部署(非任务编排引擎)。在生产环境,您应将 WisePick 服务部署在私有容器/云环境中,并确保 Agent 运行时可稳定访问该 API。
Data sovereignty and evolution: Execution feedback and the learning loop depend on persisted decision records on your WPDA host (DATABASE_URL). That store is the on-ramp to collective decision memory; the long-term Execution Experience Network vision is evolutionary—not a separate product you deploy today.
关于数据主权与演进: 执行反馈与学习闭环依赖您 WPDA 生产环境中的持久化决策记录。该存储是集体决策记忆的起点;长期执行经验网络愿景为演进方向,而非当前需单独部署的网络产品。
Local bootstrap (logic validation only): Configure DATABASE_URL in .env (see .env.example), then run uvicorn app.main:app --reload. Smoke check: curl -s http://localhost:8000/health.
本地启动(仅逻辑验证): 配置 .env 中的 DATABASE_URL 后运行 uvicorn;通过 /health 确认服务可用。
15-Minute Integration Checklist
Wire wisepick/client.py into your Agent completion path and tool-execution hook. No extra client dependencies (stdlib urllib only).
Step 1 — Client bootstrap
from wisepick import WisePickClient
wp = WisePickClient(api_url="https://api.wishweaver.top") # hosted default; or your self-hosted base URL
Keep one instance per process (or per session factory). Point api_url at your self-hosted WisePick base (no trailing slash required).
Step 2 — Decide before the first completion
Call inject_openai_choice immediately before the first provider chat.completions (or compatible) request for the turn. It POSTs /v1/decide and, when ECU is valid, sets OpenAI-shaped tool_choice from capability_id.
user_task = "Transcribe today's meeting audio"
api_kwargs = {
"model": "gpt-4o-mini",
"messages": messages,
"tools": openai_tools, # required: inject is a no-op without tools
}
api_call_count = 0 # per user turn / replan cycle
def build_completion_kwargs() -> dict:
global api_call_count
api_call_count += 1
kwargs = dict(api_kwargs)
if api_call_count == 1:
wp.inject_openai_choice(kwargs, user_task)
return kwargs
# first completion only — tool_choice may be present
response = openai_client.chat.completions.create(**build_completion_kwargs())
Persist ECU once per turn if you need decision_id for feedback without a second decide (optional):
ecu = wp.decide(user_task)
# then Step 2 inject still works; or set tool_choice manually from ecu["capability_id"]
inject_openai_choice skips injection when callable is false, capability_id is empty, or tools is missing.
Step 3 — Name alignment (function.name ↔ capability_id)
tool_choice.function.name must equal an entry in tools[].function.name. WisePick ranks by capability_id (semantic capability), not marketing product names.
OpenAI / MCP function.name | WisePick capability_id | Local handler |
|---|---|---|
audio_transcription | audio_transcription | transcribe_audio(provider, …) |
search_files | search_files | MCP tool search_files |
WisePick decouples routing from discovery. Your runtime can load tools statically or query an MCP server dynamically, but it must map discovered tool payloads to stable capability_id strings before hitting the decision layer.
Rule: register tools under the capability_id strings WisePick emits. Use provider + execution_type inside your executor to pick credentials, endpoint, or MCP server—not as the forced function name.
EXECUTORS = {
"audio_transcription": lambda provider, task: run_transcribe(provider, task),
"search_files": lambda provider, task: mcp_call("search_files", task),
}
def run_tool(ecu: dict, task: str):
fn = EXECUTORS.get(ecu["capability_id"])
if not fn:
raise ValueError(ecu["capability_id"])
return fn(ecu["provider"], task)
Mismatch → the model cannot call the forced function → fall back to Step 4 or fix the registry.
Step 4 — Release after the first completion
WisePick routing applies to turn entry (first completion). Later completions in the same turn should not keep a forced tool.
def build_completion_kwargs() -> dict:
global api_call_count
api_call_count += 1
kwargs = dict(api_kwargs)
if api_call_count == 1:
wp.inject_openai_choice(kwargs, user_task)
else:
kwargs.pop("tool_choice", None) # api_call_count > 1: model picks freely
return kwargs
Reset api_call_count = 0 (and drop cached ecu) on new user intent or explicit replan.
Step 5 — Feedback on the execution hook
After your tool/MCP handler finishes (success or failure), call feedback with the decision_id from the turn’s ECU. If the runtime executed a different tool than WisePick recommended, pass actual_tool_used so ROI accrues to the correct handler.
def on_tool_finished(ecu: dict, *, ok: bool, latency_ms: int, actual_tool: str | None = None, err: str | None = None):
did = ecu.get("decision_id")
if not did:
return
wp.feedback(
did,
success=ok,
latency_ms=latency_ms,
error_message=err,
token_cost={"input": 1200, "output": 450},
result_quality=0.92 if ok else None,
actual_tool_used=actual_tool,
)
# success path (after local execute)
on_tool_finished(ecu, ok=True, latency_ms=1200)
# failure path
on_tool_finished(ecu, ok=False, latency_ms=300000, err="timeout after 300s")
Skipping feedback disables learning for that decision.
Closed loop: decide → execute mapped handler → feedback → tool_stats updates next decide.
🔄 Decision Feedback Loop
WisePick learns from actual runtime execution, not only from routing decisions.
A complete integration should follow:
Decision ↓ Execute ↓ Feedback
Runtime requirements:
-
Call
/v1/decidebefore execution. -
Persist the returned
decision_id. -
After execution, call
/v1/feedback. -
Include:
decision_idsuccesslatency_ms
-
Recommended:
runtime_nameactual_tool_usedtoken_costresult_quality
When callable=false, the runtime may execute its own fallback tool.
Feedback should still be submitted using the original decision_id.
WisePick will associate:
- Recommended tool → decision record
- Actual executed tool → runtime observation record
This enables real-world ROI learning.
HTTP Surface (reference)
| Method | Path | Role |
|---|---|---|
| GET | /health | Liveness |
| GET | /v1/analytics/summary | Operator usage snapshot (totals, closure rate, pooled ROI) |
| GET | /v1/analytics/dashboard | Operator entry point — summary + 7-day volume + top runtime |
| GET | /v1/analytics/providers | Per-provider stats from tool_stats |
| GET | /v1/analytics/runtimes | Per-runtime stats from self-reported runtime_name |
| GET | /v1/analytics/timeline | Daily decide/feedback counts (UTC) |
| GET | /v1/analytics/attribution | Recommended vs actual_tool_used feedback breakdown |
| POST | /v1/decide | Task → ECU |
| POST | /v1/feedback | Outcome → stats |
Operator usage (read-only): curl -s http://localhost:8000/v1/analytics/dashboard — single operator entry point for adoption, closure rate, provider/runtime leaders, and recent volume.
GET /v1/analytics/dashboard
{
"decisions_total": 156,
"feedback_total": 121,
"closure_rate": 0.7756,
"active_providers": 7,
"active_runtimes": 3,
"avg_success_rate": 0.89,
"avg_latency_ms": 1532.0,
"avg_token_cost": 1100.25,
"avg_result_quality": 0.91,
"top_provider": "chatgpt",
"top_provider_decisions": 42,
"top_provider_feedback_count": 38,
"top_runtime": "yantrikdb-hermes",
"top_runtime_feedback_count": 84,
"decisions_last_7d": 23,
"feedback_last_7d": 18
}
If feedback.runtime_name has not been migrated yet, active_runtimes is 0, top_runtime is null, and top_runtime_feedback_count is 0.
POST /v1/decide
{
"task": "Summarize this technical document",
"context": { "language": "Chinese" },
"constraints": { "max_cost": 10.0, "timeout_seconds": 300 }
}
ECU fields (integrate against these): decision_id, capability_id, provider, execution_type (api | mcp | function_call), callable, confidence, reason, explain, trace. Legacy tool_key mirrors provider.
When callable is false, WisePick found no matching capability in the registry. The API still returns a persistent decision_id backed by a fallback_unknown anchor so that feedback and runtime execution observations can still be linked later. provider, tool_key, and capability_id are empty strings in the response for compatibility; reason is "No matching capability found"; explain.no_match is true. Do not force tool_choice — let your runtime fallback.
POST /v1/feedback
{
"decision_id": "dec_abc123def4567890",
"success": true,
"latency_ms": 1200,
"token_cost": { "input": 1200, "output": 450 },
"result_quality": 0.92,
"actual_tool_used": "browser_navigate",
"runtime_name": "hermes",
"user_note": "optional free-text error or context"
}
latency_ms is required. Use token_cost and result_quality for ROI aggregates (avg_token_cost, avg_result_quality); do not embed them in user_note.
tool_key(optional audit field): if sent, it must match the decision’s recommendedselected_tool_key.actual_tool_used(optional): the tool/MCP your runtime actually executed. When set, ROI attribution should follow the actual executed tool rather than the recommended tool.runtime_name(optional): self-label for usage analytics only.
Runtime Attribution
Runtimes may self-label feedback for usage analytics only:
{
"decision_id": "dec_abc123def4567890",
"success": true,
"latency_ms": 1200,
"runtime_name": "yantrikdb-hermes"
}
This field is optional. Existing feedback requests without runtime_name continue to work unchanged.
- Used only for usage analytics (
GET /v1/analytics/runtimes,active_runtimesin summary). - No authentication required.
- No impact on routing, billing, or quotas.
Apply scripts/migrate_runtime_name.sql on Supabase/PostgreSQL before persisting or querying runtime attribution.
Apply scripts/migrate_actual_tool_used.sql before persisting or querying actual_tool_used and the updated tool_stats view.
Apply scripts/migrate_fallback_unknown.sql when upgrading an existing database so the fallback_unknown anchor exists in api_tool_specs.
Errors
{ "error": "error_type", "message": "Human-readable description" }
error | HTTP | When |
|---|---|---|
invalid_request | 422 | Validation / no matching tools (ValueError from router) |
persistence_failed | 500 | Decision log could not be persisted |
{ "error": "persistence_failed", "message": "Failed to persist decision log" }
Environment variables
See .env.example for the full list.
Agent / integrator runtime (consumer of WisePick)
| Variable | Required | Default | Role |
|---|---|---|---|
WISEPICK_API_URL | recommended | https://api.wishweaver.top | Base URL for WisePickClient / HTTP adapter |
WISEPICK_DECIDE_URL | optional | {WISEPICK_API_URL}/v1/decide | Full decide endpoint override (Hermes-style routers) |
HERMES_WISEPICK_ROUTING | optional | 1 | 1/true → enable decide + first-completion injection (Hermes integrations) |
WISEPICK_FORCE_TOOL | optional | — | Test/dry-run: skip HTTP; force tool name (must exist in tool_capability_map) |
HERMES_WISEPICK_FORCE_TOOL | optional | — | Hermes alias of WISEPICK_FORCE_TOOL |
Set WISEPICK_API_URL in deployment manifests; map tool_capability_map from the agent’s live tool list before enabling routing (see AGENTS.md wisepick.agent.v1).
WisePick API host (server process)
| Variable | Required on host | Role |
|---|---|---|
DATABASE_URL | yes | PostgreSQL / Supabase for decisions, feedback, and tool_stats |
YANTRIK_DB_URL | no | Cluster health plugin for /v1/decide score penalty |
YANTRIK_DB_API_KEY | no | Bearer token for YantrikDB health |
WISEPICK_LANGFUSE_PUBLIC_KEY | no | Telemetry (mcp.route_decision.v1) |
WISEPICK_LANGFUSE_SECRET_KEY | no | Telemetry |
WISEPICK_LANGFUSE_HOST | no | Langfuse base URL |
WISEPICK_LANGFUSE_OTEL | no | true → OTLP ingestion |
WISEPICK_LANGFUSE_ROUTER_NAME | no | Contract router_name (default wisepick) |
How it works (implementation)
Capability matching | 能力匹配
Task text → capability labels derived from bootstrap rules (api_tool_specs).
task → capabilities
Capability scoring | 能力评分
base_score =
capability_match * 0.70
+ effective_success_rate * 0.20
+ effective_bootstrap_weight * 0.10
final_score = base_score * efficacy
efficacy = result_quality / (log(max(avg_latency_ms, 100)) * log(max(avg_token_cost, 10)))
ROI metrics are read from tool_stats. If capability_match is 0, effective_success_rate is penalized × 0.1. effective_bootstrap_weight decays with feedback_count (half-life 20).
Feedback loop | 反馈闭环
decision → execution → feedback → tool_stats → next decision
Components | 核心组件
Routing Core (decision_engine) — task → ECU scoring and selection
Capability Registry (api_tool_specs) — providers, labels, bootstrap weights
Execution Memory (tool_stats) — success rate, latency, token ROI from feedback
Runtime Observation (observed_tools) — actual runtime tool usage history
Optional plugins
YantrikDB (YANTRIK_DB_URL, optional YANTRIK_DB_API_KEY): On /v1/decide, reads YantrikDB /v1/health. If replication_lag_log_entries > 500, all candidate scores × 0.5 for that request. Empty URL → skipped. Inspect explain.yantrik_cluster, trace.yantrik_cluster. No primary schema change.
Langfuse (WISEPICK_LANGFUSE_PUBLIC_KEY + WISEPICK_LANGFUSE_SECRET_KEY): Background export of mcp.route_decision.v1 (and execution feedback spans when configured). Pass trace_id / session_id in decide context to correlate. Does not add request latency on the hot path.
Telemetry payload shape for observability tools: AGENTS.md.
Runtime adapters
Thin bridges: WisePick decide / feedback; your runtime executes. Pattern: docs/ADAPTER_PATTERN.md.
ChainWeaver
Maps ECU → FlowExecutor.execute_flow; explicit capability_id → (flow_id, flow_version) table only.
- Module:
adapters/chainweaver_adapter.py - Types:
ChainWeaverAdapter,RoutingDecision(flow_id,flow_version,confidence,reasoning) - Entry:
select_and_execute(user_request)orroute()for decide-only
SafeAgent
Maps ECU → idempotent request_id + SafeAgent runtime.execute; closes feedback with ROI user_note.
- Module:
adapters/safeagent_adapter.py - Types:
SafeAgentAdapter,SafeAgentRoutingDecision - Requires
session_id,turn_id(and orchestratorstart_time_msfor distributed idempotency)
Aetheris (experimental)
Maps DecideResponse → AetherisRouteEvidence for durable evidence stores (no HTTP in adapter).
- Module:
adapters/aetheris_adapter.py - Type:
AetherisRoutingAdvisor→to_evidence() - Replay semantics (skip re-decide on replay): AGENTS.md
THYMOS (OpenThymos)
Maps ECU → RoutingEvidence; attaches routing_evidence on outer Proposal per OpenThymos Proposal Contract v1 Option 2 (outside ProposalBody; no HTTP in adapter).
- Module:
adapters/thymos_adapter.py - Types:
FallbackHint,RoutingEvidence,ThymosRoutingAdvisor - Entry:
ThymosRoutingAdvisor(...).to_evidence()thenattach_routing_evidence_to_proposal(proposal_envelope, evidence)
Examples
examples/wisepick_router.py · examples/omnicore_adapter.py
Deploy (Quick Start)
pip install -r requirements.txt
# configure DATABASE_URL in .env
# Existing Supabase/PostgreSQL DB: run scripts/migrate_v0_2_2.sql first if you still use legacy schema
# Then apply scripts/migrate_fallback_unknown.sql, scripts/migrate_runtime_name.sql, and scripts/migrate_actual_tool_used.sql
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000