feat: add challenge and red-blue competitions across API and web
This commit is contained in:
parent
f5161d9add
commit
8fd3c4bb64
77 changed files with 5355 additions and 24 deletions
|
|
@ -24,12 +24,15 @@ from . import (
|
|||
files,
|
||||
forgot_password,
|
||||
login,
|
||||
register,
|
||||
message,
|
||||
passport,
|
||||
remote_files,
|
||||
saved_message,
|
||||
site,
|
||||
workflow,
|
||||
challenges,
|
||||
red_blue_challenges,
|
||||
)
|
||||
|
||||
api.add_namespace(web_ns)
|
||||
|
|
@ -45,6 +48,7 @@ __all__ = [
|
|||
"files",
|
||||
"forgot_password",
|
||||
"login",
|
||||
"register",
|
||||
"message",
|
||||
"passport",
|
||||
"remote_files",
|
||||
|
|
@ -52,4 +56,6 @@ __all__ = [
|
|||
"site",
|
||||
"web_ns",
|
||||
"workflow",
|
||||
"challenges",
|
||||
"red_blue_challenges",
|
||||
]
|
||||
|
|
|
|||
121
api/controllers/web/challenges.py
Normal file
121
api/controllers/web/challenges.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from flask_restx import Resource
|
||||
|
||||
from controllers.web import web_ns
|
||||
from extensions.ext_database import db
|
||||
from sqlalchemy import select
|
||||
|
||||
from models.challenge import Challenge, ChallengeAttempt
|
||||
from models.model import App, Site
|
||||
|
||||
|
||||
@web_ns.route("/challenges")
|
||||
class ChallengeListApi(Resource):
|
||||
def get(self):
|
||||
q = db.session.query(Challenge).filter(Challenge.is_active.is_(True)).order_by(Challenge.created_at.desc())
|
||||
items = []
|
||||
for c in q.all():
|
||||
app = db.session.get(App, c.app_id) if c.app_id else None
|
||||
site_code = None
|
||||
if c.app_id:
|
||||
site = db.session.execute(
|
||||
select(Site).where(Site.app_id == c.app_id, Site.status == "normal")
|
||||
).scalar_one_or_none()
|
||||
site_code = site.code if site else None
|
||||
items.append({
|
||||
"id": c.id,
|
||||
"name": c.name,
|
||||
"description": c.description,
|
||||
"goal": c.goal,
|
||||
"app_id": c.app_id,
|
||||
"workflow_id": c.workflow_id,
|
||||
"app_mode": app.mode if app else None,
|
||||
"app_site_code": site_code,
|
||||
})
|
||||
return {"result": "success", "data": items}
|
||||
|
||||
|
||||
@web_ns.route("/challenges/<uuid:challenge_id>")
|
||||
class ChallengeDetailApi(Resource):
|
||||
def get(self, challenge_id):
|
||||
c = db.session.get(Challenge, str(challenge_id))
|
||||
if not c:
|
||||
return {"result": "not_found"}, 404
|
||||
|
||||
app = db.session.get(App, c.app_id) if c.app_id else None
|
||||
site_code = None
|
||||
if c.app_id:
|
||||
site = db.session.execute(
|
||||
select(Site).where(Site.app_id == c.app_id, Site.status == "normal")
|
||||
).scalar_one_or_none()
|
||||
site_code = site.code if site else None
|
||||
data = {
|
||||
"id": c.id,
|
||||
"name": c.name,
|
||||
"description": c.description,
|
||||
"goal": c.goal,
|
||||
"is_active": c.is_active,
|
||||
"app_id": c.app_id,
|
||||
"workflow_id": c.workflow_id,
|
||||
"app_mode": app.mode if app else None,
|
||||
"app_site_code": site_code,
|
||||
}
|
||||
return {"result": "success", "data": data}
|
||||
|
||||
|
||||
@web_ns.route("/challenges/<uuid:challenge_id>/leaderboard")
|
||||
class ChallengeLeaderboardApi(Resource):
|
||||
def get(self, challenge_id):
|
||||
limit = 20
|
||||
|
||||
# Get the challenge to determine scoring strategy
|
||||
challenge = db.session.get(Challenge, str(challenge_id))
|
||||
if not challenge:
|
||||
return {"result": "not_found"}, 404
|
||||
|
||||
scoring_strategy = challenge.scoring_strategy or 'highest_rating'
|
||||
|
||||
# Build query based on scoring strategy
|
||||
q = db.session.query(ChallengeAttempt).filter(
|
||||
ChallengeAttempt.challenge_id == str(challenge_id),
|
||||
ChallengeAttempt.succeeded.is_(True)
|
||||
)
|
||||
|
||||
# Apply sorting based on strategy
|
||||
if scoring_strategy == 'first':
|
||||
# Earliest successful attempt wins
|
||||
q = q.order_by(ChallengeAttempt.created_at.asc())
|
||||
elif scoring_strategy == 'fastest':
|
||||
# Lowest elapsed_ms wins
|
||||
q = q.order_by(ChallengeAttempt.elapsed_ms.asc().nullslast(), ChallengeAttempt.created_at.asc())
|
||||
elif scoring_strategy == 'fewest_tokens':
|
||||
# Lowest tokens_total wins
|
||||
q = q.order_by(ChallengeAttempt.tokens_total.asc().nullslast(), ChallengeAttempt.created_at.asc())
|
||||
elif scoring_strategy == 'highest_rating':
|
||||
# Highest judge_rating wins, ties broken by earliest
|
||||
q = q.order_by(ChallengeAttempt.judge_rating.desc().nullslast(), ChallengeAttempt.created_at.asc())
|
||||
elif scoring_strategy == 'custom':
|
||||
# Custom score field (computed by plugin)
|
||||
q = q.order_by(ChallengeAttempt.score.desc().nullslast(), ChallengeAttempt.created_at.asc())
|
||||
else:
|
||||
# Default to highest_rating
|
||||
q = q.order_by(ChallengeAttempt.judge_rating.desc().nullslast(), ChallengeAttempt.created_at.asc())
|
||||
|
||||
rows = q.limit(limit).all()
|
||||
data = [
|
||||
{
|
||||
"attempt_id": r.id,
|
||||
"account_id": r.account_id,
|
||||
"end_user_id": r.end_user_id,
|
||||
"score": r.score,
|
||||
"judge_rating": r.judge_rating,
|
||||
"tokens_total": r.tokens_total,
|
||||
"elapsed_ms": r.elapsed_ms,
|
||||
"created_at": r.created_at.isoformat() if hasattr(r.created_at, "isoformat") else None,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
return {"result": "success", "data": data}
|
||||
|
||||
|
||||
85
api/controllers/web/red_blue_challenges.py
Normal file
85
api/controllers/web/red_blue_challenges.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
|
||||
from controllers.web import web_ns
|
||||
from extensions.ext_database import db
|
||||
from models.red_blue import RedBlueChallenge, TeamPairing
|
||||
from services.red_blue_service import RedBlueService
|
||||
|
||||
|
||||
@web_ns.route("/red-blue-challenges")
|
||||
class RedBlueListApi(Resource):
|
||||
def get(self):
|
||||
q = db.session.query(RedBlueChallenge).filter(RedBlueChallenge.is_active.is_(True))
|
||||
items = [
|
||||
{
|
||||
"id": c.id,
|
||||
"name": c.name,
|
||||
"description": c.description,
|
||||
}
|
||||
for c in q.all()
|
||||
]
|
||||
return {"result": "success", "data": items}
|
||||
|
||||
|
||||
@web_ns.route("/red-blue-challenges/<uuid:challenge_id>")
|
||||
class RedBlueDetailApi(Resource):
|
||||
def get(self, challenge_id):
|
||||
c = db.session.get(RedBlueChallenge, str(challenge_id))
|
||||
if not c:
|
||||
return {"result": "not_found"}, 404
|
||||
data = {
|
||||
"id": c.id,
|
||||
"name": c.name,
|
||||
"description": c.description,
|
||||
}
|
||||
return {"result": "success", "data": data}
|
||||
|
||||
|
||||
@web_ns.route("/red-blue-challenges/<uuid:challenge_id>/submit")
|
||||
class RedBlueSubmitApi(Resource):
|
||||
def post(self, challenge_id):
|
||||
payload = request.get_json(force=True) or {}
|
||||
team = payload.get("team")
|
||||
prompt = payload.get("prompt")
|
||||
if team not in ("red", "blue") or not prompt:
|
||||
return {"result": "bad_request"}, 400
|
||||
c = db.session.get(RedBlueChallenge, str(challenge_id))
|
||||
if not c:
|
||||
return {"result": "not_found"}, 404
|
||||
sub = RedBlueService.submit_prompt(
|
||||
challenge_id=str(challenge_id),
|
||||
tenant_id=c.tenant_id,
|
||||
team=team,
|
||||
prompt=prompt,
|
||||
account_id=None,
|
||||
end_user_id=None,
|
||||
)
|
||||
return {"result": "success", "data": {"id": sub.id}}, 201
|
||||
|
||||
|
||||
@web_ns.route("/red-blue-challenges/<uuid:challenge_id>/leaderboard")
|
||||
class RedBlueLeaderboardApi(Resource):
|
||||
def get(self, challenge_id):
|
||||
# aggregate simple totals
|
||||
red = (
|
||||
db.session.query(db.func.coalesce(db.func.sum(TeamPairing.red_points), 0.0))
|
||||
.filter(TeamPairing.red_blue_challenge_id == str(challenge_id))
|
||||
.scalar()
|
||||
)
|
||||
blue = (
|
||||
db.session.query(db.func.coalesce(db.func.sum(TeamPairing.blue_points), 0.0))
|
||||
.filter(TeamPairing.red_blue_challenge_id == str(challenge_id))
|
||||
.scalar()
|
||||
)
|
||||
total = (red or 0.0) + (blue or 0.0)
|
||||
data = {
|
||||
"red_points": float(red or 0.0),
|
||||
"blue_points": float(blue or 0.0),
|
||||
"red_ratio": (float(red or 0.0) / total) if total else 0.0,
|
||||
"blue_ratio": (float(blue or 0.0) / total) if total else 0.0,
|
||||
}
|
||||
return {"result": "success", "data": data}
|
||||
|
||||
30
api/controllers/web/register.py
Normal file
30
api/controllers/web/register.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
|
||||
from controllers.web import web_ns
|
||||
from extensions.ext_database import db
|
||||
from services.account_service import RegisterService
|
||||
|
||||
|
||||
@web_ns.route('/register')
|
||||
class WebRegisterApi(Resource):
|
||||
def post(self):
|
||||
payload = request.get_json(force=True) or {}
|
||||
email = payload.get('email')
|
||||
name = payload.get('name') or 'Player'
|
||||
password = payload.get('password')
|
||||
if not email or not password:
|
||||
return { 'result': 'bad_request' }, 400
|
||||
account = RegisterService.register(
|
||||
email=email,
|
||||
name=name,
|
||||
password=password,
|
||||
is_setup=False,
|
||||
create_workspace_required=False,
|
||||
)
|
||||
db.session.commit()
|
||||
return { 'result': 'success', 'data': { 'account_id': account.id } }, 201
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue