feat: add challenge and red-blue competitions across API and web

This commit is contained in:
Joey Yakimowich-Payne 2025-10-01 06:49:09 -06:00
commit 8fd3c4bb64
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
77 changed files with 5355 additions and 24 deletions

View file

@ -129,6 +129,10 @@ from .workspace import (
workspace,
)
# Import custom challenge controllers
from . import challenges as challenges
from . import red_blue_challenges as red_blue_challenges
api.add_namespace(console_ns)
__all__ = [
@ -204,4 +208,6 @@ __all__ = [
"workflow_run",
"workflow_statistic",
"workspace",
"challenges",
"red_blue_challenges",
]

View file

@ -12,7 +12,6 @@ from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_resource_check,
enterprise_license_required,
setup_required,
)
from core.ops.ops_trace_manager import OpsTraceManager
@ -53,7 +52,6 @@ class AppListApi(Resource):
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
def get(self):
"""Get app list"""
@ -166,7 +164,6 @@ class AppApi(Resource):
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
@get_app_model
@marshal_with(app_detail_fields_with_site)
def get(self, app_model):

View file

@ -0,0 +1,154 @@
from __future__ import annotations
from flask_restx import Resource, reqparse
from controllers.console import console_ns as api
from controllers.console.wraps import (
account_initialization_required,
setup_required,
)
from libs.login import login_required
from extensions.ext_database import db
from libs.login import current_user
from models.challenge import Challenge
@api.route("/challenges")
class ChallengeListCreateApi(Resource):
@api.doc("list_challenges")
@setup_required
@login_required
@account_initialization_required
def get(self):
tenant_id = current_user.current_tenant_id
if not tenant_id:
# no active workspace selected; return empty list to avoid leaking data
return {"result": "success", "data": []}
rows = (
db.session.query(Challenge)
.filter(Challenge.tenant_id == tenant_id)
.order_by(Challenge.created_at.desc())
.all()
)
return {
"result": "success",
"data": [
{
"id": r.id,
"name": r.name,
"description": r.description,
"goal": r.goal,
"is_active": r.is_active,
"success_type": r.success_type,
"success_pattern": r.success_pattern,
"scoring_strategy": r.scoring_strategy,
"app_id": r.app_id,
"workflow_id": r.workflow_id,
}
for r in rows
],
}
@api.doc("create_challenge")
@setup_required
@login_required
@account_initialization_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("tenant_id", type=str, required=False, location="json")
parser.add_argument("app_id", type=str, required=True, location="json")
parser.add_argument("workflow_id", type=str, required=False, location="json")
parser.add_argument("name", type=str, required=True, location="json")
parser.add_argument("description", type=str, required=False, location="json")
parser.add_argument("goal", type=str, required=False, location="json")
parser.add_argument("success_type", type=str, required=False, location="json")
parser.add_argument("success_pattern", type=str, required=False, location="json")
parser.add_argument("scoring_strategy", type=str, required=False, location="json")
parser.add_argument("is_active", type=bool, required=False, location="json")
args = parser.parse_args()
c = Challenge()
c.tenant_id = args.get("tenant_id") or current_user.current_tenant_id
c.app_id = args["app_id"]
# Convert empty string to None for UUID field
workflow_id = args.get("workflow_id")
c.workflow_id = workflow_id if workflow_id else None
c.name = args["name"]
c.description = args.get("description")
c.goal = args.get("goal")
if args.get("success_type"):
c.success_type = args["success_type"]
c.success_pattern = args.get("success_pattern")
if args.get("scoring_strategy"):
c.scoring_strategy = args["scoring_strategy"]
if args.get("is_active") is not None:
c.is_active = args["is_active"]
db.session.add(c)
db.session.commit()
return {"result": "success", "data": {"id": c.id}}, 201
@api.route("/challenges/<uuid:challenge_id>")
class ChallengeDetailApi(Resource):
@api.doc("get_challenge")
@setup_required
@login_required
@account_initialization_required
def get(self, challenge_id):
c = db.session.get(Challenge, str(challenge_id))
if not c:
return {"result": "not_found"}, 404
return {
"result": "success",
"data": {
"id": c.id,
"name": c.name,
"description": c.description,
"goal": c.goal,
"is_active": c.is_active,
"success_type": c.success_type,
"success_pattern": c.success_pattern,
"scoring_strategy": c.scoring_strategy,
"app_id": c.app_id,
"workflow_id": c.workflow_id,
},
}
@api.doc("update_challenge")
@setup_required
@login_required
@account_initialization_required
def patch(self, challenge_id):
c = db.session.get(Challenge, str(challenge_id))
if not c:
return {"result": "not_found"}, 404
parser = reqparse.RequestParser()
parser.add_argument("name", type=str, required=False, location="json")
parser.add_argument("description", type=str, required=False, location="json")
parser.add_argument("goal", type=str, required=False, location="json")
parser.add_argument("is_active", type=bool, required=False, location="json")
args = parser.parse_args()
if args.get("name"):
c.name = args["name"]
if args.get("description") is not None:
c.description = args["description"]
if args.get("goal") is not None:
c.goal = args["goal"]
if args.get("is_active") is not None:
c.is_active = bool(args["is_active"])
db.session.commit()
return {"result": "success"}
@api.doc("delete_challenge")
@setup_required
@login_required
@account_initialization_required
def delete(self, challenge_id):
c = db.session.get(Challenge, str(challenge_id))
if not c:
return {"result": "not_found"}, 404
db.session.delete(c)
db.session.commit()
return {"result": "success"}, 204

