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

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по+готовому+плану)
*[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<Partial<GraphState>> {
// Присваиваем 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<Partial<GraphState>> {
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<Partial<GraphState
Для генерации изображений использую российскую нейросеть Kandinsky через API:
Для работы с api я написал простенькую обёртку для упрощения работы [@brojs/kandinsky](https://gitverse.ru/primakov.a.a/kandinsky-js)
```typescript
async function generateImageNode(data: {
slide: Slide,
imageStyle: string
}): Promise<Partial<GraphState>> {
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<Partial<GraphState>> {
const messages = [
@ -278,20 +284,23 @@ async function generateContentNode(state: GraphState): Promise<Partial<GraphStat
### Сборка графа воедино
```typescript
const graph = new StateGraph(GraphState)
.addNode("prepare", prepareNode)
.addNode("webresearch", webSearchNode)
.addNode("generateImages", generateImageNode)
.addNode("generateContent", generateContentNode)
.addNode("final", finalNode)
.setEntryPoint("prepare")
.addConditionalEdges("prepare", routeToWebSearch)
.addEdge("webresearch", "generateImages")
.addEdge("generateImages", "generateContent")
.addEdge("generateContent", "final")
.setFinishPoint("final");
const builder = new StateGraph(agentState)
.addNode("prepare", prepareData)
.addNode('webResearch', webResearch)
.addNode('toGenerate', () => ({}))
.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)
---