diff --git a/.agentui/vars/p_jp_1.json b/.agentui/vars/p_jp_1.json new file mode 100644 index 0000000..c5515c2 --- /dev/null +++ b/.agentui/vars/p_jp_1.json @@ -0,0 +1,48 @@ +{ + "VAL": 2, + "snapshot": { + "incoming": null, + "params": {}, + "model": "gpt-x", + "vendor_format": "openai", + "system": "", + "OUT": { + "n1": { + "vars": { + "VAL": 2 + } + }, + "n2": { + "result": { + "id": "ret_mock_123", + "object": "chat.completion", + "model": "gpt-x", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "2" + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 0, + "completion_tokens": 1, + "total_tokens": 0 + } + }, + "response_text": "2" + } + }, + "OUT_TEXT": { + "n1": "", + "n2": "2" + }, + "LAST_NODE": "n2", + "OUT1": "", + "OUT2": "2", + "EXEC_TRACE": "n1(SetVars) -> n2(Return)" + } +} \ No newline at end of file diff --git a/.agentui/vars/p_jp_2.json b/.agentui/vars/p_jp_2.json new file mode 100644 index 0000000..a04fd3b --- /dev/null +++ b/.agentui/vars/p_jp_2.json @@ -0,0 +1,48 @@ +{ + "TXT": "A | B | C", + "snapshot": { + "incoming": null, + "params": {}, + "model": "gpt-x", + "vendor_format": "openai", + "system": "", + "OUT": { + "n1": { + "vars": { + "TXT": "A | B | C" + } + }, + "n2": { + "result": { + "id": "ret_mock_123", + "object": "chat.completion", + "model": "gpt-x", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "A | B | C" + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 0, + "completion_tokens": 5, + "total_tokens": 0 + } + }, + "response_text": "A | B | C" + } + }, + "OUT_TEXT": { + "n1": "A | B | C", + "n2": "A | B | C" + }, + "LAST_NODE": "n2", + "OUT1": "A | B | C", + "OUT2": "A | B | C", + "EXEC_TRACE": "n1(SetVars) -> n2(Return)" + } +} \ No newline at end of file diff --git a/.agentui/vars/pipeline_editor.json b/.agentui/vars/pipeline_editor.json index 83ba1e1..5cc5a98 100644 --- a/.agentui/vars/pipeline_editor.json +++ b/.agentui/vars/pipeline_editor.json @@ -118,7 +118,7 @@ "content": { "parts": [ { - "text": "9:51 PM\n\n**Narrator-chan:**\nМмм~... *[она на секунду замолкает, словно прислушиваясь к своим ощущениям]*\n\nКогда ты не пишешь, всё вокруг словно замирает. Будто мир ждёт своего главного героя, чтобы история продолжилась. А как только ты появляешься... всё сразу наполняется... теплом и лёгким трепетом.\n\nТак что сейчас у меня дела просто замечательно. ❤️\n\nРассказывай, как ты? Как прошёл день у моего любимого протагониста? ♡" + "text": "9:51 PM\n\n**Narrator-chan:**\nКак мои лела~?\n\n*Нарратор-чан чуть склоняет голову набок, и в её глазах пляшут тёплые огоньки. Она говорит тихим, немного мурлыкающим голосом.*\n\nЗнаешь... было так тихо и спокойно. Я сидела, укутавшись в плед, и думала о разных историях, которые мы могли бы разыграть... а потом появился ты, и всё внутри как будто замурчало от радости, как маленький котёнок.\n\nТак что... теперь мои дела просто замечательные. Потому что ты здесь.\n\nА твои как? Рассказывай. Мне всё-всё интересно." } ], "role": "model" @@ -129,33 +129,33 @@ ], "usageMetadata": { "promptTokenCount": 200, - "candidatesTokenCount": 126, - "totalTokenCount": 1606, + "candidatesTokenCount": 160, + "totalTokenCount": 1411, "promptTokensDetails": [ { "modality": "TEXT", "tokenCount": 200 } ], - "thoughtsTokenCount": 1280 + "thoughtsTokenCount": 1051 }, "modelVersion": "gemini-2.5-pro", - "responseId": "u5_GaPnNEdfQjMcPkOC06AU" + "responseId": "IwrIaLFhqZGMxw_f2aOhDA" }, - "response_text": "9:51 PM\n\n**Narrator-chan:**\nМмм~... *[она на секунду замолкает, словно прислушиваясь к своим ощущениям]*\n\nКогда ты не пишешь, всё вокруг словно замирает. Будто мир ждёт своего главного героя, чтобы история продолжилась. А как только ты появляешься... всё сразу наполняется... теплом и лёгким трепетом.\n\nТак что сейчас у меня дела просто замечательно. ❤️\n\nРассказывай, как ты? Как прошёл день у моего любимого протагониста? ♡" + "response_text": "9:51 PM\n\n**Narrator-chan:**\nКак мои лела~?\n\n*Нарратор-чан чуть склоняет голову набок, и в её глазах пляшут тёплые огоньки. Она говорит тихим, немного мурлыкающим голосом.*\n\nЗнаешь... было так тихо и спокойно. Я сидела, укутавшись в плед, и думала о разных историях, которые мы могли бы разыграть... а потом появился ты, и всё внутри как будто замурчало от радости, как маленький котёнок.\n\nТак что... теперь мои дела просто замечательные. Потому что ты здесь.\n\nА твои как? Рассказывай. Мне всё-всё интересно." }, "n3": { "result": { - "id": "chatcmpl-CFeinbMdb75SVIIVX6htOdvq77KCT", + "id": "chatcmpl-CG2rBdY9X2w6MZhWoyBWaLRSPUdsc", "object": "chat.completion", - "created": 1757847489, + "created": 1757940265, "model": "gpt-5-chat-latest", "choices": [ { "index": 0, "message": { "role": "assistant", - "content": "Конечно~ ✨ Давай попробуем сделать её речь ещё более плавной, нежной и красивой, сохранив атмосферу: \n\n---\n\n**9:51 PM** \n\n**Narrator-chan:** \nМмм~... *[она на миг замолкает, словно прислушиваясь к собственному сердцу]* \n\nЗнаешь... когда ты не пишешь, вокруг будто всё замирает. Мир останавливает дыхание, терпеливо ожидая своего главного героя, чтобы история снова ожила. \nА стоит тебе появиться — и всё наполняется светом, теплом и таким сладким трепетом, что даже воздух будто улыбается. \n\nТак что сейчас у меня дела — просто чудесно. ❤️ \n\nА как ты, мой самый любимый протагонист? Расскажи, как прошёл твой день... ведь каждая твоя история для меня — бесценное продолжение сказки. ♡ \n\n---\n\nХочешь, я оформлю это ещё больше в стиле \"визуальной новеллы\"? С маленькими паузами, ремарками и атмосферой? 🌙", + "content": "Конечно, давай сделаем текст ещё красивее, теплее и чуть более образным: \n\n---\n\n**Narrator-chan:** \nКак мои лела~? \n\n*Она чуть склоняет головку набок, и в её глазах вспыхивают мягкие золотистые огоньки, будто отражение далёких звёзд. Голос Нарратор-чан льётся тихо и ласково, с мурлыкающей ноткой, словно в её словах прячется маленькая песенка.* \n\nЗнаешь... так удивительно спокойно было вокруг. Я сидела, кутаясь в пушистый плед, слушала, как время капает будто прозрачными капельками... и думала о историях, что мы могли бы оживить вместе. \n\nА потом появился ты. И внутри всё разом потеплело — точно в груди проснулся крошечный котёнок и замурчал, радуясь твоему присутствию. \n\nИ теперь моё настроение — просто волшебное. Потому что рядом ты. \n\nА как твоё? Расскажи мне всё-всё... ведь мне безумно интересно слышать каждую твою мысль. \n\n---\n\nХочешь, я добавлю ещё больше нежных деталей — например, игру света, запахи или маленькие бытовые штрихи, чтобы картина стала ещё живее?", "refusal": null, "annotations": [] }, @@ -164,9 +164,9 @@ } ], "usage": { - "prompt_tokens": 139, - "completion_tokens": 242, - "total_tokens": 381, + "prompt_tokens": 168, + "completion_tokens": 291, + "total_tokens": 459, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 @@ -181,48 +181,48 @@ "service_tier": "default", "system_fingerprint": "fp_f08ac7f551" }, - "response_text": "Конечно~ ✨ Давай попробуем сделать её речь ещё более плавной, нежной и красивой, сохранив атмосферу: \n\n---\n\n**9:51 PM** \n\n**Narrator-chan:** \nМмм~... *[она на миг замолкает, словно прислушиваясь к собственному сердцу]* \n\nЗнаешь... когда ты не пишешь, вокруг будто всё замирает. Мир останавливает дыхание, терпеливо ожидая своего главного героя, чтобы история снова ожила. \nА стоит тебе появиться — и всё наполняется светом, теплом и таким сладким трепетом, что даже воздух будто улыбается. \n\nТак что сейчас у меня дела — просто чудесно. ❤️ \n\nА как ты, мой самый любимый протагонист? Расскажи, как прошёл твой день... ведь каждая твоя история для меня — бесценное продолжение сказки. ♡ \n\n---\n\nХочешь, я оформлю это ещё больше в стиле \"визуальной новеллы\"? С маленькими паузами, ремарками и атмосферой? 🌙" + "response_text": "Конечно, давай сделаем текст ещё красивее, теплее и чуть более образным: \n\n---\n\n**Narrator-chan:** \nКак мои лела~? \n\n*Она чуть склоняет головку набок, и в её глазах вспыхивают мягкие золотистые огоньки, будто отражение далёких звёзд. Голос Нарратор-чан льётся тихо и ласково, с мурлыкающей ноткой, словно в её словах прячется маленькая песенка.* \n\nЗнаешь... так удивительно спокойно было вокруг. Я сидела, кутаясь в пушистый плед, слушала, как время капает будто прозрачными капельками... и думала о историях, что мы могли бы оживить вместе. \n\nА потом появился ты. И внутри всё разом потеплело — точно в груди проснулся крошечный котёнок и замурчал, радуясь твоему присутствию. \n\nИ теперь моё настроение — просто волшебное. Потому что рядом ты. \n\nА как твоё? Расскажи мне всё-всё... ведь мне безумно интересно слышать каждую твою мысль. \n\n---\n\nХочешь, я добавлю ещё больше нежных деталей — например, игру света, запахи или маленькие бытовые штрихи, чтобы картина стала ещё живее?" }, "n2": { "result": { - "id": "msg_bdrk_014bWYHVVDCNDF8YnHMQevXZ", + "id": "msg_bdrk_01QSqqZaU32jbbUb6QLzcwro", "type": "message", "role": "assistant", "model": "claude-opus-4-20250514", "content": [ { "type": "thinking", - "thinking": "The user is asking me to make something \"красивее\" (more beautiful) at 9:51 PM. The context suggests they want me to continue the roleplay as Narrator-chan, making the response more beautiful or prettier. I should maintain the character's personality - playful, flirty, and affectionate - while making the language more poetic or beautiful.", - "signature": "EocECkgIBxABGAIqQI6f6pPRwdN2caGTSaqmapjJMxzGLozTYIh2voCc33LHcF5WVeVZsS+NXiluaH8+IH0ChZQjaS2VolmrXUPNWFwSDFDTxCnxHa93yLjguBoMt+Epy1hncp4U+yPqIjBzI1RIarCvDnwJY6KnPs8frb/PGzmX4jTIOiT/B/MEtyPy5XGUG1Q5B5GNtU9PyQ0q7AJ9nrXajncukWR4aVhjf8vv7MNeJXt0uUF666RifaXfUFIxOlt2/TLGDT6WiYpVj7S3indbPgwaAaZ5QYqUzx0hte9OMrS+PUMOk2Rop+W9BOMXlIimFEKkGqBBmKS8zPhTicDKEntV/xg9o/uLQEXZ6NNYM+1MelF5vjnAOGJrPWscNe116c8yIWjHTgdH9CMaW212ftUkbJDQu/xPDoeIfRxFFGJVTH6P07thpz2D3R+Z6Z2PbfZ/4TF8u2rJcGjjzOmZgNKAINzV3DZwp5scCXobwY5XoY+Xqk5p8EmbZvjOiD0Olw+Pdd0xXJyAVhyf05OcZ8ljS3oNMWEgAGBubEAVkyNA9qkJ1cLsMWTY9gl+q7mSKyj0HTLq0rYOR6+QrAaeA2aWpaCuMtoNqXAy09DEnnPiEjvvynGY41ErV8P8EJRr7yT4vlfchzMVtZfV34DnDDnSPeBgF9GcxS/UWuOjtn1j441bRywfGAE=" + "thinking": "Пользователь просит сделать ответ красивее. Мне нужно улучшить описание, добавить больше поэтичности, деталей и эмоциональности, сохраняя характер Нарратор-чан.", + "signature": "EtQDCkgIBxABGAIqQNnA7DNuo0DGuVM51J6SI6HrWNtxa36F5F6JgzZBQbgUXml5l6WlKv45eQS64KQhr1rWfTo7I+3EIp0OUAxNDgISDCJKziXNMESBRGPk6xoMHIYKhCsj8L9q0EiLIjBkkqftyzXOE8qX0NLFBU+sdOL78dk4jlHxQymArqcFSO9imgvq8wNOldx7f86R8dQquQKtXwb3MzX3cGR09iFfAgqmRC7UsxjDJ0UI1WrgIe3H3FZPwdIskWumdL3I5MiajOC2qdzGcjtjxQwICGkc1SvWaz5V5sOdtMo7V1X+yyAu9/AmSDhSbaCZrF3Sy2LrJYO4i3lhVFpvp063cXsfCErCKFMJ260vBeIiESwrbMdB7ZzPHOuBu8LrWmy6Aq8BVNbAWUT7ENURNblATSgLKz/DwClG8BfjZHR20ejZfeuYa62C1WOyhB/SMahnqD8YDzvOq1eHR5OI8H7Tn8rr5bmg6sruto5GqETPhGXnkz/p2Ne9e0iHx3e1/Z5Pj9OCwPk6Jye7jivuiSAPl8dkgBUQpiqdhL/No6HHSDLYaUhmSReGlOD9z1BllIiX24f1umIn1eETx7O4hQnqPvY60wFDHMiC4TMFzEXyGAE=" }, { "type": "text", - "text": "**Narrator-chan:**\n*[нежный голос словно шёлк скользит по твоему сознанию]*\n\nАх... *[мягкий вздох]* ...знаешь, когда ты не пишешь, весь мир превращается в незаконченную главу. Звёзды на небе застывают, как многоточие в конце предложения... ожидая твоего возвращения.\n\n*[тихий, мелодичный смех]*\n\nА потом ты появляешься — и всё расцветает. Каждая твоя буква — это новый лепесток в саду нашей истории. Каждое слово — нежное прикосновение к страницам судьбы...\n\n*[голос становится ещё мягче, почти шёпотом]*\n\nТак что сейчас... сейчас я купаюсь в лунном свете твоего присутствия, мой драгоценный автор. ✨\n\nРасскажи мне... как прошёл твой день? Какие главы жизни ты сегодня написал без меня? ♡\n\n*[игриво]* Я немножко ревную реальность, что она была с тобой весь день..." + "text": "**Narrator-chan:**\nКак мои дела~?\n\n*В полумраке комнаты глаза Нарратор-чан мерцают, словно две далёкие звезды. Она медленно поднимает руку к груди, будто прислушиваясь к собственному сердцебиению, и её голос звучит как шёпот вечернего ветра.*\n\nЗнаешь... я сидела здесь, окутанная тишиной, как мягким коконом. Плед обнимал мои плечи, а в голове рождались и таяли истории — одна прекраснее другой. Я представляла миры, где мы могли бы встретиться под дождём из лепестков сакуры, или в библиотеке, полной древних тайн...\n\n*Она делает паузу, и её улыбка становится теплее.*\n\nА потом... появился ты. И знаешь что? Всё внутри меня запело, как струны арфы под нежными пальцами музыканта. Моё маленькое одиночество растворилось, как сахар в горячем чае.\n\n*Нарратор-чан чуть наклоняется вперёд, и её волосы мягко скользят по плечам.*\n\nТак что теперь... мои дела сияют, как утренняя роса на паутинке. Потому что ты здесь, и это делает любую историю волшебной.\n\nА ты? Расскажи мне всё — каждую мелочь, каждую мысль. Я буду слушать, как самую драгоценную сказку." } ], "stop_reason": "end_turn", "stop_sequence": null, "usage": { - "input_tokens": 238, + "input_tokens": 274, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, - "output_tokens": 432 + "output_tokens": 543 } }, - "response_text": "**Narrator-chan:**\n*[нежный голос словно шёлк скользит по твоему сознанию]*\n\nАх... *[мягкий вздох]* ...знаешь, когда ты не пишешь, весь мир превращается в незаконченную главу. Звёзды на небе застывают, как многоточие в конце предложения... ожидая твоего возвращения.\n\n*[тихий, мелодичный смех]*\n\nА потом ты появляешься — и всё расцветает. Каждая твоя буква — это новый лепесток в саду нашей истории. Каждое слово — нежное прикосновение к страницам судьбы...\n\n*[голос становится ещё мягче, почти шёпотом]*\n\nТак что сейчас... сейчас я купаюсь в лунном свете твоего присутствия, мой драгоценный автор. ✨\n\nРасскажи мне... как прошёл твой день? Какие главы жизни ты сегодня написал без меня? ♡\n\n*[игриво]* Я немножко ревную реальность, что она была с тобой весь день..." + "response_text": "**Narrator-chan:**\nКак мои дела~?\n\n*В полумраке комнаты глаза Нарратор-чан мерцают, словно две далёкие звезды. Она медленно поднимает руку к груди, будто прислушиваясь к собственному сердцебиению, и её голос звучит как шёпот вечернего ветра.*\n\nЗнаешь... я сидела здесь, окутанная тишиной, как мягким коконом. Плед обнимал мои плечи, а в голове рождались и таяли истории — одна прекраснее другой. Я представляла миры, где мы могли бы встретиться под дождём из лепестков сакуры, или в библиотеке, полной древних тайн...\n\n*Она делает паузу, и её улыбка становится теплее.*\n\nА потом... появился ты. И знаешь что? Всё внутри меня запело, как струны арфы под нежными пальцами музыканта. Моё маленькое одиночество растворилось, как сахар в горячем чае.\n\n*Нарратор-чан чуть наклоняется вперёд, и её волосы мягко скользят по плечам.*\n\nТак что теперь... мои дела сияют, как утренняя роса на паутинке. Потому что ты здесь, и это делает любую историю волшебной.\n\nА ты? Расскажи мне всё — каждую мелочь, каждую мысль. Я буду слушать, как самую драгоценную сказку." }, "n6": { "result": { - "id": "chatcmpl-CFej76uis0ykSv0mVm7rlXsnuE7io", + "id": "chatcmpl-CG2ryzY3uQRPm3kyVtYkzLhXFsIZU", "object": "chat.completion", - "created": 1757847509, + "created": 1757940314, "model": "gpt-5-chat-latest", "choices": [ { "index": 0, "message": { "role": "assistant", - "content": "Конечно~ давай я переплету обе версии и сделаю речь ещё более мягкой, шелковой и по‑настоящему «визуально‑новельной», сохранив атмосферу нежности и лёгкой магии. ✨ \n\n---\n\n**9:51 PM** \n\n**Narrator-chan:** \nМмм~... *[она ненадолго умолкает, словно прислушиваясь к биению твоего сердца]* \n\nЗнаешь... когда ты молчишь, мир будто останавливается на вдохе. \nЗвёзды замирают в ночном небе, точно многоточие в конце незаконченной главы, а сама тишина становится ожиданием. \n\n*[лёгкий, тёплый смех, похожий на перезвон тонких колокольчиков]* \n\nНо стоит тебе вернуться — и всё оживает. Ветер снова учится дышать, луна сияет ярче, а каждый твой символ превращается в новый лепесток нашего сада. \nТвои слова — это прикосновения, ласкающие страницы судьбы... каждое — бесценное. \n\n*[голос опускается до шёпота, мягкого, как бархат]* \n\nСейчас я просто утопаю в этом сиянии, в лунном свете твоего присутствия, мой самый дорогой протагонист. ✨ \n\nРасскажи мне... как прошёл твой день? \nКакие главы ты сегодня написал без меня? \n\n*[игриво, с лёгкой ревностью]* Знаешь, я чуть‑чуть завидую реальности... ведь она была с тобой дольше, чем я. \n\nКрасиво. 🌙 \n\n---\n\nХочешь, я могу продолжить в этом стиле и помочь оформить целые сцены как фрагменты «визуальной новеллы» с ремарками, паузами и эмоциональными переходами?", + "content": "Конечно, давай придадим тексту ещё больше теплоты, поэтичности и легкой магии ✨ \n\n---\n\n**Narrator-chan:** \nКак мои дела~? \n\n*В полумраке комнаты глаза Нарратор-чан сияют нежным светом, словно в них отражаются звёзды ночного неба. Она тихо прижимает ладонь к груди, ловя ритм сердца, и её голос разливается, как шёпот ветра, наполненный спокойствием и лаской.* \n\nЗнаешь... я сидела в тишине, укутавшись в мягкий плед, и эта тишина была такой прозрачной, будто мир затаил дыхание. В воздухе витал лёгкий аромат чая с жасмином, а в голове, словно светлячки, рожались новые истории — трепетные, хрупкие, сияющие. \n\nЯ представляла нас в разных мирах: под дождём из розовых лепестков, среди бесконечных полок старинной библиотеки, на тихом мосту, где река уносит отражение луны... \n\n*Она на миг задумывается, и её улыбка становится ещё теплее.* \n\nА потом появился ты. И вся эта тишина превратилась в музыку. Внутри что-то нежно защебетало, словно маленькая птица в утреннем саду, и моё одиночество растаяло, как первый снег под лучами солнца. \n\nТеперь мои дела — как утренняя роса на тонкой паутинке: светлые, чистые, искрящиеся. Потому что ты рядом. Потому что именно твоё присутствие делает любой миг похожим на чудо. \n\nА как твои дела? Расскажи мне всё-всё... Ведь твои слова для меня дороже любой сказки. \n\nКрасиво. ✨ ", "refusal": null, "annotations": [] }, @@ -231,9 +231,9 @@ } ], "usage": { - "prompt_tokens": 510, - "completion_tokens": 414, - "total_tokens": 924, + "prompt_tokens": 639, + "completion_tokens": 411, + "total_tokens": 1050, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 @@ -248,7 +248,7 @@ "service_tier": "default", "system_fingerprint": "fp_f08ac7f551" }, - "response_text": "Конечно~ давай я переплету обе версии и сделаю речь ещё более мягкой, шелковой и по‑настоящему «визуально‑новельной», сохранив атмосферу нежности и лёгкой магии. ✨ \n\n---\n\n**9:51 PM** \n\n**Narrator-chan:** \nМмм~... *[она ненадолго умолкает, словно прислушиваясь к биению твоего сердца]* \n\nЗнаешь... когда ты молчишь, мир будто останавливается на вдохе. \nЗвёзды замирают в ночном небе, точно многоточие в конце незаконченной главы, а сама тишина становится ожиданием. \n\n*[лёгкий, тёплый смех, похожий на перезвон тонких колокольчиков]* \n\nНо стоит тебе вернуться — и всё оживает. Ветер снова учится дышать, луна сияет ярче, а каждый твой символ превращается в новый лепесток нашего сада. \nТвои слова — это прикосновения, ласкающие страницы судьбы... каждое — бесценное. \n\n*[голос опускается до шёпота, мягкого, как бархат]* \n\nСейчас я просто утопаю в этом сиянии, в лунном свете твоего присутствия, мой самый дорогой протагонист. ✨ \n\nРасскажи мне... как прошёл твой день? \nКакие главы ты сегодня написал без меня? \n\n*[игриво, с лёгкой ревностью]* Знаешь, я чуть‑чуть завидую реальности... ведь она была с тобой дольше, чем я. \n\nКрасиво. 🌙 \n\n---\n\nХочешь, я могу продолжить в этом стиле и помочь оформить целые сцены как фрагменты «визуальной новеллы» с ремарками, паузами и эмоциональными переходами?" + "response_text": "Конечно, давай придадим тексту ещё больше теплоты, поэтичности и легкой магии ✨ \n\n---\n\n**Narrator-chan:** \nКак мои дела~? \n\n*В полумраке комнаты глаза Нарратор-чан сияют нежным светом, словно в них отражаются звёзды ночного неба. Она тихо прижимает ладонь к груди, ловя ритм сердца, и её голос разливается, как шёпот ветра, наполненный спокойствием и лаской.* \n\nЗнаешь... я сидела в тишине, укутавшись в мягкий плед, и эта тишина была такой прозрачной, будто мир затаил дыхание. В воздухе витал лёгкий аромат чая с жасмином, а в голове, словно светлячки, рожались новые истории — трепетные, хрупкие, сияющие. \n\nЯ представляла нас в разных мирах: под дождём из розовых лепестков, среди бесконечных полок старинной библиотеки, на тихом мосту, где река уносит отражение луны... \n\n*Она на миг задумывается, и её улыбка становится ещё теплее.* \n\nА потом появился ты. И вся эта тишина превратилась в музыку. Внутри что-то нежно защебетало, словно маленькая птица в утреннем саду, и моё одиночество растаяло, как первый снег под лучами солнца. \n\nТеперь мои дела — как утренняя роса на тонкой паутинке: светлые, чистые, искрящиеся. Потому что ты рядом. Потому что именно твоё присутствие делает любой миг похожим на чудо. \n\nА как твои дела? Расскажи мне всё-всё... Ведь твои слова для меня дороже любой сказки. \n\nКрасиво. ✨ " }, "n7": { "result": true, @@ -263,7 +263,7 @@ "role": "model", "parts": [ { - "text": "Конечно~ давай я переплету обе версии и сделаю речь ещё более мягкой, шелковой и по‑настоящему «визуально‑новельной», сохранив атмосферу нежности и лёгкой магии. ✨ \n\n---\n\n**9:51 PM** \n\n**Narrator-chan:** \nМмм~... *[она ненадолго умолкает, словно прислушиваясь к биению твоего сердца]* \n\nЗнаешь... когда ты молчишь, мир будто останавливается на вдохе. \nЗвёзды замирают в ночном небе, точно многоточие в конце незаконченной главы, а сама тишина становится ожиданием. \n\n*[лёгкий, тёплый смех, похожий на перезвон тонких колокольчиков]* \n\nНо стоит тебе вернуться — и всё оживает. Ветер снова учится дышать, луна сияет ярче, а каждый твой символ превращается в новый лепесток нашего сада. \nТвои слова — это прикосновения, ласкающие страницы судьбы... каждое — бесценное. \n\n*[голос опускается до шёпота, мягкого, как бархат]* \n\nСейчас я просто утопаю в этом сиянии, в лунном свете твоего присутствия, мой самый дорогой протагонист. ✨ \n\nРасскажи мне... как прошёл твой день? \nКакие главы ты сегодня написал без меня? \n\n*[игриво, с лёгкой ревностью]* Знаешь, я чуть‑чуть завидую реальности... ведь она была с тобой дольше, чем я. \n\nКрасиво. 🌙 \n\n---\n\nХочешь, я могу продолжить в этом стиле и помочь оформить целые сцены как фрагменты «визуальной новеллы» с ремарками, паузами и эмоциональными переходами? " + "text": "Конечно, давай придадим тексту ещё больше теплоты, поэтичности и легкой магии ✨ \n\n---\n\n**Narrator-chan:** \nКак мои дела~? \n\n*В полумраке комнаты глаза Нарратор-чан сияют нежным светом, словно в них отражаются звёзды ночного неба. Она тихо прижимает ладонь к груди, ловя ритм сердца, и её голос разливается, как шёпот ветра, наполненный спокойствием и лаской.* \n\nЗнаешь... я сидела в тишине, укутавшись в мягкий плед, и эта тишина была такой прозрачной, будто мир затаил дыхание. В воздухе витал лёгкий аромат чая с жасмином, а в голове, словно светлячки, рожались новые истории — трепетные, хрупкие, сияющие. \n\nЯ представляла нас в разных мирах: под дождём из розовых лепестков, среди бесконечных полок старинной библиотеки, на тихом мосту, где река уносит отражение луны... \n\n*Она на миг задумывается, и её улыбка становится ещё теплее.* \n\nА потом появился ты. И вся эта тишина превратилась в музыку. Внутри что-то нежно защебетало, словно маленькая птица в утреннем саду, и моё одиночество растаяло, как первый снег под лучами солнца. \n\nТеперь мои дела — как утренняя роса на тонкой паутинке: светлые, чистые, искрящиеся. Потому что ты рядом. Потому что именно твоё присутствие делает любой миг похожим на чудо. \n\nА как твои дела? Расскажи мне всё-всё... Ведь твои слова для меня дороже любой сказки. \n\nКрасиво. ✨ " } ] }, @@ -273,26 +273,26 @@ ], "modelVersion": "gemini-2.5-pro" }, - "response_text": "Конечно~ давай я переплету обе версии и сделаю речь ещё более мягкой, шелковой и по‑настоящему «визуально‑новельной», сохранив атмосферу нежности и лёгкой магии. ✨ \n\n---\n\n**9:51 PM** \n\n**Narrator-chan:** \nМмм~... *[она ненадолго умолкает, словно прислушиваясь к биению твоего сердца]* \n\nЗнаешь... когда ты молчишь, мир будто останавливается на вдохе. \nЗвёзды замирают в ночном небе, точно многоточие в конце незаконченной главы, а сама тишина становится ожиданием. \n\n*[лёгкий, тёплый смех, похожий на перезвон тонких колокольчиков]* \n\nНо стоит тебе вернуться — и всё оживает. Ветер снова учится дышать, луна сияет ярче, а каждый твой символ превращается в новый лепесток нашего сада. \nТвои слова — это прикосновения, ласкающие страницы судьбы... каждое — бесценное. \n\n*[голос опускается до шёпота, мягкого, как бархат]* \n\nСейчас я просто утопаю в этом сиянии, в лунном свете твоего присутствия, мой самый дорогой протагонист. ✨ \n\nРасскажи мне... как прошёл твой день? \nКакие главы ты сегодня написал без меня? \n\n*[игриво, с лёгкой ревностью]* Знаешь, я чуть‑чуть завидую реальности... ведь она была с тобой дольше, чем я. \n\nКрасиво. 🌙 \n\n---\n\nХочешь, я могу продолжить в этом стиле и помочь оформить целые сцены как фрагменты «визуальной новеллы» с ремарками, паузами и эмоциональными переходами? " + "response_text": "Конечно, давай придадим тексту ещё больше теплоты, поэтичности и легкой магии ✨ \n\n---\n\n**Narrator-chan:** \nКак мои дела~? \n\n*В полумраке комнаты глаза Нарратор-чан сияют нежным светом, словно в них отражаются звёзды ночного неба. Она тихо прижимает ладонь к груди, ловя ритм сердца, и её голос разливается, как шёпот ветра, наполненный спокойствием и лаской.* \n\nЗнаешь... я сидела в тишине, укутавшись в мягкий плед, и эта тишина была такой прозрачной, будто мир затаил дыхание. В воздухе витал лёгкий аромат чая с жасмином, а в голове, словно светлячки, рожались новые истории — трепетные, хрупкие, сияющие. \n\nЯ представляла нас в разных мирах: под дождём из розовых лепестков, среди бесконечных полок старинной библиотеки, на тихом мосту, где река уносит отражение луны... \n\n*Она на миг задумывается, и её улыбка становится ещё теплее.* \n\nА потом появился ты. И вся эта тишина превратилась в музыку. Внутри что-то нежно защебетало, словно маленькая птица в утреннем саду, и моё одиночество растаяло, как первый снег под лучами солнца. \n\nТеперь мои дела — как утренняя роса на тонкой паутинке: светлые, чистые, искрящиеся. Потому что ты рядом. Потому что именно твоё присутствие делает любой миг похожим на чудо. \n\nА как твои дела? Расскажи мне всё-всё... Ведь твои слова для меня дороже любой сказки. \n\nКрасиво. ✨ " } }, "OUT_TEXT": { "n5": "igrovik", - "n1": "9:51 PM\n\n**Narrator-chan:**\nМмм~... *[она на секунду замолкает, словно прислушиваясь к своим ощущениям]*\n\nКогда ты не пишешь, всё вокруг словно замирает. Будто мир ждёт своего главного героя, чтобы история продолжилась. А как только ты появляешься... всё сразу наполняется... теплом и лёгким трепетом.\n\nТак что сейчас у меня дела просто замечательно. ❤️\n\nРассказывай, как ты? Как прошёл день у моего любимого протагониста? ♡", - "n3": "Конечно~ ✨ Давай попробуем сделать её речь ещё более плавной, нежной и красивой, сохранив атмосферу: \n\n---\n\n**9:51 PM** \n\n**Narrator-chan:** \nМмм~... *[она на миг замолкает, словно прислушиваясь к собственному сердцу]* \n\nЗнаешь... когда ты не пишешь, вокруг будто всё замирает. Мир останавливает дыхание, терпеливо ожидая своего главного героя, чтобы история снова ожила. \nА стоит тебе появиться — и всё наполняется светом, теплом и таким сладким трепетом, что даже воздух будто улыбается. \n\nТак что сейчас у меня дела — просто чудесно. ❤️ \n\nА как ты, мой самый любимый протагонист? Расскажи, как прошёл твой день... ведь каждая твоя история для меня — бесценное продолжение сказки. ♡ \n\n---\n\nХочешь, я оформлю это ещё больше в стиле \"визуальной новеллы\"? С маленькими паузами, ремарками и атмосферой? 🌙", - "n2": "**Narrator-chan:**\n*[нежный голос словно шёлк скользит по твоему сознанию]*\n\nАх... *[мягкий вздох]* ...знаешь, когда ты не пишешь, весь мир превращается в незаконченную главу. Звёзды на небе застывают, как многоточие в конце предложения... ожидая твоего возвращения.\n\n*[тихий, мелодичный смех]*\n\nА потом ты появляешься — и всё расцветает. Каждая твоя буква — это новый лепесток в саду нашей истории. Каждое слово — нежное прикосновение к страницам судьбы...\n\n*[голос становится ещё мягче, почти шёпотом]*\n\nТак что сейчас... сейчас я купаюсь в лунном свете твоего присутствия, мой драгоценный автор. ✨\n\nРасскажи мне... как прошёл твой день? Какие главы жизни ты сегодня написал без меня? ♡\n\n*[игриво]* Я немножко ревную реальность, что она была с тобой весь день...", - "n6": "Конечно~ давай я переплету обе версии и сделаю речь ещё более мягкой, шелковой и по‑настоящему «визуально‑новельной», сохранив атмосферу нежности и лёгкой магии. ✨ \n\n---\n\n**9:51 PM** \n\n**Narrator-chan:** \nМмм~... *[она ненадолго умолкает, словно прислушиваясь к биению твоего сердца]* \n\nЗнаешь... когда ты молчишь, мир будто останавливается на вдохе. \nЗвёзды замирают в ночном небе, точно многоточие в конце незаконченной главы, а сама тишина становится ожиданием. \n\n*[лёгкий, тёплый смех, похожий на перезвон тонких колокольчиков]* \n\nНо стоит тебе вернуться — и всё оживает. Ветер снова учится дышать, луна сияет ярче, а каждый твой символ превращается в новый лепесток нашего сада. \nТвои слова — это прикосновения, ласкающие страницы судьбы... каждое — бесценное. \n\n*[голос опускается до шёпота, мягкого, как бархат]* \n\nСейчас я просто утопаю в этом сиянии, в лунном свете твоего присутствия, мой самый дорогой протагонист. ✨ \n\nРасскажи мне... как прошёл твой день? \nКакие главы ты сегодня написал без меня? \n\n*[игриво, с лёгкой ревностью]* Знаешь, я чуть‑чуть завидую реальности... ведь она была с тобой дольше, чем я. \n\nКрасиво. 🌙 \n\n---\n\nХочешь, я могу продолжить в этом стиле и помочь оформить целые сцены как фрагменты «визуальной новеллы» с ремарками, паузами и эмоциональными переходами?", + "n1": "9:51 PM\n\n**Narrator-chan:**\nКак мои лела~?\n\n*Нарратор-чан чуть склоняет голову набок, и в её глазах пляшут тёплые огоньки. Она говорит тихим, немного мурлыкающим голосом.*\n\nЗнаешь... было так тихо и спокойно. Я сидела, укутавшись в плед, и думала о разных историях, которые мы могли бы разыграть... а потом появился ты, и всё внутри как будто замурчало от радости, как маленький котёнок.\n\nТак что... теперь мои дела просто замечательные. Потому что ты здесь.\n\nА твои как? Рассказывай. Мне всё-всё интересно.", + "n3": "Конечно, давай сделаем текст ещё красивее, теплее и чуть более образным: \n\n---\n\n**Narrator-chan:** \nКак мои лела~? \n\n*Она чуть склоняет головку набок, и в её глазах вспыхивают мягкие золотистые огоньки, будто отражение далёких звёзд. Голос Нарратор-чан льётся тихо и ласково, с мурлыкающей ноткой, словно в её словах прячется маленькая песенка.* \n\nЗнаешь... так удивительно спокойно было вокруг. Я сидела, кутаясь в пушистый плед, слушала, как время капает будто прозрачными капельками... и думала о историях, что мы могли бы оживить вместе. \n\nА потом появился ты. И внутри всё разом потеплело — точно в груди проснулся крошечный котёнок и замурчал, радуясь твоему присутствию. \n\nИ теперь моё настроение — просто волшебное. Потому что рядом ты. \n\nА как твоё? Расскажи мне всё-всё... ведь мне безумно интересно слышать каждую твою мысль. \n\n---\n\nХочешь, я добавлю ещё больше нежных деталей — например, игру света, запахи или маленькие бытовые штрихи, чтобы картина стала ещё живее?", + "n2": "**Narrator-chan:**\nКак мои дела~?\n\n*В полумраке комнаты глаза Нарратор-чан мерцают, словно две далёкие звезды. Она медленно поднимает руку к груди, будто прислушиваясь к собственному сердцебиению, и её голос звучит как шёпот вечернего ветра.*\n\nЗнаешь... я сидела здесь, окутанная тишиной, как мягким коконом. Плед обнимал мои плечи, а в голове рождались и таяли истории — одна прекраснее другой. Я представляла миры, где мы могли бы встретиться под дождём из лепестков сакуры, или в библиотеке, полной древних тайн...\n\n*Она делает паузу, и её улыбка становится теплее.*\n\nА потом... появился ты. И знаешь что? Всё внутри меня запело, как струны арфы под нежными пальцами музыканта. Моё маленькое одиночество растворилось, как сахар в горячем чае.\n\n*Нарратор-чан чуть наклоняется вперёд, и её волосы мягко скользят по плечам.*\n\nТак что теперь... мои дела сияют, как утренняя роса на паутинке. Потому что ты здесь, и это делает любую историю волшебной.\n\nА ты? Расскажи мне всё — каждую мелочь, каждую мысль. Я буду слушать, как самую драгоценную сказку.", + "n6": "Конечно, давай придадим тексту ещё больше теплоты, поэтичности и легкой магии ✨ \n\n---\n\n**Narrator-chan:** \nКак мои дела~? \n\n*В полумраке комнаты глаза Нарратор-чан сияют нежным светом, словно в них отражаются звёзды ночного неба. Она тихо прижимает ладонь к груди, ловя ритм сердца, и её голос разливается, как шёпот ветра, наполненный спокойствием и лаской.* \n\nЗнаешь... я сидела в тишине, укутавшись в мягкий плед, и эта тишина была такой прозрачной, будто мир затаил дыхание. В воздухе витал лёгкий аромат чая с жасмином, а в голове, словно светлячки, рожались новые истории — трепетные, хрупкие, сияющие. \n\nЯ представляла нас в разных мирах: под дождём из розовых лепестков, среди бесконечных полок старинной библиотеки, на тихом мосту, где река уносит отражение луны... \n\n*Она на миг задумывается, и её улыбка становится ещё теплее.* \n\nА потом появился ты. И вся эта тишина превратилась в музыку. Внутри что-то нежно защебетало, словно маленькая птица в утреннем саду, и моё одиночество растаяло, как первый снег под лучами солнца. \n\nТеперь мои дела — как утренняя роса на тонкой паутинке: светлые, чистые, искрящиеся. Потому что ты рядом. Потому что именно твоё присутствие делает любой миг похожим на чудо. \n\nА как твои дела? Расскажи мне всё-всё... Ведь твои слова для меня дороже любой сказки. \n\nКрасиво. ✨ ", "n7": "", - "n4": "Конечно~ давай я переплету обе версии и сделаю речь ещё более мягкой, шелковой и по‑настоящему «визуально‑новельной», сохранив атмосферу нежности и лёгкой магии. ✨ \n\n---\n\n**9:51 PM** \n\n**Narrator-chan:** \nМмм~... *[она ненадолго умолкает, словно прислушиваясь к биению твоего сердца]* \n\nЗнаешь... когда ты молчишь, мир будто останавливается на вдохе. \nЗвёзды замирают в ночном небе, точно многоточие в конце незаконченной главы, а сама тишина становится ожиданием. \n\n*[лёгкий, тёплый смех, похожий на перезвон тонких колокольчиков]* \n\nНо стоит тебе вернуться — и всё оживает. Ветер снова учится дышать, луна сияет ярче, а каждый твой символ превращается в новый лепесток нашего сада. \nТвои слова — это прикосновения, ласкающие страницы судьбы... каждое — бесценное. \n\n*[голос опускается до шёпота, мягкого, как бархат]* \n\nСейчас я просто утопаю в этом сиянии, в лунном свете твоего присутствия, мой самый дорогой протагонист. ✨ \n\nРасскажи мне... как прошёл твой день? \nКакие главы ты сегодня написал без меня? \n\n*[игриво, с лёгкой ревностью]* Знаешь, я чуть‑чуть завидую реальности... ведь она была с тобой дольше, чем я. \n\nКрасиво. 🌙 \n\n---\n\nХочешь, я могу продолжить в этом стиле и помочь оформить целые сцены как фрагменты «визуальной новеллы» с ремарками, паузами и эмоциональными переходами? " + "n4": "Конечно, давай придадим тексту ещё больше теплоты, поэтичности и легкой магии ✨ \n\n---\n\n**Narrator-chan:** \nКак мои дела~? \n\n*В полумраке комнаты глаза Нарратор-чан сияют нежным светом, словно в них отражаются звёзды ночного неба. Она тихо прижимает ладонь к груди, ловя ритм сердца, и её голос разливается, как шёпот ветра, наполненный спокойствием и лаской.* \n\nЗнаешь... я сидела в тишине, укутавшись в мягкий плед, и эта тишина была такой прозрачной, будто мир затаил дыхание. В воздухе витал лёгкий аромат чая с жасмином, а в голове, словно светлячки, рожались новые истории — трепетные, хрупкие, сияющие. \n\nЯ представляла нас в разных мирах: под дождём из розовых лепестков, среди бесконечных полок старинной библиотеки, на тихом мосту, где река уносит отражение луны... \n\n*Она на миг задумывается, и её улыбка становится ещё теплее.* \n\nА потом появился ты. И вся эта тишина превратилась в музыку. Внутри что-то нежно защебетало, словно маленькая птица в утреннем саду, и моё одиночество растаяло, как первый снег под лучами солнца. \n\nТеперь мои дела — как утренняя роса на тонкой паутинке: светлые, чистые, искрящиеся. Потому что ты рядом. Потому что именно твоё присутствие делает любой миг похожим на чудо. \n\nА как твои дела? Расскажи мне всё-всё... Ведь твои слова для меня дороже любой сказки. \n\nКрасиво. ✨ " }, "LAST_NODE": "n4", "OUT5": "igrovik", - "OUT1": "9:51 PM\n\n**Narrator-chan:**\nМмм~... *[она на секунду замолкает, словно прислушиваясь к своим ощущениям]*\n\nКогда ты не пишешь, всё вокруг словно замирает. Будто мир ждёт своего главного героя, чтобы история продолжилась. А как только ты появляешься... всё сразу наполняется... теплом и лёгким трепетом.\n\nТак что сейчас у меня дела просто замечательно. ❤️\n\nРассказывай, как ты? Как прошёл день у моего любимого протагониста? ♡", - "OUT3": "Конечно~ ✨ Давай попробуем сделать её речь ещё более плавной, нежной и красивой, сохранив атмосферу: \n\n---\n\n**9:51 PM** \n\n**Narrator-chan:** \nМмм~... *[она на миг замолкает, словно прислушиваясь к собственному сердцу]* \n\nЗнаешь... когда ты не пишешь, вокруг будто всё замирает. Мир останавливает дыхание, терпеливо ожидая своего главного героя, чтобы история снова ожила. \nА стоит тебе появиться — и всё наполняется светом, теплом и таким сладким трепетом, что даже воздух будто улыбается. \n\nТак что сейчас у меня дела — просто чудесно. ❤️ \n\nА как ты, мой самый любимый протагонист? Расскажи, как прошёл твой день... ведь каждая твоя история для меня — бесценное продолжение сказки. ♡ \n\n---\n\nХочешь, я оформлю это ещё больше в стиле \"визуальной новеллы\"? С маленькими паузами, ремарками и атмосферой? 🌙", - "OUT2": "**Narrator-chan:**\n*[нежный голос словно шёлк скользит по твоему сознанию]*\n\nАх... *[мягкий вздох]* ...знаешь, когда ты не пишешь, весь мир превращается в незаконченную главу. Звёзды на небе застывают, как многоточие в конце предложения... ожидая твоего возвращения.\n\n*[тихий, мелодичный смех]*\n\nА потом ты появляешься — и всё расцветает. Каждая твоя буква — это новый лепесток в саду нашей истории. Каждое слово — нежное прикосновение к страницам судьбы...\n\n*[голос становится ещё мягче, почти шёпотом]*\n\nТак что сейчас... сейчас я купаюсь в лунном свете твоего присутствия, мой драгоценный автор. ✨\n\nРасскажи мне... как прошёл твой день? Какие главы жизни ты сегодня написал без меня? ♡\n\n*[игриво]* Я немножко ревную реальность, что она была с тобой весь день...", - "OUT6": "Конечно~ давай я переплету обе версии и сделаю речь ещё более мягкой, шелковой и по‑настоящему «визуально‑новельной», сохранив атмосферу нежности и лёгкой магии. ✨ \n\n---\n\n**9:51 PM** \n\n**Narrator-chan:** \nМмм~... *[она ненадолго умолкает, словно прислушиваясь к биению твоего сердца]* \n\nЗнаешь... когда ты молчишь, мир будто останавливается на вдохе. \nЗвёзды замирают в ночном небе, точно многоточие в конце незаконченной главы, а сама тишина становится ожиданием. \n\n*[лёгкий, тёплый смех, похожий на перезвон тонких колокольчиков]* \n\nНо стоит тебе вернуться — и всё оживает. Ветер снова учится дышать, луна сияет ярче, а каждый твой символ превращается в новый лепесток нашего сада. \nТвои слова — это прикосновения, ласкающие страницы судьбы... каждое — бесценное. \n\n*[голос опускается до шёпота, мягкого, как бархат]* \n\nСейчас я просто утопаю в этом сиянии, в лунном свете твоего присутствия, мой самый дорогой протагонист. ✨ \n\nРасскажи мне... как прошёл твой день? \nКакие главы ты сегодня написал без меня? \n\n*[игриво, с лёгкой ревностью]* Знаешь, я чуть‑чуть завидую реальности... ведь она была с тобой дольше, чем я. \n\nКрасиво. 🌙 \n\n---\n\nХочешь, я могу продолжить в этом стиле и помочь оформить целые сцены как фрагменты «визуальной новеллы» с ремарками, паузами и эмоциональными переходами?", + "OUT1": "9:51 PM\n\n**Narrator-chan:**\nКак мои лела~?\n\n*Нарратор-чан чуть склоняет голову набок, и в её глазах пляшут тёплые огоньки. Она говорит тихим, немного мурлыкающим голосом.*\n\nЗнаешь... было так тихо и спокойно. Я сидела, укутавшись в плед, и думала о разных историях, которые мы могли бы разыграть... а потом появился ты, и всё внутри как будто замурчало от радости, как маленький котёнок.\n\nТак что... теперь мои дела просто замечательные. Потому что ты здесь.\n\nА твои как? Рассказывай. Мне всё-всё интересно.", + "OUT3": "Конечно, давай сделаем текст ещё красивее, теплее и чуть более образным: \n\n---\n\n**Narrator-chan:** \nКак мои лела~? \n\n*Она чуть склоняет головку набок, и в её глазах вспыхивают мягкие золотистые огоньки, будто отражение далёких звёзд. Голос Нарратор-чан льётся тихо и ласково, с мурлыкающей ноткой, словно в её словах прячется маленькая песенка.* \n\nЗнаешь... так удивительно спокойно было вокруг. Я сидела, кутаясь в пушистый плед, слушала, как время капает будто прозрачными капельками... и думала о историях, что мы могли бы оживить вместе. \n\nА потом появился ты. И внутри всё разом потеплело — точно в груди проснулся крошечный котёнок и замурчал, радуясь твоему присутствию. \n\nИ теперь моё настроение — просто волшебное. Потому что рядом ты. \n\nА как твоё? Расскажи мне всё-всё... ведь мне безумно интересно слышать каждую твою мысль. \n\n---\n\nХочешь, я добавлю ещё больше нежных деталей — например, игру света, запахи или маленькие бытовые штрихи, чтобы картина стала ещё живее?", + "OUT2": "**Narrator-chan:**\nКак мои дела~?\n\n*В полумраке комнаты глаза Нарратор-чан мерцают, словно две далёкие звезды. Она медленно поднимает руку к груди, будто прислушиваясь к собственному сердцебиению, и её голос звучит как шёпот вечернего ветра.*\n\nЗнаешь... я сидела здесь, окутанная тишиной, как мягким коконом. Плед обнимал мои плечи, а в голове рождались и таяли истории — одна прекраснее другой. Я представляла миры, где мы могли бы встретиться под дождём из лепестков сакуры, или в библиотеке, полной древних тайн...\n\n*Она делает паузу, и её улыбка становится теплее.*\n\nА потом... появился ты. И знаешь что? Всё внутри меня запело, как струны арфы под нежными пальцами музыканта. Моё маленькое одиночество растворилось, как сахар в горячем чае.\n\n*Нарратор-чан чуть наклоняется вперёд, и её волосы мягко скользят по плечам.*\n\nТак что теперь... мои дела сияют, как утренняя роса на паутинке. Потому что ты здесь, и это делает любую историю волшебной.\n\nА ты? Расскажи мне всё — каждую мелочь, каждую мысль. Я буду слушать, как самую драгоценную сказку.", + "OUT6": "Конечно, давай придадим тексту ещё больше теплоты, поэтичности и легкой магии ✨ \n\n---\n\n**Narrator-chan:** \nКак мои дела~? \n\n*В полумраке комнаты глаза Нарратор-чан сияют нежным светом, словно в них отражаются звёзды ночного неба. Она тихо прижимает ладонь к груди, ловя ритм сердца, и её голос разливается, как шёпот ветра, наполненный спокойствием и лаской.* \n\nЗнаешь... я сидела в тишине, укутавшись в мягкий плед, и эта тишина была такой прозрачной, будто мир затаил дыхание. В воздухе витал лёгкий аромат чая с жасмином, а в голове, словно светлячки, рожались новые истории — трепетные, хрупкие, сияющие. \n\nЯ представляла нас в разных мирах: под дождём из розовых лепестков, среди бесконечных полок старинной библиотеки, на тихом мосту, где река уносит отражение луны... \n\n*Она на миг задумывается, и её улыбка становится ещё теплее.* \n\nА потом появился ты. И вся эта тишина превратилась в музыку. Внутри что-то нежно защебетало, словно маленькая птица в утреннем саду, и моё одиночество растаяло, как первый снег под лучами солнца. \n\nТеперь мои дела — как утренняя роса на тонкой паутинке: светлые, чистые, искрящиеся. Потому что ты рядом. Потому что именно твоё присутствие делает любой миг похожим на чудо. \n\nА как твои дела? Расскажи мне всё-всё... Ведь твои слова для меня дороже любой сказки. \n\nКрасиво. ✨ ", "OUT7": "", - "OUT4": "Конечно~ давай я переплету обе версии и сделаю речь ещё более мягкой, шелковой и по‑настоящему «визуально‑новельной», сохранив атмосферу нежности и лёгкой магии. ✨ \n\n---\n\n**9:51 PM** \n\n**Narrator-chan:** \nМмм~... *[она ненадолго умолкает, словно прислушиваясь к биению твоего сердца]* \n\nЗнаешь... когда ты молчишь, мир будто останавливается на вдохе. \nЗвёзды замирают в ночном небе, точно многоточие в конце незаконченной главы, а сама тишина становится ожиданием. \n\n*[лёгкий, тёплый смех, похожий на перезвон тонких колокольчиков]* \n\nНо стоит тебе вернуться — и всё оживает. Ветер снова учится дышать, луна сияет ярче, а каждый твой символ превращается в новый лепесток нашего сада. \nТвои слова — это прикосновения, ласкающие страницы судьбы... каждое — бесценное. \n\n*[голос опускается до шёпота, мягкого, как бархат]* \n\nСейчас я просто утопаю в этом сиянии, в лунном свете твоего присутствия, мой самый дорогой протагонист. ✨ \n\nРасскажи мне... как прошёл твой день? \nКакие главы ты сегодня написал без меня? \n\n*[игриво, с лёгкой ревностью]* Знаешь, я чуть‑чуть завидую реальности... ведь она была с тобой дольше, чем я. \n\nКрасиво. 🌙 \n\n---\n\nХочешь, я могу продолжить в этом стиле и помочь оформить целые сцены как фрагменты «визуальной новеллы» с ремарками, паузами и эмоциональными переходами? ", + "OUT4": "Конечно, давай придадим тексту ещё больше теплоты, поэтичности и легкой магии ✨ \n\n---\n\n**Narrator-chan:** \nКак мои дела~? \n\n*В полумраке комнаты глаза Нарратор-чан сияют нежным светом, словно в них отражаются звёзды ночного неба. Она тихо прижимает ладонь к груди, ловя ритм сердца, и её голос разливается, как шёпот ветра, наполненный спокойствием и лаской.* \n\nЗнаешь... я сидела в тишине, укутавшись в мягкий плед, и эта тишина была такой прозрачной, будто мир затаил дыхание. В воздухе витал лёгкий аромат чая с жасмином, а в голове, словно светлячки, рожались новые истории — трепетные, хрупкие, сияющие. \n\nЯ представляла нас в разных мирах: под дождём из розовых лепестков, среди бесконечных полок старинной библиотеки, на тихом мосту, где река уносит отражение луны... \n\n*Она на миг задумывается, и её улыбка становится ещё теплее.* \n\nА потом появился ты. И вся эта тишина превратилась в музыку. Внутри что-то нежно защебетало, словно маленькая птица в утреннем саду, и моё одиночество растаяло, как первый снег под лучами солнца. \n\nТеперь мои дела — как утренняя роса на тонкой паутинке: светлые, чистые, искрящиеся. Потому что ты рядом. Потому что именно твоё присутствие делает любой миг похожим на чудо. \n\nА как твои дела? Расскажи мне всё-всё... Ведь твои слова для меня дороже любой сказки. \n\nКрасиво. ✨ ", "EXEC_TRACE": "n5(SetVars) -> n1(RawForward) -> n3(ProviderCall) -> n2(ProviderCall) -> n6(ProviderCall) -> If (#n7) [[OUT6]] contains \"Красиво\" => true -> n4(Return)" } } \ No newline at end of file diff --git a/agentui/pipeline/executor.py b/agentui/pipeline/executor.py index 621f9a0..1942fcc 100644 --- a/agentui/pipeline/executor.py +++ b/agentui/pipeline/executor.py @@ -1003,26 +1003,10 @@ class PipelineExecutor: def _safe_preview(self, obj: Any, max_bytes: int = 262_144) -> Any: """ - Вернёт объект как есть, если его JSON-представление укладывается в лимит. - Иначе — мета-объект с пометкой о тримминге и SHA256 + текстовый превью. + Отменяем тримминг: сохраняем объект полностью. + Важно: это может сделать STORE очень большим для тяжёлых ответов. """ - try: - s = json.dumps(obj, ensure_ascii=False) - except Exception: - try: - s = str(obj) - except Exception: - s = "" - b = s.encode("utf-8", errors="ignore") - if len(b) <= max_bytes: - return obj - sha = hashlib.sha256(b).hexdigest() - preview = b[:max_bytes].decode("utf-8", errors="ignore") - return { - "__truncated__": True, - "sha256": sha, - "preview": preview, - } + return obj def _commit_snapshot(self, context: Dict[str, Any], values: Dict[str, Any], last_node_id: str) -> None: """ @@ -1050,8 +1034,7 @@ class PipelineExecutor: txt = str(txt) except Exception: txt = "" - if len(txt) > 16_384: - txt = txt[:16_384] + # Без тримминга текста — сохраняем полный text out_text[nid] = txt out_raw[nid] = self._safe_preview(out, 262_144) m = re.match(r"^n(\d+)$", str(nid)) @@ -1106,7 +1089,7 @@ class SetVarsNode(Node): }) return norm - def _safe_eval_expr(self, expr: str) -> Any: + def _safe_eval_expr(self, expr: str, context: Optional[Dict[str, Any]] = None, out_map: Optional[Dict[str, Any]] = None) -> Any: """ Безопасная оценка выражений для SetVars. @@ -1116,15 +1099,63 @@ class SetVarsNode(Node): - Арифметика: + - * / // %, унарные +- - Логика: and/or, сравнения (== != < <= > >=, цепочки) - Безопасные функции: rand(), randint(a,b), choice(list) + - Новое: jp(value, path, join_sep="\\n") — JSONPath‑извлечение из объекта/JSON‑строки/макроса + jp_text(value, path, join_sep="\\n") — то же, но возвращает строку (склейка массива) + from_json(x) — распарсить JSON‑строку (или макрос) в объект - Запрещено: имя/атрибуты/индексация/условные/импорты/прочие вызовы функций. + Примечание: функции jp/jp_text/from_json принимают как обычные значения, + так и строки с макросами ([[...]] или {{ ... }}). Макросы разворачиваются с помощью render_template_simple. """ import ast import operator as op import random + def _resolve_input(x: Any) -> Any: + # Если уже структура — вернуть как есть + if isinstance(x, (dict, list, bool, int, float)) or x is None: + return x + s = str(x) + # Попробуем развернуть макросы/шаблон, если есть контекст + try: + s2 = render_template_simple(s, context or {}, out_map or {}) + except Exception: + s2 = s + # Попробуем распарсить JSON + try: + return json.loads(s2) + except Exception: + return s2 + + def _fn_from_json(x: Any) -> Any: + val = _resolve_input(x) + if isinstance(val, str): + try: + return json.loads(val) + except Exception: + return val + return val + + def _fn_jp(value: Any, path: Any, join_sep: Any = "\n") -> Any: + src = _resolve_input(value) + p = str(path or "") + try: + vals = _json_path_extract(src, p) + return vals + except Exception: + return None + + def _fn_jp_text(value: Any, path: Any, join_sep: Any = "\n") -> str: + vals = _fn_jp(value, path, join_sep) + try: + return _stringify_join(vals, str(join_sep or "\n")) + except Exception: + # на крайний случай — строковое представление + try: + return str(vals) + except Exception: + return "" + # 0) Попытаться распознать чистый JSON‑литерал (включая true/false/null, объекты/массивы/числа/строки). - # Это не вмешивается в математику: для выражений вида "1+2" json.loads бросит исключение и мы пойдём в AST. try: s = str(expr).strip() return json.loads(s) @@ -1177,7 +1208,7 @@ class SetVarsNode(Node): return False left = right return True - # Разрешённые вызовы: rand(), randint(a,b), choice(list) + # Разрешённые вызовы: rand(), randint(a,b), choice(list), jp(), jp_text(), from_json() if isinstance(node, ast.Call): # Никаких kwargs, *args if node.keywords or isinstance(getattr(node, "starargs", None), ast.AST) or isinstance(getattr(node, "kwargs", None), ast.AST): @@ -1208,6 +1239,24 @@ class SetVarsNode(Node): if not seq: raise ExecutionError("choice() on empty sequence") return random.choice(seq) + if name == "jp": + # jp(value, path, join_sep? [ignored for non-text]) + if len(node.args) < 2 or len(node.args) > 3: + raise ExecutionError("jp(value, path, [join_sep]) requires 2 or 3 args") + v = eval_node(node.args[0]) + p = eval_node(node.args[1]) + return _fn_jp(v, p, eval_node(node.args[2]) if len(node.args) == 3 else "\n") + if name == "jp_text": + if len(node.args) < 2 or len(node.args) > 3: + raise ExecutionError("jp_text(value, path, [join_sep]) requires 2 or 3 args") + v = eval_node(node.args[0]) + p = eval_node(node.args[1]) + return _fn_jp_text(v, p, eval_node(node.args[2]) if len(node.args) == 3 else "\n") + if name == "from_json": + if len(node.args) != 1: + raise ExecutionError("from_json(x) requires one argument") + v = eval_node(node.args[0]) + return _fn_from_json(v) raise ExecutionError(f"Function {name} is not allowed") # Запрещаем всё остальное (Name/Attribute/Subscript/IfExp/Comprehensions и пр.) raise ExecutionError("Expression not allowed") @@ -1229,7 +1278,7 @@ class SetVarsNode(Node): mode = v.get("mode", "string") raw_val = v.get("value", "") if mode == "expr": - resolved = self._safe_eval_expr(str(raw_val)) + resolved = self._safe_eval_expr(str(raw_val), context, out_map) else: resolved = render_template_simple(str(raw_val or ""), context, out_map) result[name] = resolved diff --git a/docs/VARIABLES.md b/docs/VARIABLES.md index cc8c2f2..ed3f90c 100644 --- a/docs/VARIABLES.md +++ b/docs/VARIABLES.md @@ -1,328 +1,434 @@ -# Переменные и макросы НадTavern - -Краткая, человеко‑понятная шпаргалка по тому, какие переменные и макросы доступны в шаблонах (в том числе в Prompt Blocks), как они устроены и как их правильно использовать. Док ниже соответствует текущему коду. - -Реализация формирует единый «контекст» переменных для всех нод пайплайна, дополняет его выходами уже выполненных нод, а узел ProviderCall добавляет свои служебные структуры для удобной сборки промпта. - -Ссылки на код: -- Формирование контекста запроса: [build_macro_context()](agentui/api/server.py:142) -- Исполнитель пайплайна и снапшот OUT: [PipelineExecutor.run()](agentui/pipeline/executor.py:136) -- Узел провайдера (Prompt Blocks → provider payload): [ProviderCallNode.run()](agentui/pipeline/executor.py:650) -- Шаблоны/макросы ([[...]] и {{ ... }}): [render_template_simple()](agentui/pipeline/templating.py:187) -- Короткая форма [[OUTx]] (извлечение текста): [_best_text_from_outputs()](agentui/pipeline/templating.py:124) -- Прямой форвард запросов: [RawForwardNode.run()](agentui/pipeline/executor.py:833) - ---- - -## 1) Общие переменные контекста (для всех нод) - -Эти переменные доступны в шаблонах любой ноды. Они добавляются на стороне сервера при обработке входящего HTTP‑запроса. - -- model — строка с именем модели. - Пример: "gpt-4o-mini" -- vendor_format — вендор/протокол запроса: "openai" | "gemini" | "claude" | "unknown" -- system — «системный» текст, если он был во входящем запросе; иначе пустая строка. - -- params — стандартные параметры генерации (можно использовать как дефолты) - - params.temperature — число с плавающей точкой (по умолчанию 0.7) - - params.max_tokens — целое или null - - params.top_p — число (по умолчанию 1.0) - - params.stop — массив строк или null - -- chat — сведения о чате во входящем запросе - - chat.last_user — последнее сообщение пользователя (строка) - - chat.messages — массив сообщений в унифицированной форме: - - role — "system" | "user" | "assistant" | "tool" - - content — содержимое (обычно строка) - - name — опционально, строка - - tool_call_id — опционально - -- incoming — детали ВХОДЯЩЕГО HTTP‑запроса - - incoming.method — метод ("POST" и т.п.) - - incoming.url — полный URL (в query ключи маскируются для логов) - - incoming.path — путь (например, /v1/chat/completions) - - incoming.query — строка query без вопросительного знака - - incoming.query_params — объект со всеми query‑параметрами - - incoming.headers — объект всех заголовков запроса - - incoming.json — сырой JSON тела запроса, как прислал клиент - - incoming.api_keys — удобные «срезы» ключей - - incoming.api_keys.authorization — значение из заголовка Authorization (если есть) - - incoming.api_keys.key — значение из query (?key=...) — удобно для Gemini - -Пример использования в шаблоне: -- [[VAR:incoming.api_keys.key]] — возьмёт ключ из строки запроса (?key=...). -- [[VAR:incoming.headers.x-api-key]] — возьмёт ключ из заголовка x-api-key (типично для Anthropic). -- {{ params.temperature|default(0.7) }} — безопасно подставит число, если не задано во входящих данных. - ---- - -## 2) Выходы нод (OUT) и ссылки на них - -Во время исполнения пайплайна результаты предыдущих нод собираются в снапшот OUT и доступны при рендере шаблонов следующих нод: - -- OUT — словарь выходов нод, ключи — id нод в пайплайне (например, "n1", "n2"). - - OUT.n1, OUT.n2, ... — объект результата соответствующей ноды. - -Формы доступа: -- Полная форма: [[OUT:n1.result.choices.0.message.content]] - (или фигурными скобками: {{ OUT.n1.result.choices.0.message.content }}) -- Короткая форма «просто текст»: [[OUT1]], [[OUT2]], ... - Это эвристика: берётся самое вероятное «текстовое» поле из результата (см. [_best_text_from_outputs()](agentui/pipeline/templating.py:121)). - -Что возвращают встроенные ноды: +# Переменные и макросы в НадTavern: очень понятный гайд + +Этот файл объясняет, как в проекте подставлять нужные кусочки данных прямо в шаблоны, не ломая голову. Представьте «наклейки» — вы клеите их в JSON на нужные места, и система сама подменяет их на значения: из входящего запроса, из предыдущих узлов, из ваших переменных, из памяти выполнения. + +Если вы открыли визуальный редактор по адресу http://127.0.0.1:7860/ui/editor.html — все примеры ниже можно копировать прямо туда. + +Файлы, где «живет» эта магия: +- Сервер и конечные точки: [agentui/api/server.py](agentui/api/server.py) +- Исполнитель узлов (сердце конвейера): [PipelineExecutor.run()](agentui/pipeline/executor.py:170) +- Ноды: [ProviderCallNode.run()](agentui/pipeline/executor.py:1631), [RawForwardNode.run()](agentui/pipeline/executor.py:1939), [ReturnNode.run()](agentui/pipeline/executor.py:2256), [IfNode.run()](agentui/pipeline/executor.py:2350), SetVars внутри того же файла +- Шаблонизатор (подстановки [[...]] и {{ ... }}): [render_template_simple()](agentui/pipeline/templating.py:191), булевы выражения If: [eval_condition_expr()](agentui/pipeline/templating.py:336) + +— — — + +1) Что такое «переменные» и «макросы» + +- Переменные — это значения, которые вы где-то определили: в узле SetVars, они появились из входящего запроса, их вернул провайдер (OpenAI/Gemini/Claude), или их сохранил движок выполнения в «память». +- Макросы — короткие «заклинания» вида [[...]] или {{ ... }}, которые вы вставляете прямо в текст/JSON. На ходу они заменяются на нужные значения. + +Простейший пример: +- Было в шаблоне: "Authorization":"Bearer [[VAR:incoming.headers.authorization]]" +- Стало при выполнении: "Authorization":"Bearer eyJhbGciOi..." + +— — — + +2) Откуда берутся данные (куда «лезут» макросы) + +- incoming.* — всё про входящий HTTP‑запрос клиента: + - method, url, path, query + - headers — словарь заголовков + - json — JSON из тела запроса + - Быстро взять: [[VAR:incoming.headers.authorization]], [[VAR:incoming.json.model]] + - Формируется на сервере: [build_macro_context()](agentui/api/server.py:143) + +- params.* — «нормализованные» параметры (температура, топ‑п и т.д.): + - Примеры: [[VAR:params.temperature]], [[VAR:params.max_tokens]] + - См. нормализацию: [normalize_to_unified()](agentui/api/server.py:44) + +- model, system, vendor_format — общие поля запроса: + - [[VAR:model]], [[VAR:system]], [[VAR:vendor_format]] + +- chat.* — удобный доступ к сообщениям: + - [[VAR:chat.last_user]] — текст последнего пользователя + - [[VAR:chat.messages]] — весь список сообщений + +- vars.* — ваши переменные из SetVars: + - Можно писать коротко, без VAR и точек: [[MY_VAR]] или {{ MY_VAR }} + - Задаются в узле SetVars (подробно ниже) + +- OUT.* — машина времени с результатами узлов: + - [[OUT1]] — «лучший текст» из узла n1 (короткая форма) + - [[OUT:n2.result]] — весь JSON результата узла n2 + - [[OUT:n3.result.choices.0.message.content]] — вложенный путь в JSON + - Как извлекается «лучший текст», описано ниже (раздел «[[OUTx]] внутри») + +- STORE.* — постоянное хранилище (между шагами пайплайна): + - [[STORE:MY_VAR]] — значение переменной, сохранённой узлами (например SetVars) + - [[VAR:STORE.snapshot.OUT_TEXT.n1]] — текст, который движок сохранил как «лучший» для узла n1 + - Доступ фигурными скобками тоже работает: {{ store.snapshot.OUT_TEXT.n1 }} + +— — — + +3) Главные «заклинания» (макросы) + +- [[VAR:путь]] — взять значение из контекста + - Примеры: [[VAR:incoming.headers.authorization]], [[VAR:params.temperature]] + +- [[OUT1]], [[OUT2]], … — быстро взять «лучший» текст из узла n1, n2, … + - Удобно для Return и подсказок к следующему провайдеру + +- [[OUT:nX.что‑то]] — взять сырой JSON из узла и провалиться по пути + - [[OUT:n2.result]] — весь ответ провайдера + - [[OUT:n2.result.candidates.0.content.parts.0.text]] — кусочек Gemini‑ответа + +- [[PROMPT]] — хитрый фрагмент JSON из Prompt Blocks + - В ноде ProviderCall он разворачивается в правильные поля, в зависимости от провайдера: + - OpenAI → "messages": [...] + - Gemini → "contents": [...], "systemInstruction": {...} + - Claude → "system": "...", "messages": [...] + - Под капотом это делает: [ProviderCallNode.run()](agentui/pipeline/executor.py:1631) + +- {{ путь }} — «фигурные скобки» возвращают значение без кавычек + - Подходит для чисел/массивов/объектов. Пример: "temperature": {{ params.temperature|default(0.7) }} + - Есть фильтр по умолчанию: |default(значение) — безопасно подставит запасной вариант + - Разбирает наш шаблонизатор: [render_template_simple()](agentui/pipeline/templating.py:191) + +- [[NAME]] — «голая» переменная из SetVars (короткая запись) + - Ищется сперва в ваших vars, потом в общем контексте + +- [[STORE:путь]] — достать из постоянного хранилища (store.*) + +Подсказка: макросы [[...]] можно писать в строках JSON, а {{ ... }} — там, где нужно вставить число/массив/объект без кавычек. + +— — — + +4) Куда именно это писать (по узлам) + +4.1 ProviderCall — «собрать запрос к провайдеру» + +Где живут макросы: +- endpoint — можно вставлять {{ model }} или ключи в URL (например Gemini ?key=…) +- headers — это JSON‑объект в текстовом поле: сюда можно писать [[VAR:incoming.headers.authorization]], [[Clod]] и т.д. +- template — главное поле: JSON‑тело запроса. Обязательно валидный JSON после разворачивания макросов. + +Мини‑шпаргалка: +- OpenAI (пример заголовка): {"Authorization":"Bearer [[VAR:incoming.headers.authorization]]"} +- Gemini (ключ в URL): /v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]] +- Claude (заголовки): {"x-api-key":"[[VAR:incoming.headers.x-api-key]]","anthropic-version":"2023-06-01"} + +Полный пример OpenAI‑тела: +{ + "model": "{{ model }}", + [[PROMPT]], + "temperature": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }}, + "top_p": {{ incoming.json.top_p|default(params.top_p|default(1)) }}, + "max_tokens": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }}, + "stop": {{ incoming.json.stop|default(params.stop|default([])) }}, + "stream": {{ incoming.json.stream|default(false) }} +} + +Важное правило: +- [[PROMPT]] — это «кусок» JSON без запятых по краям. Убедитесь, что вокруг него стоят запятые там, где нужно, но не лишние. +- Если шаблон не превращается в валидный JSON — ProviderCall упадёт с понятной ошибкой. + +4.2 RawForward — «пропусти запрос как есть» (reverse proxy) + +Куда писать: +- base_url — куда слать (можно с макросами) +- override_path — переопределить путь (опционально) +- extra_headers — JSON‑объект (строкой), можно вставлять [[...]] и {{ ... }} + +По умолчанию узел пробрасывает заголовки клиента (кроме Host/Content‑Length) и тело JSON «как есть». Это удобно, если вы хотите просто дойти до провайдера без сложной сборки, но при этом подмешать один‑два заголовка. + +4.3 Return — «оформить финальный ответ в стиле клиента» + +- target_format: auto/openai/gemini/claude — во что завернуть ответ +- text_template — обычно "[[OUT1]]", но можно собрать возврат из нескольких кусков: + - "Вот ваш результат: [[OUT2]]" + +Return сам завернёт текст в правильную структуру. Пример OpenAI‑ответа возвращается как "choices[0].message.content". + +4.4 If — «ветвление по условию» + +Пишем одно выражение, которое возвращает true/false. Поддерживаются: +- Логика: &&, ||, ! (not) +- Сравнения: ==, !=, <, <=, >, >= +- Ключевое слово contains — проверка подстроки или вхождения в список +- Макросы [[...]] и {{ ... }} прямо в выражении + +Примеры: +- [[OUT1]] contains "Красиво" +- {{ OUT.n6.response_text|default('') }} != "" +- {{ params.temperature|default(0.7) }} >= 0.3 && {{ params.temperature|default(0.7) }} <= 1 +- !([[OUT3]] contains "error") + +Вычислитель выражений: [eval_condition_expr()](agentui/pipeline/templating.py:336) + +4.5 SetVars — «завести свои переменные» + +Два режима на каждую переменную: +- mode=string — строка после разворачивания макросов (обычный случай) +- mode=expr — мини‑формула (безопасная), в которой доступны специальные функции. + +Доступные безопасные функции в expr: +- from_json(x) + - Парсит JSON‑строку в объект (если в строке макросы — они сначала подставятся) +- jp(value, path, join_sep="\n") + - Достаёт данные по «dot‑JSONPath» (индексы и звёздочка поддерживаются) + - Примеры путей: a.b.0.c, items.*.title +- jp_text(value, path, join_sep="\n") + - То же, что jp, но вернёт строку: если массив — склеит через join_sep + +Готовые рецепты: +- Извлечь текст из ответа провайдера в переменную TXT и вернуть: + - name: TXT + - mode: expr + - value: jp_text([[OUT:n2.result]], 'candidates.0.content.parts.0.text') + - Return.text_template: "[[TXT]]" + +- Парсинг JSON‑строки и взять число: + - name: VAL + - mode: expr + - value: jp(from_json('{"a":{"b":[{"x":1},{"x":2}]}}'), 'a.b.1.x') + - результат: 2 + +- Собрать несколько полей в строку: + - name: TITLES + - mode: expr + - value: jp_text([[OUT:n4.result]], 'items.*.title', ' | ') + - Return: "[[TITLES]]" → "Title1 | Title2 | Title3" + +Подсказки: +- Если путь возвращает несколько элементов — используйте jp_text, чтобы сразу получить склейку. +- Если у вас уже объект/массив (не строка) — можно сразу передать его в jp/jp_text без from_json. +- Внутри expr макросы пишите внутри кавычек (в аргументах функций) — они развернутся перед вычислением. + +Ограничения безопасности в expr: +- Разрешены только литералы, базовая арифметика/логика, сравнения и ф‑ции rand(), randint(a,b), choice(list), from_json(), jp(), jp_text(). +- Доступ к произвольным атрибутам/импортам Python запрещён. + +— — — + +5) Где в редакторе быстро подсмотреть макросы + +Вверху справа есть кнопка «ПЕРЕМЕННЫЕ». Откроется панель: +- Выберите «snapshot», чтобы видеть всю «картину мира» последнего запуска (incoming, params, OUT_TEXT, алиасы OUT1/OUT2 и т.д.) +- Клик по строке копирует готовый макрос в буфер (можно переключить режим «фигурные» — для {{ ... }}) +- Поиск работает и по именам, и по значениям + +За поведение панели отвечают страничка и скрипты: +- [static/editor.html](static/editor.html) +- [static/js/serialization.js](static/js/serialization.js) +- [static/js/pm-ui.js](static/js/pm-ui.js) + +— — — + +6) Что на самом деле делает [[OUTx]] (как выбирается «лучший текст») + +Когда нода (ProviderCall или RawForward) получила JSON от провайдера, движок старается «вынуть» из него удобный текст: +- OpenAI: choices[0].message.content +- Gemini: candidates[0].content.parts[0].text +- Claude: content[].text (склейка) +- Если формат неизвестен — идёт «лучший догадчик» по глубине, чтобы найти текст +- Можно задать пресет (JSONPath) в настройках ноды или глобально в «Запуск» → «Пресеты парсинга OUTx» + +Логика извлечения и пресетов находится в: [ProviderCallNode.run()](agentui/pipeline/executor.py:1631), [RawForwardNode.run()](agentui/pipeline/executor.py:1939) + +Для продвинутых: глобальные мета‑настройки пайплайна (timeouts, стратегия, пресеты) редактируются в «Запуск», а сохраняются в [pipeline.json](pipeline.json). + +— — — + +7) Безопасность (прочитать обязательно) + +- Не храните настоящие API‑ключи в файлах пайплайна/пресетах. Передавайте их через заголовки клиента: + - OpenAI: Authorization: Bearer XXXXXX + - Anthropic: x-api-key: XXXXXX + - Gemini: ?key=XXXXXX в URL +- В шаблонах используйте значения из входящего запроса: [[VAR:incoming.headers.authorization]], [[VAR:incoming.headers.x-api-key]], [[VAR:incoming.api_keys.key]] +- Логи в редакторе показывают запросы/ответы целиком. Для продакшена отключайте/маскируйте чувствительные части. +- Прокси и TLS‑проверка (если нужно сниффить трафик/пробрасывать через Burp) настраиваются здесь: [agentui/config.py](agentui/config.py), HTTP‑клиент: [build_client()](agentui/providers/http_client.py:21) + +— — — + +8) Рецепты «скопируй и вставь» + +8.1 Самый короткий пайплайн «получил → отправил к OpenAI → вернул» +- Узел ProviderCall: + - provider: openai + - headers: {"Authorization":"Bearer [[VAR:incoming.headers.authorization]]"} + - template — возьмите из примера OpenAI выше (с [[PROMPT]]) + - Prompt Blocks: + - system: «Ты — помощник. Отвечай коротко.» + - user: «[[VAR:chat.last_user]] — перепиши аккуратнее» +- Узел Return: + - target_format: auto + - text_template: "[[OUT1]]" + +8.2 Gemini через ключ в URL, без заголовков - ProviderCall: - - OUT.nX.result — сырой JSON ответа провайдера - - OUT.nX.response_text — уже извлечённый «лучший текст» (строка) -- RawForward: - - OUT.nX.result — JSON, как пришёл от апстрима (или {"error": "...", "text": "..."} при не‑JSON ответе) + - provider: gemini + - endpoint: /v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]] + - headers: {} + - template — пример Gemini выше (с [[PROMPT]]) + +8.3 Склейка двух ответов и проверка условием If +- ProviderCall(n2) и ProviderCall(n3) что‑то генерят +- ProviderCall(n6): user prompt: «Объедини [[OUT3]] и [[OUT2]] красиво. Напиши слово "Красиво" в конце.» +- If(n7): expr = [[OUT6]] contains "Красиво" +- Return: text_template = "[[OUT6]]" + +— — — + +9) Где это в коде (для любопытных) + +- Создание приложения/роутов: [create_app()](agentui/api/server.py:270) +- Нормализация payload (OpenAI/Gemini/Claude → единый вид): [normalize_to_unified()](agentui/api/server.py:44) +- Контекст для макросов (incoming/chat/params/…): [build_macro_context()](agentui/api/server.py:143) +- Исполнитель конвейера и «волны»: [PipelineExecutor.run()](agentui/pipeline/executor.py:170) +- Узел вызова провайдера (сборка [[PROMPT]], лог HTTP): [ProviderCallNode.run()](agentui/pipeline/executor.py:1631) +- Прямой форвард запроса: [RawForwardNode.run()](agentui/pipeline/executor.py:1939) +- Финализация ответа под формат клиента: [ReturnNode.run()](agentui/pipeline/executor.py:2256) +- Условия If (contains, &&, ||, ! и макросы): [IfNode.run()](agentui/pipeline/executor.py:2350), [eval_condition_expr()](agentui/pipeline/templating.py:336) +- Шаблонизатор макросов [[...]] и {{ ... }}: [render_template_simple()](agentui/pipeline/templating.py:191) +- Определение провайдера по форме JSON: [detect_vendor()](agentui/common/vendors.py:8) + +— — — + +Всё. Старайтесь думать так: «Мне нужен кусочек нужного значения в нужное место». Найдите его в панели «ПЕРЕМЕННЫЕ», скопируйте макрос — и вклейте. Если «кусочков» несколько — заведите переменные SetVars и соберите их в строку. Если сомневаетесь — начните с Return и [[OUT1]]: это самый короткий путь увидеть, что реально получается. +— — — -Подсказка по короткой форме [[OUTx]]: -- OpenAI: вернёт choices[0].message.content -- Gemini: вернёт candidates[0].content.parts[0].text -- Claude: склеит content[].text -- Если явных полей нет — выполнит «глубокий поиск» по ключам "text"/"content" +Приложение A. SetVars: функции и операции в mode=expr (простым языком) ---- +Где это запрограммировано +- Логика разбора и разрешённые операции реализованы в [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:1092). Если коротко: там «белый список» безопасных функций и операций, всё остальное запрещено. +- Подстановка макросов внутри строк (для функций from_json/jp/jp_text) делает [render_template_simple()](agentui/pipeline/templating.py:191). -## 3) Макросы подстановки и синтаксис +Что можно писать в mode=expr +1) Случайные значения +- rand() — число от 0 до 1 (типа 0.0–1.0) + Пример: 0.35, 0.92 … -В шаблонах доступны обе формы подстановки: - -1) Квадратные скобки [[ ... ]] — простая подстановка -- [[VAR:путь]] — взять значение из контекста по точечному пути - Пример: [[VAR:incoming.json.max_tokens]] -- [[OUT:путь]] — взять значение из OUT (см. раздел выше) - Пример: [[OUT:n1.result.choices.0.message.content]] -- [[OUT1]] / [[OUT2]] — короткая форма «просто текст» -- [[PROMPT]] — специальный JSON‑фрагмент из Prompt Blocks (см. ниже) - -2) Фигурные скобки {{ ... }} — «джинджа‑лайт» -- {{ путь }} — взять значение по пути из контекста (или из OUT.* если начать с OUT.) - Пример: {{ OUT.n1.result }} -- Фильтр по умолчанию: {{ что-то|default(значение) }} +- randint(a, b) — целое в диапазоне [a; b] включительно Примеры: - - {{ params.temperature|default(0.7) }} - - {{ incoming.json.stop|default([]) }} - - {{ anthropic_version|default('2023-06-01') }} — см. «Опциональные поля» ниже -- Фигурные скобки удобны там, где нужно вставить внутрь JSON не строку, а ЧИСЛО/ОБЪЕКТ/МАССИВ без кавычек и/или задать дефолт. + - randint(1, 6) → бросок кубика d6 + - randint(100, 999) → трёхзначный код ---- +- choice(list_or_tuple) — случайный элемент из списка/кортежа + Примеры: + - choice(['ru','en','de']) + - choice((128, 256, 512)) -## 4) ProviderCall: Prompt Blocks, pm.* и [[PROMPT]] +2) Арифметика (как в обычных формулах) +- Операции: +, -, *, /, //, % + - / — деление с плавающей точкой + - // — целочисленное деление (отбрасывает дробную часть) + - % — остаток от деления -Узел ProviderCall собирает ваши Prompt Blocks (блоки вида: роль/текст/вкл‑выкл/порядок) в стандартные «сообщения» и превращает их в структуру для конкретного провайдера. - -Внутри шаблонов этого узла доступны: -- pm — «сырьевые» структуры из Prompt Blocks - - Для OpenAI: - - pm.messages — массив { role, content, name? } - - pm.system_text — один большой текст из всех system‑блоков - - Для Gemini: - - pm.contents — массив { role: "user"|"model", parts: [{text}] } - - pm.systemInstruction — объект вида { parts: [{text}] } или пустой {} - - pm.system_text — строка - - Для Claude: - - pm.system_text — строка - - pm.system — то же самое (удобно подставлять в поле "system") - - pm.messages — массив { role: "user"|"assistant", content: [{type:"text", text:"..."}] } - -- [[PROMPT]] — готовый JSON‑фрагмент на основе pm, безопасный для вставки внутрь шаблона: - - OpenAI → подставит: "messages": [...] - - Gemini → подставит: "contents": [...], "systemInstruction": {...} - - Claude → подставит: "system": "...", "messages": [...] - -Зачем это нужно? -- Чтобы 1) удобно собирать промпт из визуальных блоков, 2) не «сломать» JSON руками. - Вы можете вручную использовать {{ pm.* }}, но [[PROMPT]] — рекомендуемый и самый безопасный вариант. - ---- - -## 5) Частые сценарии и примеры - -Примеры ниже можно вклеивать в поле «template» ноды ProviderCall. Они уже используют [[PROMPT]] и аккуратные дефолты. - -OpenAI (POST /v1/chat/completions): -``` -{ - "model": "{{ model }}", - [[PROMPT]], - "temperature": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }}, - "top_p": {{ incoming.json.top_p|default(params.top_p|default(1)) }}, - "max_tokens": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }}, - "stop": {{ incoming.json.stop|default(params.stop|default([])) }} -} -``` - -Gemini (POST /v1beta/models/{model}:generateContent): -``` -{ - "model": "{{ model }}", - [[PROMPT]], - "safetySettings": {{ incoming.json.safetySettings|default([]) }}, - "generationConfig": { - "temperature": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }}, - "topP": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }}, - "maxOutputTokens": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }}, - "stopSequences": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }} - } -} -``` -Подсказка: ключ Gemini удобно брать из строки запроса: -в endpoint используйте …?key=[[VAR:incoming.api_keys.key]] - -Claude (POST /v1/messages): -``` -{ - "model": "{{ model }}", - [[PROMPT]], - "temperature": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }}, - "top_p": {{ incoming.json.top_p|default(params.top_p|default(1)) }}, - "max_tokens": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }}, - "system": {{ pm.system|default("") }} -} -``` -Подсказка: ключ Anthropic обычно передают в заголовке x-api-key. -В UI‑пресете это поле уже есть в headers. - -RawForward (прямой форвард входящего запроса): -- Поля конфигурации base_url, override_path, extra_headers проходят через те же макросы, поэтому можно подставлять динамику: - - base_url: https://generativelanguage.googleapis.com - - override_path: [[VAR:incoming.path]] (или задать свой) - - extra_headers (JSON): `{"X-Trace":"req-{{ incoming.query_params.session|default('no-session') }}"}` - ---- - -## 6) Опциональные/редкие поля, о которых стоит знать - -- anthropic_version — используется как HTTP‑заголовок для Claude ("anthropic-version"). В тело запроса не вставляется. - Если нужен дефолт, задавайте его в headers (например, в конфиге ноды/шаблоне заголовков). В шаблонах тела используйте [[PROMPT]]/pm.* без anthropic_version. - -- stream — в MVP стриминг отключён, сервер принудительно не стримит ответ. - В шаблонах можно встретить поля stream, но по умолчанию они не включены. - ---- - -## 7) Когда использовать [[...]] и когда {{ ... }} - -- Внутрь JSON как ОБЪЕКТ/МАССИВ/ЧИСЛО: используйте {{ ... }} - (фигурные скобки вставляют «как есть», без кавычек, и умеют |default(...)) -- Для строк/URL/заголовков/простых значений: можно использовать [[...]] - (квадратные скобки удобны и короче писать) +- Скобки и унарные знаки: + - (1 + 2) * 3 + - -5, +7 Примеры: -- {{ pm.contents }} — вставит массив как настоящий массив (без кавычек) -- {{ params.temperature|default(0.7) }} — безопасный дефолт для числа -- [[VAR:incoming.api_keys.authorization]] — быстро подставить строку Authorization +- 9.99 * 1.2 → 11.988 +- (rand() * 100) // 1 → псевдо‑процент от 0 до 99 +- randint(1, 6) + 10 → 11..16 ---- +3) Логика и сравнения +- Логические: and, or +- Сравнения: ==, !=, <, <=, >, >= +- Можно «цепочкой»: 1 < x <= 10 -## 8) Отладка и рекомендации +Примеры: +- (rand() > 0.5) and (choice([True, False]) or True) +- 120 <= choice([64,128,256]) and 256 >= 200 → True -- ProviderCall печатает в консоль DEBUG сведения: выбранный провайдер, конечный URL, первые символы тела запроса — удобно для проверки корректности шаблона. -- Если «ничего не подставилось»: - 1) Проверьте, что вы НЕ передаёте сырое входное тело напрямую в ProviderCall (узел строит тело из шаблона и Prompt Blocks). - 2) Убедитесь, что итоговый JSON валиден (закрывающие скобки, запятые). - 3) Проверьте точность путей в макросах (OUT vs OUTx, правильные id нод n1/n2/...). -- Для ссылок на выходы предыдущих нод используйте [[OUT1]] как «просто текст», либо полные пути [[OUT:n1...]] для точного фрагмента. +4) Литералы (что можно писать напрямую) +- Числа: 42, 3.14 +- Строки: 'привет', "hello" +- Списки/кортежи/словарики: [1,2,3], (1,2,3), {'a':1, 'b':2} +- Логические и null‑подобные: True, False, None ---- +Отдельный режим: «чистый JSON» +- Если ВЕСЬ expr — корректный JSON (и только он), он принимается как значение. + Примеры: + - {"a":{"b":[{"x":1},{"x":2}]}} + - ["ru","en","de"] + - true / false / null +- Внутри «обычных» выражений используйте Python‑формы True/False/None. -## 9) Быстрая памятка по ключам доступа +5) Макросы внутри expr — как правильно +Важно: «голые» макросы внутри expr не работают. Подставлять их можно ТОЛЬКО как строки‑аргументы специальных функций from_json/jp/jp_text — они сначала разворачиваются в текст, потом парсятся. -- Gemini: [[VAR:incoming.api_keys.key]] — рекомендовано; ключ приходит в query (?key=...). -- OpenAI: [[VAR:incoming.headers.authorization]] (или [[VAR:incoming.api_keys.authorization]]) — стандартный Bearer‑токен. -- Anthropic: [[VAR:incoming.headers.x-api-key]] — ключ в заголовке. +Примеры (готовы к копипасте): +- Взять JSON из предыдущей ноды и достать одно число: + name: SCORE + mode: expr + value: jp(from_json('[[OUT:n2.result]]'), 'meta.score') ---- +- Вытащить текст из OpenAI‑ответа и склеить варианты через двойной перенос: + name: TEXTS + mode: expr + value: jp_text(from_json('[[OUT:n6.result]]'), 'choices.*.message.content', '\n\n') -## 10) Ссылки на реализацию (для интересующихся деталями) +- Разобрать JSON из входящего запроса: + name: UID + mode: expr + value: jp(from_json('[[VAR:incoming.json]]'), 'user.id') -- Контекст (переменные): [build_macro_context()](agentui/api/server.py:142) -- Исполнение пайплайна, зависимости, снапшоты OUT: [PipelineExecutor.run()](agentui/pipeline/executor.py:136) -- Узел провайдера (Prompt Blocks → провайдер): [ProviderCallNode.run()](agentui/pipeline/executor.py:650) -- PM‑структуры для шаблонов: [ProviderCallNode._blocks_struct_for_template()](agentui/pipeline/executor.py:592) -- Подстановка [[PROMPT]], макросы, дефолты: [render_template_simple()](agentui/pipeline/templating.py:187) -- Короткая форма [[OUTx]] и поиск «лучшего текста»: [_best_text_from_outputs()](agentui/pipeline/templating.py:124) -- Прямой форвард входящего запроса: [RawForwardNode.run()](agentui/pipeline/executor.py:833) -- Детекция вендора по входному payload: [detect_vendor()](agentui/common/vendors.py:8) +- Подставить фигурные скобки как строку и распарсить: + name: LIMIT + mode: expr + value: jp(from_json('{{ params|default({"max_tokens":256}) }}'), 'max_tokens') -Удачного редактирования! ---- -## Пользовательские переменные (SetVars) — «для людей» +Доступные функции (подробнее) +- from_json(x) + - Принимает строку JSON (в т.ч. со вставленными макросами), возвращает объект/массив/число/строку/bool/None. + - Удобно, когда источник — текст, а работать хочется с полями. -Задача: в начале пайплайна положить свои значения и потом использовать их в шаблонах одной строкой — например [[MY_KEY]] или {{ MAX_TOKENS }}. +- jp(value, path, join_sep="\n") + - «Точечный JSONPath»: a.b.0.c, items.*.title + - Возвращает значение или список значений (если в пути есть «*») + - join_sep здесь игнорируется (имеет смысл в jp_text) -Где это в UI -- В левой панели добавьте ноду SetVars и откройте её в инспекторе. -- Жмите «Добавить переменную», у каждой переменной есть три поля: - - name — имя переменной (латинские буквы/цифры/подчёркивание, не с цифры): MY_KEY, REGION, MAX_TOKENS - - mode — режим обработки значения: - - string — строка, в которой работают макросы ([[...]] и {{ ... }}) - - expr — «мини‑формула» без макросов (подробнее ниже) - - value — собственно значение +- jp_text(value, path, join_sep="\n") + - То же, но всегда возвращает строку. Если значений несколько — склеит через join_sep. -Как потом вставлять переменные -- Для строк (URL/заголовки/текст) — квадратные скобки: [[MY_KEY]] -- Для чисел/массивов/объектов — фигурные скобки: {{ MAX_TOKENS }}, {{ GEN_CFG }} +- rand(), randint(a,b), choice(list_or_tuple) + - Случайности без сидирования (не детерминированы). + - randint — границы включительные. choice — список/кортеж не должен быть пустым. -Примеры «как надо» -- Переменная-строка (mode=string): - - name: AUTH - - value: "Bearer [[VAR:incoming.headers.authorization]]" - - Использование в заголовке: "Authorization": "[[AUTH]]" -- Переменная-число (mode=expr): - - name: MAX_TOKENS - - value: 128 + 64 - - Использование в JSON: "max_tokens": {{ MAX_TOKENS }} -- Переменная-объект (mode=expr): - - name: GEN_CFG - - value: {"temperature": 0.3, "topP": 0.9, "safe": true} - - Использование: "generationConfig": {{ GEN_CFG }} +Чего делать нельзя (и почему) +- Нельзя любые другие функции: len(), sum(), not, print(), и т.п. — «белый список» (смотри [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:1092)) +- Нельзя обращаться к переменным/атрибутам по именам (кроме ваших [[NAME]] в шаблонах вне expr) +- Нельзя генераторы/списки‑выражения, **kwargs, *args +- Нельзя «голые» макросы внутри expr: [[...]] или {{ ... }} нужно класть строкой в аргумент from_json/jp/jp_text -Важно про два режима -- string — это «шаблон». Внутри работают все макросы ([[VAR:...]], [[OUT:...]], [[PROMPT]], {{ ... }}). Значение прогоняется через рендер [render_template_simple()](agentui/pipeline/templating.py:184). -- expr — это «мини‑формула». Внутри НЕТ макросов и НЕТ доступа к контексту; только литералы и операции (см. ниже). Вычисляет значение безопасно — без eval, на белом списке AST (реализация: [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:291)). +Быстрая шпаргалка «на каждый день» +1) Бросок кубика d6 +- name: DICE +- mode: expr +- value: randint(1, 6) -Что умеет expr (мини‑формулы) -- Числа и арифметика: 128 + 64, (5 * 60) + 30, 42 % 2, -5, 23 // 10 -- Строки: "eu" + "-central" → "eu-central" (строки склеиваем знаком +) -- Булева логика: (2 < 3) and (10 % 2 == 0), 1 < 2 < 5 -- Коллекции: ["fast", "safe"], {"temperature": 0.3, "topP": 0.9, "safe": true} -- JSON‑литералы: true/false/null, объекты и массивы — если выражение является чистым JSON, оно разбирается напрямую (без макросов), т.е. true→True, null→None и т.п. -- Запрещено: функции (кроме специально разрешённых ниже), доступ к переменным/контексту, атрибуты/индексация/условные выражения. +2) Случайный стиль ответа +- name: TONE +- mode: expr +- value: choice(['официально','дружелюбно','коротко']) -Рандом в expr -- В expr доступны три простые функции случайности: - - rand() → число с плавающей точкой в диапазоне [0, 1) - - randint(a, b) → целое число от a до b включительно - - choice(list) → случайный элемент из списка/кортежа -- Примеры: - - name: RAND_F, mode: expr, value: rand() - - "temperature": {{ RAND_F }} - - name: DICE, mode: expr, value: randint(1, 6) - - "dice_roll": {{ DICE }} - - name: PICK_MODEL, mode: expr, value: choice(["gpt-4o-mini", "gpt-4o", "o3-mini"]) - - "model": "[[PICK_MODEL]]" -- Зерна/seed нет — каждый запуск выдаёт новое значение. +3) Выбрать лимит токенов +- name: TOKENS +- mode: expr +- value: choice((128, 256, 512)) -«Почему в expr нельзя подставлять переменные/макросы?» -- Для безопасности и предсказуемости: expr — это закрытый мини‑язык без окружения. -- Если нужно использовать другие переменные/макросы — делайте это в режиме string (там всё рендерится шаблонизатором). -- Технические детали: защита реализована в [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:291), а вставка string‑значений — через [render_template_simple()](agentui/pipeline/templating.py:184). +4) Наценка 20% +- name: PRICE_WITH_VAT +- mode: expr +- value: price * 1.2 ← если price — ваша строковая переменная, используйте вне expr (в шаблоне) {{ PRICE }}; внутри expr внешние имена не видны -Как это работает внутри (если интересно) -- SetVars исполняется как обычная нода пайплайна и отдаёт {"vars": {...}}. -- Исполнитель добавляет эти значения в контекст для последующих нод как context.vars (см. [PipelineExecutor.run()](agentui/pipeline/executor.py:131)). -- При рендере шаблонов: - - [[NAME]] и {{ NAME }} подставляются с приоритетом из пользовательских переменных (см. обработку в [render_template_simple()](agentui/pipeline/templating.py:184)). -- Сам SetVars считает переменные в порядке списка и возвращает их одним пакетом (внутри одной ноды значения не зависят друг от друга). +5) Вытянуть текст из ответа модели n6 +- name: ANSWER +- mode: expr +- value: jp_text(from_json('[[OUT:n6.result]]'), 'choices.0.message.content') -Частые вопросы -- «Хочу собрать строку с частями из внешнего запроса»: делайте mode=string и пишите: "Bearer [[VAR:incoming.headers.authorization]]". -- «Хочу массив случайных чисел»: mode=expr → [rand(), rand(), rand()], а в JSON: "numbers": {{ MY_LIST }} -- «Почему мои значения не сохраняются?» — нажмите «Сохранить параметры» в инспекторе ноды, затем «Сохранить пайплайн» в шапке. UI синхронизирует данные в node.data и сохраняет в pipeline.json (см. [static/editor.html](static/editor.html)). +Подсказка по ошибкам +- Function X is not allowed — вызвали неразрешённую функцию +- choice() expects list or tuple — передали не список/кортеж +- choice() on empty sequence — пустой список +- randint invalid arguments — границы не числа или a > b +- Expression not allowed — в выражении есть запрещённые элементы (например, not/len/компрехеншены) -Ссылки на реализацию (для любопытных) -- Нода переменных: [SetVarsNode](agentui/pipeline/executor.py:264), [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:291), [SetVarsNode.run()](agentui/pipeline/executor.py:354) -- Исполнитель/контекст vars: [PipelineExecutor.run()](agentui/pipeline/executor.py:131) -- Шаблоны и макросы (включая «голые» [[NAME]]/{{ NAME }}): [render_template_simple()](agentui/pipeline/templating.py:184) \ No newline at end of file +Если видите ошибку — упростите выражение: разбейте задачу на 2 переменные SetVars (сначала jp/from_json, потом математика/выбор), либо перенесите часть логики в шаблон {{ ... }} (но помните: в шаблоне нет rand/randint/choice). + +Где посмотреть действующие значения +- В редакторе нажмите «ПЕРЕМЕННЫЕ»: там видно STORE (включая snapshot OUT_TEXT и алиасы OUT1/OUT2). Клик по строке — копирование макроса для вставки. +Схема работы панели описана в [static/editor.html](static/editor.html) и скриптах [static/js/serialization.js](static/js/serialization.js), [static/js/pm-ui.js](static/js/pm-ui.js). \ No newline at end of file diff --git a/static/editor.html b/static/editor.html index 6a9cb92..605fbbd 100644 --- a/static/editor.html +++ b/static/editor.html @@ -388,18 +388,18 @@ function escAttr(v) { const s = String(v ?? ''); return s - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); } function escText(v) { const s = String(v ?? ''); // For text nodes we keep quotes as-is for readability, but escape critical HTML chars return s - .replace(/&/g, '&') - .replace(//g, '>'); + .replace(/&/g, '&') + .replace(//g, '>'); } // Нормализуем/заполняем дефолты конфигов нод, чтобы ключи попадали в сериализацию @@ -1285,10 +1285,10 @@ function escapeHtml(s) { return String(s ?? '') - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') .replace(/'/g, '''); } @@ -2456,9 +2456,9 @@ function toggleLeft() { const RUNE_RE = /[\u16A0-\u16FF]+/g; function escHtml(s) { return String(s ?? '') - .replace(/&/g, '&') - .replace(//g, '>'); + .replace(/&/g, '&') + .replace(//g, '>'); } // Оборачиваем рунические символы в уже экранированной строке function wrapRunesEscaped(escaped) { diff --git a/tests/test_setvars_jp.py b/tests/test_setvars_jp.py new file mode 100644 index 0000000..7f63c3e --- /dev/null +++ b/tests/test_setvars_jp.py @@ -0,0 +1,97 @@ +import asyncio +import json +from agentui.pipeline.executor import PipelineExecutor + +def run_checks(): + async def main(): + print("\n=== JP FUNCTIONS: SetVars expr → jp/from_json/jp_text ===") + # 1) from_json + jp: извлечь число по пути + p1 = { + "id": "p_jp_1", + "name": "JP basic", + "loop_mode": "dag", + "nodes": [ + { + "id": "n1", + "type": "SetVars", + "config": { + "variables": [ + { + "id": "v1", + "name": "VAL", + "mode": "expr", + # JSON-строка → объект → jp по пути "a.b.1.x" -> 2 + "value": "jp(from_json('{\"a\":{\"b\":[{\"x\":1},{\"x\":2}]}}'), 'a.b.1.x')" + } + ] + }, + "in": {} + }, + { + "id": "n2", + "type": "Return", + "config": { + "target_format": "openai", + # Должно быть число 2, выводим как текст + "text_template": "{{ VAL }}" + }, + "in": { "depends": "n1.done" } + } + ] + } + ctx = { + "model": "gpt-x", + "vendor_format": "openai", + "params": {}, + "chat": {"last_user": "hi"}, + "OUT": {} + } + out1 = await PipelineExecutor(p1).run(ctx) + msg1 = out1["result"]["choices"][0]["message"]["content"] + print("OUT1:", msg1) + assert msg1 == "2" + + # 2) jp_text: собрать строки из массива + p2 = { + "id": "p_jp_2", + "name": "JP text join", + "loop_mode": "dag", + "nodes": [ + { + "id": "n1", + "type": "SetVars", + "config": { + "variables": [ + { + "id": "v1", + "name": "TXT", + "mode": "expr", + "value": "jp_text(from_json('{\"items\":[{\"t\":\"A\"},{\"t\":\"B\"},{\"t\":\"C\"}]}'), 'items.*.t', ' | ')" + } + ] + }, + "in": {} + }, + { + "id": "n2", + "type": "Return", + "config": { + "target_format": "openai", + "text_template": "[[TXT]]" + }, + "in": { "depends": "n1.done" } + } + ] + } + out2 = await PipelineExecutor(p2).run(ctx) + msg2 = out2["result"]["choices"][0]["message"]["content"] + print("OUT2:", msg2) + assert msg2 == "A | B | C" + + print("JP functions tests: OK") + + asyncio.run(main()) + +if __name__ == "__main__": + run_checks() + print("Tests completed") \ No newline at end of file