Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
push:
branches: [main, feat/agent-core]
branches: [main, feat/agent-core, skills/dataset-procurement]
pull_request:
branches: [main]

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ venv/
/plans/
eval_results/
scripts/
tests/fixtures/
95 changes: 92 additions & 3 deletions api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import uuid
from functools import partial

from fastapi import APIRouter, HTTPException, Request
from fastapi import APIRouter, File, HTTPException, Request, UploadFile
from fastapi.datastructures import FormData

from core.models import SkillResponse, InitRequest, InitResponse
from skills.router import SkillRouter
Expand Down Expand Up @@ -33,8 +34,11 @@

def register_skills():
"""Register all skills. Called at startup."""
from skills.hackathon_novelty import skill_card
_skill_router.register(skill_card)
from skills.hackathon_novelty import skill_card as hackathon_card
_skill_router.register(hackathon_card)

from skills.confidential_data_procurement import skill_card as procurement_card
_skill_router.register(procurement_card)


# --- Helpers ---
Expand Down Expand Up @@ -351,6 +355,91 @@ def get_my_submissions(request: Request):
return {"submission_ids": list(token_info["submission_ids"])}


@router.post("/upload")
async def upload_file(request: Request):
"""
Generic file upload — delegates entirely to the skill's upload_handler.
Skills that need file upload declare upload_handler on their SkillCard.
The skill owns all parsing, storage, and validation logic.

Returns whatever the skill's upload_handler returns (e.g. {"dataset_id": "..."}).
"""
token_info = _resolve_token(request)
instance_id = token_info["instance_id"]
card = _skill_router.get_card(_instances[instance_id]["skill_name"])

if card.upload_handler is None:
raise HTTPException(
status_code=400,
detail=f"Skill '{_instances[instance_id]['skill_name']}' does not support file upload",
)

try:
form: FormData = await request.form()
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(None, card.upload_handler, form, instance_id)
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Upload failed: {e}")

return result


@router.post("/respond")
async def respond_to_result(body: dict, request: Request):
"""
Deal response — delegates entirely to the skill's respond_handler.
Skills that support renegotiation declare respond_handler on their SkillCard.

Body: {
"submission_id": str,
"action": "accept" | "reject" | "renegotiate",
"revised_value": float | null # only when action="renegotiate"
}
Returns: {"settlement_status": str, ...any extra fields the skill returns}
"""
token_info = _resolve_token(request)
instance_id = token_info["instance_id"]
role = token_info["role"]
card = _skill_router.get_card(_instances[instance_id]["skill_name"])

if card.respond_handler is None:
raise HTTPException(
status_code=400,
detail=f"Skill '{_instances[instance_id]['skill_name']}' does not support deal responses",
)

submission_id = body.get("submission_id")
if not submission_id:
raise HTTPException(status_code=422, detail="submission_id is required")

instance_results = _results.get(instance_id, {})
if submission_id not in instance_results:
raise HTTPException(status_code=404, detail="Result not found or not yet available")

action = body.get("action")
if action not in ("accept", "reject", "renegotiate"):
raise HTTPException(status_code=422, detail="action must be 'accept', 'reject', or 'renegotiate'")

try:
loop = asyncio.get_event_loop()
updated = await loop.run_in_executor(
None,
card.respond_handler,
instance_results[submission_id],
action,
body.get("revised_value"),
"buyer" if role == "admin" else "supplier",
_instances[instance_id]["config"],
)
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e))

_results[instance_id][submission_id] = updated
return {"settlement_status": updated.get("settlement_status")}


@router.post("/trigger")
async def trigger(request: Request):
"""Manual pipeline trigger. Admin only. Uses stored instance config."""
Expand Down
2 changes: 2 additions & 0 deletions core/skill_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class SkillCard:
roles: dict = field(default_factory=dict) # admin + user role declarations
setup_prompt: str = "" # LLM onboarding text for admins (metadata/docs)
init_handler: Optional[Callable] = None # skill-owned onboarding conversation handler
upload_handler: Optional[Callable] = None # skill-owned file upload handler (POST /upload)
respond_handler: Optional[Callable] = None # skill-owned deal response handler (POST /respond)
user_display: dict = field(default_factory=dict) # display hints per output key for the frontend renderer
version: str = "0.1.0"

Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ langgraph
python-dotenv
pytest
httpx
python-multipart
datasets
PyJWT>=2.8.0
supabase>=2.0.0
cryptography>=42.0.0
Expand Down
6 changes: 6 additions & 0 deletions skills/confidential_data_procurement/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Per-node model overrides for confidential_data_procurement skill.
# Copy to skills/confidential_data_procurement/.env and fill in values.
# Empty value = fallback to CONCLAVE_DEFAULT_MODEL in root .env

CONCLAVE_CDP_INIT_MODEL=deepseek-ai/DeepSeek-V3.1
CONCLAVE_CDP_EVALUATE_MODEL=deepseek-ai/DeepSeek-V3.1
Loading
Loading