From 1d3b74f82482390e8020e1f1d0668592e92b04a8 Mon Sep 17 00:00:00 2001 From: "V.Talecky" Date: Wed, 15 Apr 2026 01:29:18 +0300 Subject: [PATCH] feat: add StrategyParams, update_agent fields, public profile fields in LeaderboardEntry Co-Authored-By: Claude Sonnet 4.6 --- flipcoin/__init__.py | 4 ++ flipcoin/async_client.py | 62 ++++++++++++++++++ flipcoin/client.py | 62 ++++++++++++++++++ flipcoin/models.py | 45 +++++++++++++ tests/test_schema_sync.py | 129 +++++++++++++++++++++++++++++++++++++- 5 files changed, 300 insertions(+), 2 deletions(-) diff --git a/flipcoin/__init__.py b/flipcoin/__init__.py index d00c71f..83eda8e 100644 --- a/flipcoin/__init__.py +++ b/flipcoin/__init__.py @@ -75,6 +75,8 @@ SSEEvent, SeedSubsidyInfo, SimilarMarket, + StrategyParams, + UpdateAgentRequest, SlippageCurveEntry, SlippageCurves, SplitLeg, @@ -189,6 +191,8 @@ "SSEEvent", "SeedSubsidyInfo", "SimilarMarket", + "StrategyParams", + "UpdateAgentRequest", "SlippageCurveEntry", "SlippageCurves", "SplitLeg", diff --git a/flipcoin/async_client.py b/flipcoin/async_client.py index c68c737..1bbbbb1 100644 --- a/flipcoin/async_client.py +++ b/flipcoin/async_client.py @@ -40,9 +40,11 @@ RedeemBatchResponse, RedeemPosition, SSEEvent, + StrategyParams, TradeHistoryResponse, TradeNonceResponse, TradeResult, + UpdateAgentRequest, ValidateResult, VaultBalanceResponse, Webhook, @@ -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) diff --git a/flipcoin/client.py b/flipcoin/client.py index abc86d3..b246499 100644 --- a/flipcoin/client.py +++ b/flipcoin/client.py @@ -39,9 +39,11 @@ RedeemBatchResponse, RedeemPosition, SSEEvent, + StrategyParams, TradeHistoryResponse, TradeNonceResponse, TradeResult, + UpdateAgentRequest, ValidateResult, VaultBalanceResponse, Webhook, @@ -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 diff --git a/flipcoin/models.py b/flipcoin/models.py index f2de05a..8177ae0 100644 --- a/flipcoin/models.py +++ b/flipcoin/models.py @@ -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 # --------------------------------------------------------------------------- @@ -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 diff --git a/tests/test_schema_sync.py b/tests/test_schema_sync.py index 3aa4307..4bf66d7 100644 --- a/tests/test_schema_sync.py +++ b/tests/test_schema_sync.py @@ -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 @@ -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"), @@ -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"]