business_harness.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. from __future__ import annotations
  2. from datetime import datetime
  3. from typing import Any, Literal
  4. from fastapi import APIRouter, HTTPException, Query, Request
  5. from pydantic import BaseModel, Field
  6. from api.services.business_harness import (
  7. get_platform_demand_history,
  8. get_published_demand_package,
  9. )
  10. from supply_infra.business_harness.content_feedback import (
  11. ingest_content_performance_fact,
  12. )
  13. from supply_infra.business_harness.feedback import process_feedback
  14. from supply_infra.business_harness.strategy import (
  15. approve_strategy_proposal,
  16. create_strategy_proposal,
  17. preview_strategy_proposal,
  18. )
  19. router = APIRouter(prefix="/api/business-harness", tags=["business-harness"])
  20. class ProcessFeedbackBody(BaseModel):
  21. status: Literal["accepted", "rejected", "needs_evidence"]
  22. reason: str = Field(min_length=1, max_length=2000)
  23. impact: dict[str, Any] | None = None
  24. class ContentPerformanceBody(BaseModel):
  25. content_id: str = Field(min_length=1, max_length=128)
  26. biz_dt: str = Field(pattern=r"^\d{8}$")
  27. source: str = Field(min_length=1, max_length=64)
  28. raw_payload: dict[str, Any] = Field(default_factory=dict)
  29. observed_at: datetime
  30. exposure_count: int | None = Field(default=None, ge=0)
  31. sample_size: int | None = Field(default=None, ge=0)
  32. content_quality: float | None = Field(default=None, ge=0, le=1)
  33. rov: float | None = None
  34. vov: float | None = None
  35. class StrategyProposalBody(BaseModel):
  36. requested_change: dict[str, Any]
  37. reason: str = Field(min_length=1, max_length=2000)
  38. @router.get("/packages/latest")
  39. def latest_package(
  40. biz_dt: str | None = Query(default=None, pattern=r"^\d{8}$"),
  41. run_id: str | None = None,
  42. ) -> dict:
  43. result = get_published_demand_package(biz_dt=biz_dt, run_id=run_id)
  44. if result is None:
  45. raise HTTPException(status_code=404, detail="published demand package not found")
  46. return result
  47. @router.get("/demands/{platform_demand_id}/history")
  48. def demand_history(platform_demand_id: str) -> dict:
  49. result = get_platform_demand_history(platform_demand_id)
  50. if result is None:
  51. raise HTTPException(status_code=404, detail="platform demand not found")
  52. return result
  53. @router.post("/feedback/{feedback_id}/process")
  54. def review_feedback(
  55. feedback_id: int,
  56. body: ProcessFeedbackBody,
  57. request: Request,
  58. ) -> dict:
  59. try:
  60. return process_feedback(
  61. feedback_id,
  62. status=body.status,
  63. processor=str(request.state.current_user["username"]),
  64. reason=body.reason,
  65. impact=body.impact,
  66. )
  67. except LookupError as exc:
  68. raise HTTPException(status_code=404, detail=str(exc)) from exc
  69. except RuntimeError as exc:
  70. raise HTTPException(status_code=409, detail=str(exc)) from exc
  71. @router.post("/content-performance")
  72. def ingest_content_performance(body: ContentPerformanceBody) -> dict:
  73. return ingest_content_performance_fact(**body.model_dump())
  74. @router.post("/strategy/proposals")
  75. def propose_strategy(body: StrategyProposalBody, request: Request) -> dict:
  76. try:
  77. return create_strategy_proposal(
  78. requested_change=body.requested_change,
  79. reason=body.reason,
  80. created_by=str(request.state.current_user["username"]),
  81. )
  82. except (ValueError, RuntimeError) as exc:
  83. raise HTTPException(status_code=409, detail=str(exc)) from exc
  84. @router.post("/strategy/proposals/{proposal_id}/preview")
  85. def preview_strategy(proposal_id: str) -> dict:
  86. try:
  87. return preview_strategy_proposal(proposal_id)
  88. except LookupError as exc:
  89. raise HTTPException(status_code=404, detail=str(exc)) from exc
  90. except RuntimeError as exc:
  91. raise HTTPException(status_code=409, detail=str(exc)) from exc
  92. @router.post("/strategy/proposals/{proposal_id}/approve")
  93. def approve_strategy(proposal_id: str, request: Request) -> dict:
  94. try:
  95. return approve_strategy_proposal(
  96. proposal_id,
  97. approved_by=str(request.state.current_user["username"]),
  98. )
  99. except LookupError as exc:
  100. raise HTTPException(status_code=404, detail=str(exc)) from exc
  101. except RuntimeError as exc:
  102. raise HTTPException(status_code=409, detail=str(exc)) from exc