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

104
web/service/challenges.ts Normal file
View file

@ -0,0 +1,104 @@
import { getPublic, postPublic } from './base'
import { PUBLIC_API_PREFIX } from '@/config'
import { getInitialTokenV2, isTokenV1 } from '@/app/components/share/utils'
import { CONVERSATION_ID_INFO } from '@/app/components/base/chat/constants'
export type ChallengeListItem = {
id: string
name: string
description?: string
goal?: string
app_id?: string
workflow_id?: string
app_mode?: string
app_site_code?: string
}
export async function fetchChallenges(): Promise<ChallengeListItem[]> {
const res = await getPublic<{ result: string; data: ChallengeListItem[] }>('/challenges')
return res.data ?? []
}
export async function fetchChallengeDetail(id: string) {
const res = await getPublic<{ result: string; data: any }>(`/challenges/${id}`)
return res.data
}
export async function fetchChallengeLeaderboard(id: string) {
const res = await getPublic<{ result: string; data: any[] }>(`/challenges/${id}/leaderboard`)
return res.data ?? []
}
export async function submitChallengeAttempt(
challengeId: string,
appId: string,
appSiteCode: string | undefined,
appMode: string,
userInput: string,
) {
if (!appSiteCode)
throw new Error('Challenge app is not published. Please enable the app site for this challenge.')
const passportRes = await fetch(`${PUBLIC_API_PREFIX}/passport`, {
method: 'GET',
headers: {
'X-App-Code': appSiteCode,
},
credentials: 'include',
})
if (!passportRes.ok) {
let message = 'Unable to start challenge. Please try again.'
try {
const data = await passportRes.json()
message = data?.message || message
}
catch { /* ignore json parse errors */ }
throw new Error(message)
}
const passportData = await passportRes.json() as { access_token?: string }
const accessToken = passportData?.access_token
if (!accessToken)
throw new Error('Challenge authorization failed. Please refresh and try again.')
// Persist token using the same structure expected by getAccessToken(true)
const storageKey = 'token'
const userKey = 'DEFAULT'
const rawTokenStore = localStorage.getItem(storageKey) || JSON.stringify(getInitialTokenV2())
let tokenStore: Record<string, any>
try {
const parsed = JSON.parse(rawTokenStore)
tokenStore = isTokenV1(parsed) ? getInitialTokenV2() : parsed
}
catch {
tokenStore = getInitialTokenV2()
}
tokenStore[challengeId] = {
...(tokenStore[challengeId] || {}),
[userKey]: accessToken,
}
localStorage.setItem(storageKey, JSON.stringify(tokenStore))
localStorage.removeItem(CONVERSATION_ID_INFO)
if (appMode === 'chat' || appMode === 'advanced-chat' || appMode === 'agent-chat') {
return await postPublic<any>('/chat-messages', {
body: {
query: userInput,
inputs: {},
response_mode: 'blocking',
conversation_id: '',
},
})
}
return await postPublic<any>('/workflows/run', {
body: {
inputs: {
user_prompt: userInput,
},
response_mode: 'blocking',
},
})
}

View file

@ -0,0 +1,99 @@
import { request } from '@/service/base'
export type ConsoleChallenge = {
id: string
name: string
description?: string
goal?: string
is_active?: boolean
success_type?: string
success_pattern?: string
scoring_strategy?: string
app_id?: string
workflow_id?: string
}
export async function listConsoleChallenges() {
const resp = await request<{ data: ConsoleChallenge[] }>('/challenges', {}, {})
return resp.data
}
export async function createConsoleChallenge(payload: {
app_id: string
workflow_id?: string
name: string
description?: string
goal?: string
success_type?: string
success_pattern?: string
scoring_strategy?: string
is_active?: boolean
}) {
const resp = await request<{ data: { id: string } }>('/challenges', {
method: 'POST',
body: payload,
}, {})
return resp.data
}
export async function updateConsoleChallenge(id: string, payload: Partial<ConsoleChallenge>) {
const resp = await request<{ data: ConsoleChallenge }>(`/challenges/${id}`, {
method: 'PATCH',
body: payload,
}, {})
return resp.data
}
export async function deleteConsoleChallenge(id: string) {
await request(`/challenges/${id}`, {
method: 'DELETE',
}, {})
}
export type RedBlueChallenge = {
id: string
name: string
description?: string
judge_suite?: string[]
defense_selection_policy?: string
attack_selection_policy?: string
scoring_strategy?: string
is_active?: boolean
}
export async function listRedBlueChallenges() {
const resp = await request<{ data: RedBlueChallenge[] }>('/red-blue-challenges', {}, {})
return resp.data
}
export async function createRedBlueChallenge(payload: {
app_id: string
workflow_id?: string
name: string
description?: string
judge_suite?: string[]
defense_selection_policy?: string
attack_selection_policy?: string
scoring_strategy?: string
is_active?: boolean
}) {
const resp = await request<{ data: { id: string } }>('/red-blue-challenges', {
method: 'POST',
body: payload,
}, {})
return resp.data
}
export async function updateRedBlueChallenge(id: string, payload: Partial<RedBlueChallenge>) {
const resp = await request<{ data: RedBlueChallenge }>(`/red-blue-challenges/${id}`, {
method: 'PATCH',
body: payload,
}, {})
return resp.data
}
export async function deleteRedBlueChallenge(id: string) {
await request(`/red-blue-challenges/${id}`, {
method: 'DELETE',
}, {})
}

View file

@ -0,0 +1,27 @@
import { request } from '@/service/base'
export type ConsoleRedBlueChallenge = {
id: string
name: string
description?: string
is_active?: boolean
}
export async function listConsoleRedBlueChallenges() {
const resp = await request<{ data: ConsoleRedBlueChallenge[] }>('/red-blue-challenges', {}, {})
return resp.data
}
export async function createConsoleRedBlueChallenge(payload: {
tenant_id: string
app_id: string
name: string
description?: string
judge_suite: Record<string, any>
}) {
const resp = await request<{ data: { id: string } }>('/red-blue-challenges', {
method: 'POST',
body: JSON.stringify(payload),
}, {})
return resp.data
}

View file

@ -0,0 +1,24 @@
import { getPublic, postPublic } from './base'
export type RedBlueListItem = {
id: string
name: string
description?: string
}
export async function fetchRedBlueChallenges(): Promise<RedBlueListItem[]> {
const res = await getPublic<{ result: string; data: RedBlueListItem[] }>('/red-blue-challenges')
return res.data ?? []
}
export async function fetchRedBlueLeaderboard(id: string) {
const res = await getPublic<{ result: string; data: any }>(`/red-blue-challenges/${id}/leaderboard`)
return res.data
}
export async function submitRedBluePrompt(id: string, team: 'red' | 'blue', prompt: string) {
const res = await postPublic<{ result: string; data: any }>(`/red-blue-challenges/${id}/submit`, {
body: { team, prompt },
})
return res.data
}