восстановил часть

This commit is contained in:
Primakov Alexandr Alexandrovich 2025-07-10 19:14:50 +03:00
parent 351a1fc134
commit 76351e365e
4 changed files with 48 additions and 47 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 MiB

Binary file not shown.

View File

@ -115,16 +115,13 @@ const result = await chain.invoke({
- Создание контента с учётом контекста - Создание контента с учётом контекста
- Условная логика и роутинг - Условная логика и роутинг
![Демонстрация аналога сервиса gamma.app](https://via.placeholder.com/800x450/4A5568/FFFFFF?text=Клон+Gamma.app%0AГенерация+презентации%0Aпо+готовому+плану) ![Демонстрация аналога сервиса gamma.app план](./primakov-article-images/bro-gamma-plan.gif)
*[0:09:55 → 0:10:57]* ![Демонстрация аналога сервиса gamma.app слайды](./primakov-article-images/bro-gamma-slides.gif)
Здесь на помощь приходит **LangGraph**. Здесь на помощь приходит **LangGraph**.
## Архитектура 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) — функциями, которые выполняют конкретные задачи: LangGraph работает с **нодами** (nodes) — функциями, которые выполняют конкретные задачи:
@ -135,6 +132,8 @@ LangGraph работает с **нодами** (nodes) — функциями,
### Архитектура моего клона ### Архитектура моего клона
![Слайд с графом клона gamma.app](./primakov-article-images/Слайд%20с%20графом%20клона%20gamma.app.PNG)
Я создал компактный и эффективный граф из нескольких нод: Я создал компактный и эффективный граф из нескольких нод:
1. **Prepare** — подготовка данных и присвоение ID слайдам 1. **Prepare** — подготовка данных и присвоение ID слайдам
@ -152,7 +151,7 @@ async function prepareNode(state: GraphState): Promise<Partial<GraphState>> {
// Присваиваем ID каждому слайду // Присваиваем ID каждому слайду
presentation.slides.forEach((slide, index) => { presentation.slides.forEach((slide, index) => {
slide.id = `slide-${index}`; slide.id = uuidv4();
}); });
return { presentation }; return { presentation };
@ -185,19 +184,17 @@ function routeToWebSearch(state: GraphState): string[] {
```typescript ```typescript
async function webSearchNode(data: { slide: Slide }): Promise<Partial<GraphState>> { async function webSearchNode(data: { slide: Slide }): Promise<Partial<GraphState>> {
const tavily = new TavilySearchResults({ const client = await tavily({ apiKey: process.env['TAVILY_API_KEY'] })
maxResults: 5,
searchDepth: "advanced"
});
const results = await tavily.search(data.slide.webSearchQuery); const researchResult = await client.search(slide.webResearchQuery, {
maxResults: 3,
// Экранируем фигурные скобки в коде })
const cleanResults = results.replace(/\{/g, '{{').replace(/\}/g, '}}');
return { return {
webSearchResults: { webResearchResult: {
[data.slide.id]: cleanResults [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<Partial<GraphState
Для генерации изображений использую российскую нейросеть Kandinsky через API: Для генерации изображений использую российскую нейросеть Kandinsky через API:
Для работы с api я написал простенькую обёртку для упрощения работы [@brojs/kandinsky](https://gitverse.ru/primakov.a.a/kandinsky-js)
```typescript ```typescript
async function generateImageNode(data: { async function generateImageNode(data: {
slide: Slide, slide: Slide,
imageStyle: string imageStyle: string
}): Promise<Partial<GraphState>> { }): Promise<Partial<GraphState>> {
const kandinsky = new KandinskyAPI(); const kandinsky = new KandinskyAPI();
const { imagePrompt, imageNegativePrompt } = slide
const imagePrompt = `${data.slide.imagePrompt}, ${data.imageStyle}`; const imageSize = {
const image = await kandinsky.generateImage({
prompt: imagePrompt,
negativePrompt: data.slide.negativeImagePrompt,
width: data.slide.type === 'title' ? 512 : 768, width: data.slide.type === 'title' ? 512 : 768,
height: data.slide.type === 'title' ? 512 : 432 height: data.slide.type === 'title' ? 512 : 432
}); }
const images = await generateKandinskyImage({
imagePrompt,
imagesStyle,
...imageSize,
imageNegativePrompt,
})
return { return {
generatedImages: { generatedImages: {
@ -236,6 +240,8 @@ async function generateImageNode(data: {
Самая интересная часть — создание контента с учётом предыдущих слайдов: Самая интересная часть — создание контента с учётом предыдущих слайдов:
```typescript ```typescript
async function generateContentNode(state: GraphState): Promise<Partial<GraphState>> { async function generateContentNode(state: GraphState): Promise<Partial<GraphState>> {
const messages = [ const messages = [
@ -278,20 +284,23 @@ async function generateContentNode(state: GraphState): Promise<Partial<GraphStat
### Сборка графа воедино ### Сборка графа воедино
```typescript ```typescript
const graph = new StateGraph(GraphState) const builder = new StateGraph(agentState)
.addNode("prepare", prepareNode) .addNode("prepare", prepareData)
.addNode("webresearch", webSearchNode) .addNode('webResearch', webResearch)
.addNode("generateImages", generateImageNode) .addNode('toGenerate', () => ({}))
.addNode("generateContent", generateContentNode) .addNode('final', state => state)
.addNode("final", finalNode) .addNode('imageGenerator', imageGenerator)
.setEntryPoint("prepare") .addNode('generateSlideContent', generateSlideContent)
.addConditionalEdges("prepare", routeToWebSearch)
.addEdge("webresearch", "generateImages")
.addEdge("generateImages", "generateContent")
.addEdge("generateContent", "final")
.setFinishPoint("final");
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({ const result = await app.invoke({
@ -316,13 +325,7 @@ LangGraph позволяет запускать ноды параллельно.
### Стоимость генерации ### Стоимость генерации
Примерная стоимость создания презентации из 8 слайдов: Примерная стоимость создания презентации из 8 слайдов около 20-30 Р
- Планирование: ~$0.02
- Веб-поиск: ~$0.01
- Генерация изображений: ~$0.06
- Создание контента: ~$0.02
**Итого: ~$0.11** себестоимость при продажной цене $2-5.
## Практические советы ## Практические советы
@ -338,9 +341,7 @@ LangGraph позволяет запускать ноды параллельно.
Главное — **пробовать и экспериментировать**. Это направление развивается очень быстро, и скоро каждый разработчик будет работать с AI-агентами. Главное — **пробовать и экспериментировать**. Это направление развивается очень быстро, и скоро каждый разработчик будет работать с AI-агентами.
[Ссылка на прототип](https://platform.bro-js.ru/bro-gamma) [Ссыылка на рабочий прототип](https://platform.bro-js.ru/bro-gamma)
*QR-код для доступа к демо-версии клона Gamma.app*
--- ---