diff --git a/primakov-article-images/bro-gamma-plan.gif b/primakov-article-images/bro-gamma-plan.gif new file mode 100644 index 0000000..03e56dc Binary files /dev/null and b/primakov-article-images/bro-gamma-plan.gif differ diff --git a/primakov-article-images/bro-gamma-slides.gif b/primakov-article-images/bro-gamma-slides.gif new file mode 100644 index 0000000..387dce4 Binary files /dev/null and b/primakov-article-images/bro-gamma-slides.gif differ diff --git a/primakov-article-images/bro-gamma-slides.mp4 b/primakov-article-images/bro-gamma-slides.mp4 new file mode 100644 index 0000000..d1ae04e Binary files /dev/null and b/primakov-article-images/bro-gamma-slides.mp4 differ diff --git a/primakov.md b/primakov.md index ece48a2..79c8a7a 100644 --- a/primakov.md +++ b/primakov.md @@ -115,16 +115,13 @@ const result = await chain.invoke({ - Создание контента с учётом контекста - Условная логика и роутинг -![Демонстрация аналога сервиса gamma.app](https://via.placeholder.com/800x450/4A5568/FFFFFF?text=Клон+Gamma.app%0AГенерация+презентации%0Aпо+готовому+плану) -*[0:09:55 → 0:10:57]* +![Демонстрация аналога сервиса gamma.app план](./primakov-article-images/bro-gamma-plan.gif) +![Демонстрация аналога сервиса gamma.app слайды](./primakov-article-images/bro-gamma-slides.gif) Здесь на помощь приходит **LangGraph**. ## Архитектура LangGraph: от нод к агентам -![Слайд с объяснением работы LangGraph](https://via.placeholder.com/800x450/2B6CB0/FFFFFF?text=LangGraph+Architecture%0ANodes+→+Edges+→+State+→+Graph) -*[0:10:57 → 0:26:20]* - ### Основные концепции LangGraph работает с **нодами** (nodes) — функциями, которые выполняют конкретные задачи: @@ -135,6 +132,8 @@ LangGraph работает с **нодами** (nodes) — функциями, ### Архитектура моего клона +![Слайд с графом клона gamma.app](./primakov-article-images/Слайд%20с%20графом%20клона%20gamma.app.PNG) + Я создал компактный и эффективный граф из нескольких нод: 1. **Prepare** — подготовка данных и присвоение ID слайдам @@ -152,7 +151,7 @@ async function prepareNode(state: GraphState): Promise> { // Присваиваем ID каждому слайду presentation.slides.forEach((slide, index) => { - slide.id = `slide-${index}`; + slide.id = uuidv4(); }); return { presentation }; @@ -185,19 +184,17 @@ function routeToWebSearch(state: GraphState): string[] { ```typescript async function webSearchNode(data: { slide: Slide }): Promise> { - const tavily = new TavilySearchResults({ - maxResults: 5, - searchDepth: "advanced" - }); - - const results = await tavily.search(data.slide.webSearchQuery); - - // Экранируем фигурные скобки в коде - const cleanResults = results.replace(/\{/g, '{{').replace(/\}/g, '}}'); - + const client = await tavily({ apiKey: process.env['TAVILY_API_KEY'] }) + + const researchResult = await client.search(slide.webResearchQuery, { + maxResults: 3, + }) + return { - webSearchResults: { - [data.slide.id]: cleanResults + webResearchResult: { + [slide.id as string]: researchResult.results + .map(r => r.content?.replace(/\{/g, '{{').replace(/\}/g, '}}')) + .join('\n\n') } }; } @@ -209,20 +206,27 @@ async function webSearchNode(data: { slide: Slide }): Promise> { const kandinsky = new KandinskyAPI(); - - const imagePrompt = `${data.slide.imagePrompt}, ${data.imageStyle}`; - const image = await kandinsky.generateImage({ - prompt: imagePrompt, - negativePrompt: data.slide.negativeImagePrompt, + const { imagePrompt, imageNegativePrompt } = slide + + const imageSize = { width: data.slide.type === 'title' ? 512 : 768, height: data.slide.type === 'title' ? 512 : 432 - }); + } + + const images = await generateKandinskyImage({ + imagePrompt, + imagesStyle, + ...imageSize, + imageNegativePrompt, + }) return { generatedImages: { @@ -236,6 +240,8 @@ async function generateImageNode(data: { Самая интересная часть — создание контента с учётом предыдущих слайдов: + + ```typescript async function generateContentNode(state: GraphState): Promise> { const messages = [ @@ -278,20 +284,23 @@ async function generateContentNode(state: GraphState): Promise ({})) + .addNode('final', state => state) + .addNode('imageGenerator', imageGenerator) + .addNode('generateSlideContent', generateSlideContent) -const app = graph.compile(); +builder + .addEdge(START, "prepare") + .addConditionalEdges("prepare", afterPrepareRouter) + .addConditionalEdges("toGenerate", generateImagesAndFirstContent) + .addConditionalEdges("generateSlideContent", toGenerateNextSlideContent) + .addEdge('webResearch', 'toGenerate') + .addEdge('final', END); + +const app = builder.compile(); // Запуск генерации const result = await app.invoke({ @@ -316,13 +325,7 @@ LangGraph позволяет запускать ноды параллельно. ### Стоимость генерации -Примерная стоимость создания презентации из 8 слайдов: -- Планирование: ~$0.02 -- Веб-поиск: ~$0.01 -- Генерация изображений: ~$0.06 -- Создание контента: ~$0.02 - -**Итого: ~$0.11** себестоимость при продажной цене $2-5. +Примерная стоимость создания презентации из 8 слайдов около 20-30 Р ## Практические советы @@ -338,9 +341,7 @@ LangGraph позволяет запускать ноды параллельно. Главное — **пробовать и экспериментировать**. Это направление развивается очень быстро, и скоро каждый разработчик будет работать с AI-агентами. -[Ссылка на прототип](https://platform.bro-js.ru/bro-gamma) - -*QR-код для доступа к демо-версии клона Gamma.app* +[Ссыылка на рабочий прототип](https://platform.bro-js.ru/bro-gamma) ---