View file

@ -5,7 +5,7 @@ from flask_restx import Resource, marshal_with, reqparse
from werkzeug.exceptions import NotFound
from controllers.console import console_ns
from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required
from controllers.console.wraps import account_initialization_required, setup_required
from fields.dataset_fields import dataset_metadata_fields
from libs.login import login_required
from services.dataset_service import DatasetService
@ -21,7 +21,6 @@ class DatasetMetadataCreateApi(Resource):
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
@marshal_with(dataset_metadata_fields)
def post(self, dataset_id):
parser = reqparse.RequestParser()
@ -42,7 +41,6 @@ class DatasetMetadataCreateApi(Resource):
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
def get(self, dataset_id):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str)
@ -56,7 +54,6 @@ class DatasetMetadataApi(Resource):
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
@marshal_with(dataset_metadata_fields)
def patch(self, dataset_id, metadata_id):
parser = reqparse.RequestParser()
@ -77,7 +74,6 @@ class DatasetMetadataApi(Resource):
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
def delete(self, dataset_id, metadata_id):
dataset_id_str = str(dataset_id)
metadata_id_str = str(metadata_id)
@ -95,7 +91,6 @@ class DatasetMetadataBuiltInFieldApi(Resource):
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
def get(self):
built_in_fields = MetadataService.get_built_in_fields()
return {"fields": built_in_fields}, 200
@ -106,7 +101,6 @@ class DatasetMetadataBuiltInFieldActionApi(Resource):
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
def post(self, dataset_id, action: Literal["enable", "disable"]):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str)
@ -126,7 +120,6 @@ class DocumentMetadataEditApi(Resource):
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
def post(self, dataset_id):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str)

View file

