show-run-errors-and-reprocess #2
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import type { ContentRule, ResearchRule } from './parse'
|
import type { ContentRule, ResearchRule } from './parse'
|
||||||
import { budgetFor, canSchedule, configFor, ruleOf, VIDEO, type ExpressAgent } from './schedules'
|
import { budgetFor, canSchedule, configFor, needsTwoSources, productionError, ruleOf, VIDEO, type ExpressAgent } from './schedules'
|
||||||
|
|
||||||
const rule = (kind: ContentRule['kind'], count = 2, per: ContentRule['per'] = 'week', topic = 'onboarding', language: ContentRule['language'] = 'English'): ContentRule =>
|
const rule = (kind: ContentRule['kind'], count = 2, per: ContentRule['per'] = 'week', topic = 'onboarding', language: ContentRule['language'] = 'English'): ContentRule =>
|
||||||
({ type: 'content', kind, count, per, topic, language })
|
({ type: 'content', kind, count, per, topic, language })
|
||||||
@@ -108,3 +108,17 @@ describe('canSchedule', () => {
|
|||||||
expect(canSchedule(rule('summary', 0))).toBe(false)
|
expect(canSchedule(rule('summary', 0))).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('production sources', () => {
|
||||||
|
it('needs two sources for planned content, not for video or research', () => {
|
||||||
|
expect(needsTwoSources(rule('quiz'))).toBe(true)
|
||||||
|
expect(needsTwoSources(rule('video', 1))).toBe(false)
|
||||||
|
expect(needsTwoSources({ type: 'research', cadence: 'weekly', weekday: 2, topic: 'sales', autoApprove: false })).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("explains the engine's refusal for too few sources and passes others through", () => {
|
||||||
|
expect(productionError('At least two ready sources with complementary evidence are required')).toMatch(/2/)
|
||||||
|
expect(productionError('At least two ready sources with complementary evidence are required')).not.toMatch(/complementary/)
|
||||||
|
expect(productionError('Something else')).toBe('Something else')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// use run_schedule (one run per day or week; a video run renders one video).
|
// use run_schedule (one run per day or week; a video run renders one video).
|
||||||
import { opKey, request } from '../api/client'
|
import { opKey, request } from '../api/client'
|
||||||
import { listDocuments, type Agent, type RunSchedule } from '../api/engine'
|
import { listDocuments, type Agent, type RunSchedule } from '../api/engine'
|
||||||
import { getLang } from '../i18n'
|
import { getLang, t } from '../i18n'
|
||||||
import {
|
import {
|
||||||
KINDS, LANGUAGES, SCHEDULABLE, videoCadence, weeklyTarget,
|
KINDS, LANGUAGES, SCHEDULABLE, videoCadence, weeklyTarget,
|
||||||
type ContentKind, type ContentRule, type OutputLanguage, type Per, type ResearchRule, type Rule,
|
type ContentKind, type ContentRule, type OutputLanguage, type Per, type ResearchRule, type Rule,
|
||||||
@@ -110,6 +110,15 @@ export const listSchedules = () => expressAgents()
|
|||||||
/** Schedules made of one engine run per slot (research, video), rather than weekly production plans. */
|
/** Schedules made of one engine run per slot (research, video), rather than weekly production plans. */
|
||||||
export const isRunSchedule = (rule: Rule) => rule.type === 'research' || rule.kind === 'video'
|
export const isRunSchedule = (rule: Rule) => rule.type === 'research' || rule.kind === 'video'
|
||||||
|
|
||||||
|
// The engine plans each production piece from a pair of documents, so a content schedule
|
||||||
|
// with fewer ready sources than this cannot plan anything. Video and research runs need one.
|
||||||
|
export const MIN_PRODUCTION_SOURCES = 2
|
||||||
|
export const needsTwoSources = (rule: Rule) => !isRunSchedule(rule)
|
||||||
|
const TOO_FEW_SOURCES = 'At least two ready sources with complementary evidence are required'
|
||||||
|
/** The engine's planner refusal, said in the UI language; any other message as it came. */
|
||||||
|
export const productionError = (message: string) =>
|
||||||
|
(message.includes(TOO_FEW_SOURCES) ? t('sch.tooFewSources', { n: MIN_PRODUCTION_SOURCES }) : message)
|
||||||
|
|
||||||
// Video length is fixed per schedule: 4 scenes, 40 seconds in all (the engine allows 4–15 s per scene).
|
// Video length is fixed per schedule: 4 scenes, 40 seconds in all (the engine allows 4–15 s per scene).
|
||||||
export const VIDEO = { scenes: 4, seconds: 40 }
|
export const VIDEO = { scenes: 4, seconds: 40 }
|
||||||
|
|
||||||
@@ -362,8 +371,8 @@ export interface RunSummary {
|
|||||||
|
|
||||||
export const listRuns = (id: number) => request<RunSummary[]>(`${AGENTS}/${id}/runs`)
|
export const listRuns = (id: number) => request<RunSummary[]>(`${AGENTS}/${id}/runs`)
|
||||||
|
|
||||||
// What stops a run with knowledge_not_ready. The engine checks the whole Library, so a blocking
|
// What stops a run with knowledge_not_ready: the schedule's own material that is not ready (the whole
|
||||||
// document need not be part of the schedule's chosen material. This is the current state, not the run's.
|
// Library only for an agent with no material). This is the current state, not the run's.
|
||||||
export interface ReadinessIssue {
|
export interface ReadinessIssue {
|
||||||
kind: 'document' | 'compose_set'
|
kind: 'document' | 'compose_set'
|
||||||
resource_id: number
|
resource_id: number
|
||||||
|
|||||||
22
src/i18n.ts
22
src/i18n.ts
@@ -126,9 +126,9 @@ const en = {
|
|||||||
'brain.needsCreditsNeed': ' (needs {credits})',
|
'brain.needsCreditsNeed': ' (needs {credits})',
|
||||||
'brain.failed': 'Could not add this to the Brain.',
|
'brain.failed': 'Could not add this to the Brain.',
|
||||||
'brain.retryWith': 'Try again ({credits})',
|
'brain.retryWith': 'Try again ({credits})',
|
||||||
'brain.failedAfterBuild': 'Processing this file failed after its knowledge was built. Schedules that use the whole Brain stop until it is fixed. Process it again: its knowledge is kept.',
|
'brain.failedAfterBuild': 'Processing this file failed after its knowledge was built. Schedules that use it stop until it is fixed. Process it again: its knowledge is kept.',
|
||||||
'brain.failedProcessing': 'This file could not be processed. Schedules that use the whole Brain stop until it is fixed. Process it again, or delete it.',
|
'brain.failedProcessing': 'This file could not be processed. Schedules that use it stop until it is fixed. Process it again, or delete it.',
|
||||||
'brain.failedVideoAfterBuild': 'Processing this video failed after its knowledge was built. Schedules that use the whole Brain stop until it is fixed: upload it again.',
|
'brain.failedVideoAfterBuild': 'Processing this video failed after its knowledge was built. Schedules that use it stop until it is fixed: upload it again.',
|
||||||
'brain.reprocess': 'Process again',
|
'brain.reprocess': 'Process again',
|
||||||
'brain.reprocessing': 'Starting…',
|
'brain.reprocessing': 'Starting…',
|
||||||
'brain.confirmCopy': 'Adding this document to the Brain uses {credits} from the workspace.',
|
'brain.confirmCopy': 'Adding this document to the Brain uses {credits} from the workspace.',
|
||||||
@@ -249,7 +249,7 @@ const en = {
|
|||||||
'sch.manual': 'Manual',
|
'sch.manual': 'Manual',
|
||||||
'run.queued': 'Queued', 'run.planning': 'Planning', 'run.running': 'Running', 'run.completed': 'Completed', 'run.failed': 'Failed', 'run.cancelled': 'Cancelled',
|
'run.queued': 'Queued', 'run.planning': 'Planning', 'run.running': 'Running', 'run.completed': 'Completed', 'run.failed': 'Failed', 'run.cancelled': 'Cancelled',
|
||||||
'run.paused_configuration': 'Needs attention', 'run.paused_budget': 'Paused: credits',
|
'run.paused_configuration': 'Needs attention', 'run.paused_budget': 'Paused: credits',
|
||||||
'runErr.knowledge_not_ready': 'Not all material in the Library is ready, so the engine will not start.',
|
'runErr.knowledge_not_ready': 'Some material this schedule uses is not ready, so the engine will not start.',
|
||||||
'runErr.video_sources_unavailable': 'No ready material matched this schedule for a video.',
|
'runErr.video_sources_unavailable': 'No ready material matched this schedule for a video.',
|
||||||
'runErr.material_sources_unavailable': 'No ready material matched this schedule.',
|
'runErr.material_sources_unavailable': 'No ready material matched this schedule.',
|
||||||
'runErr.knowledge_search_failed': 'Searching the material failed. Try again.',
|
'runErr.knowledge_search_failed': 'Searching the material failed. Try again.',
|
||||||
@@ -469,6 +469,9 @@ const en = {
|
|||||||
'brainPanel.notReady': 'Can be chosen once it is in the Brain',
|
'brainPanel.notReady': 'Can be chosen once it is in the Brain',
|
||||||
'ins.sourcesChosen': 'Source: chosen material ({n})',
|
'ins.sourcesChosen': 'Source: chosen material ({n})',
|
||||||
'ins.sourcesAll': 'Source: the whole Brain',
|
'ins.sourcesAll': 'Source: the whole Brain',
|
||||||
|
'ins.tooFewChosen': ' Content schedules (summary, quiz and the like) need at least {n} materials. Choose one more in the Brain panel.',
|
||||||
|
'ins.tooFewInBrain': ' Content schedules (summary, quiz and the like) need at least {n} ready materials in the Brain. Upload another one first.',
|
||||||
|
'sch.tooFewSources': 'This schedule has fewer than {n} ready materials, and content is planned from pairs of materials. Delete it and make it again with {n} or more.',
|
||||||
'brainPanel.upload': 'Upload material',
|
'brainPanel.upload': 'Upload material',
|
||||||
|
|
||||||
// Guide
|
// Guide
|
||||||
@@ -615,9 +618,9 @@ const id: Record<Key, string> = {
|
|||||||
'brain.needsCreditsNeed': ' (butuh {credits})',
|
'brain.needsCreditsNeed': ' (butuh {credits})',
|
||||||
'brain.failed': 'Gagal memasukkan ke Otak.',
|
'brain.failed': 'Gagal memasukkan ke Otak.',
|
||||||
'brain.retryWith': 'Coba lagi ({credits})',
|
'brain.retryWith': 'Coba lagi ({credits})',
|
||||||
'brain.failedAfterBuild': 'Pemrosesan file ini gagal setelah pengetahuannya dibangun. Jadwal yang memakai seluruh Otak berhenti sampai ini diperbaiki. Proses ulang: pengetahuannya tetap disimpan.',
|
'brain.failedAfterBuild': 'Pemrosesan file ini gagal setelah pengetahuannya dibangun. Jadwal yang memakainya berhenti sampai ini diperbaiki. Proses ulang: pengetahuannya tetap disimpan.',
|
||||||
'brain.failedProcessing': 'File ini gagal diproses. Jadwal yang memakai seluruh Otak berhenti sampai ini diperbaiki. Proses ulang, atau hapus.',
|
'brain.failedProcessing': 'File ini gagal diproses. Jadwal yang memakainya berhenti sampai ini diperbaiki. Proses ulang, atau hapus.',
|
||||||
'brain.failedVideoAfterBuild': 'Pemrosesan video ini gagal setelah pengetahuannya dibangun. Jadwal yang memakai seluruh Otak berhenti sampai ini diperbaiki: unggah ulang.',
|
'brain.failedVideoAfterBuild': 'Pemrosesan video ini gagal setelah pengetahuannya dibangun. Jadwal yang memakainya berhenti sampai ini diperbaiki: unggah ulang.',
|
||||||
'brain.reprocess': 'Proses ulang',
|
'brain.reprocess': 'Proses ulang',
|
||||||
'brain.reprocessing': 'Memulai…',
|
'brain.reprocessing': 'Memulai…',
|
||||||
'brain.confirmCopy': 'Memasukkan dokumen ini ke Otak memakai {credits} dari ruang kerja.',
|
'brain.confirmCopy': 'Memasukkan dokumen ini ke Otak memakai {credits} dari ruang kerja.',
|
||||||
@@ -735,7 +738,7 @@ const id: Record<Key, string> = {
|
|||||||
'sch.manual': 'Manual',
|
'sch.manual': 'Manual',
|
||||||
'run.queued': 'Antre', 'run.planning': 'Merencanakan', 'run.running': 'Berjalan', 'run.completed': 'Selesai', 'run.failed': 'Gagal', 'run.cancelled': 'Dibatalkan',
|
'run.queued': 'Antre', 'run.planning': 'Merencanakan', 'run.running': 'Berjalan', 'run.completed': 'Selesai', 'run.failed': 'Gagal', 'run.cancelled': 'Dibatalkan',
|
||||||
'run.paused_configuration': 'Perlu tindakan', 'run.paused_budget': 'Tertahan: kredit',
|
'run.paused_configuration': 'Perlu tindakan', 'run.paused_budget': 'Tertahan: kredit',
|
||||||
'runErr.knowledge_not_ready': 'Belum semua materi di Library siap, jadi engine tidak mau mulai.',
|
'runErr.knowledge_not_ready': 'Sebagian materi yang dipakai jadwal ini belum siap, jadi engine tidak mau mulai.',
|
||||||
'runErr.video_sources_unavailable': 'Tidak ada materi siap yang cocok untuk video jadwal ini.',
|
'runErr.video_sources_unavailable': 'Tidak ada materi siap yang cocok untuk video jadwal ini.',
|
||||||
'runErr.material_sources_unavailable': 'Tidak ada materi siap yang cocok dengan jadwal ini.',
|
'runErr.material_sources_unavailable': 'Tidak ada materi siap yang cocok dengan jadwal ini.',
|
||||||
'runErr.knowledge_search_failed': 'Pencarian materi gagal. Coba lagi.',
|
'runErr.knowledge_search_failed': 'Pencarian materi gagal. Coba lagi.',
|
||||||
@@ -954,6 +957,9 @@ const id: Record<Key, string> = {
|
|||||||
'brainPanel.notReady': 'Bisa dipilih setelah masuk Brain',
|
'brainPanel.notReady': 'Bisa dipilih setelah masuk Brain',
|
||||||
'ins.sourcesChosen': 'Sumber: {n} materi dipilih',
|
'ins.sourcesChosen': 'Sumber: {n} materi dipilih',
|
||||||
'ins.sourcesAll': 'Sumber: seluruh Brain',
|
'ins.sourcesAll': 'Sumber: seluruh Brain',
|
||||||
|
'ins.tooFewChosen': ' Jadwal konten (ringkasan, kuis, dan sejenisnya) butuh minimal {n} materi. Pilih satu lagi di panel Brain.',
|
||||||
|
'ins.tooFewInBrain': ' Jadwal konten (ringkasan, kuis, dan sejenisnya) butuh minimal {n} materi siap di Brain. Unggah satu lagi dulu.',
|
||||||
|
'sch.tooFewSources': 'Jadwal ini punya kurang dari {n} materi siap, padahal konten direncanakan dari pasangan materi. Hapus lalu buat ulang dengan {n} materi atau lebih.',
|
||||||
'brainPanel.upload': 'Upload materi',
|
'brainPanel.upload': 'Upload materi',
|
||||||
|
|
||||||
// Panduan
|
// Panduan
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { locale, t } from '../i18n'
|
|||||||
type Phase = 'ready' | 'working' | 'answer' | 'failed'
|
type Phase = 'ready' | 'working' | 'answer' | 'failed'
|
||||||
|
|
||||||
function phaseOf(s: Source): Phase {
|
function phaseOf(s: Source): Phase {
|
||||||
// A failed document blocks every whole-Brain schedule in the engine, even with a built knowledge base.
|
// A failed document blocks every schedule that uses it, even with a built knowledge base.
|
||||||
if (s.status === 'failed' || s.knowledge === 'failed') return 'failed'
|
if (s.status === 'failed' || s.knowledge === 'failed') return 'failed'
|
||||||
// A reprocessed document keeps its built knowledge while it is read again, and still blocks schedules.
|
// A reprocessed document keeps its built knowledge while it is read again, and still blocks schedules.
|
||||||
if (s.status !== 'completed') return 'working'
|
if (s.status !== 'completed') return 'working'
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from '../express/parse'
|
} from '../express/parse'
|
||||||
import { ask, chatLangOf, editRule, EMPTY, nextQuestion, removeRule, respond, type AgentState } from '../express/agent'
|
import { ask, chatLangOf, editRule, EMPTY, nextQuestion, removeRule, respond, type AgentState } from '../express/agent'
|
||||||
import { inLang, t, useLang, type Key, type Lang } from '../i18n'
|
import { inLang, t, useLang, type Key, type Lang } from '../i18n'
|
||||||
import { budgetFor, canSchedule, createSchedule, VIDEO, videoQuote } from '../express/schedules'
|
import { budgetFor, canSchedule, createSchedule, MIN_PRODUCTION_SOURCES, needsTwoSources, VIDEO, videoQuote } from '../express/schedules'
|
||||||
|
|
||||||
const EXAMPLES: Key[] = ['ins.ex1', 'ins.ex2', 'ins.ex3', 'ins.ex4', 'ins.ex5']
|
const EXAMPLES: Key[] = ['ins.ex1', 'ins.ex2', 'ins.ex3', 'ins.ex4', 'ins.ex5']
|
||||||
const THINKING: Key[] = ['agent.think1', 'agent.think2', 'agent.think3']
|
const THINKING: Key[] = ['agent.think1', 'agent.think2', 'agent.think3']
|
||||||
@@ -59,6 +59,9 @@ export function InstructionPage() {
|
|||||||
const view: AgentState = { ...agent, sources }
|
const view: AgentState = { ...agent, sources }
|
||||||
const question = nextQuestion(view)
|
const question = nextQuestion(view)
|
||||||
const ready = agent.items.filter(i => canSchedule(i.rule))
|
const ready = agent.items.filter(i => canSchedule(i.rule))
|
||||||
|
// Without a choice the whole Brain is attached, so every ready document counts.
|
||||||
|
const sourceCount = chosen.length || choosable(docs.data).length
|
||||||
|
const tooFewSources = sourceCount < MIN_PRODUCTION_SOURCES && ready.some(i => needsTwoSources(i.rule))
|
||||||
const hasContent = agent.items.some(i => i.rule.type === 'content')
|
const hasContent = agent.items.some(i => i.rule.type === 'content')
|
||||||
// The agent's current turn: its question, or what it is waiting for. It stays live, so an edit in the plan updates it.
|
// The agent's current turn: its question, or what it is waiting for. It stays live, so an edit in the plan updates it.
|
||||||
const live = inLang(lang, () => saving
|
const live = inLang(lang, () => saving
|
||||||
@@ -108,7 +111,7 @@ export function InstructionPage() {
|
|||||||
|
|
||||||
async function approve(state = agent, replyLang = lang) {
|
async function approve(state = agent, replyLang = lang) {
|
||||||
const items = state.items.filter(i => canSchedule(i.rule))
|
const items = state.items.filter(i => canSchedule(i.rule))
|
||||||
if (!items.length) return
|
if (!items.length || tooFewSources) return
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
const done: Outcome[] = []
|
const done: Outcome[] = []
|
||||||
const created = new Set<number>()
|
const created = new Set<number>()
|
||||||
@@ -201,8 +204,9 @@ export function InstructionPage() {
|
|||||||
<small>
|
<small>
|
||||||
<b className="rule-sources"><Icon name="brain" />{chosen.length ? t('ins.sourcesChosen', { n: chosen.length }) : t('ins.sourcesAll')}</b>
|
<b className="rule-sources"><Icon name="brain" />{chosen.length ? t('ins.sourcesChosen', { n: chosen.length }) : t('ins.sourcesAll')}</b>
|
||||||
{t('ins.approveNote')}
|
{t('ins.approveNote')}
|
||||||
|
{tooFewSources && <span className="error">{t(chosen.length ? 'ins.tooFewChosen' : 'ins.tooFewInBrain', { n: MIN_PRODUCTION_SOURCES })}</span>}
|
||||||
</small>
|
</small>
|
||||||
<button className="btn primary" disabled={saving || !idle} onClick={() => approve()}>
|
<button className="btn primary" disabled={saving || !idle || tooFewSources} onClick={() => approve()}>
|
||||||
<Icon name="check" />{saving ? t('ins.approving') : t('ins.approve', { n: ready.length })}
|
<Icon name="check" />{saving ? t('ins.approving') : t('ins.approve', { n: ready.length })}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { describeRule } from '../express/parse'
|
|||||||
import { locale, t, type Key } from '../i18n'
|
import { locale, t, type Key } from '../i18n'
|
||||||
import {
|
import {
|
||||||
archiveSchedule, confirmProduction, isRunSchedule, knowledgeReadiness, listRuns, listSchedules, pauseSchedule, PLAN_ACTIVE, productionStatus, quoteProduction, resumeSchedule,
|
archiveSchedule, confirmProduction, isRunSchedule, knowledgeReadiness, listRuns, listSchedules, pauseSchedule, PLAN_ACTIVE, productionStatus, quoteProduction, resumeSchedule,
|
||||||
planProgress, runNow as startRun, updateTopic, type ContentPlan, type RunSummary, type Schedule,
|
planProgress, productionError, runNow as startRun, updateTopic, type ContentPlan, type RunSummary, type Schedule,
|
||||||
} from '../express/schedules'
|
} from '../express/schedules'
|
||||||
|
|
||||||
const STUDIO_URL: string = import.meta.env.VITE_STUDIO_URL
|
const STUDIO_URL: string = import.meta.env.VITE_STUDIO_URL
|
||||||
@@ -201,7 +201,7 @@ function ProducePanel({ schedule: { agent, rule }, onClose }: { schedule: Schedu
|
|||||||
<input type="number" min={1} max={remaining} value={n} onChange={e => setCount(Number(e.target.value) || 1)} />
|
<input type="number" min={1} max={remaining} value={n} onChange={e => setCount(Number(e.target.value) || 1)} />
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{error && <span className="error">{error.message}</span>}
|
{error && <span className="error">{productionError(error.message)}</span>}
|
||||||
<div className="produce-actions">
|
<div className="produce-actions">
|
||||||
{!running && remaining > 0 && (quote.data
|
{!running && remaining > 0 && (quote.data
|
||||||
? <button className="btn small primary" disabled={start.isPending} onClick={() => start.mutate(quote.data)}>{start.isPending ? t('sch.starting') : t('sch.startProduction')}</button>
|
? <button className="btn small primary" disabled={start.isPending} onClick={() => start.mutate(quote.data)}>{start.isPending ? t('sch.starting') : t('sch.startProduction')}</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user