This commit is contained in:
Anthony Stirling
2026-01-05 15:00:37 +00:00
parent acbddf88d2
commit 3ffe489e32
7 changed files with 82 additions and 7 deletions
+14 -1
View File
@@ -72,6 +72,11 @@ def log_job_request_sequence() -> None:
def _json_body() -> Dict[str, Any]:
return request.get_json(silent=True) or {}
def _require_ai_enabled() -> Optional[Any]:
if CLIENT_MODE != "langchain":
return jsonify({"error": "AI is disabled. Set OPENAI_API_KEY to enable AI features."}), 503
return None
def _java_url(path: str) -> str:
base = JAVA_BACKEND_URL.rstrip("/")
@@ -612,6 +617,9 @@ def generate_stream() -> Any:
@app.route("/api/create/sessions/<session_id>/stream", methods=["GET"])
def create_stream(session_id: str) -> Any:
disabled = _require_ai_enabled()
if disabled:
return disabled
phase = (request.args.get("phase") or "outline").strip().lower()
try:
session = _fetch_ai_session(session_id)
@@ -749,9 +757,14 @@ def create_stream(session_id: str) -> Any:
@app.route("/api/create/sessions/<session_id>/fields", methods=["POST"])
def fill_fields(session_id: str) -> Any:
disabled = _require_ai_enabled()
if disabled:
return disabled
try:
logger.info("[AI create] fill_fields session_id=%s", session_id)
session = _fetch_ai_session(session_id)
except Exception: # noqa: BLE001
except Exception as exc: # noqa: BLE001
logger.warning("[AI create] fill_fields session lookup failed session_id=%s error=%s", session_id, exc)
return jsonify({"error": "Session not found"}), 404
data = _json_body()
@@ -23,6 +23,9 @@ os.makedirs(TEMPLATE_DIR, exist_ok=True)
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL")
JAVA_BACKEND_URL = os.environ.get("JAVA_BACKEND_URL", "http://localhost:8080")
if not OPENAI_API_KEY:
raise RuntimeError("OPENAI_API_KEY is required to start the AI backend.")
# Default to GPT-5.1 for full document generation (smart model).
# Allow override via SMART_MODEL or legacy OPENAI_MODEL.
SMART_MODEL = os.environ.get("SMART_MODEL") or os.environ.get("OPENAI_MODEL") or "gpt-5.1"
@@ -103,7 +103,22 @@ function App() {
setStyleDraft={workflow.setStyleDraft}
applyStyleAndRegenerate={workflow.applyStyleAndRegenerate}
onAddPromptInfo={workflow.addPromptForFields}
onStageSelect={(nextStage) => workflow.setStage(nextStage)}
onStageSelect={(nextStage) => {
if (workflow.isGenerating || workflow.isStageLoading) return
if (nextStage === 'text' && workflow.stage === 'outline') {
workflow.approveOutline()
return
}
if (nextStage === 'styling' && workflow.stage === 'text') {
workflow.approveDraft()
return
}
if (nextStage === 'review' && workflow.stage === 'styling') {
workflow.saveAndReview()
return
}
workflow.setStage(nextStage)
}}
imagePlaceholdersCount={workflow.imagePlaceholdersCount}
isAssetUploading={workflow.isAssetUploading}
assetError={workflow.assetError}
@@ -900,6 +900,11 @@ export function useDocumentWorkflow() {
const fillFieldsFromAI = useCallback(async (extraPrompt?: string) => {
if (!_aiSessionId) return
console.debug('[AI create] fillFieldsFromAI start', {
sessionId: _aiSessionId,
hasExtraPrompt: Boolean(extraPrompt?.trim()),
outlineCount: outlineRows.length,
})
setIsStageLoading(true)
try {
const res = await fetch(`/api/v1/ai/create/sessions/${_aiSessionId}/fields`, {
@@ -914,6 +919,19 @@ export function useDocumentWorkflow() {
}),
})
const data = await res.json().catch(() => ({}))
if (!res.ok) {
if (res.status === 503) {
addMessage('assistant', data.error || 'AI is disabled. Set an API key to enable AI features.')
autoFilledRef.current = true
autoFillAttemptsRef.current = 0
return
}
console.warn('[AI create] fillFieldsFromAI failed', {
sessionId: _aiSessionId,
status: res.status,
data,
})
}
if (!res.ok) {
throw new Error(data.error || 'Failed to auto-fill fields')
}
@@ -950,7 +968,7 @@ export function useDocumentWorkflow() {
} finally {
setIsStageLoading(false)
}
}, [_aiSessionId, outlineRows, autoFillFieldsFromPrompt, prompt, stage, view])
}, [_aiSessionId, outlineRows, autoFillFieldsFromPrompt, prompt, stage, view, addMessage])
const addPromptForFields = useCallback(
(extraPrompt: string) => {
@@ -1085,15 +1103,19 @@ export function useDocumentWorkflow() {
setIsGenerating(true)
setIsLivePreviewing(true)
try {
await fetch(`/api/v1/ai/create/sessions/${_aiSessionId}/outline`, {
const res = await fetch(`/api/v1/ai/create/sessions/${_aiSessionId}/outline`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ outlineText, constraints: outlineConstraints }),
})
if (!res.ok) {
const data = await res.json().catch(() => ({}))
throw new Error(data.error || 'Failed to approve outline')
}
await runDraft(_aiSessionId)
} catch (error) {
console.error('Outline approval failed:', error)
addMessage('assistant', 'Failed to approve outline. Please try again.')
addMessage('assistant', error instanceof Error ? error.message : 'Failed to approve outline. Please try again.')
} finally {
setIsGenerating(false)
setIsLivePreviewing(false)
@@ -1116,16 +1138,20 @@ export function useDocumentWorkflow() {
setIsGenerating(true)
setIsLivePreviewing(true)
try {
await fetch(`/api/v1/ai/create/sessions/${_aiSessionId}/draft`, {
const res = await fetch(`/api/v1/ai/create/sessions/${_aiSessionId}/draft`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ draftSections }),
})
if (!res.ok) {
const data = await res.json().catch(() => ({}))
throw new Error(data.error || 'Failed to approve draft')
}
setStage('styling')
await runPolish(_aiSessionId)
} catch (error) {
console.error('Draft approval failed:', error)
addMessage('assistant', 'Failed to approve draft. Please try again.')
addMessage('assistant', error instanceof Error ? error.message : 'Failed to approve draft. Please try again.')
} finally {
setIsGenerating(false)
setIsLivePreviewing(false)
@@ -1229,6 +1255,12 @@ export function useDocumentWorkflow() {
})
const createData = await createResponse.json().catch(() => ({}))
console.debug('[AI create] create session response', {
status: createResponse.status,
ok: createResponse.ok,
sessionId: createData.sessionId,
error: createData.error,
})
if (!createResponse.ok || !createData.sessionId) {
throw new Error(createData.error || 'Failed to create AI session')
}
@@ -49,6 +49,12 @@ public class AiCreateController {
}
AiCreateSession session =
sessionService.createSession(request.prompt(), request.docType(), request.templateId());
log.info(
"AI create session created sessionId={} userId={} docType={} templateId={}",
session.getSessionId(),
session.getUserId(),
session.getDocType(),
session.getTemplateId());
return ResponseEntity.ok(new CreateSessionResponse(session.getSessionId()));
}
@@ -122,6 +128,7 @@ public class AiCreateController {
public ResponseEntity<StreamingResponseBody> fillFields(
@PathVariable String sessionId, HttpServletRequest request) {
sessionService.getSessionForCurrentUser(sessionId);
log.info("AI create fillFields sessionId={}", sessionId);
return proxy("POST", "/api/create/sessions/" + sessionId + "/fields", request, false);
}
@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.proprietary.model.ai.AiCreateSession;
import stirling.software.proprietary.model.ai.AiCreateSessionStatus;
@@ -17,6 +18,7 @@ import stirling.software.proprietary.service.ai.AiCreateSessionService;
@RestController
@RequestMapping("/api/v1/ai/create/internal")
@RequiredArgsConstructor
@Slf4j
public class AiCreateInternalController {
private final AiCreateSessionService sessionService;
@@ -24,6 +26,7 @@ public class AiCreateInternalController {
@GetMapping("/sessions/{sessionId}")
public ResponseEntity<AiCreateController.AiCreateSessionResponse> getSession(
@PathVariable String sessionId) {
log.info("AI create internal getSession sessionId={}", sessionId);
AiCreateSession session = sessionService.getSession(sessionId);
return ResponseEntity.ok(AiCreateController.AiCreateSessionResponse.from(session));
}
@@ -31,6 +34,7 @@ public class AiCreateInternalController {
@PostMapping("/sessions/{sessionId}/update")
public ResponseEntity<AiCreateController.AiCreateSessionResponse> updateSession(
@PathVariable String sessionId, @RequestBody UpdateSessionRequest request) {
log.info("AI create internal updateSession sessionId={}", sessionId);
AiCreateSession session =
sessionService.applyInternalUpdate(
sessionId,
@@ -35,6 +35,7 @@ services:
- OPENAI_API_KEY=sk-proj-mVg... etc please insert
- SMART_MODEL=gpt-5.1
- FAST_MODEL=gpt-4.1-nano
- JAVA_BACKEND_URL=http://backend:8080
networks:
- ai-stack