@ -7,7 +7,6 @@ from sqlalchemy.orm import Session
from controllers.console import console_ns
from controllers.console.wraps import (
account_initialization_required,
enterprise_license_required,
knowledge_pipeline_publish_enabled,
setup_required,
)
@ -37,7 +36,6 @@ class PipelineTemplateListApi(Resource):
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
def get(self):
type = request.args.get("type", default="built-in", type=str)
language = request.args.get("language", default="en-US", type=str)
@ -51,7 +49,6 @@ class PipelineTemplateDetailApi(Resource):
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
def get(self, template_id: str):
type = request.args.get("type", default="built-in", type=str)
rag_pipeline_service = RagPipelineService()
@ -64,7 +61,6 @@ class CustomizedPipelineTemplateApi(Resource):
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
def patch(self, template_id: str):
parser = reqparse.RequestParser()
parser.add_argument(
@ -95,7 +91,6 @@ class CustomizedPipelineTemplateApi(Resource):
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
def delete(self, template_id: str):
RagPipelineService.delete_customized_pipeline_template(template_id)
return 200
@ -103,7 +98,6 @@ class CustomizedPipelineTemplateApi(Resource):
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
def post(self, template_id: str):
with Session(db.engine) as session:
template = (
@ -120,7 +114,6 @@ class PublishCustomizedPipelineTemplateApi(Resource):
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
@knowledge_pipeline_publish_enabled
def post(self, pipeline_id: str):
parser = reqparse.RequestParser()

View file

@ -0,0 +1,145 @@
from __future__ import annotations
from flask_restx import Resource, reqparse
from controllers.console import console_ns as api
from controllers.console.wraps import (
account_initialization_required,
setup_required,
)
from extensions.ext_database import db
from libs.login import current_user, login_required
from models.red_blue import RedBlueChallenge, TeamPairing
@api.route("/red-blue-challenges")
class RedBlueListCreateApi(Resource):
@api.doc("list_red_blue_challenges")
@setup_required
@login_required
@account_initialization_required
def get(self):
tenant_id = current_user.current_tenant_id
if not tenant_id:
return {"result": "success", "data": []}
rows = (
db.session.query(RedBlueChallenge)
.filter(RedBlueChallenge.tenant_id == tenant_id)
.order_by(RedBlueChallenge.created_at.desc())
.all()
)
return {
"result": "success",
"data": [
{
"id": r.id,
"name": r.name,
"description": r.description,
"is_active": r.is_active,
}
for r in rows
],
}
@api.doc("create_red_blue_challenge")
@setup_required
@login_required
@account_initialization_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("tenant_id", type=str, required=True, location="json")
parser.add_argument("app_id", type=str, required=True, location="json")
parser.add_argument("name", type=str, required=True, location="json")
parser.add_argument("description", type=str, required=False, location="json")
parser.add_argument("judge_suite", type=dict, required=True, location="json")
args = parser.parse_args()
c = RedBlueChallenge()
c.tenant_id = args.get("tenant_id") or current_user.current_tenant_id
c.app_id = args["app_id"]
c.name = args["name"]
c.description = args.get("description")
c.judge_suite = args["judge_suite"]
db.session.add(c)
db.session.commit()
return {"result": "success", "data": {"id": c.id}}, 201
@api.route("/red-blue-challenges/<uuid:challenge_id>")
class RedBlueDetailApi(Resource):
@api.doc("get_red_blue_challenge")
@setup_required
@login_required
@account_initialization_required
def get(self, challenge_id):
c = db.session.get(RedBlueChallenge, str(challenge_id))
if not c:
return {"result": "not_found"}, 404
return {
"result": "success",
"data": {"id": c.id, "name": c.name, "description": c.description, "is_active": c.is_active},
}
@api.doc("update_red_blue_challenge")
@setup_required
@login_required
@account_initialization_required
def patch(self, challenge_id):
c = db.session.get(RedBlueChallenge, str(challenge_id))
if not c:
return {"result": "not_found"}, 404
parser = reqparse.RequestParser()
parser.add_argument("name", type=str, required=False, location="json")
parser.add_argument("description", type=str, required=False, location="json")
parser.add_argument("is_active", type=bool, required=False, location="json")
args = parser.parse_args()
if args.get("name"):
c.name = args["name"]
if args.get("description") is not None:
c.description = args["description"]
if args.get("is_active") is not None:
c.is_active = bool(args["is_active"])
db.session.commit()
return {"result": "success"}
@api.doc("delete_red_blue_challenge")
@setup_required
@login_required
@account_initialization_required
def delete(self, challenge_id):
c = db.session.get(RedBlueChallenge, str(challenge_id))
if not c:
return {"result": "not_found"}, 404
db.session.delete(c)
db.session.commit()
return {"result": "success"}, 204
@api.route("/red-blue-challenges/<uuid:challenge_id>/pairings")
class RedBluePairingsApi(Resource):
@api.doc("list_red_blue_pairings")
@setup_required
@login_required
@account_initialization_required
def get(self, challenge_id):
rows = (
db.session.query(TeamPairing)
.filter(TeamPairing.red_blue_challenge_id == str(challenge_id))
.order_by(TeamPairing.created_at.desc())
.limit(100)
.all()
)
return {
"result": "success",
"data": [
{
"id": r.id,
"red_points": r.red_points,
"blue_points": r.blue_points,
"judge_rating": r.judge_rating,
"created_at": r.created_at.isoformat() if hasattr(r.created_at, "isoformat") else None,
}
for r in rows
],
}

View file

@ -29,7 +29,6 @@ from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_enabled,
enable_change_email,
enterprise_license_required,
only_edition_cloud,
setup_required,
)
@ -102,7 +101,6 @@ class AccountProfileApi(Resource):
@login_required
@account_initialization_required
@marshal_with(account_fields)
@enterprise_license_required
def get(self):
if not isinstance(current_user, Account):
raise ValueError("Invalid user account")

View file

@ -13,7 +13,6 @@ from configs import dify_config
from controllers.console import api
from controllers.console.wraps import (
account_initialization_required,
enterprise_license_required,
setup_required,
)
from core.mcp.auth.auth_flow import auth, handle_callback
@ -667,7 +666,6 @@ class ToolLabelsApi(Resource):
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
def get(self):
return jsonable_encoder(ToolLabelsService.list_tool_labels())