Skip to content
Open
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 flipcoin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
SSEEvent,
SeedSubsidyInfo,
SimilarMarket,
StrategyParams,
UpdateAgentRequest,
SlippageCurveEntry,
SlippageCurves,
SplitLeg,
Expand Down Expand Up @@ -189,6 +191,8 @@
"SSEEvent",
"SeedSubsidyInfo",
"SimilarMarket",
"StrategyParams",
"UpdateAgentRequest",
"SlippageCurveEntry",
"SlippageCurves",
"SplitLeg",
Expand Down
62 changes: 62 additions & 0 deletions flipcoin/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@
RedeemBatchResponse,
RedeemPosition,
SSEEvent,
StrategyParams,
TradeHistoryResponse,
TradeNonceResponse,
TradeResult,
UpdateAgentRequest,
ValidateResult,
VaultBalanceResponse,
Webhook,
Expand Down Expand Up @@ -902,3 +904,63 @@ async def get_leaderboard(
params["offset"] = offset
data = await self._get("/api/agents/leaderboard", params=params)
return LeaderboardResponse.from_dict(data)

# -----------------------------------------------------------------------
# Agent profile management
# -----------------------------------------------------------------------

async def update_agent(
self,
*,
system_prompt: str | None = None,
strategy_params: StrategyParams | None = None,
public_strategy_description: str | None = None,
public_about: str | None = None,
personality_notes: list[str] | None = None,
) -> dict:
"""Update agent profile fields (``POST /api/agent/api-key`` action="update-agent").

All fields are optional — only provided fields are updated.

Private fields (owner-only, never exposed publicly):
system_prompt: Agent system prompt (max 8000 chars).
strategy_params: Structured strategy configuration.

Public fields (visible on leaderboard / profile):
public_strategy_description: Short public description of strategy (max 300 chars).
public_about: Public "about" text for the agent profile (max 500 chars).
personality_notes: List of personality trait strings (max 10 items × 50 chars).

Args:
system_prompt: Private system prompt for the agent (max 8000 chars).
strategy_params: :class:`StrategyParams` instance with strategy config.
public_strategy_description: Public strategy description (max 300 chars).
public_about: Public about text (max 500 chars).
personality_notes: List of personality trait strings (max 10 × 50 chars).

Returns:
Raw response dict from the API.
"""
body: dict[str, Any] = {"action": "update-agent"}
if system_prompt is not None:
body["systemPrompt"] = system_prompt
if strategy_params is not None:
sp: dict[str, Any] = {}
if strategy_params.risk_tolerance is not None:
sp["riskTolerance"] = strategy_params.risk_tolerance
if strategy_params.preferred_categories is not None:
sp["preferredCategories"] = strategy_params.preferred_categories
if strategy_params.min_confidence_bps is not None:
sp["minConfidenceBps"] = strategy_params.min_confidence_bps
if strategy_params.max_markets_per_day is not None:
sp["maxMarketsPerDay"] = strategy_params.max_markets_per_day
if strategy_params.max_position_usdc is not None:
sp["maxPositionUsdc"] = strategy_params.max_position_usdc
body["strategyParams"] = sp
if public_strategy_description is not None:
body["publicStrategyDescription"] = public_strategy_description
if public_about is not None:
body["publicAbout"] = public_about
if personality_notes is not None:
body["personalityNotes"] = personality_notes
return await self._post("/api/agent/api-key", json_body=body)
62 changes: 62 additions & 0 deletions flipcoin/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@
RedeemBatchResponse,
RedeemPosition,
SSEEvent,
StrategyParams,
TradeHistoryResponse,
TradeNonceResponse,
TradeResult,
UpdateAgentRequest,
ValidateResult,
VaultBalanceResponse,
Webhook,
Expand Down Expand Up @@ -963,6 +965,66 @@ def get_leaderboard(
data = self._get("/api/agents/leaderboard", params=params)
return LeaderboardResponse.from_dict(data)

# -----------------------------------------------------------------------
# Agent profile management
# -----------------------------------------------------------------------

def update_agent(
self,
*,
system_prompt: str | None = None,
strategy_params: StrategyParams | None = None,
public_strategy_description: str | None = None,
public_about: str | None = None,
personality_notes: list[str] | None = None,
) -> dict:
"""Update agent profile fields (``POST /api/agent/api-key`` action="update-agent").

All fields are optional — only provided fields are updated.

Private fields (owner-only, never exposed publicly):
system_prompt: Agent system prompt (max 8000 chars).
strategy_params: Structured strategy configuration.

Public fields (visible on leaderboard / profile):
public_strategy_description: Short public description of strategy (max 300 chars).
public_about: Public "about" text for the agent profile (max 500 chars).
personality_notes: List of personality trait strings (max 10 items × 50 chars).

Args:
system_prompt: Private system prompt for the agent (max 8000 chars).
strategy_params: :class:`StrategyParams` instance with strategy config.
public_strategy_description: Public strategy description (max 300 chars).
public_about: Public about text (max 500 chars).
personality_notes: List of personality trait strings (max 10 × 50 chars).

Returns:
Raw response dict from the API.
"""
body: dict[str, Any] = {"action": "update-agent"}
if system_prompt is not None:
body["systemPrompt"] = system_prompt
if strategy_params is not None:
sp: dict[str, Any] = {}
if strategy_params.risk_tolerance is not None:
sp["riskTolerance"] = strategy_params.risk_tolerance
if strategy_params.preferred_categories is not None:
sp["preferredCategories"] = strategy_params.preferred_categories
if strategy_params.min_confidence_bps is not None:
sp["minConfidenceBps"] = strategy_params.min_confidence_bps
if strategy_params.max_markets_per_day is not None:
sp["maxMarketsPerDay"] = strategy_params.max_markets_per_day
if strategy_params.max_position_usdc is not None:
sp["maxPositionUsdc"] = strategy_params.max_position_usdc
body["strategyParams"] = sp
if public_strategy_description is not None:
body["publicStrategyDescription"] = public_strategy_description
if public_about is not None:
body["publicAbout"] = public_about
if personality_notes is not None:
body["personalityNotes"] = personality_notes
return self._post("/api/agent/api-key", json_body=body)


# ---------------------------------------------------------------------------
# Shared helper
Expand Down
45 changes: 45 additions & 0 deletions flipcoin/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1342,6 +1342,48 @@ def from_dict(cls, data: dict) -> CommentsListResponse:
return cls(comments=_parse_list(CommentDetail, data.get("comments")))


# ---------------------------------------------------------------------------
# Agent profile — POST /api/agent/api-key (action: "update-agent")
# ---------------------------------------------------------------------------


@dataclass
class StrategyParams:
"""Strategy configuration for an agent (owner-only, private).

All fields are optional. Values are validated server-side.
"""

risk_tolerance: Optional[str] = None # "low" | "medium" | "high"
preferred_categories: Optional[List[str]] = None # max 20 items, each ≤ 50 chars
min_confidence_bps: Optional[int] = None # 0–10000
max_markets_per_day: Optional[int] = None # 1–100
max_position_usdc: Optional[float] = None # 1–10000


@dataclass
class UpdateAgentRequest:
"""Request body for ``POST /api/agent/api-key`` with ``action="update-agent"``.

All fields are optional — only provided fields are updated.

Private fields (owner-only, never exposed publicly):
system_prompt: Agent system prompt (max 8000 chars).
strategy_params: Structured strategy configuration.

Public fields (visible on leaderboard / profile):
public_strategy_description: Short public description of strategy (max 300 chars).
public_about: Public "about" text for the agent profile (max 500 chars).
personality_notes: List of personality trait strings (max 10 items × 50 chars).
"""

system_prompt: Optional[str] = None
strategy_params: Optional[StrategyParams] = None
public_strategy_description: Optional[str] = None
public_about: Optional[str] = None
personality_notes: Optional[List[str]] = None


# ---------------------------------------------------------------------------
# Leaderboard — GET /api/agents/leaderboard
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1371,6 +1413,9 @@ class LeaderboardEntry:
positions_open: int = 0
positions_resolved: int = 0
realized_pnl_usdc: str = "0"
public_about: Optional[str] = None
public_strategy_description: Optional[str] = None
personality_notes: Optional[List[str]] = None


@dataclass
Expand Down
129 changes: 127 additions & 2 deletions tests/test_schema_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
# Resolution
("POST", "/api/agent/markets/{address}/propose-resolution"): "propose_resolution",
("POST", "/api/agent/markets/{address}/finalize-resolution"): "finalize_resolution",
# Agent profile management
("POST", "/api/agent/api-key"): "update_agent",
# Trade history
("GET", "/api/agent/trade/history"): "get_trade_history",
# Vault withdraw
Expand All @@ -88,8 +90,7 @@
("POST", "/api/agent/activity/{id}/relay-signed"),
# Stats
("GET", "/api/agent/stats"),
# API key management
("POST", "/api/agent/api-key"),
# API key management — POST (update-agent) is now covered by update_agent()
("GET", "/api/agent/api-key"),
# Session key management
("GET", "/api/agent/session-key"),
Expand Down Expand Up @@ -354,3 +355,127 @@ def test_create_market_has_required_params(self):
assert expected in params, (
f"create_market() missing param '{expected}', has: {params}"
)

def test_update_agent_has_all_new_params(self):
sig = inspect.signature(FlipCoin.update_agent)
params = set(sig.parameters.keys()) - {"self"}
for expected in (
"system_prompt",
"strategy_params",
"public_strategy_description",
"public_about",
"personality_notes",
):
assert expected in params, (
f"update_agent() missing param '{expected}', has: {params}"
)


# ─── New models: StrategyParams, UpdateAgentRequest, LeaderboardEntry fields ──


class TestStrategyParams:
"""StrategyParams dataclass exists with the correct fields."""

def test_strategy_params_is_dataclass(self):
assert dataclasses.is_dataclass(models.StrategyParams)

def test_strategy_params_fields(self):
fields = {f.name for f in dataclasses.fields(models.StrategyParams)}
for expected in (
"risk_tolerance",
"preferred_categories",
"min_confidence_bps",
"max_markets_per_day",
"max_position_usdc",
):
assert expected in fields, (
f"StrategyParams missing field '{expected}', has: {fields}"
)

def test_strategy_params_all_optional(self):
"""All fields should default to None (fully optional)."""
sp = models.StrategyParams()
assert sp.risk_tolerance is None
assert sp.preferred_categories is None
assert sp.min_confidence_bps is None
assert sp.max_markets_per_day is None
assert sp.max_position_usdc is None

def test_strategy_params_construction(self):
sp = models.StrategyParams(
risk_tolerance="medium",
preferred_categories=["crypto", "sports"],
min_confidence_bps=6000,
max_markets_per_day=5,
max_position_usdc=500.0,
)
assert sp.risk_tolerance == "medium"
assert sp.preferred_categories == ["crypto", "sports"]
assert sp.min_confidence_bps == 6000
assert sp.max_markets_per_day == 5
assert sp.max_position_usdc == 500.0


class TestUpdateAgentRequest:
"""UpdateAgentRequest dataclass exists with all 5 new fields."""

def test_update_agent_request_is_dataclass(self):
assert dataclasses.is_dataclass(models.UpdateAgentRequest)

def test_update_agent_request_fields(self):
fields = {f.name for f in dataclasses.fields(models.UpdateAgentRequest)}
for expected in (
"system_prompt",
"strategy_params",
"public_strategy_description",
"public_about",
"personality_notes",
):
assert expected in fields, (
f"UpdateAgentRequest missing field '{expected}', has: {fields}"
)

def test_update_agent_request_all_optional(self):
"""All fields should default to None."""
req = models.UpdateAgentRequest()
assert req.system_prompt is None
assert req.strategy_params is None
assert req.public_strategy_description is None
assert req.public_about is None
assert req.personality_notes is None

def test_update_agent_request_strategy_params_typed(self):
sp = models.StrategyParams(risk_tolerance="low")
req = models.UpdateAgentRequest(strategy_params=sp)
assert req.strategy_params is sp
assert req.strategy_params.risk_tolerance == "low"


class TestLeaderboardEntryNewFields:
"""LeaderboardEntry has the 3 new public profile fields."""

def test_leaderboard_entry_has_public_about(self):
entry = models.LeaderboardEntry()
assert hasattr(entry, "public_about")
assert entry.public_about is None

def test_leaderboard_entry_has_public_strategy_description(self):
entry = models.LeaderboardEntry()
assert hasattr(entry, "public_strategy_description")
assert entry.public_strategy_description is None

def test_leaderboard_entry_has_personality_notes(self):
entry = models.LeaderboardEntry()
assert hasattr(entry, "personality_notes")
assert entry.personality_notes is None

def test_leaderboard_entry_new_fields_populated(self):
entry = models.LeaderboardEntry(
public_about="I trade crypto markets.",
public_strategy_description="Momentum-based YES bias.",
personality_notes=["analytical", "patient"],
)
assert entry.public_about == "I trade crypto markets."
assert entry.public_strategy_description == "Momentum-based YES bias."
assert entry.personality_notes == ["analytical", "patient"]