返回 presentation-ai
route.ts
根目录 / src / app / api / presentation / generate / route.ts
1 import { toUIMessageStream } from "@ai-sdk/langchain";
2 import { RunnableSequence } from "@langchain/core/runnables";
3 import { createUIMessageStreamResponse } from "ai";
4 import { NextResponse } from "next/server";
5
6 import {
7 assertModelIsConfigured,
8 ensureModelIsReady,
9 modelPicker,
10 } from "@/lib/modelPicker";
11 import { createLogger } from "@/lib/observability/logger";
12 import {
13 buildPresentationPromptValues,
14 presentationGenerationPromptTemplate,
15 type PresentationGenerationPromptInput,
16 } from "@/lib/presentation/generation-prompt";
17 import { auth } from "@/server/auth";
18
19 type SlidesRequest = Omit<PresentationGenerationPromptInput, "currentDate"> & {
20 modelId?: string;
21 modelProvider?: "openai" | "ollama" | "lmstudio";
22 presentationId?: string;
23 };
24
25 export async function POST(req: Request) {
26 const requestId = crypto.randomUUID();
27 const routeLogger = createLogger("api:presentation-generate");
28
29 try {
30 routeLogger.info("Presentation generation request received", { requestId });
31 const session = await auth();
32 if (!session) {
33 routeLogger.warn("Presentation generation request rejected: unauthorized", {
34 requestId,
35 });
36 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
37 }
38
39 const request = (await req.json()) as SlidesRequest;
40 const { modelId, modelProvider = "openai" } = request;
41
42 if (
43 !request.title ||
44 !request.outline ||
45 !Array.isArray(request.outline) ||
46 !request.language
47 ) {
48 routeLogger.warn(
49 "Presentation generation request rejected: missing required fields",
50 {
51 requestId,
52 hasTitle: Boolean(request.title),
53 hasOutline: Array.isArray(request.outline),
54 language: request.language,
55 },
56 );
57 return NextResponse.json(
58 { error: "Missing required fields" },
59 { status: 400 },
60 );
61 }
62
63 const totalSlides = request.outline.length;
64 const templateCount = request.selectedTemplateCount ?? 0;
65
66 const currentDate = new Date().toLocaleDateString("en-US", {
67 weekday: "long",
68 year: "numeric",
69 month: "long",
70 day: "numeric",
71 });
72
73 routeLogger.info("Validated presentation generation request", {
74 requestId,
75 title: request.title,
76 totalSlides,
77 language: request.language,
78 tone: request.tone,
79 modelProvider,
80 modelId: modelId || "gpt-4o-mini",
81 imageSource: request.imageSource || "automatic",
82 templateCount,
83 searchResultGroupCount: request.searchResults?.length ?? 0,
84 imageSearchResultGroupCount: request.imageSearchResults?.length ?? 0,
85 selectedChunkCount: request.selectedChunks?.length ?? 0,
86 presentationId: request.presentationId,
87 });
88
89 try {
90 assertModelIsConfigured(modelProvider, modelId);
91 } catch (error) {
92 routeLogger.error(
93 "Presentation generation request rejected: invalid model configuration",
94 error,
95 {
96 requestId,
97 modelProvider,
98 modelId: modelId || "gpt-4o-mini",
99 },
100 );
101 return NextResponse.json(
102 {
103 error:
104 error instanceof Error
105 ? error.message
106 : "Invalid model configuration",
107 },
108 { status: 400 },
109 );
110 }
111
112 try {
113 await ensureModelIsReady(modelProvider, modelId);
114 } catch (error) {
115 routeLogger.error(
116 "Presentation generation request rejected: selected model could not be prepared",
117 error,
118 {
119 requestId,
120 modelProvider,
121 modelId: modelId || "gpt-4o-mini",
122 },
123 );
124 return NextResponse.json(
125 {
126 error:
127 error instanceof Error
128 ? error.message
129 : "Failed to prepare selected model",
130 },
131 { status: 503 },
132 );
133 }
134
135 const model = modelPicker(modelProvider, modelId);
136 const chain = RunnableSequence.from([
137 presentationGenerationPromptTemplate,
138 model,
139 ]);
140
141 routeLogger.info("Presentation generation started", {
142 requestId,
143 title: request.title,
144 totalSlides,
145 modelProvider,
146 modelId: modelId || "gpt-4o-mini",
147 });
148
149 const stream = await chain.stream(
150 buildPresentationPromptValues({
151 ...request,
152 currentDate,
153 }),
154 );
155
156 routeLogger.info("Presentation generation stream created", {
157 requestId,
158 title: request.title,
159 totalSlides,
160 });
161
162 return createUIMessageStreamResponse({ stream: toUIMessageStream(stream) });
163 } catch (error) {
164 routeLogger.error("Presentation generation failed", error, { requestId });
165 return NextResponse.json(
166 { error: "Failed to generate presentation slides" },
167 { status: 500 },
168 );
169 }
170 }
171
171 lines TYPESCRIPT