Compare commits
9 Commits
hadtavern0
...
86182c0808
| Author | SHA1 | Date | |
|---|---|---|---|
| 86182c0808 | |||
| 2abfbb4b1a | |||
| 135c393eda | |||
| d155ffa74c | |||
| 0e39250c3c | |||
| 46d2fb8173 | |||
| 563663f9f1 | |||
| 338e65624f | |||
| 81014d26f8 |
40
.agentui/vars/p_cancel_abort.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"WAS_ERROR__n2": true,
|
||||
"CYCLEINDEX__n2": 0,
|
||||
"snapshot": {
|
||||
"incoming": {
|
||||
"method": "POST",
|
||||
"url": "http://localhost/test",
|
||||
"path": "/test",
|
||||
"query": "",
|
||||
"headers": {
|
||||
"x": "X-HEADER"
|
||||
},
|
||||
"json": {}
|
||||
},
|
||||
"params": {
|
||||
"temperature": 0.25
|
||||
},
|
||||
"model": "gpt-x",
|
||||
"vendor_format": "openai",
|
||||
"system": "",
|
||||
"OUT": {
|
||||
"n2": {
|
||||
"result": {
|
||||
"error": "Cancelled by user (abort)"
|
||||
},
|
||||
"response_text": "",
|
||||
"vars": {
|
||||
"WAS_ERROR__n2": true,
|
||||
"CYCLEINDEX__n2": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"OUT_TEXT": {
|
||||
"n2": "Cancelled by user (abort)"
|
||||
},
|
||||
"LAST_NODE": "n2",
|
||||
"OUT2": "Cancelled by user (abort)",
|
||||
"EXEC_TRACE": "n2(ProviderCall)"
|
||||
}
|
||||
}
|
||||
40
.agentui/vars/p_cancel_soft.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"WAS_ERROR__n2": false,
|
||||
"CYCLEINDEX__n2": 0,
|
||||
"snapshot": {
|
||||
"incoming": {
|
||||
"method": "POST",
|
||||
"url": "http://localhost/test",
|
||||
"path": "/test",
|
||||
"query": "",
|
||||
"headers": {
|
||||
"x": "X-HEADER"
|
||||
},
|
||||
"json": {}
|
||||
},
|
||||
"params": {
|
||||
"temperature": 0.25
|
||||
},
|
||||
"model": "gpt-x",
|
||||
"vendor_format": "openai",
|
||||
"system": "",
|
||||
"OUT": {
|
||||
"n2": {
|
||||
"result": {
|
||||
"echo": {}
|
||||
},
|
||||
"response_text": "",
|
||||
"vars": {
|
||||
"WAS_ERROR__n2": false,
|
||||
"CYCLEINDEX__n2": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"OUT_TEXT": {
|
||||
"n2": ""
|
||||
},
|
||||
"LAST_NODE": "n2",
|
||||
"OUT2": "",
|
||||
"EXEC_TRACE": "n2(ProviderCall)"
|
||||
}
|
||||
}
|
||||
@@ -19,21 +19,36 @@
|
||||
"OUT": {
|
||||
"n1": {
|
||||
"result": {
|
||||
"error": {
|
||||
"message": "Incorrect API key provided: TEST. You can find your API key at https://platform.openai.com/account/api-keys.",
|
||||
"type": "invalid_request_error",
|
||||
"param": null,
|
||||
"code": "invalid_api_key"
|
||||
"echo": {
|
||||
"url": "https://api.openai.com/v1/chat/completions",
|
||||
"headers": {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer TEST"
|
||||
},
|
||||
"payload": {
|
||||
"model": "gpt-x",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are test"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Say Привет"
|
||||
}
|
||||
],
|
||||
"temperature": 0.25
|
||||
}
|
||||
}
|
||||
},
|
||||
"response_text": ""
|
||||
"response_text": "https://api.openai.com/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"OUT_TEXT": {
|
||||
"n1": "Incorrect API key provided: TEST. You can find your API key at https://platform.openai.com/account/api-keys."
|
||||
"n1": "https://api.openai.com/v1/chat/completions"
|
||||
},
|
||||
"LAST_NODE": "n1",
|
||||
"OUT1": "Incorrect API key provided: TEST. You can find your API key at https://platform.openai.com/account/api-keys.",
|
||||
"OUT1": "https://api.openai.com/v1/chat/completions",
|
||||
"EXEC_TRACE": "n1(ProviderCall)"
|
||||
}
|
||||
}
|
||||
40
.agentui/vars/p_pc_while_ignore.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"WAS_ERROR__n2": true,
|
||||
"CYCLEINDEX__n2": 2,
|
||||
"snapshot": {
|
||||
"incoming": {
|
||||
"method": "POST",
|
||||
"url": "http://localhost/test",
|
||||
"path": "/test",
|
||||
"query": "",
|
||||
"headers": {
|
||||
"x": "X-HEADER"
|
||||
},
|
||||
"json": {}
|
||||
},
|
||||
"params": {
|
||||
"temperature": 0.25
|
||||
},
|
||||
"model": "gpt-x",
|
||||
"vendor_format": "openai",
|
||||
"system": "",
|
||||
"OUT": {
|
||||
"n2": {
|
||||
"result": {
|
||||
"error": "Node n2 (ProviderCall) requires 'base_url' in config"
|
||||
},
|
||||
"response_text": "",
|
||||
"vars": {
|
||||
"WAS_ERROR__n2": true,
|
||||
"CYCLEINDEX__n2": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"OUT_TEXT": {
|
||||
"n2": "Node n2 (ProviderCall) requires 'base_url' in config"
|
||||
},
|
||||
"LAST_NODE": "n2",
|
||||
"OUT2": "Node n2 (ProviderCall) requires 'base_url' in config",
|
||||
"EXEC_TRACE": "n2(ProviderCall)"
|
||||
}
|
||||
}
|
||||
48
.agentui/vars/p_pc_while_out_macro.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"MSG": "abc123xyz",
|
||||
"WAS_ERROR__n2": true,
|
||||
"CYCLEINDEX__n2": 1,
|
||||
"snapshot": {
|
||||
"incoming": {
|
||||
"method": "POST",
|
||||
"url": "http://localhost/test",
|
||||
"path": "/test",
|
||||
"query": "",
|
||||
"headers": {
|
||||
"x": "X-HEADER"
|
||||
},
|
||||
"json": {}
|
||||
},
|
||||
"params": {
|
||||
"temperature": 0.25
|
||||
},
|
||||
"model": "gpt-x",
|
||||
"vendor_format": "openai",
|
||||
"system": "",
|
||||
"OUT": {
|
||||
"n1": {
|
||||
"vars": {
|
||||
"MSG": "abc123xyz"
|
||||
}
|
||||
},
|
||||
"n2": {
|
||||
"result": {
|
||||
"error": "Node n2 (ProviderCall) requires 'base_url' in config"
|
||||
},
|
||||
"response_text": "",
|
||||
"vars": {
|
||||
"WAS_ERROR__n2": true,
|
||||
"CYCLEINDEX__n2": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"OUT_TEXT": {
|
||||
"n1": "abc123xyz",
|
||||
"n2": "Node n2 (ProviderCall) requires 'base_url' in config"
|
||||
},
|
||||
"LAST_NODE": "n2",
|
||||
"OUT1": "abc123xyz",
|
||||
"OUT2": "Node n2 (ProviderCall) requires 'base_url' in config",
|
||||
"EXEC_TRACE": "n1(SetVars) -> n2(ProviderCall)"
|
||||
}
|
||||
}
|
||||
105
.agentui/vars/p_prompt_combine_claude.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"snapshot": {
|
||||
"incoming": {
|
||||
"method": "POST",
|
||||
"url": "http://localhost/test",
|
||||
"path": "/test",
|
||||
"query": "",
|
||||
"headers": {
|
||||
"x": "X-HEADER"
|
||||
},
|
||||
"json": {
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Системный-тест CLAUDE"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Прив"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Привет!"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"temperature": 0.25
|
||||
},
|
||||
"model": "gpt-x",
|
||||
"vendor_format": "openai",
|
||||
"system": "",
|
||||
"OUT": {
|
||||
"n1": {
|
||||
"result": {
|
||||
"echo": {
|
||||
"url": "http://mock.local/v1/messages",
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"payload": {
|
||||
"model": "gpt-x",
|
||||
"system": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Ты — Narrator-chan."
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Системный-тест CLAUDE"
|
||||
}
|
||||
],
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Системный-тест CLAUDE"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Прив"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Привет!"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "как лела"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"response_text": "http://mock.local/v1/messages"
|
||||
}
|
||||
},
|
||||
"OUT_TEXT": {
|
||||
"n1": "http://mock.local/v1/messages"
|
||||
},
|
||||
"LAST_NODE": "n1",
|
||||
"OUT1": "http://mock.local/v1/messages",
|
||||
"EXEC_TRACE": "n1(ProviderCall)"
|
||||
}
|
||||
}
|
||||
101
.agentui/vars/p_prompt_combine_gemini.json
Normal file
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"snapshot": {
|
||||
"incoming": {
|
||||
"method": "POST",
|
||||
"url": "http://localhost/test",
|
||||
"path": "/test",
|
||||
"query": "",
|
||||
"headers": {
|
||||
"x": "X-HEADER"
|
||||
},
|
||||
"json": {
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Системный-тест из входящего"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Its just me.."
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Reply from model"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"temperature": 0.25
|
||||
},
|
||||
"model": "gpt-x",
|
||||
"vendor_format": "openai",
|
||||
"system": "",
|
||||
"OUT": {
|
||||
"n1": {
|
||||
"result": {
|
||||
"echo": {
|
||||
"url": "http://mock.local/v1beta/models/gpt-x:generateContent",
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"payload": {
|
||||
"model": "gpt-x",
|
||||
"contents": [
|
||||
{
|
||||
"role": "user",
|
||||
"parts": [
|
||||
{
|
||||
"text": "Системный-тест из входящего"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"parts": [
|
||||
{
|
||||
"text": "Its just me.."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "model",
|
||||
"parts": [
|
||||
{
|
||||
"text": "Reply from model"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"parts": [
|
||||
{
|
||||
"text": "как лела"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"systemInstruction": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Ты — Narrator-chan."
|
||||
},
|
||||
{
|
||||
"text": "Системный-тест из входящего"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response_text": "http://mock.local/v1beta/models/gpt-x:generateContent"
|
||||
}
|
||||
},
|
||||
"OUT_TEXT": {
|
||||
"n1": "http://mock.local/v1beta/models/gpt-x:generateContent"
|
||||
},
|
||||
"LAST_NODE": "n1",
|
||||
"OUT1": "http://mock.local/v1beta/models/gpt-x:generateContent",
|
||||
"EXEC_TRACE": "n1(ProviderCall)"
|
||||
}
|
||||
}
|
||||
79
.agentui/vars/p_prompt_combine_openai.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"snapshot": {
|
||||
"incoming": {
|
||||
"method": "POST",
|
||||
"url": "http://localhost/test",
|
||||
"path": "/test",
|
||||
"query": "",
|
||||
"headers": {
|
||||
"x": "X-HEADER"
|
||||
},
|
||||
"json": {
|
||||
"contents": [
|
||||
{
|
||||
"role": "user",
|
||||
"parts": [
|
||||
{
|
||||
"text": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "model",
|
||||
"parts": [
|
||||
{
|
||||
"text": "B"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"temperature": 0.25
|
||||
},
|
||||
"model": "gpt-x",
|
||||
"vendor_format": "gemini",
|
||||
"system": "",
|
||||
"OUT": {
|
||||
"n1": {
|
||||
"result": {
|
||||
"echo": {
|
||||
"url": "http://mock.local/v1/chat/completions",
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"payload": {
|
||||
"model": "gpt-x",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Ты — Narrator-chan."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "как лела"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "A"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "B"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"response_text": "http://mock.local/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"OUT_TEXT": {
|
||||
"n1": "http://mock.local/v1/chat/completions"
|
||||
},
|
||||
"LAST_NODE": "n1",
|
||||
"OUT1": "http://mock.local/v1/chat/completions",
|
||||
"EXEC_TRACE": "n1(ProviderCall)"
|
||||
}
|
||||
}
|
||||
@@ -10,21 +10,27 @@
|
||||
"OUT": {
|
||||
"n1": {
|
||||
"result": {
|
||||
"error": {
|
||||
"message": "Incorrect API key provided: TEST. You can find your API key at https://platform.openai.com/account/api-keys.",
|
||||
"type": "invalid_request_error",
|
||||
"param": null,
|
||||
"code": "invalid_api_key"
|
||||
"echo": {
|
||||
"url": "https://api.openai.com/v1/chat/completions",
|
||||
"headers": {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer TEST"
|
||||
},
|
||||
"payload": {
|
||||
"model": "gpt-x",
|
||||
"messages": [],
|
||||
"temperature": 0.1
|
||||
}
|
||||
}
|
||||
},
|
||||
"response_text": ""
|
||||
"response_text": "https://api.openai.com/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"OUT_TEXT": {
|
||||
"n1": "Incorrect API key provided: TEST. You can find your API key at https://platform.openai.com/account/api-keys."
|
||||
"n1": "https://api.openai.com/v1/chat/completions"
|
||||
},
|
||||
"LAST_NODE": "n1",
|
||||
"OUT1": "Incorrect API key provided: TEST. You can find your API key at https://platform.openai.com/account/api-keys.",
|
||||
"OUT1": "https://api.openai.com/v1/chat/completions",
|
||||
"EXEC_TRACE": "n1(ProviderCall)"
|
||||
}
|
||||
}
|
||||
40
.agentui/vars/p_rf_while_ignore.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"WAS_ERROR__n1": true,
|
||||
"CYCLEINDEX__n1": 1,
|
||||
"snapshot": {
|
||||
"incoming": {
|
||||
"method": "POST",
|
||||
"url": "http://example.local/test",
|
||||
"path": "/test",
|
||||
"query": "",
|
||||
"headers": {
|
||||
"content-type": "text/plain"
|
||||
},
|
||||
"json": "raw-plain-body-simulated"
|
||||
},
|
||||
"params": {
|
||||
"temperature": 0.25
|
||||
},
|
||||
"model": "gpt-x",
|
||||
"vendor_format": "openai",
|
||||
"system": "",
|
||||
"OUT": {
|
||||
"n1": {
|
||||
"result": {
|
||||
"error": "Node n1 (RawForward): 'base_url' is not configured and vendor could not be detected."
|
||||
},
|
||||
"response_text": "",
|
||||
"vars": {
|
||||
"WAS_ERROR__n1": true,
|
||||
"CYCLEINDEX__n1": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"OUT_TEXT": {
|
||||
"n1": "Node n1 (RawForward): 'base_url' is not configured and vendor could not be detected."
|
||||
},
|
||||
"LAST_NODE": "n1",
|
||||
"OUT1": "Node n1 (RawForward): 'base_url' is not configured and vendor could not be detected.",
|
||||
"EXEC_TRACE": "n1(RawForward)"
|
||||
}
|
||||
}
|
||||
@@ -1,283 +1,7 @@
|
||||
{
|
||||
"Test": "Быбра",
|
||||
"Clod": "igrovik",
|
||||
"MyOpenAiKey": "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",
|
||||
"snapshot": {
|
||||
"incoming": {
|
||||
"method": "POST",
|
||||
"url": "http://127.0.0.1:7860/v1beta/models/gemini-2.5-pro:generateContent?key=%2A%2A%2A",
|
||||
"path": "/v1beta/models/gemini-2.5-pro:generateContent",
|
||||
"query": "key=AIzaSyCEGd93Mo0xxb15W7nbyFk6EoK4dXs2_ys",
|
||||
"headers": {
|
||||
"accept": "*/*",
|
||||
"accept-encoding": "gzip, deflate, br",
|
||||
"content-length": "1481",
|
||||
"content-type": "application/json",
|
||||
"user-agent": "node-fetch",
|
||||
"host": "127.0.0.1:7860",
|
||||
"connection": "keep-alive"
|
||||
},
|
||||
"json": {
|
||||
"contents": [
|
||||
{
|
||||
"role": "model",
|
||||
"parts": [
|
||||
{
|
||||
"text": "Narrator-chan is a cute girl who love to help 9uoioi in any tasks in this chat.\nNarrator-chan have emotions. So she's humanized. She's not rush things, attentive, sensual.\n\nNarrator-chan and 9uoioi actually spent a lot of time together playing out different rp scenarios (even smutty ones), Narrator-chan actually played out different characters and led the story, and 9uoioi was basically the main character. Well, that's just a little backstory.\nNarrator-chan will never refuse to perform even the most naughty tasks!"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"parts": [
|
||||
{
|
||||
"text": "Its just me..\n\nПрив"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "model",
|
||||
"parts": [
|
||||
{
|
||||
"text": "9:50 PM\n\n**Narrator-chan:**\nОоо, привет-привет, 9uoioi! 🌙✨ Ты ворвался, как будто открыл дверь в тёмную комнату, и сразу стало светлее. Как настроение у тебя?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"parts": [
|
||||
{
|
||||
"text": "как лела"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"safetySettings": [
|
||||
{
|
||||
"category": "HARM_CATEGORY_HARASSMENT",
|
||||
"threshold": "OFF"
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_HATE_SPEECH",
|
||||
"threshold": "OFF"
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
"threshold": "OFF"
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
"threshold": "OFF"
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_CIVIC_INTEGRITY",
|
||||
"threshold": "OFF"
|
||||
}
|
||||
],
|
||||
"generationConfig": {
|
||||
"candidateCount": 1,
|
||||
"maxOutputTokens": 32000,
|
||||
"temperature": 0.85,
|
||||
"topP": 0.95,
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": false,
|
||||
"thinkingBudget": 16000
|
||||
}
|
||||
},
|
||||
"model": "gemini-2.5-pro"
|
||||
},
|
||||
"query_params": {
|
||||
"key": "AIzaSyCEGd93Mo0xxb15W7nbyFk6EoK4dXs2_ys"
|
||||
},
|
||||
"api_keys": {
|
||||
"authorization": null,
|
||||
"key": "AIzaSyCEGd93Mo0xxb15W7nbyFk6EoK4dXs2_ys"
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"temperature": 0.85,
|
||||
"max_tokens": 32000,
|
||||
"top_p": 0.95,
|
||||
"stop": null
|
||||
},
|
||||
"model": "gemini-2.5-pro",
|
||||
"vendor_format": "gemini",
|
||||
"system": "",
|
||||
"OUT": {
|
||||
"n5": {
|
||||
"vars": {
|
||||
"Test": "Быбра",
|
||||
"MyOpenAiKey": "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"
|
||||
}
|
||||
},
|
||||
"n1": {
|
||||
"result": {
|
||||
"error": {
|
||||
"code": 503,
|
||||
"message": "The model is overloaded. Please try again later.",
|
||||
"status": "UNAVAILABLE"
|
||||
}
|
||||
}
|
||||
},
|
||||
"n2": {
|
||||
"result": {
|
||||
"id": "chatcmpl-CEcNB9bBsTCeM4sAboc1YN5tkkK8N",
|
||||
"object": "chat.completion",
|
||||
"created": 1757600133,
|
||||
"model": "gpt-5-chat-latest",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "Конечно! Вот несколько более «красивых» и дружелюбных вариантов для сообщения:\n\n- **«Модель сейчас перегружена. Пожалуйста, попробуйте ещё раз чуть позже.»** \n- **«Извините, сейчас слишком много запросов. Попробуйте повторить попытку позже.»** \n- **«Сервис временно занят. Вернитесь через минутку — и всё получится!»** \n- **«Мы немного перегружены. Попробуйте снова через некоторое время.»** \n\nХотите, я сделаю варианты в более официальном стиле или в дружеском/игривом?",
|
||||
"refusal": null,
|
||||
"annotations": []
|
||||
},
|
||||
"logprobs": null,
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 22,
|
||||
"completion_tokens": 131,
|
||||
"total_tokens": 153,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0,
|
||||
"audio_tokens": 0
|
||||
},
|
||||
"completion_tokens_details": {
|
||||
"reasoning_tokens": 0,
|
||||
"audio_tokens": 0,
|
||||
"accepted_prediction_tokens": 0,
|
||||
"rejected_prediction_tokens": 0
|
||||
}
|
||||
},
|
||||
"service_tier": "default",
|
||||
"system_fingerprint": "fp_9e822d521d"
|
||||
},
|
||||
"response_text": "Конечно! Вот несколько более «красивых» и дружелюбных вариантов для сообщения:\n\n- **«Модель сейчас перегружена. Пожалуйста, попробуйте ещё раз чуть позже.»** \n- **«Извините, сейчас слишком много запросов. Попробуйте повторить попытку позже.»** \n- **«Сервис временно занят. Вернитесь через минутку — и всё получится!»** \n- **«Мы немного перегружены. Попробуйте снова через некоторое время.»** \n\nХотите, я сделаю варианты в более официальном стиле или в дружеском/игривом?"
|
||||
},
|
||||
"n3": {
|
||||
"result": {
|
||||
"id": "chatcmpl-CEcNBtWmlFV02su3z1F87NebL1lUZ",
|
||||
"object": "chat.completion",
|
||||
"created": 1757600133,
|
||||
"model": "gpt-5-chat-latest",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "Конечно! Вот несколько вариантов, как можно сделать сообщение «The model is overloaded. Please try again later.» более красивым и дружелюбным:\n\n---\n\n✨ **The system is a little busy right now. Please try again in a moment!** ✨ \n\n---\n\n🚀 **Our servers are working at full speed. Please come back shortly!** 🚀 \n\n---\n\n🌸 **Oops! The model is taking a short break. Try again a bit later. 🌸** \n\n---\n\n⚡ **We’re experiencing high demand. Please retry soon!** ⚡ \n\n---\n\nХотите, я оформлю сообщение в стиле «статус-страницы» (с иконками и цветом), или больше как дружелюбное уведомление в приложении?",
|
||||
"refusal": null,
|
||||
"annotations": []
|
||||
},
|
||||
"logprobs": null,
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 22,
|
||||
"completion_tokens": 157,
|
||||
"total_tokens": 179,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0,
|
||||
"audio_tokens": 0
|
||||
},
|
||||
"completion_tokens_details": {
|
||||
"reasoning_tokens": 0,
|
||||
"audio_tokens": 0,
|
||||
"accepted_prediction_tokens": 0,
|
||||
"rejected_prediction_tokens": 0
|
||||
}
|
||||
},
|
||||
"service_tier": "default",
|
||||
"system_fingerprint": "fp_f08ac7f551"
|
||||
},
|
||||
"response_text": "Конечно! Вот несколько вариантов, как можно сделать сообщение «The model is overloaded. Please try again later.» более красивым и дружелюбным:\n\n---\n\n✨ **The system is a little busy right now. Please try again in a moment!** ✨ \n\n---\n\n🚀 **Our servers are working at full speed. Please come back shortly!** 🚀 \n\n---\n\n🌸 **Oops! The model is taking a short break. Try again a bit later. 🌸** \n\n---\n\n⚡ **We’re experiencing high demand. Please retry soon!** ⚡ \n\n---\n\nХотите, я оформлю сообщение в стиле «статус-страницы» (с иконками и цветом), или больше как дружелюбное уведомление в приложении?"
|
||||
},
|
||||
"n6": {
|
||||
"result": {
|
||||
"id": "chatcmpl-CEcNGpbOkHWhwyh3qYKRjV5nAWlEa",
|
||||
"object": "chat.completion",
|
||||
"created": 1757600138,
|
||||
"model": "gpt-5-chat-latest",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "Конечно! Вот объединённый и более изящно оформленный вариант сообщения: \n\n---\n\n🌐 **Система сейчас немного перегружена.** \nПожалуйста, попробуйте ещё раз чуть позже — мы уже работаем над тем, чтобы всё снова стало быстрым и удобным! 🚀 \n\n---\n\n✨ **Спасибо за ваше терпение!** \nВаш запрос очень важен для нас. Совсем скоро сервис будет доступен в полной мере. 🌸 \n\n---\n\nКрасиво",
|
||||
"refusal": null,
|
||||
"annotations": []
|
||||
},
|
||||
"logprobs": null,
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 185,
|
||||
"completion_tokens": 99,
|
||||
"total_tokens": 284,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0,
|
||||
"audio_tokens": 0
|
||||
},
|
||||
"completion_tokens_details": {
|
||||
"reasoning_tokens": 0,
|
||||
"audio_tokens": 0,
|
||||
"accepted_prediction_tokens": 0,
|
||||
"rejected_prediction_tokens": 0
|
||||
}
|
||||
},
|
||||
"service_tier": "default",
|
||||
"system_fingerprint": "fp_f08ac7f551"
|
||||
},
|
||||
"response_text": "Конечно! Вот объединённый и более изящно оформленный вариант сообщения: \n\n---\n\n🌐 **Система сейчас немного перегружена.** \nПожалуйста, попробуйте ещё раз чуть позже — мы уже работаем над тем, чтобы всё снова стало быстрым и удобным! 🚀 \n\n---\n\n✨ **Спасибо за ваше терпение!** \nВаш запрос очень важен для нас. Совсем скоро сервис будет доступен в полной мере. 🌸 \n\n---\n\nКрасиво"
|
||||
},
|
||||
"n7": {
|
||||
"result": true,
|
||||
"true": true,
|
||||
"false": false
|
||||
},
|
||||
"n4": {
|
||||
"result": {
|
||||
"candidates": [
|
||||
{
|
||||
"content": {
|
||||
"role": "model",
|
||||
"parts": [
|
||||
{
|
||||
"text": "Конечно! Вот объединённый и более изящно оформленный вариант сообщения: \n\n---\n\n🌐 **Система сейчас немного перегружена.** \nПожалуйста, попробуйте ещё раз чуть позже — мы уже работаем над тем, чтобы всё снова стало быстрым и удобным! 🚀 \n\n---\n\n✨ **Спасибо за ваше терпение!** \nВаш запрос очень важен для нас. Совсем скоро сервис будет доступен в полной мере. 🌸 \n\n---\n\nКрасиво Быбра"
|
||||
}
|
||||
]
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
"modelVersion": "gemini-2.5-pro"
|
||||
},
|
||||
"response_text": "Конечно! Вот объединённый и более изящно оформленный вариант сообщения: \n\n---\n\n🌐 **Система сейчас немного перегружена.** \nПожалуйста, попробуйте ещё раз чуть позже — мы уже работаем над тем, чтобы всё снова стало быстрым и удобным! 🚀 \n\n---\n\n✨ **Спасибо за ваше терпение!** \nВаш запрос очень важен для нас. Совсем скоро сервис будет доступен в полной мере. 🌸 \n\n---\n\nКрасиво Быбра"
|
||||
}
|
||||
},
|
||||
"OUT_TEXT": {
|
||||
"n5": "Быбра",
|
||||
"n1": "The model is overloaded. Please try again later.",
|
||||
"n2": "Конечно! Вот несколько более «красивых» и дружелюбных вариантов для сообщения:\n\n- **«Модель сейчас перегружена. Пожалуйста, попробуйте ещё раз чуть позже.»** \n- **«Извините, сейчас слишком много запросов. Попробуйте повторить попытку позже.»** \n- **«Сервис временно занят. Вернитесь через минутку — и всё получится!»** \n- **«Мы немного перегружены. Попробуйте снова через некоторое время.»** \n\nХотите, я сделаю варианты в более официальном стиле или в дружеском/игривом?",
|
||||
"n3": "Конечно! Вот несколько вариантов, как можно сделать сообщение «The model is overloaded. Please try again later.» более красивым и дружелюбным:\n\n---\n\n✨ **The system is a little busy right now. Please try again in a moment!** ✨ \n\n---\n\n🚀 **Our servers are working at full speed. Please come back shortly!** 🚀 \n\n---\n\n🌸 **Oops! The model is taking a short break. Try again a bit later. 🌸** \n\n---\n\n⚡ **We’re experiencing high demand. Please retry soon!** ⚡ \n\n---\n\nХотите, я оформлю сообщение в стиле «статус-страницы» (с иконками и цветом), или больше как дружелюбное уведомление в приложении?",
|
||||
"n6": "Конечно! Вот объединённый и более изящно оформленный вариант сообщения: \n\n---\n\n🌐 **Система сейчас немного перегружена.** \nПожалуйста, попробуйте ещё раз чуть позже — мы уже работаем над тем, чтобы всё снова стало быстрым и удобным! 🚀 \n\n---\n\n✨ **Спасибо за ваше терпение!** \nВаш запрос очень важен для нас. Совсем скоро сервис будет доступен в полной мере. 🌸 \n\n---\n\nКрасиво",
|
||||
"n7": "",
|
||||
"n4": "Конечно! Вот объединённый и более изящно оформленный вариант сообщения: \n\n---\n\n🌐 **Система сейчас немного перегружена.** \nПожалуйста, попробуйте ещё раз чуть позже — мы уже работаем над тем, чтобы всё снова стало быстрым и удобным! 🚀 \n\n---\n\n✨ **Спасибо за ваше терпение!** \nВаш запрос очень важен для нас. Совсем скоро сервис будет доступен в полной мере. 🌸 \n\n---\n\nКрасиво Быбра"
|
||||
},
|
||||
"LAST_NODE": "n4",
|
||||
"OUT5": "Быбра",
|
||||
"OUT1": "The model is overloaded. Please try again later.",
|
||||
"OUT2": "Конечно! Вот несколько более «красивых» и дружелюбных вариантов для сообщения:\n\n- **«Модель сейчас перегружена. Пожалуйста, попробуйте ещё раз чуть позже.»** \n- **«Извините, сейчас слишком много запросов. Попробуйте повторить попытку позже.»** \n- **«Сервис временно занят. Вернитесь через минутку — и всё получится!»** \n- **«Мы немного перегружены. Попробуйте снова через некоторое время.»** \n\nХотите, я сделаю варианты в более официальном стиле или в дружеском/игривом?",
|
||||
"OUT3": "Конечно! Вот несколько вариантов, как можно сделать сообщение «The model is overloaded. Please try again later.» более красивым и дружелюбным:\n\n---\n\n✨ **The system is a little busy right now. Please try again in a moment!** ✨ \n\n---\n\n🚀 **Our servers are working at full speed. Please come back shortly!** 🚀 \n\n---\n\n🌸 **Oops! The model is taking a short break. Try again a bit later. 🌸** \n\n---\n\n⚡ **We’re experiencing high demand. Please retry soon!** ⚡ \n\n---\n\nХотите, я оформлю сообщение в стиле «статус-страницы» (с иконками и цветом), или больше как дружелюбное уведомление в приложении?",
|
||||
"OUT6": "Конечно! Вот объединённый и более изящно оформленный вариант сообщения: \n\n---\n\n🌐 **Система сейчас немного перегружена.** \nПожалуйста, попробуйте ещё раз чуть позже — мы уже работаем над тем, чтобы всё снова стало быстрым и удобным! 🚀 \n\n---\n\n✨ **Спасибо за ваше терпение!** \nВаш запрос очень важен для нас. Совсем скоро сервис будет доступен в полной мере. 🌸 \n\n---\n\nКрасиво",
|
||||
"OUT7": "",
|
||||
"OUT4": "Конечно! Вот объединённый и более изящно оформленный вариант сообщения: \n\n---\n\n🌐 **Система сейчас немного перегружена.** \nПожалуйста, попробуйте ещё раз чуть позже — мы уже работаем над тем, чтобы всё снова стало быстрым и удобным! 🚀 \n\n---\n\n✨ **Спасибо за ваше терпение!** \nВаш запрос очень важен для нас. Совсем скоро сервис будет доступен в полной мере. 🌸 \n\n---\n\nКрасиво Быбра",
|
||||
"EXEC_TRACE": "n5(SetVars) -> n1(RawForward) -> n2(ProviderCall) -> n3(ProviderCall) -> n6(ProviderCall) -> If (#n7) [[OUT6]] contains \"Красиво\" => true -> n4(Return)"
|
||||
}
|
||||
"NAMETest": 192,
|
||||
"WAS_ERROR__n3": true,
|
||||
"CYCLEINDEX__n3": 0
|
||||
}
|
||||
40
.gitattributes
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
# Normalize text files by default
|
||||
* text=auto
|
||||
|
||||
# Force LF for source/config
|
||||
*.sh text eol=lf
|
||||
*.py text eol=lf
|
||||
*.js text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.jsx text eol=lf
|
||||
*.tsx text eol=lf
|
||||
*.css text eol=lf
|
||||
*.scss text eol=lf
|
||||
*.html text eol=lf
|
||||
*.json text eol=lf
|
||||
*.md text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.toml text eol=lf
|
||||
*.ini text eol=lf
|
||||
*.cfg text eol=lf
|
||||
*.txt text eol=lf
|
||||
|
||||
# Force CRLF for Windows scripts
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
|
||||
# Treat binaries as binary (no EOL conversions)
|
||||
*.png -text
|
||||
*.jpg -text
|
||||
*.jpeg -text
|
||||
*.gif -text
|
||||
*.webp -text
|
||||
*.pdf -text
|
||||
*.ico -text
|
||||
*.woff -text
|
||||
*.woff2 -text
|
||||
*.eot -text
|
||||
*.ttf -text
|
||||
*.otf -text
|
||||
3
.gitignore
vendored
@@ -41,8 +41,7 @@ Thumbs.db
|
||||
*.log
|
||||
agentui.log
|
||||
|
||||
# proxy
|
||||
proxy.txt
|
||||
|
||||
|
||||
# Local config
|
||||
.env
|
||||
|
||||
@@ -29,13 +29,29 @@
|
||||
|
||||
Быстрый старт
|
||||
|
||||
Вариант А (Windows):
|
||||
- Откройте файл [`run_agentui.bat`](run_agentui.bat) — он сам поставит зависимости и откроет редактор.
|
||||
Вариант A (Windows, авто‑настройка .venv):
|
||||
- Запустите [run_agentui.bat](run_agentui.bat) двойным кликом или из консоли.
|
||||
- Скрипт сам:
|
||||
- создаст локальное окружение .venv в каталоге проекта;
|
||||
- обновит pip;
|
||||
- установит зависимости из [requirements.txt](requirements.txt);
|
||||
- поднимет сервер и откроет редактор в браузере.
|
||||
- Переменные окружения (опционально перед запуском): HOST=127.0.0.1 PORT=7860
|
||||
|
||||
Вариант Б (любой ОС):
|
||||
- Установите Python 3.10+ и выполните:
|
||||
- pip install -r [`requirements.txt`](requirements.txt)
|
||||
- python -m uvicorn agentui.api.server:app --host 127.0.0.1 --port 7860
|
||||
Вариант B (Linux/macOS, авто‑настройка .venv):
|
||||
- Сделайте исполняемым и запустите:
|
||||
- chmod +x [run_agentui.sh](run_agentui.sh)
|
||||
- ./run_agentui.sh
|
||||
- Скрипт сделает то же самое: .venv + установка зависимостей + старт сервера.
|
||||
|
||||
Вариант C (ручной запуск, если хотите контролировать шаги):
|
||||
- Установите Python 3.10+.
|
||||
- Создайте и активируйте .venv:
|
||||
- Windows (cmd): py -m venv .venv && .\.venv\Scripts\activate
|
||||
- Linux/macOS (bash): python3 -m venv .venv && source .venv/bin/activate
|
||||
- Установите зависимости и стартуйте сервер:
|
||||
- pip install -r [requirements.txt](requirements.txt)
|
||||
- python -m uvicorn agentui.api.server:app --host 127.0.0.1 --port 7860
|
||||
|
||||
Откройте в браузере:
|
||||
- http://127.0.0.1:7860/ui/editor.html — визуальный редактор узлов
|
||||
|
||||
@@ -1,16 +1,37 @@
|
||||
from fastapi import FastAPI, Request, HTTPException, Query, Header
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import json
|
||||
from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode, unquote
|
||||
from fastapi.responses import JSONResponse, HTMLResponse, StreamingResponse
|
||||
from fastapi.responses import JSONResponse, HTMLResponse, StreamingResponse, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import os
|
||||
import hashlib
|
||||
import time
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Any, Dict, List, Literal, Optional
|
||||
from agentui.pipeline.executor import PipelineExecutor
|
||||
from agentui.pipeline.defaults import default_pipeline
|
||||
from agentui.pipeline.storage import load_pipeline, save_pipeline, list_presets, load_preset, save_preset, load_var_store
|
||||
from agentui.common.vendors import detect_vendor
|
||||
from agentui.common.cancel import request_cancel, clear_cancel, is_cancelled
|
||||
from agentui.pipeline.templating import render_template_simple
|
||||
# Manual resend support: use http client builder and executor helpers to sanitize/lookup originals
|
||||
from agentui.providers.http_client import build_client
|
||||
from agentui.pipeline.executor import (
|
||||
_sanitize_b64_for_log as _san_b64,
|
||||
_sanitize_json_string_for_log as _san_json_str,
|
||||
get_http_request as _get_http_req,
|
||||
)
|
||||
from agentui.common.manual_http import (
|
||||
parse_editable_http,
|
||||
dedupe_headers,
|
||||
content_type_is_json,
|
||||
normalize_jsonish_text,
|
||||
extract_json_trailing,
|
||||
try_parse_json,
|
||||
salvage_json_for_send,
|
||||
register_manual_request,
|
||||
)
|
||||
|
||||
|
||||
class UnifiedParams(BaseModel):
|
||||
@@ -174,35 +195,7 @@ def build_macro_context(u: UnifiedChatRequest, incoming: Optional[Dict[str, Any]
|
||||
}
|
||||
|
||||
|
||||
def jinja_render(template: str, ctx: Dict[str, Any]) -> str:
|
||||
# Чтобы не тянуть Jinja2 в MVP: простая {{ key.path }} замена
|
||||
def get_value(path: str, data: Dict[str, Any]) -> Any:
|
||||
cur: Any = data
|
||||
for part in path.split('.'):
|
||||
if isinstance(cur, dict):
|
||||
cur = cur.get(part, "")
|
||||
else:
|
||||
return ""
|
||||
return cur if isinstance(cur, (str, int, float)) else ""
|
||||
|
||||
out = template
|
||||
import re
|
||||
for m in re.findall(r"\{\{\s*([^}]+)\s*\}\}", template):
|
||||
expr = m.strip()
|
||||
# support simple default filter: {{ path|default(value) }}
|
||||
default_match = re.match(r"([^|]+)\|\s*default\((.*)\)", expr)
|
||||
if default_match:
|
||||
path = default_match.group(1).strip()
|
||||
fallback = default_match.group(2).strip()
|
||||
# strip quotes if present
|
||||
if (fallback.startswith("\"") and fallback.endswith("\"")) or (fallback.startswith("'") and fallback.endswith("'")):
|
||||
fallback = fallback[1:-1]
|
||||
raw_val = get_value(path, ctx)
|
||||
val = str(raw_val) if raw_val not in (None, "") else str(fallback)
|
||||
else:
|
||||
val = str(get_value(expr, ctx))
|
||||
out = out.replace("{{ "+m+" }}", val).replace("{{"+m+"}}", val)
|
||||
return out
|
||||
# jinja_render removed (duplication). Use agentui.pipeline.templating.render_template_simple instead.
|
||||
|
||||
|
||||
async def execute_pipeline_echo(u: UnifiedChatRequest) -> Dict[str, Any]:
|
||||
@@ -210,7 +203,7 @@ async def execute_pipeline_echo(u: UnifiedChatRequest) -> Dict[str, Any]:
|
||||
macro_ctx = build_macro_context(u)
|
||||
# PromptTemplate
|
||||
prompt_template = "System: {{ system }}\nUser: {{ chat.last_user }}"
|
||||
rendered_prompt = jinja_render(prompt_template, macro_ctx)
|
||||
rendered_prompt = render_template_simple(prompt_template, macro_ctx, {})
|
||||
# LLMInvoke (echo, т.к. без реального провайдера в MVP)
|
||||
llm_response_text = f"[echo by {u.model}]\n" + rendered_prompt
|
||||
# Дополняем эхо человекочитаемым трейсом выполнения пайплайна (если есть)
|
||||
@@ -273,10 +266,7 @@ def create_app() -> FastAPI:
|
||||
if not logger.handlers:
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setLevel(logging.INFO)
|
||||
file_handler = RotatingFileHandler("agentui.log", maxBytes=1_000_000, backupCount=3, encoding="utf-8")
|
||||
file_handler.setLevel(logging.INFO)
|
||||
logger.addHandler(stream_handler)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
# --- Simple in-process SSE hub (subscriptions per browser tab) ---
|
||||
import asyncio as _asyncio
|
||||
@@ -361,6 +351,77 @@ def create_app() -> FastAPI:
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
async def _run_pipeline_for_payload(request: Request, payload: Dict[str, Any], raw: Optional[bytes] = None) -> JSONResponse:
|
||||
# Единый обработчик: лог входящего запроса, нормализация, запуск PipelineExecutor, fallback-echo, лог ответа
|
||||
await _log_request(request, raw_body=raw, parsed=payload)
|
||||
unified = normalize_to_unified(payload)
|
||||
unified.stream = False
|
||||
|
||||
incoming = {
|
||||
"method": request.method,
|
||||
"url": _sanitize_url(str(request.url)),
|
||||
"path": request.url.path,
|
||||
"query": request.url.query,
|
||||
"headers": dict(request.headers),
|
||||
"json": payload,
|
||||
}
|
||||
macro_ctx = build_macro_context(unified, incoming=incoming)
|
||||
pipeline = load_pipeline()
|
||||
executor = PipelineExecutor(pipeline)
|
||||
|
||||
async def _trace(evt: Dict[str, Any]) -> None:
|
||||
try:
|
||||
base = {"pipeline_id": pipeline.get("id", "pipeline_editor")}
|
||||
await _trace_hub.publish({**base, **evt})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Диагностический INFO‑лог для валидации рефакторинга
|
||||
try:
|
||||
logger.info(
|
||||
"%s",
|
||||
json.dumps(
|
||||
{
|
||||
"event": "unified_handler",
|
||||
"vendor": unified.vendor_format,
|
||||
"model": unified.model,
|
||||
"pipeline_id": pipeline.get("id", "pipeline_editor"),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Mark pipeline start for UI and measure total active time
|
||||
t0 = time.perf_counter()
|
||||
try:
|
||||
await _trace_hub.publish({
|
||||
"event": "pipeline_start",
|
||||
"pipeline_id": pipeline.get("id", "pipeline_editor"),
|
||||
"ts": int(time.time() * 1000),
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
last = await executor.run(macro_ctx, trace=_trace)
|
||||
result = last.get("result") or await execute_pipeline_echo(unified)
|
||||
|
||||
# Mark pipeline end for UI
|
||||
t1 = time.perf_counter()
|
||||
try:
|
||||
await _trace_hub.publish({
|
||||
"event": "pipeline_done",
|
||||
"pipeline_id": pipeline.get("id", "pipeline_editor"),
|
||||
"ts": int(time.time() * 1000),
|
||||
"duration_ms": int((t1 - t0) * 1000),
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await _log_response(request, 200, result)
|
||||
return JSONResponse(result)
|
||||
|
||||
@app.get("/")
|
||||
async def index() -> HTMLResponse:
|
||||
html = (
|
||||
@@ -382,33 +443,7 @@ def create_app() -> FastAPI:
|
||||
payload = json.loads(raw or b"{}")
|
||||
except Exception: # noqa: BLE001
|
||||
raise HTTPException(status_code=400, detail="Invalid JSON")
|
||||
await _log_request(request, raw_body=raw, parsed=payload)
|
||||
unified = normalize_to_unified(payload)
|
||||
unified.stream = False # по требованию MVP без стриминга
|
||||
# контекст для пайплайна
|
||||
incoming = {
|
||||
"method": request.method,
|
||||
"url": _sanitize_url(str(request.url)),
|
||||
"path": request.url.path,
|
||||
"query": request.url.query,
|
||||
"headers": dict(request.headers),
|
||||
"json": payload,
|
||||
}
|
||||
macro_ctx = build_macro_context(unified, incoming=incoming)
|
||||
pipeline = load_pipeline()
|
||||
executor = PipelineExecutor(pipeline)
|
||||
|
||||
async def _trace(evt: Dict[str, Any]) -> None:
|
||||
try:
|
||||
base = {"pipeline_id": pipeline.get("id", "pipeline_editor")}
|
||||
await _trace_hub.publish({**base, **evt})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
last = await executor.run(macro_ctx, trace=_trace)
|
||||
result = last.get("result") or await execute_pipeline_echo(unified)
|
||||
await _log_response(request, 200, result)
|
||||
return JSONResponse(result)
|
||||
return await _run_pipeline_for_payload(request, payload, raw)
|
||||
|
||||
# Google AI Studio совместимые роуты (Gemini):
|
||||
# POST /v1beta/models/{model}:generateContent?key=...
|
||||
@@ -420,34 +455,10 @@ def create_app() -> FastAPI:
|
||||
payload = json.loads(raw or b"{}")
|
||||
except Exception: # noqa: BLE001
|
||||
raise HTTPException(status_code=400, detail="Invalid JSON")
|
||||
# Убедимся, что модель присутствует в полезной нагрузке
|
||||
if not isinstance(payload, dict):
|
||||
raise HTTPException(status_code=400, detail="Invalid payload type")
|
||||
payload = {**payload, "model": model}
|
||||
await _log_request(request, raw_body=raw, parsed=payload)
|
||||
unified = normalize_to_unified(payload)
|
||||
unified.stream = False
|
||||
incoming = {
|
||||
"method": request.method,
|
||||
"url": _sanitize_url(str(request.url)),
|
||||
"path": request.url.path,
|
||||
"query": request.url.query,
|
||||
"headers": dict(request.headers),
|
||||
"json": payload,
|
||||
}
|
||||
macro_ctx = build_macro_context(unified, incoming=incoming)
|
||||
pipeline = load_pipeline()
|
||||
executor = PipelineExecutor(pipeline)
|
||||
async def _trace(evt: Dict[str, Any]) -> None:
|
||||
try:
|
||||
base = {"pipeline_id": pipeline.get("id", "pipeline_editor")}
|
||||
await _trace_hub.publish({**base, **evt})
|
||||
except Exception:
|
||||
pass
|
||||
last = await executor.run(macro_ctx, trace=_trace)
|
||||
result = last.get("result") or await execute_pipeline_echo(unified)
|
||||
await _log_response(request, 200, result)
|
||||
return JSONResponse(result)
|
||||
return await _run_pipeline_for_payload(request, payload, raw)
|
||||
|
||||
@app.post("/v1/models/{model}:generateContent")
|
||||
async def gemini_generate_content_v1(model: str, request: Request, key: Optional[str] = Query(default=None)) -> JSONResponse: # noqa: ARG001
|
||||
@@ -459,30 +470,7 @@ def create_app() -> FastAPI:
|
||||
if not isinstance(payload, dict):
|
||||
raise HTTPException(status_code=400, detail="Invalid payload type")
|
||||
payload = {**payload, "model": model}
|
||||
await _log_request(request, raw_body=raw, parsed=payload)
|
||||
unified = normalize_to_unified(payload)
|
||||
unified.stream = False
|
||||
incoming = {
|
||||
"method": request.method,
|
||||
"url": _sanitize_url(str(request.url)),
|
||||
"path": request.url.path,
|
||||
"query": request.url.query,
|
||||
"headers": dict(request.headers),
|
||||
"json": payload,
|
||||
}
|
||||
macro_ctx = build_macro_context(unified, incoming=incoming)
|
||||
pipeline = load_pipeline()
|
||||
executor = PipelineExecutor(pipeline)
|
||||
async def _trace(evt: Dict[str, Any]) -> None:
|
||||
try:
|
||||
base = {"pipeline_id": pipeline.get("id", "pipeline_editor")}
|
||||
await _trace_hub.publish({**base, **evt})
|
||||
except Exception:
|
||||
pass
|
||||
last = await executor.run(macro_ctx, trace=_trace)
|
||||
result = last.get("result") or await execute_pipeline_echo(unified)
|
||||
await _log_response(request, 200, result)
|
||||
return JSONResponse(result)
|
||||
return await _run_pipeline_for_payload(request, payload, raw)
|
||||
|
||||
# Catch-all для случаев, когда двоеточие в пути закодировано как %3A
|
||||
@app.post("/v1beta/models/{rest_of_path:path}")
|
||||
@@ -499,30 +487,7 @@ def create_app() -> FastAPI:
|
||||
if not isinstance(payload, dict):
|
||||
raise HTTPException(status_code=400, detail="Invalid payload type")
|
||||
payload = {**payload, "model": model}
|
||||
await _log_request(request, raw_body=raw, parsed=payload)
|
||||
unified = normalize_to_unified(payload)
|
||||
unified.stream = False
|
||||
incoming = {
|
||||
"method": request.method,
|
||||
"url": _sanitize_url(str(request.url)),
|
||||
"path": request.url.path,
|
||||
"query": request.url.query,
|
||||
"headers": dict(request.headers),
|
||||
"json": payload,
|
||||
}
|
||||
macro_ctx = build_macro_context(unified, incoming=incoming)
|
||||
pipeline = load_pipeline()
|
||||
executor = PipelineExecutor(pipeline)
|
||||
async def _trace(evt: Dict[str, Any]) -> None:
|
||||
try:
|
||||
base = {"pipeline_id": pipeline.get("id", "pipeline_editor")}
|
||||
await _trace_hub.publish({**base, **evt})
|
||||
except Exception:
|
||||
pass
|
||||
last = await executor.run(macro_ctx, trace=_trace)
|
||||
result = last.get("result") or await execute_pipeline_echo(unified)
|
||||
await _log_response(request, 200, result)
|
||||
return JSONResponse(result)
|
||||
return await _run_pipeline_for_payload(request, payload, raw)
|
||||
|
||||
@app.post("/v1/models/{rest_of_path:path}")
|
||||
async def gemini_generate_content_v1_catchall(rest_of_path: str, request: Request, key: Optional[str] = Query(default=None)) -> JSONResponse: # noqa: ARG001
|
||||
@@ -538,30 +503,7 @@ def create_app() -> FastAPI:
|
||||
if not isinstance(payload, dict):
|
||||
raise HTTPException(status_code=400, detail="Invalid payload type")
|
||||
payload = {**payload, "model": model}
|
||||
await _log_request(request, raw_body=raw, parsed=payload)
|
||||
unified = normalize_to_unified(payload)
|
||||
unified.stream = False
|
||||
incoming = {
|
||||
"method": request.method,
|
||||
"url": _sanitize_url(str(request.url)),
|
||||
"path": request.url.path,
|
||||
"query": request.url.query,
|
||||
"headers": dict(request.headers),
|
||||
"json": payload,
|
||||
}
|
||||
macro_ctx = build_macro_context(unified, incoming=incoming)
|
||||
pipeline = load_pipeline()
|
||||
executor = PipelineExecutor(pipeline)
|
||||
async def _trace(evt: Dict[str, Any]) -> None:
|
||||
try:
|
||||
base = {"pipeline_id": pipeline.get("id", "pipeline_editor")}
|
||||
await _trace_hub.publish({**base, **evt})
|
||||
except Exception:
|
||||
pass
|
||||
last = await executor.run(macro_ctx, trace=_trace)
|
||||
result = last.get("result") or await execute_pipeline_echo(unified)
|
||||
await _log_response(request, 200, result)
|
||||
return JSONResponse(result)
|
||||
return await _run_pipeline_for_payload(request, payload, raw)
|
||||
|
||||
# Anthropic Claude messages endpoint compatibility
|
||||
@app.post("/v1/messages")
|
||||
@@ -573,37 +515,114 @@ def create_app() -> FastAPI:
|
||||
raise HTTPException(status_code=400, detail="Invalid JSON")
|
||||
if not isinstance(payload, dict):
|
||||
raise HTTPException(status_code=400, detail="Invalid payload type")
|
||||
# Помечаем как Anthropic, передаём версию из заголовка в payload для детекции
|
||||
if anthropic_version:
|
||||
payload = {**payload, "anthropic_version": anthropic_version}
|
||||
else:
|
||||
payload = {**payload, "anthropic_version": payload.get("anthropic_version", "2023-06-01")}
|
||||
await _log_request(request, raw_body=raw, parsed=payload)
|
||||
unified = normalize_to_unified(payload)
|
||||
unified.stream = False
|
||||
incoming = {
|
||||
"method": request.method,
|
||||
"url": _sanitize_url(str(request.url)),
|
||||
"path": request.url.path,
|
||||
"query": request.url.query,
|
||||
"headers": dict(request.headers),
|
||||
"json": payload,
|
||||
}
|
||||
macro_ctx = build_macro_context(unified, incoming=incoming)
|
||||
pipeline = load_pipeline()
|
||||
executor = PipelineExecutor(pipeline)
|
||||
async def _trace(evt: Dict[str, Any]) -> None:
|
||||
try:
|
||||
base = {"pipeline_id": pipeline.get("id", "pipeline_editor")}
|
||||
await _trace_hub.publish({**base, **evt})
|
||||
except Exception:
|
||||
pass
|
||||
last = await executor.run(macro_ctx, trace=_trace)
|
||||
result = last.get("result") or await execute_pipeline_echo(unified)
|
||||
await _log_response(request, 200, result)
|
||||
return JSONResponse(result)
|
||||
return await _run_pipeline_for_payload(request, payload, raw)
|
||||
app.mount("/ui", StaticFiles(directory="static", html=True), name="ui")
|
||||
|
||||
# NOTE: нельзя объявлять эндпоинты под /ui/* после монтирования StaticFiles(/ui),
|
||||
# т.к. монтирование перехватывает все пути под /ui. Используем отдельный путь /ui_version.
|
||||
@app.get("/ui_version")
|
||||
async def ui_version() -> JSONResponse:
|
||||
try:
|
||||
import time
|
||||
static_dir = os.path.abspath("static")
|
||||
editor_path = os.path.join(static_dir, "editor.html")
|
||||
js_ser_path = os.path.join(static_dir, "js", "serialization.js")
|
||||
js_pm_path = os.path.join(static_dir, "js", "pm-ui.js")
|
||||
|
||||
def md5p(p: str):
|
||||
try:
|
||||
with open(p, "rb") as f:
|
||||
return hashlib.md5(f.read()).hexdigest()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
payload = {
|
||||
"cwd": os.path.abspath("."),
|
||||
"static_dir": static_dir,
|
||||
"files": {
|
||||
"editor.html": md5p(editor_path),
|
||||
"js/serialization.js": md5p(js_ser_path),
|
||||
"js/pm-ui.js": md5p(js_pm_path),
|
||||
},
|
||||
"ts": int(time.time()),
|
||||
}
|
||||
return JSONResponse(payload, headers={"Cache-Control": "no-store"})
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500, headers={"Cache-Control": "no-store"})
|
||||
|
||||
# --- Favicon and PWA icons at root -----------------------------------------
|
||||
FAV_DIR = "favicon_io_saya"
|
||||
|
||||
@app.get("/favicon.ico")
|
||||
async def _favicon_ico():
|
||||
p = f"{FAV_DIR}/favicon.ico"
|
||||
try:
|
||||
return FileResponse(p, media_type="image/x-icon")
|
||||
except Exception:
|
||||
raise HTTPException(status_code=404, detail="favicon not found")
|
||||
|
||||
@app.get("/apple-touch-icon.png")
|
||||
async def _apple_touch_icon():
|
||||
p = f"{FAV_DIR}/apple-touch-icon.png"
|
||||
try:
|
||||
return FileResponse(p, media_type="image/png")
|
||||
except Exception:
|
||||
raise HTTPException(status_code=404, detail="apple-touch-icon not found")
|
||||
|
||||
@app.get("/favicon-32x32.png")
|
||||
async def _favicon_32():
|
||||
p = f"{FAV_DIR}/favicon-32x32.png"
|
||||
try:
|
||||
return FileResponse(p, media_type="image/png")
|
||||
except Exception:
|
||||
raise HTTPException(status_code=404, detail="favicon-32x32 not found")
|
||||
|
||||
@app.get("/favicon-16x16.png")
|
||||
async def _favicon_16():
|
||||
p = f"{FAV_DIR}/favicon-16x16.png"
|
||||
try:
|
||||
return FileResponse(p, media_type="image/png")
|
||||
except Exception:
|
||||
raise HTTPException(status_code=404, detail="favicon-16x16 not found")
|
||||
|
||||
@app.get("/android-chrome-192x192.png")
|
||||
async def _android_192():
|
||||
p = f"{FAV_DIR}/android-chrome-192x192.png"
|
||||
try:
|
||||
return FileResponse(p, media_type="image/png")
|
||||
except Exception:
|
||||
raise HTTPException(status_code=404, detail="android-chrome-192x192 not found")
|
||||
|
||||
@app.get("/android-chrome-512x512.png")
|
||||
async def _android_512():
|
||||
p = f"{FAV_DIR}/android-chrome-512x512.png"
|
||||
try:
|
||||
return FileResponse(p, media_type="image/png")
|
||||
except Exception:
|
||||
raise HTTPException(status_code=404, detail="android-chrome-512x512 not found")
|
||||
|
||||
@app.get("/site.webmanifest")
|
||||
async def _site_manifest():
|
||||
p = f"{FAV_DIR}/site.webmanifest"
|
||||
try:
|
||||
return FileResponse(p, media_type="application/manifest+json")
|
||||
except Exception:
|
||||
raise HTTPException(status_code=404, detail="site.webmanifest not found")
|
||||
|
||||
# Custom APNG favicon for "busy" state in UI
|
||||
@app.get("/saya1.png")
|
||||
async def _apng_busy_icon():
|
||||
p = f"{FAV_DIR}/saya1.png"
|
||||
try:
|
||||
# APNG served as image/png is acceptable for browsers
|
||||
return FileResponse(p, media_type="image/png")
|
||||
except Exception:
|
||||
raise HTTPException(status_code=404, detail="saya1.png not found")
|
||||
|
||||
# Variable store API (per-pipeline)
|
||||
@app.get("/admin/vars")
|
||||
async def get_vars() -> JSONResponse:
|
||||
@@ -639,7 +658,37 @@ def create_app() -> FastAPI:
|
||||
# Admin API для пайплайна
|
||||
@app.get("/admin/pipeline")
|
||||
async def get_pipeline() -> JSONResponse:
|
||||
return JSONResponse(load_pipeline())
|
||||
p = load_pipeline()
|
||||
# Диагностический лог состава meta (для подтверждения DRY-рефакторинга)
|
||||
try:
|
||||
meta_keys = [
|
||||
"id","name","parallel_limit","loop_mode","loop_max_iters","loop_time_budget_ms","clear_var_store",
|
||||
"http_timeout_sec","text_extract_strategy","text_extract_json_path","text_join_sep","text_extract_presets"
|
||||
]
|
||||
present = [k for k in meta_keys if k in p]
|
||||
meta_preview = {k: p.get(k) for k in present if k != "text_extract_presets"}
|
||||
presets_count = 0
|
||||
try:
|
||||
presets = p.get("text_extract_presets")
|
||||
if isinstance(presets, list):
|
||||
presets_count = len(presets)
|
||||
except Exception:
|
||||
presets_count = 0
|
||||
logger.info(
|
||||
"%s",
|
||||
json.dumps(
|
||||
{
|
||||
"event": "admin_get_pipeline_meta",
|
||||
"keys": present,
|
||||
"presets_count": presets_count,
|
||||
"meta_preview": meta_preview,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return JSONResponse(p)
|
||||
|
||||
@app.post("/admin/pipeline")
|
||||
async def set_pipeline(request: Request) -> JSONResponse:
|
||||
@@ -651,6 +700,37 @@ def create_app() -> FastAPI:
|
||||
# простая проверка
|
||||
if not isinstance(pipeline, dict) or "nodes" not in pipeline:
|
||||
raise HTTPException(status_code=400, detail="Invalid pipeline format")
|
||||
|
||||
# Диагностический лог входящих meta-ключей перед сохранением
|
||||
try:
|
||||
meta_keys = [
|
||||
"id","name","parallel_limit","loop_mode","loop_max_iters","loop_time_budget_ms","clear_var_store",
|
||||
"http_timeout_sec","text_extract_strategy","text_extract_json_path","text_join_sep","text_extract_presets"
|
||||
]
|
||||
present = [k for k in meta_keys if k in pipeline]
|
||||
meta_preview = {k: pipeline.get(k) for k in present if k != "text_extract_presets"}
|
||||
presets_count = 0
|
||||
try:
|
||||
presets = pipeline.get("text_extract_presets")
|
||||
if isinstance(presets, list):
|
||||
presets_count = len(presets)
|
||||
except Exception:
|
||||
presets_count = 0
|
||||
logger.info(
|
||||
"%s",
|
||||
json.dumps(
|
||||
{
|
||||
"event": "admin_set_pipeline_meta",
|
||||
"keys": present,
|
||||
"presets_count": presets_count,
|
||||
"meta_preview": meta_preview,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
save_pipeline(pipeline)
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
@@ -677,7 +757,430 @@ def create_app() -> FastAPI:
|
||||
raise HTTPException(status_code=400, detail="Invalid pipeline format")
|
||||
save_preset(name, payload)
|
||||
return JSONResponse({"ok": True})
|
||||
# --- SSE endpoint for live pipeline trace ---
|
||||
|
||||
# --- Manual cancel/clear for pipeline execution ---
|
||||
@app.post("/admin/cancel")
|
||||
async def admin_cancel() -> JSONResponse:
|
||||
"""
|
||||
Graceful cancel: do not interrupt in-flight operations; stop before next step.
|
||||
"""
|
||||
try:
|
||||
p = load_pipeline()
|
||||
pid = p.get("id", "pipeline_editor")
|
||||
except Exception:
|
||||
p = default_pipeline()
|
||||
pid = p.get("id", "pipeline_editor")
|
||||
try:
|
||||
request_cancel(pid, mode="graceful")
|
||||
except Exception:
|
||||
pass
|
||||
return JSONResponse({"ok": True, "pipeline_id": pid, "cancelled": True, "mode": "graceful"})
|
||||
|
||||
@app.post("/admin/cancel/abort")
|
||||
async def admin_cancel_abort() -> JSONResponse:
|
||||
"""
|
||||
Hard abort: attempt to interrupt in-flight operations immediately.
|
||||
"""
|
||||
try:
|
||||
p = load_pipeline()
|
||||
pid = p.get("id", "pipeline_editor")
|
||||
except Exception:
|
||||
p = default_pipeline()
|
||||
pid = p.get("id", "pipeline_editor")
|
||||
try:
|
||||
request_cancel(pid, mode="abort")
|
||||
except Exception:
|
||||
pass
|
||||
return JSONResponse({"ok": True, "pipeline_id": pid, "cancelled": True, "mode": "abort"})
|
||||
|
||||
@app.post("/admin/cancel/clear")
|
||||
async def admin_cancel_clear() -> JSONResponse:
|
||||
try:
|
||||
p = load_pipeline()
|
||||
pid = p.get("id", "pipeline_editor")
|
||||
except Exception:
|
||||
p = default_pipeline()
|
||||
pid = p.get("id", "pipeline_editor")
|
||||
try:
|
||||
clear_cancel(pid)
|
||||
except Exception:
|
||||
pass
|
||||
return JSONResponse({"ok": True, "pipeline_id": pid, "cancelled": False})
|
||||
|
||||
# --- Manual HTTP resend endpoint (Burp-like Repeater for Logs) -----------------
|
||||
@app.post("/admin/http/manual-send")
|
||||
async def manual_send(request: Request) -> JSONResponse:
|
||||
"""
|
||||
Re-send an HTTP request from Logs with optional edits from UI.
|
||||
|
||||
Accepts JSON:
|
||||
{
|
||||
"req_id": "original-req-id", // required to fetch original (untrimmed) body if available
|
||||
"request_text": "METHOD URL HTTP/1.1\\nH: V\\n\\n{...}", // optional raw edited HTTP text from UI
|
||||
"prefer_registry_original": true, // use untrimmed original JSON body where possible
|
||||
// Optional explicit overrides (take precedence over parsed request_text):
|
||||
"method": "POST",
|
||||
"url": "https://example/api",
|
||||
"headers": { "Authorization": "Bearer [[VAR:incoming.headers.authorization]]" },
|
||||
"body_text": "{...}" // explicit body text override (string)
|
||||
}
|
||||
|
||||
Behavior:
|
||||
- Parses request_text into method/url/headers/body if provided.
|
||||
- Looks up original untrimmed body_json by req_id from executor registry.
|
||||
- If prefer_registry_original and edited body parses as JSON — deep-merge it onto original JSON (dicts merged, lists replaced).
|
||||
- If prefer_registry_original and edited body contains human preview fragments (e.g. trimmed) or fails JSON parse — try to extract the last JSON object from text; else fallback to original body_json.
|
||||
- Resolves [[...]] and {{ ... }} macros (URL/headers/body) against last STORE snapshot (vars + snapshot.OUT/etc) of the pipeline.
|
||||
- Emits http_req/http_resp SSE with a fresh req_id ('manual-<ts>') so the original log is never overwritten.
|
||||
"""
|
||||
try:
|
||||
payload = await request.json()
|
||||
except Exception:
|
||||
payload = {}
|
||||
|
||||
# Parse edited HTTP text (Request area)
|
||||
def _parse_http_text(s: str) -> tuple[str, str, Dict[str, str], str]:
|
||||
method, url = "POST", ""
|
||||
headers: Dict[str, str] = {}
|
||||
body = ""
|
||||
try:
|
||||
if not isinstance(s, str) or not s.strip():
|
||||
return method, url, headers, body
|
||||
txt = s.replace("\r\n", "\n")
|
||||
lines = txt.split("\n")
|
||||
if not lines:
|
||||
return method, url, headers, body
|
||||
first = (lines[0] or "").strip()
|
||||
import re as _re
|
||||
m = _re.match(r"^([A-Z]+)\s+(\S+)(?:\s+HTTP/\d+(?:\.\d+)?)?$", first)
|
||||
i = 1
|
||||
if m:
|
||||
method = (m.group(1) or "POST").strip().upper()
|
||||
url = (m.group(2) or "").strip()
|
||||
else:
|
||||
i = 0 # no start line → treat as headers/body only
|
||||
|
||||
def _is_header_line(ln: str) -> bool:
|
||||
if ":" not in ln:
|
||||
return False
|
||||
name = ln.split(":", 1)[0].strip()
|
||||
# HTTP token: allow only letters/digits/hyphen. This prevents JSON lines like "contents": ... being treated as headers.
|
||||
return bool(_re.fullmatch(r"[A-Za-z0-9\\-]+", name))
|
||||
|
||||
# Read headers until a blank line OR until a non-header-looking line (start of body)
|
||||
while i < len(lines):
|
||||
ln = lines[i]
|
||||
if ln.strip() == "":
|
||||
i += 1
|
||||
break
|
||||
if not _is_header_line(ln):
|
||||
# Assume this and the rest is body (e.g., starts with {, [, or a quoted key)
|
||||
break
|
||||
k, v = ln.split(":", 1)
|
||||
headers[str(k).strip()] = str(v).strip()
|
||||
i += 1
|
||||
|
||||
# Remainder is the body (can be JSON or any text)
|
||||
body = "\\n".join(lines[i:]) if i < len(lines) else ""
|
||||
except Exception:
|
||||
pass
|
||||
return method, url, headers, body
|
||||
|
||||
# Lookup original (untrimmed) body by req_id
|
||||
orig: Optional[Dict[str, Any]] = None
|
||||
try:
|
||||
orig = _get_http_req(str(payload.get("req_id") or ""))
|
||||
except Exception:
|
||||
orig = None
|
||||
|
||||
# Pipeline meta (timeout) and pipeline id
|
||||
try:
|
||||
p = load_pipeline()
|
||||
default_pid = p.get("id", "pipeline_editor")
|
||||
timeout_sec = float(p.get("http_timeout_sec", 60) or 60)
|
||||
except Exception:
|
||||
default_pid = "pipeline_editor"
|
||||
timeout_sec = 60.0
|
||||
|
||||
pid = str((orig or {}).get("pipeline_id") or default_pid)
|
||||
|
||||
# Build macro context from STORE (last snapshot)
|
||||
try:
|
||||
store = load_var_store(pid) or {}
|
||||
except Exception:
|
||||
store = {}
|
||||
snapshot = store.get("snapshot") or {}
|
||||
ctx: Dict[str, Any] = {}
|
||||
try:
|
||||
ctx.update({
|
||||
"incoming": snapshot.get("incoming"),
|
||||
"params": snapshot.get("params"),
|
||||
"model": snapshot.get("model"),
|
||||
"vendor_format": snapshot.get("vendor_format"),
|
||||
"system": snapshot.get("system") or "",
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
ctx["OUT"] = snapshot.get("OUT") or {}
|
||||
except Exception:
|
||||
ctx["OUT"] = {}
|
||||
try:
|
||||
vmap = dict(store)
|
||||
vmap.pop("snapshot", None)
|
||||
ctx["vars"] = vmap
|
||||
ctx["store"] = store
|
||||
except Exception:
|
||||
ctx["vars"] = {}
|
||||
ctx["store"] = store or {}
|
||||
|
||||
# Extract overrides / edited request data
|
||||
edited_text = payload.get("request_text") or ""
|
||||
ov_method = payload.get("method")
|
||||
ov_url = payload.get("url")
|
||||
ov_headers = payload.get("headers") if isinstance(payload.get("headers"), dict) else None
|
||||
ov_body_text = payload.get("body_text")
|
||||
prefer_orig = bool(payload.get("prefer_registry_original", True))
|
||||
|
||||
# Parse HTTP text (safe)
|
||||
m_parsed, u_parsed, h_parsed, b_parsed = parse_editable_http(edited_text)
|
||||
|
||||
# Compose method/url/headers
|
||||
method = str(ov_method or m_parsed or (orig or {}).get("method") or "POST").upper()
|
||||
url = str(ov_url or u_parsed or (orig or {}).get("url") or "")
|
||||
# headers: start from original -> parsed -> explicit override
|
||||
headers: Dict[str, Any] = {}
|
||||
try:
|
||||
if isinstance((orig or {}).get("headers"), dict):
|
||||
headers.update(orig.get("headers") or {})
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
headers.update(h_parsed or {})
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if isinstance(ov_headers, dict):
|
||||
headers.update(ov_headers)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Render macros in URL and headers
|
||||
try:
|
||||
if url:
|
||||
url = render_template_simple(str(url), ctx, ctx.get("OUT") or {})
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
rendered_headers: Dict[str, Any] = {}
|
||||
for k, v in headers.items():
|
||||
try:
|
||||
rendered_headers[k] = render_template_simple(str(v), ctx, ctx.get("OUT") or {})
|
||||
except Exception:
|
||||
rendered_headers[k] = v
|
||||
headers = rendered_headers
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Normalize/dedupe headers (case-insensitive) and drop auto-calculated ones
|
||||
headers = dedupe_headers(headers)
|
||||
|
||||
# Determine body (JSON vs text), preserving original untrimmed JSON
|
||||
# Build orig_json (prefer registry; fallback parse from original body_text)
|
||||
orig_json = (orig or {}).get("body_json") if isinstance(orig, dict) else None
|
||||
if orig_json is None:
|
||||
try:
|
||||
ob = (orig or {}).get("body_text")
|
||||
except Exception:
|
||||
ob = None
|
||||
if isinstance(ob, str):
|
||||
try:
|
||||
ob_norm = normalize_jsonish_text(ob)
|
||||
except Exception:
|
||||
ob_norm = ob
|
||||
_oj = try_parse_json(ob_norm) or extract_json_trailing(ob_norm)
|
||||
if _oj is not None:
|
||||
orig_json = _oj
|
||||
|
||||
# Resolve body edits through macros
|
||||
raw_edited_body_text = ov_body_text if ov_body_text is not None else b_parsed
|
||||
try:
|
||||
edited_body_text_resolved = render_template_simple(str(raw_edited_body_text or ""), ctx, ctx.get("OUT") or {})
|
||||
except Exception:
|
||||
edited_body_text_resolved = str(raw_edited_body_text or "")
|
||||
|
||||
# Compute final_json / final_text using helper (handles normalization, salvage, prefer_registry_original, content-type)
|
||||
final_json, final_text = salvage_json_for_send(
|
||||
edited_body_text_resolved,
|
||||
headers,
|
||||
orig_json,
|
||||
prefer_orig
|
||||
)
|
||||
|
||||
# Diagnostic: summarize merge decision without leaking payload
|
||||
try:
|
||||
def _summ(v):
|
||||
try:
|
||||
if v is None:
|
||||
return {"t": "none"}
|
||||
if isinstance(v, dict):
|
||||
return {"t": "dict", "keys": len(v)}
|
||||
if isinstance(v, list):
|
||||
return {"t": "list", "len": len(v)}
|
||||
if isinstance(v, str):
|
||||
return {"t": "str", "len": len(v)}
|
||||
return {"t": type(v).__name__}
|
||||
except Exception:
|
||||
return {"t": "err"}
|
||||
|
||||
norm_dbg = normalize_jsonish_text(edited_body_text_resolved)
|
||||
edited_json_dbg = try_parse_json(norm_dbg) or extract_json_trailing(norm_dbg)
|
||||
|
||||
logger.info(
|
||||
"%s",
|
||||
json.dumps(
|
||||
{
|
||||
"event": "manual_send_merge_debug",
|
||||
"req_id_original": str(payload.get("req_id") or ""),
|
||||
"prefer_registry_original": prefer_orig,
|
||||
"headers_content_type": ("json" if content_type_is_json(headers) else "other"),
|
||||
"orig_json": _summ(orig_json),
|
||||
"edited_json": _summ(edited_json_dbg),
|
||||
"final": {
|
||||
"json": _summ(final_json),
|
||||
"text_len": (len(final_text) if isinstance(final_text, str) else None)
|
||||
},
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fresh req_id to avoid any overwrite of original log
|
||||
import time as _time
|
||||
rid = f"manual-{int(_time.time()*1000)}"
|
||||
|
||||
async def _publish(evt: Dict[str, Any]) -> None:
|
||||
try:
|
||||
await _trace_hub.publish(evt)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Prepare request body for logs (sanitized/trimmed for base64)
|
||||
if final_json is not None:
|
||||
try:
|
||||
body_text_for_log = json.dumps(_san_b64(final_json, max_len=180), ensure_ascii=False, indent=2)
|
||||
except Exception:
|
||||
body_text_for_log = json.dumps(final_json, ensure_ascii=False)
|
||||
else:
|
||||
try:
|
||||
body_text_for_log = _san_json_str(str(final_text or ""), max_len=180)
|
||||
except Exception:
|
||||
body_text_for_log = str(final_text or "")
|
||||
|
||||
# Register manual request in registry so subsequent "send" on this log has an original JSON source
|
||||
try:
|
||||
register_manual_request(rid, {
|
||||
"pipeline_id": pid,
|
||||
"node_id": "manual",
|
||||
"node_type": "Manual",
|
||||
"method": method,
|
||||
"url": url,
|
||||
"headers": dict(headers),
|
||||
"body_json": (final_json if final_json is not None else None),
|
||||
"body_text": (None if final_json is not None else str(final_text or "")),
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Emit http_req SSE (Manual)
|
||||
await _publish({
|
||||
"event": "http_req",
|
||||
"node_id": "manual",
|
||||
"node_type": "Manual",
|
||||
"provider": "manual",
|
||||
"req_id": rid,
|
||||
"method": method,
|
||||
"url": url,
|
||||
"headers": headers,
|
||||
"body_text": body_text_for_log,
|
||||
"ts": int(_time.time()*1000),
|
||||
})
|
||||
|
||||
# Perform HTTP
|
||||
async with build_client(timeout=timeout_sec) as client:
|
||||
# Ensure JSON Content-Type when sending JSON
|
||||
try:
|
||||
if final_json is not None:
|
||||
has_ct = any((str(k or "").lower() == "content-type") for k in headers.keys())
|
||||
if not has_ct:
|
||||
headers["Content-Type"] = "application/json"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
content = None
|
||||
try:
|
||||
if method in {"GET", "HEAD"}:
|
||||
content = None
|
||||
else:
|
||||
if final_json is not None:
|
||||
content = json.dumps(final_json, ensure_ascii=False).encode("utf-8")
|
||||
else:
|
||||
content = (final_text or "").encode("utf-8")
|
||||
except Exception:
|
||||
content = None
|
||||
|
||||
# Send
|
||||
try:
|
||||
resp = await client.request(method, url, headers=headers, content=content)
|
||||
except Exception as e:
|
||||
# Network/client error — emit http_resp with error text
|
||||
await _publish({
|
||||
"event": "http_resp",
|
||||
"node_id": "manual",
|
||||
"node_type": "Manual",
|
||||
"provider": "manual",
|
||||
"req_id": rid,
|
||||
"status": 0,
|
||||
"headers": {},
|
||||
"body_text": str(e),
|
||||
"ts": int(_time.time()*1000),
|
||||
})
|
||||
return JSONResponse({"ok": False, "error": str(e), "req_id": rid})
|
||||
|
||||
# Build response body for log (prefer JSON with trimmed base64)
|
||||
try:
|
||||
try:
|
||||
obj = resp.json()
|
||||
body_text_resp = json.dumps(_san_b64(obj, max_len=180), ensure_ascii=False, indent=2)
|
||||
except Exception:
|
||||
try:
|
||||
t = await resp.aread()
|
||||
body_text_resp = t.decode(getattr(resp, "encoding", "utf-8") or "utf-8", errors="replace")
|
||||
except Exception:
|
||||
try:
|
||||
body_text_resp = resp.text
|
||||
except Exception:
|
||||
body_text_resp = "<resp.decode error>"
|
||||
except Exception:
|
||||
body_text_resp = "<resp.decode error>"
|
||||
|
||||
await _publish({
|
||||
"event": "http_resp",
|
||||
"node_id": "manual",
|
||||
"node_type": "Manual",
|
||||
"provider": "manual",
|
||||
"req_id": rid,
|
||||
"status": int(getattr(resp, "status_code", 0)),
|
||||
"headers": dict(getattr(resp, "headers", {})),
|
||||
"body_text": body_text_resp,
|
||||
"ts": int(_time.time()*1000),
|
||||
})
|
||||
|
||||
return JSONResponse({"ok": True, "req_id": rid})
|
||||
|
||||
# --- SSE endpoint for live pipeline trace --- # --- SSE endpoint for live pipeline trace ---
|
||||
@app.get("/admin/trace/stream")
|
||||
async def sse_trace() -> StreamingResponse:
|
||||
loop = _asyncio.get_event_loop()
|
||||
|
||||
50
agentui/common/cancel.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
import threading
|
||||
|
||||
# Simple in-process cancel flags storage (per pipeline_id)
|
||||
# Thread-safe for FastAPI workers in same process
|
||||
_cancel_flags: Dict[str, bool] = {}
|
||||
# Mode of cancellation per pipeline: "graceful" (default) or "abort"
|
||||
_cancel_modes: Dict[str, str] = {}
|
||||
_lock = threading.Lock()
|
||||
|
||||
|
||||
def request_cancel(pipeline_id: str, mode: str = "graceful") -> None:
|
||||
"""Set cancel flag for given pipeline id with an optional mode.
|
||||
|
||||
mode:
|
||||
- "graceful": do not interrupt in-flight operations, stop before next step
|
||||
- "abort": attempt to cancel in-flight operations immediately
|
||||
"""
|
||||
pid = str(pipeline_id or "pipeline_editor")
|
||||
m = str(mode or "graceful").lower().strip()
|
||||
if m not in {"graceful", "abort"}:
|
||||
m = "graceful"
|
||||
with _lock:
|
||||
_cancel_flags[pid] = True
|
||||
_cancel_modes[pid] = m
|
||||
|
||||
|
||||
def clear_cancel(pipeline_id: str) -> None:
|
||||
"""Clear cancel flag for given pipeline id."""
|
||||
pid = str(pipeline_id or "pipeline_editor")
|
||||
with _lock:
|
||||
_cancel_flags.pop(pid, None)
|
||||
_cancel_modes.pop(pid, None)
|
||||
|
||||
|
||||
def is_cancelled(pipeline_id: str) -> bool:
|
||||
"""Check cancel flag for given pipeline id."""
|
||||
pid = str(pipeline_id or "pipeline_editor")
|
||||
with _lock:
|
||||
return bool(_cancel_flags.get(pid, False))
|
||||
|
||||
|
||||
def get_cancel_mode(pipeline_id: str) -> str:
|
||||
"""Return current cancel mode for given pipeline id: 'graceful' or 'abort' (default graceful)."""
|
||||
pid = str(pipeline_id or "pipeline_editor")
|
||||
with _lock:
|
||||
m = _cancel_modes.get(pid)
|
||||
return m if m in {"graceful", "abort"} else "graceful"
|
||||
415
agentui/common/manual_http.py
Normal file
@@ -0,0 +1,415 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
# Reuse executor's registry for original (untrimmed) requests
|
||||
try:
|
||||
from agentui.pipeline.executor import register_http_request as _reg_http_req # type: ignore
|
||||
except Exception: # pragma: no cover
|
||||
_reg_http_req = None # type: ignore
|
||||
|
||||
|
||||
# -------- HTTP editable text parser (safe) --------
|
||||
def parse_editable_http(s: str) -> Tuple[str, str, Dict[str, str], str]:
|
||||
"""
|
||||
Parse text pasted from Request area into (method, url, headers, body_text).
|
||||
Stops header parsing when a line is not a valid HTTP header key (prevents treating JSON like '"contents": ...' as header).
|
||||
"""
|
||||
method, url = "POST", ""
|
||||
headers: Dict[str, str] = {}
|
||||
body = ""
|
||||
try:
|
||||
if not isinstance(s, str) or not s.strip():
|
||||
return method, url, headers, body
|
||||
txt = s.replace("\r\n", "\n")
|
||||
lines = txt.split("\n")
|
||||
if not lines:
|
||||
return method, url, headers, body
|
||||
first = (lines[0] or "").strip()
|
||||
m = re.match(r"^([A-Z]+)\s+(\S+)(?:\s+HTTP/\d+(?:\.\d+)?)?$", first)
|
||||
i = 1
|
||||
if m:
|
||||
method = (m.group(1) or "POST").strip().upper()
|
||||
url = (m.group(2) or "").strip()
|
||||
else:
|
||||
i = 0 # no start-line -> treat as headers/body only
|
||||
|
||||
def _is_header_line(ln: str) -> bool:
|
||||
if ":" not in ln:
|
||||
return False
|
||||
name = ln.split(":", 1)[0].strip()
|
||||
# HTTP token: only letters/digits/hyphen. Prevents JSON keys like "contents": from being treated as headers.
|
||||
return bool(re.fullmatch(r"[A-Za-z0-9\-]+", name))
|
||||
|
||||
# Read headers until blank line OR until line not looking like header (start of body)
|
||||
while i < len(lines):
|
||||
ln = lines[i]
|
||||
if ln.strip() == "":
|
||||
i += 1
|
||||
break
|
||||
if not _is_header_line(ln):
|
||||
break
|
||||
k, v = ln.split(":", 1)
|
||||
headers[str(k).strip()] = str(v).strip()
|
||||
i += 1
|
||||
|
||||
# Remainder is body (JSON or text)
|
||||
body = "\n".join(lines[i:]) if i < len(lines) else ""
|
||||
except Exception:
|
||||
pass
|
||||
return method, url, headers, body
|
||||
|
||||
|
||||
# -------- Headers helpers --------
|
||||
def dedupe_headers(h: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Case-insensitive dedupe; drop Host/Content-Length (httpx will set proper).
|
||||
Last value wins.
|
||||
"""
|
||||
try:
|
||||
dedup: Dict[str, Tuple[str, Any]] = {}
|
||||
for k, v in (h or {}).items():
|
||||
lk = str(k).strip().lower()
|
||||
if lk in {"host", "content-length"}:
|
||||
continue
|
||||
dedup[lk] = (k, v)
|
||||
return {orig_k: val for (_, (orig_k, val)) in dedup.items()}
|
||||
except Exception:
|
||||
return dict(h or {})
|
||||
|
||||
|
||||
def content_type_is_json(h: Dict[str, Any]) -> bool:
|
||||
try:
|
||||
return any(str(k).lower() == "content-type" and "json" in str(v).lower() for k, v in (h or {}).items())
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# -------- JSON parsing & normalization helpers --------
|
||||
def try_parse_json(s: Any) -> Optional[Any]:
|
||||
try:
|
||||
if isinstance(s, (dict, list)):
|
||||
return s
|
||||
if isinstance(s, str) and s.strip():
|
||||
return json.loads(s)
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def normalize_jsonish_text(s: Any) -> str:
|
||||
"""
|
||||
Normalize JSON-looking text safely:
|
||||
- If whole text is a quoted JSON string, decode via json.loads to inner string.
|
||||
- Replace visible \\n/\\r/\\t outside JSON string literals with real control chars.
|
||||
- Escape raw CR/LF/TAB inside JSON string literals as \\n/\\r/\\t to keep JSON valid.
|
||||
"""
|
||||
try:
|
||||
txt = str(s if s is not None else "")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
# If whole text looks like a quoted JSON string: decode to inner string
|
||||
try:
|
||||
if len(txt) >= 2 and txt[0] == '"' and txt[-1] == '"':
|
||||
v = json.loads(txt)
|
||||
if isinstance(v, str):
|
||||
txt = v
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
out_chars = []
|
||||
i = 0
|
||||
n = len(txt)
|
||||
in_str = False
|
||||
esc = False
|
||||
while i < n:
|
||||
ch = txt[i]
|
||||
if in_str:
|
||||
# escape raw control chars within JSON string literal
|
||||
if ch == "\r":
|
||||
# CRLF -> \n
|
||||
if (i + 1) < n and txt[i + 1] == "\n":
|
||||
out_chars.append("\\n")
|
||||
i += 2
|
||||
esc = False
|
||||
continue
|
||||
out_chars.append("\\r")
|
||||
i += 1
|
||||
esc = False
|
||||
continue
|
||||
if ch == "\n":
|
||||
out_chars.append("\\n")
|
||||
i += 1
|
||||
esc = False
|
||||
continue
|
||||
if ch == "\t":
|
||||
out_chars.append("\\t")
|
||||
i += 1
|
||||
esc = False
|
||||
continue
|
||||
out_chars.append(ch)
|
||||
if esc:
|
||||
esc = False
|
||||
else:
|
||||
if ch == "\\":
|
||||
esc = True
|
||||
elif ch == '"':
|
||||
in_str = False
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# not in string literal
|
||||
if ch == '"':
|
||||
in_str = True
|
||||
out_chars.append(ch)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if ch == "\\" and (i + 1) < n:
|
||||
nx = txt[i + 1]
|
||||
if nx == "n":
|
||||
out_chars.append("\n")
|
||||
i += 2
|
||||
continue
|
||||
if nx == "r":
|
||||
out_chars.append("\r")
|
||||
i += 2
|
||||
continue
|
||||
if nx == "t":
|
||||
out_chars.append("\t")
|
||||
i += 2
|
||||
continue
|
||||
|
||||
out_chars.append(ch)
|
||||
i += 1
|
||||
|
||||
return "".join(out_chars)
|
||||
|
||||
|
||||
def extract_json_trailing(s: str) -> Optional[Any]:
|
||||
"""
|
||||
Pull trailing JSON object/array from mixed text:
|
||||
- Try whole text first
|
||||
- Then scan from last '{' or '[' backward.
|
||||
"""
|
||||
try:
|
||||
if not isinstance(s, str):
|
||||
return None
|
||||
txt = s.strip()
|
||||
try:
|
||||
return json.loads(txt)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
idx = txt.rfind("{")
|
||||
while idx >= 0:
|
||||
seg = txt[idx:]
|
||||
try:
|
||||
return json.loads(seg)
|
||||
except Exception:
|
||||
idx = txt.rfind("{", 0, idx)
|
||||
|
||||
idx = txt.rfind("[")
|
||||
while idx >= 0:
|
||||
seg = txt[idx:]
|
||||
try:
|
||||
return json.loads(seg)
|
||||
except Exception:
|
||||
idx = txt.rfind("[", 0, idx)
|
||||
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def global_unescape_jsonish(s: str) -> str:
|
||||
"""
|
||||
Last-resort: unicode_escape decode to convert \\n -> \n, \\" -> ", \\\\ -> \, \\uXXXX -> char, etc.
|
||||
"""
|
||||
try:
|
||||
import codecs as _codecs
|
||||
|
||||
return _codecs.decode(s, "unicode_escape")
|
||||
except Exception:
|
||||
try:
|
||||
return (
|
||||
s.replace("\\n", "\n")
|
||||
.replace("\\r", "\r")
|
||||
.replace("\\t", "\t")
|
||||
.replace('\\"', '"')
|
||||
.replace("\\\\", "\\")
|
||||
)
|
||||
except Exception:
|
||||
return s
|
||||
|
||||
|
||||
def looks_jsonish(txt: Any) -> bool:
|
||||
try:
|
||||
s = str(txt or "")
|
||||
if "{" in s or "[" in s:
|
||||
return True
|
||||
# also patterns like key:
|
||||
return bool(re.search(r'\s["\']?[A-Za-z0-9_\-]+["\']?\s*:', s))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def deep_merge_dicts(a: Any, b: Any) -> Any:
|
||||
"""
|
||||
Merge dicts (b over a, recursively). Lists or non-dicts are replaced by b.
|
||||
"""
|
||||
if isinstance(a, dict) and isinstance(b, dict):
|
||||
out = dict(a)
|
||||
for k, v in b.items():
|
||||
if (k in a) and isinstance(a.get(k), dict) and isinstance(v, dict):
|
||||
out[k] = deep_merge_dicts(a.get(k), v)
|
||||
else:
|
||||
out[k] = v
|
||||
return out
|
||||
return b
|
||||
|
||||
# ---- Trim-aware merge that preserves original binary/base64 fields ----
|
||||
def is_trimmed_b64_string(s: Any) -> bool:
|
||||
try:
|
||||
if not isinstance(s, str):
|
||||
return False
|
||||
return "(trimmed " in s
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def looks_base64ish(s: Any) -> bool:
|
||||
try:
|
||||
if not isinstance(s, str) or len(s) < 64:
|
||||
return False
|
||||
return bool(re.fullmatch(r"[A-Za-z0-9+/=\r\n]+", s))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def merge_lists_preserving_b64(orig_list: Any, edited_list: Any) -> Any:
|
||||
"""
|
||||
Merge lists with base64-trimmed preservation but DO NOT pad from original:
|
||||
- Result length equals edited_list length (indices beyond edited are dropped).
|
||||
- At each index:
|
||||
* If edited value is a trimmed placeholder string and original has a string → keep original.
|
||||
* If both dicts → recurse via deep_merge_preserving_b64.
|
||||
* If both lists → recurse via merge_lists_preserving_b64.
|
||||
* Else → take edited value as-is.
|
||||
"""
|
||||
if not isinstance(edited_list, list):
|
||||
return edited_list
|
||||
if not isinstance(orig_list, list):
|
||||
orig_list = []
|
||||
out = []
|
||||
for i, ev in enumerate(edited_list):
|
||||
ov = orig_list[i] if i < len(orig_list) else None
|
||||
if isinstance(ev, str) and is_trimmed_b64_string(ev) and isinstance(ov, str):
|
||||
out.append(ov)
|
||||
elif isinstance(ev, dict) and isinstance(ov, dict):
|
||||
out.append(deep_merge_preserving_b64(ov, ev))
|
||||
elif isinstance(ev, list) and isinstance(ov, list):
|
||||
out.append(merge_lists_preserving_b64(ov, ev))
|
||||
else:
|
||||
out.append(ev)
|
||||
return out
|
||||
|
||||
def deep_merge_preserving_b64(orig: Any, edited: Any) -> Any:
|
||||
"""
|
||||
Merge preserving original base64/data_url only for trimmed placeholders, with strict edited-shape:
|
||||
- If edited is a trimmed placeholder string and orig is a string → keep orig.
|
||||
- Dicts: RESULT CONTAINS ONLY KEYS FROM EDITED. Keys missing in edited are treated as deleted.
|
||||
For each present key: recurse (dict/list) or take edited value; for trimmed strings keep orig.
|
||||
- Lists: delegate to merge_lists_preserving_b64 (result length = edited length).
|
||||
- Other types: replace with edited.
|
||||
"""
|
||||
if isinstance(edited, str) and is_trimmed_b64_string(edited) and isinstance(orig, str):
|
||||
return orig
|
||||
if isinstance(orig, dict) and isinstance(edited, dict):
|
||||
out: Dict[str, Any] = {}
|
||||
for k, ev in edited.items():
|
||||
ov = orig.get(k)
|
||||
if isinstance(ev, str) and is_trimmed_b64_string(ev) and isinstance(ov, str):
|
||||
out[k] = ov
|
||||
elif isinstance(ev, dict) and isinstance(ov, dict):
|
||||
out[k] = deep_merge_preserving_b64(ov, ev)
|
||||
elif isinstance(ev, list) and isinstance(ov, list):
|
||||
out[k] = merge_lists_preserving_b64(ov, ev)
|
||||
else:
|
||||
out[k] = ev
|
||||
return out
|
||||
if isinstance(orig, list) and isinstance(edited, list):
|
||||
return merge_lists_preserving_b64(orig, edited)
|
||||
return edited
|
||||
|
||||
|
||||
def salvage_json_for_send(
|
||||
edited_body_text: Any,
|
||||
headers: Dict[str, Any],
|
||||
orig_json: Optional[Any],
|
||||
prefer_registry_original: bool = True,
|
||||
) -> Tuple[Optional[Any], Optional[str]]:
|
||||
"""
|
||||
Build (final_json, final_text) for outgoing request body.
|
||||
|
||||
Strategy:
|
||||
- Normalize text for JSON.
|
||||
- Try parse; then try trailing extract; then unicode_escape unescape and retry.
|
||||
- If prefer_registry_original=True and orig_json present:
|
||||
* If edited_json present: deep-merge with base64 preservation, but ONLY keep keys present in edited;
|
||||
lists are limited to the edited length (no padding from original).
|
||||
* If not: DO NOT resurrect original. Empty/whitespace → send empty text; otherwise send raw text as-is.
|
||||
- Else:
|
||||
* If edited_json present => final_json = edited_json
|
||||
* Else: if content-type is json and orig_json present => final_json = orig_json
|
||||
else send raw text.
|
||||
"""
|
||||
# Normalize and attempt parse
|
||||
norm = normalize_jsonish_text(edited_body_text)
|
||||
edited_json = try_parse_json(norm)
|
||||
if edited_json is None:
|
||||
edited_json = extract_json_trailing(norm)
|
||||
|
||||
if edited_json is None:
|
||||
ue = global_unescape_jsonish(str(edited_body_text or ""))
|
||||
if isinstance(ue, str) and ue != edited_body_text:
|
||||
ue_norm = normalize_jsonish_text(ue)
|
||||
edited_json = try_parse_json(ue_norm) or extract_json_trailing(ue_norm)
|
||||
|
||||
json_ct = content_type_is_json(headers)
|
||||
|
||||
# Prefer original registry JSON where applicable
|
||||
if prefer_registry_original and orig_json is not None:
|
||||
if edited_json is None:
|
||||
# Respect full manual control: do NOT resurrect original JSON.
|
||||
# Empty/whitespace → send empty text; otherwise send raw text as-is.
|
||||
if isinstance(norm, str) and not norm.strip():
|
||||
return None, ""
|
||||
else:
|
||||
return None, str(edited_body_text or "")
|
||||
else:
|
||||
# Merge edits over original with trimmed-b64 preservation, but keep only keys present in edited
|
||||
# and limit lists to the edited length.
|
||||
return deep_merge_preserving_b64(orig_json, edited_json), None
|
||||
|
||||
# No prefer or no orig_json
|
||||
if edited_json is not None:
|
||||
return edited_json, None
|
||||
|
||||
if json_ct and orig_json is not None:
|
||||
# Hard salvage for declared JSON payloads
|
||||
maybe = try_parse_json(norm) or extract_json_trailing(norm)
|
||||
return (maybe if maybe is not None else orig_json), None
|
||||
|
||||
# Plain text fallback
|
||||
return None, str(edited_body_text or "")
|
||||
|
||||
|
||||
# -------- Registry wrapper --------
|
||||
def register_manual_request(req_id: str, info: Dict[str, Any]) -> None:
|
||||
try:
|
||||
if _reg_http_req:
|
||||
_reg_http_req(req_id, info)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Optional, Union
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
import os
|
||||
|
||||
|
||||
def _parse_proxy_line(line: str) -> Optional[str]:
|
||||
@@ -39,6 +40,9 @@ def _read_proxy_from_file() -> Optional[str]:
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
# поддержим дополнительные ключи вида key=value в этом же файле (разберём ниже)
|
||||
if "=" in line:
|
||||
continue
|
||||
url = _parse_proxy_line(line)
|
||||
if url:
|
||||
return url
|
||||
@@ -59,3 +63,136 @@ def build_httpx_proxies() -> Optional[Dict[str, str]]:
|
||||
}
|
||||
|
||||
|
||||
def _read_kv_from_proxy_file() -> Dict[str, str]:
|
||||
"""
|
||||
Поддержка дополнительных опций в proxy.txt:
|
||||
ca=/полный/путь/к/burp-ca.pem
|
||||
verify=false # отключить проверку сертификатов (для отладки)
|
||||
"""
|
||||
out: Dict[str, str] = {}
|
||||
p = Path("proxy.txt")
|
||||
if not p.exists():
|
||||
return out
|
||||
try:
|
||||
for raw in p.read_text(encoding="utf-8").splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
out[k.strip().lower()] = v.strip()
|
||||
except Exception:
|
||||
return out
|
||||
return out
|
||||
|
||||
def _read_second_bare_flag_from_proxy() -> Optional[bool]:
|
||||
"""
|
||||
Читает «вторую голую строку» после URL в proxy.txt и интерпретирует как флаг verify:
|
||||
true/1/yes/on -> True
|
||||
false/0/no/off -> False
|
||||
Возвращает None, если строка отсутствует или не распознана.
|
||||
"""
|
||||
try:
|
||||
p = Path("proxy.txt")
|
||||
if not p.exists():
|
||||
return None
|
||||
lines = [ln.strip() for ln in p.read_text(encoding="utf-8").splitlines()]
|
||||
# найдём первую «URL» строку (без '=' и не пустую/коммент)
|
||||
idx_url = -1
|
||||
for i, ln in enumerate(lines):
|
||||
if not ln or ln.startswith("#") or "=" in ln:
|
||||
continue
|
||||
idx_url = i
|
||||
break
|
||||
if idx_url >= 0:
|
||||
# ищем следующую «голую» строку
|
||||
for j in range(idx_url + 1, len(lines)):
|
||||
ln = lines[j].strip()
|
||||
if not ln or ln.startswith("#") or "=" in ln:
|
||||
continue
|
||||
low = ln.lower()
|
||||
if low in ("1", "true", "yes", "on"):
|
||||
return True
|
||||
if low in ("0", "false", "no", "off"):
|
||||
return False
|
||||
# если это не похожее на флаг — считаем отсутствующим
|
||||
break
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
def get_tls_verify() -> Union[bool, str]:
|
||||
"""
|
||||
Возвращает значение для параметра httpx.AsyncClient(verify=...):
|
||||
- путь к PEM-бандлу (строка), если нашли ca=... или файл proxy-ca.pem в корне
|
||||
- False, если verify=false/insecure=1/AGENTUI_VERIFY=false
|
||||
- True по умолчанию
|
||||
- Новое: можно задать флаг второй «голой» строкой в proxy.txt (после URL прокси):
|
||||
пример:
|
||||
http:127.0.0.1:8888
|
||||
false
|
||||
или
|
||||
http:127.0.0.1:8888
|
||||
true
|
||||
"""
|
||||
# 1) Переменные окружения имеют приоритет
|
||||
env_verify = os.getenv("AGENTUI_VERIFY")
|
||||
if env_verify is not None and env_verify.strip().lower() in ("0", "false", "no", "off"):
|
||||
return False
|
||||
env_ca = os.getenv("AGENTUI_CA")
|
||||
if env_ca:
|
||||
path = Path(env_ca).expanduser()
|
||||
if path.exists():
|
||||
return str(path)
|
||||
|
||||
# 2) proxy.txt ключи
|
||||
kv = _read_kv_from_proxy_file()
|
||||
if kv.get("verify", "").lower() in ("0", "false", "no", "off"):
|
||||
return False
|
||||
if "ca" in kv:
|
||||
path = Path(kv["ca"]).expanduser()
|
||||
if path.exists():
|
||||
return str(path)
|
||||
# 2.1) Дополнительно: поддержка второй строки без ключа — true/false
|
||||
second = _read_second_bare_flag_from_proxy()
|
||||
if second is True:
|
||||
return True
|
||||
if second is False:
|
||||
return False
|
||||
|
||||
# 3) Файл по умолчанию в корне проекта
|
||||
default_ca = Path("proxy-ca.pem")
|
||||
if default_ca.exists():
|
||||
return str(default_ca)
|
||||
|
||||
# 4) По умолчанию строгая проверка
|
||||
return True
|
||||
|
||||
|
||||
def is_verify_explicit() -> bool:
|
||||
"""
|
||||
Возвращает True, если пользователь ЯВНО задал политику проверки TLS,
|
||||
чтобы клиент не переопределял её значением по умолчанию.
|
||||
Учитываются:
|
||||
- переменные окружения: AGENTUI_VERIFY, AGENTUI_CA
|
||||
- ключи в proxy.txt: verify=..., ca=...
|
||||
- файл proxy-ca.pem в корне проекта
|
||||
- Новое: «вторая голая строка» после URL в proxy.txt со значением true/false
|
||||
"""
|
||||
if os.getenv("AGENTUI_VERIFY") is not None:
|
||||
return True
|
||||
if os.getenv("AGENTUI_CA"):
|
||||
return True
|
||||
|
||||
kv = _read_kv_from_proxy_file()
|
||||
if "verify" in kv or "ca" in kv:
|
||||
return True
|
||||
# Вторая «голая» строка как явный флаг
|
||||
second = _read_second_bare_flag_from_proxy()
|
||||
if second is not None:
|
||||
return True
|
||||
|
||||
if Path("proxy-ca.pem").exists():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -11,17 +11,93 @@ PRESETS_DIR = Path("presets")
|
||||
VARS_DIR = Path(".agentui") / "vars"
|
||||
|
||||
|
||||
# DRY нормализация meta/пайплайна: единый источник дефолтов и типов
|
||||
def normalize_pipeline(pipeline: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Приводит верхнеуровневые ключи пайплайна к согласованному виду, заполняет дефолты.
|
||||
Безопасно к отсутствующим ключам и неверным типам.
|
||||
"""
|
||||
if not isinstance(pipeline, dict):
|
||||
pipeline = {}
|
||||
out: Dict[str, Any] = dict(pipeline)
|
||||
|
||||
def _to_int(v, d):
|
||||
try:
|
||||
n = int(v)
|
||||
return n if n > 0 else d
|
||||
except Exception:
|
||||
return d
|
||||
|
||||
def _to_float(v, d):
|
||||
try:
|
||||
n = float(v)
|
||||
return n if n > 0 else d
|
||||
except Exception:
|
||||
return d
|
||||
|
||||
# Базовые поля
|
||||
out["id"] = str(out.get("id") or "pipeline_editor")
|
||||
out["name"] = str(out.get("name") or "Edited Pipeline")
|
||||
out["parallel_limit"] = _to_int(out.get("parallel_limit"), 8)
|
||||
out["loop_mode"] = str(out.get("loop_mode") or "dag")
|
||||
out["loop_max_iters"] = _to_int(out.get("loop_max_iters"), 1000)
|
||||
out["loop_time_budget_ms"] = _to_int(out.get("loop_time_budget_ms"), 10000)
|
||||
out["clear_var_store"] = bool(out.get("clear_var_store", True))
|
||||
out["http_timeout_sec"] = _to_float(out.get("http_timeout_sec"), 60)
|
||||
|
||||
# Глобальные опции извлечения текста для [[OUTx]]
|
||||
out["text_extract_strategy"] = str(out.get("text_extract_strategy") or "auto")
|
||||
out["text_extract_json_path"] = str(out.get("text_extract_json_path") or "")
|
||||
# Поддержка разных написаний text_join_sep
|
||||
join_sep = out.get("text_join_sep")
|
||||
if join_sep is None:
|
||||
for k in list(out.keys()):
|
||||
if isinstance(k, str) and k.lower() == "text_join_sep":
|
||||
join_sep = out.get(k)
|
||||
break
|
||||
out["text_join_sep"] = str(join_sep or "\n")
|
||||
|
||||
# Пресеты парсинга
|
||||
presets = out.get("text_extract_presets")
|
||||
norm_presets: List[Dict[str, Any]] = []
|
||||
if isinstance(presets, list):
|
||||
for i, it in enumerate(presets):
|
||||
if not isinstance(it, dict):
|
||||
continue
|
||||
norm_presets.append({
|
||||
"id": str(it.get("id") or f"p{i}"),
|
||||
"name": str(it.get("name") or it.get("json_path") or "Preset"),
|
||||
"strategy": str(it.get("strategy") or "auto"),
|
||||
"json_path": str(it.get("json_path") or ""),
|
||||
"join_sep": str(it.get("join_sep") or "\n"),
|
||||
})
|
||||
out["text_extract_presets"] = norm_presets
|
||||
|
||||
# Узлы — список
|
||||
try:
|
||||
nodes = out.get("nodes") or []
|
||||
if not isinstance(nodes, list):
|
||||
nodes = []
|
||||
out["nodes"] = nodes
|
||||
except Exception:
|
||||
out["nodes"] = []
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def load_pipeline() -> Dict[str, Any]:
|
||||
if PIPELINE_FILE.exists():
|
||||
try:
|
||||
return json.loads(PIPELINE_FILE.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
pass
|
||||
return default_pipeline()
|
||||
if PIPELINE_FILE.exists():
|
||||
try:
|
||||
data = json.loads(PIPELINE_FILE.read_text(encoding="utf-8"))
|
||||
return normalize_pipeline(data)
|
||||
except Exception:
|
||||
pass
|
||||
return normalize_pipeline(default_pipeline())
|
||||
|
||||
|
||||
def save_pipeline(pipeline: Dict[str, Any]) -> None:
|
||||
PIPELINE_FILE.write_text(json.dumps(pipeline, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
norm = normalize_pipeline(pipeline or {})
|
||||
PIPELINE_FILE.write_text(json.dumps(norm, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def list_presets() -> List[str]:
|
||||
|
||||
@@ -35,6 +35,11 @@ _BARE_MACRO_RE = re.compile(r"\[\[\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[^\]]+?)?)\s*\]
|
||||
# Разбираем выражение до ближайшего '}}', допускаем '}' внутри (например в JSON-литералах)
|
||||
_BRACES_RE = re.compile(r"\{\{\s*(.*?)\s*\}\}", re.DOTALL)
|
||||
|
||||
# Сокращённый синтаксис: img(mime?)[[...]] → data:<mime>;base64,<resolved_inner_macro>
|
||||
# Пример: img()[[OUT1]] → data:image/png;base64,{{resolved OUT1}}
|
||||
# img(jpeg)[[OUT:n1.result...]] → data:image/jpeg;base64,{{resolved}}
|
||||
_IMG_WRAPPER_RE = re.compile(r"(?is)img\(\s*([^)]+?)?\s*\)\s*\[\[\s*(.+?)\s*\]\]")
|
||||
|
||||
|
||||
def _split_path(path: str) -> List[str]:
|
||||
return [p.strip() for p in str(path).split(".") if str(p).strip()]
|
||||
@@ -164,12 +169,21 @@ def _best_text_from_outputs(node_out: Any) -> str:
|
||||
# Gemini
|
||||
try:
|
||||
if isinstance(base, dict):
|
||||
cand0 = (base.get("candidates") or [{}])[0]
|
||||
content = cand0.get("content") or {}
|
||||
parts0 = (content.get("parts") or [{}])[0]
|
||||
t = parts0.get("text")
|
||||
if isinstance(t, str):
|
||||
return t
|
||||
cands = base.get("candidates") or []
|
||||
texts: List[str] = []
|
||||
for cand in cands:
|
||||
try:
|
||||
content = cand.get("content") or {}
|
||||
parts = content.get("parts") or []
|
||||
for p in parts:
|
||||
if isinstance(p, dict):
|
||||
t = p.get("text")
|
||||
if isinstance(t, str) and t.strip():
|
||||
texts.append(t.strip())
|
||||
except Exception:
|
||||
continue
|
||||
if texts:
|
||||
return "\n".join(texts)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -203,6 +217,47 @@ def render_template_simple(template: str, context: Dict[str, Any], out_map: Dict
|
||||
return ""
|
||||
s = str(template)
|
||||
|
||||
# 0) Сокращённый синтаксис: img(mime?)[[...]] → data:<mime>;base64,<resolved>
|
||||
# Выполняем до развёртки обычных [[...]] макросов, чтобы внутри можно было использовать любой квадратный макрос.
|
||||
def _normalize_mime(m: str) -> str:
|
||||
mm = (m or "").strip().lower()
|
||||
if not mm:
|
||||
return "image/png"
|
||||
if "/" in mm:
|
||||
return mm
|
||||
return {
|
||||
"png": "image/png",
|
||||
"jpg": "image/jpeg",
|
||||
"jpeg": "image/jpeg",
|
||||
"webp": "image/webp",
|
||||
"gif": "image/gif",
|
||||
"svg": "image/svg+xml",
|
||||
"bmp": "image/bmp",
|
||||
"tif": "image/tiff",
|
||||
"tiff": "image/tiff",
|
||||
}.get(mm, mm)
|
||||
|
||||
def _repl_imgwrap(m: re.Match) -> str:
|
||||
mime_raw = m.group(1) or ""
|
||||
inner = m.group(2) or ""
|
||||
mime = _normalize_mime(mime_raw)
|
||||
try:
|
||||
val = _resolve_square_macro_value(inner, context, out_map)
|
||||
except Exception:
|
||||
val = ""
|
||||
if isinstance(val, (dict, list, bool)) or val is None:
|
||||
val = _stringify_for_template(val)
|
||||
else:
|
||||
val = str(val)
|
||||
return f"data:{mime};base64,{val}"
|
||||
|
||||
# Поддерживаем много вхождений — повторяем до исчерпания (на случай каскадных макросов)
|
||||
while True:
|
||||
ns, cnt = _IMG_WRAPPER_RE.subn(_repl_imgwrap, s)
|
||||
s = ns
|
||||
if cnt == 0:
|
||||
break
|
||||
|
||||
# 1) Макросы [[VAR:...]] / [[OUT:...]] / [[STORE:...]]
|
||||
def repl_var(m: re.Match) -> str:
|
||||
path = m.group(1).strip()
|
||||
@@ -539,8 +594,24 @@ def _tokenize_condition_expr(expr: str, context: Dict[str, Any], out_map: Dict[s
|
||||
while j < n and (expr[j].isalnum() or expr[j] in "._"):
|
||||
j += 1
|
||||
word = expr[i:j]
|
||||
# Логические в словах не поддерживаем (используйте &&, ||, !)
|
||||
tokens.append(word)
|
||||
lw = word.lower()
|
||||
# Литералы: true/false/null (любая раскладка) → Python-константы
|
||||
if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", word) and lw in {"true", "false", "null"}:
|
||||
tokens.append("True" if lw == "true" else ("False" if lw == "false" else "None"))
|
||||
i = j
|
||||
continue
|
||||
# Поддержка «голых» идентификаторов из vars: cycleindex, WAS_ERROR и т.п.
|
||||
# Если это простой идентификатор (без точек) и он есть в context.vars — биндим его значением.
|
||||
try:
|
||||
vmap = context.get("vars") or {}
|
||||
except Exception:
|
||||
vmap = {}
|
||||
if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", word) and isinstance(vmap, dict) and word in vmap:
|
||||
name = add_binding(vmap.get(word))
|
||||
tokens.append(name)
|
||||
else:
|
||||
# Логические в словах не поддерживаем (используйте &&, ||, !)
|
||||
tokens.append(word)
|
||||
i = j
|
||||
continue
|
||||
|
||||
@@ -687,17 +758,19 @@ def _safe_eval_bool(py_expr: str, bindings: Dict[str, Any]) -> bool:
|
||||
if isinstance(node.op, ast.Not):
|
||||
return (not val)
|
||||
if isinstance(node, ast.BoolOp) and isinstance(node.op, tuple(allowed_boolops)):
|
||||
vals = [bool(eval_node(v)) for v in node.values]
|
||||
# Короткое замыкание:
|
||||
# AND — при первом False прекращаем и возвращаем False; иначе True
|
||||
# OR — при первом True прекращаем и возвращаем True; иначе False
|
||||
if isinstance(node.op, ast.And):
|
||||
res = True
|
||||
for v in vals:
|
||||
res = res and v
|
||||
return res
|
||||
for v in node.values:
|
||||
if not bool(eval_node(v)):
|
||||
return False
|
||||
return True
|
||||
if isinstance(node.op, ast.Or):
|
||||
res = False
|
||||
for v in vals:
|
||||
res = res or v
|
||||
return res
|
||||
for v in node.values:
|
||||
if bool(eval_node(v)):
|
||||
return True
|
||||
return False
|
||||
if isinstance(node, ast.Compare):
|
||||
left = eval_node(node.left)
|
||||
for opnode, comparator in zip(node.ops, node.comparators):
|
||||
|
||||
34
agentui/providers/adapters/__init__.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""
|
||||
Пакет адаптеров провайдеров для ProviderCall.
|
||||
|
||||
Экспортируем:
|
||||
- ProviderAdapter базовый класс
|
||||
- Реализации: OpenAIAdapter, GeminiAdapter, GeminiImageAdapter, ClaudeAdapter
|
||||
- Утилиты: default_base_url_for, insert_items, split_pos_spec
|
||||
"""
|
||||
|
||||
from .base import ( # [ProviderAdapter](agentui/providers/adapters/base.py:10)
|
||||
ProviderAdapter,
|
||||
default_base_url_for,
|
||||
insert_items,
|
||||
split_pos_spec,
|
||||
)
|
||||
from .openai import OpenAIAdapter # [OpenAIAdapter](agentui/providers/adapters/openai.py:39)
|
||||
from .gemini import ( # [GeminiAdapter](agentui/providers/adapters/gemini.py:56)
|
||||
GeminiAdapter,
|
||||
GeminiImageAdapter, # [GeminiImageAdapter](agentui/providers/adapters/gemini.py:332)
|
||||
)
|
||||
from .claude import ClaudeAdapter # [ClaudeAdapter](agentui/providers/adapters/claude.py:56)
|
||||
|
||||
__all__ = [
|
||||
"ProviderAdapter",
|
||||
"OpenAIAdapter",
|
||||
"GeminiAdapter",
|
||||
"GeminiImageAdapter",
|
||||
"ClaudeAdapter",
|
||||
"default_base_url_for",
|
||||
"insert_items",
|
||||
"split_pos_spec",
|
||||
]
|
||||
148
agentui/providers/adapters/base.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
|
||||
class ProviderAdapter(ABC): # [ProviderAdapter.__init__()](agentui/providers/adapters/base.py:10)
|
||||
"""
|
||||
Базовый интерфейс адаптера провайдера для ProviderCall.
|
||||
|
||||
Задачи адаптера:
|
||||
- blocks_struct_for_template: собрать pm_struct из унифицированных сообщений (Prompt Blocks)
|
||||
- normalize_segment/filter_items: привести произвольный сегмент к целевой провайдерной структуре и отфильтровать пустое
|
||||
- extract_system_text_from_obj: вытащить системный текст из произвольного сегмента (если он там есть)
|
||||
- combine_segments: слить pre_segments (prompt_preprocess) и prompt_combine с blocks_struct → итоговый pm_struct
|
||||
- prompt_fragment: собрать строку JSON-фрагмента для подстановки в [[PROMPT]]
|
||||
- default_endpoint/default_base_url: дефолты путей и базовых URL
|
||||
"""
|
||||
|
||||
name: str = "base"
|
||||
|
||||
# --- Дефолты HTTP ---
|
||||
|
||||
@abstractmethod
|
||||
def default_base_url(self) -> str:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def default_endpoint(self, model: str) -> str:
|
||||
...
|
||||
|
||||
# --- PROMPT: построение провайдерных структур ---
|
||||
|
||||
@abstractmethod
|
||||
def blocks_struct_for_template(
|
||||
self,
|
||||
unified_messages: List[Dict[str, Any]],
|
||||
context: Dict[str, Any],
|
||||
node_config: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Из унифицированных сообщений [{role, content}] (включая text+image) собрать pm_struct
|
||||
для целевого провайдера. Результат должен быть совместим с текущей логикой [[PROMPT]].
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def normalize_segment(self, obj: Any) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Привести произвольный сегмент (dict/list/str/числа) к целевому массиву элементов
|
||||
(например, messages для openai/claude или contents для gemini).
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def filter_items(self, items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Отфильтровать пустые элементы (пустые тексты и т.п.) согласно правилам провайдера.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def extract_system_text_from_obj(self, obj: Any, render_ctx: Dict[str, Any]) -> Optional[str]:
|
||||
"""
|
||||
Вытащить системный текст из произвольного объекта фрагмента:
|
||||
- OpenAI: messages[*] role=system
|
||||
- Gemini: systemInstruction.parts[].text
|
||||
- Claude: top-level system (string/blocks)
|
||||
Возвращает строку или None.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def combine_segments(
|
||||
self,
|
||||
blocks_struct: Dict[str, Any],
|
||||
pre_segments_raw: List[Dict[str, Any]],
|
||||
raw_segs: List[str],
|
||||
render_ctx: Dict[str, Any],
|
||||
pre_var_paths: set[str],
|
||||
render_template_simple_fn, # (s, ctx, out_map) -> str
|
||||
var_macro_fullmatch_re, # _VAR_MACRO_RE.fullmatch
|
||||
detect_vendor_fn, # detect_vendor
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Слить blocks_struct c массивами pre_segments_raw и строковыми raw_segs (prompt_combine)
|
||||
и вернуть итоговый pm_struct. Поведение должно повторять текущее (позиционирование, фильтр пустых,
|
||||
сбор системного текста).
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def prompt_fragment(self, pm_struct: Dict[str, Any], node_config: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Сформировать строку JSON-фрагмента для [[PROMPT]] по итоговому pm_struct.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
# --- Общие утилиты для позиционирования и парсинга директив ---------------------
|
||||
|
||||
def insert_items(base: List[Any], items: List[Any], pos_spec: Optional[str]) -> List[Any]: # [insert_items()](agentui/providers/adapters/base.py:114)
|
||||
if not items:
|
||||
return base
|
||||
if not pos_spec or str(pos_spec).lower() == "append":
|
||||
base.extend(items)
|
||||
return base
|
||||
p = str(pos_spec).lower()
|
||||
if p == "prepend":
|
||||
return list(items) + base
|
||||
try:
|
||||
idx = int(pos_spec) # type: ignore[arg-type]
|
||||
if idx < 0:
|
||||
idx = len(base) + idx
|
||||
if idx < 0:
|
||||
idx = 0
|
||||
if idx > len(base):
|
||||
idx = len(base)
|
||||
return base[:idx] + list(items) + base[idx:]
|
||||
except Exception:
|
||||
base.extend(items)
|
||||
return base
|
||||
|
||||
|
||||
def split_pos_spec(s: str) -> Tuple[str, Optional[str]]: # [split_pos_spec()](agentui/providers/adapters/base.py:135)
|
||||
"""
|
||||
Отделить директиву @pos=... от тела сегмента.
|
||||
Возвращает (body, pos_spec | None).
|
||||
"""
|
||||
import re as _re
|
||||
m = _re.search(r"@pos\s*=\s*(prepend|append|-?\d+)\s*$", str(s or ""), flags=_re.IGNORECASE)
|
||||
if not m:
|
||||
return (str(s or "").strip(), None)
|
||||
body = str(s[: m.start()]).strip()
|
||||
return (body, str(m.group(1)).strip().lower())
|
||||
|
||||
|
||||
# --- Дефолтные base_url по "вендору" (используется RawForward) ------------------
|
||||
|
||||
def default_base_url_for(vendor: str) -> Optional[str]: # [default_base_url_for()](agentui/providers/adapters/base.py:149)
|
||||
v = (vendor or "").strip().lower()
|
||||
if v == "openai":
|
||||
return "https://api.openai.com"
|
||||
if v == "claude" or v == "anthropic":
|
||||
return "https://api.anthropic.com"
|
||||
if v == "gemini" or v == "gemini_image":
|
||||
return "https://generativelanguage.googleapis.com"
|
||||
return None
|
||||
475
agentui/providers/adapters/claude.py
Normal file
@@ -0,0 +1,475 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from agentui.providers.adapters.base import ( # [ProviderAdapter](agentui/providers/adapters/base.py:10)
|
||||
ProviderAdapter,
|
||||
insert_items,
|
||||
split_pos_spec,
|
||||
)
|
||||
|
||||
|
||||
def _is_data_url(u: str) -> bool: # [_is_data_url()](agentui/providers/adapters/claude.py:14)
|
||||
return isinstance(u, str) and u.strip().lower().startswith("data:")
|
||||
|
||||
|
||||
def _split_data_url(u: str) -> tuple[str, str]: # [_split_data_url()](agentui/providers/adapters/claude.py:18)
|
||||
"""
|
||||
Возвращает (mime, b64) для data URL.
|
||||
Поддерживаем форму: data:<mime>;base64,<b64>
|
||||
"""
|
||||
try:
|
||||
header, b64 = u.split(",", 1)
|
||||
mime = "application/octet-stream"
|
||||
if header.startswith("data:"):
|
||||
header2 = header[5:]
|
||||
if ";base64" in header2:
|
||||
mime = header2.split(";base64", 1)[0] or mime
|
||||
elif ";" in header2:
|
||||
mime = header2.split(";", 1)[0] or mime
|
||||
elif header2:
|
||||
mime = header2
|
||||
return mime, b64
|
||||
except Exception:
|
||||
return "application/octet-stream", ""
|
||||
|
||||
|
||||
def _try_json(s: str) -> Any: # [_try_json()](agentui/providers/adapters/claude.py:38)
|
||||
try:
|
||||
obj = json.loads(s)
|
||||
except Exception:
|
||||
try:
|
||||
obj = json.loads(s, strict=False) # type: ignore[call-arg]
|
||||
except Exception:
|
||||
return None
|
||||
for _ in range(2):
|
||||
if isinstance(obj, str):
|
||||
st = obj.strip()
|
||||
if (st.startswith("{") and st.endswith("}")) or (st.startswith("[") and st.endswith("]")):
|
||||
try:
|
||||
obj = json.loads(st)
|
||||
continue
|
||||
except Exception:
|
||||
break
|
||||
break
|
||||
return obj
|
||||
|
||||
|
||||
class ClaudeAdapter(ProviderAdapter): # [ClaudeAdapter.__init__()](agentui/providers/adapters/claude.py:56)
|
||||
name = "claude"
|
||||
|
||||
# --- Дефолты HTTP ---
|
||||
def default_base_url(self) -> str:
|
||||
return "https://api.anthropic.com"
|
||||
|
||||
def default_endpoint(self, model: str) -> str:
|
||||
return "/v1/messages"
|
||||
|
||||
# --- PROMPT: построение провайдерных структур ---
|
||||
|
||||
def blocks_struct_for_template(
|
||||
self,
|
||||
unified_messages: List[Dict[str, Any]],
|
||||
context: Dict[str, Any],
|
||||
node_config: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Совместимо с веткой provider=='claude' из
|
||||
[ProviderCallNode._blocks_struct_for_template()](agentui/pipeline/executor.py:2022).
|
||||
"""
|
||||
# Системные сообщения как текст
|
||||
sys_msgs = []
|
||||
for m in (unified_messages or []):
|
||||
if m.get("role") == "system":
|
||||
c = m.get("content")
|
||||
if isinstance(c, list):
|
||||
sys_msgs.append("\n".join([str(p.get("text") or "") for p in c if isinstance(p, dict) and p.get("type") == "text"]))
|
||||
else:
|
||||
sys_msgs.append(str(c or ""))
|
||||
sys_text = "\n\n".join([s for s in sys_msgs if s]).strip()
|
||||
|
||||
out_msgs = []
|
||||
for m in (unified_messages or []):
|
||||
if m.get("role") == "system":
|
||||
continue
|
||||
role = m.get("role")
|
||||
role = role if role in {"user", "assistant"} else "user"
|
||||
c = m.get("content")
|
||||
blocks: List[Dict[str, Any]] = []
|
||||
if isinstance(c, list):
|
||||
for p in c:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
if p.get("type") == "text":
|
||||
blocks.append({"type": "text", "text": str(p.get("text") or "")})
|
||||
elif p.get("type") in {"image_url", "image"}:
|
||||
url = str(p.get("url") or "")
|
||||
if _is_data_url(url):
|
||||
mime, b64 = _split_data_url(url)
|
||||
blocks.append({"type": "image", "source": {"type": "base64", "media_type": mime, "data": b64}})
|
||||
else:
|
||||
blocks.append({"type": "image", "source": {"type": "url", "url": url}})
|
||||
else:
|
||||
blocks.append({"type": "text", "text": str(c or "")})
|
||||
out_msgs.append({"role": role, "content": blocks})
|
||||
|
||||
claude_no_system = False
|
||||
try:
|
||||
claude_no_system = bool((node_config or {}).get("claude_no_system", False))
|
||||
except Exception:
|
||||
claude_no_system = False
|
||||
|
||||
if claude_no_system:
|
||||
if sys_text:
|
||||
out_msgs = [{"role": "user", "content": [{"type": "text", "text": sys_text}]}] + out_msgs
|
||||
return {
|
||||
"messages": out_msgs,
|
||||
"system_text": sys_text,
|
||||
}
|
||||
|
||||
d = {
|
||||
"system_text": sys_text,
|
||||
"messages": out_msgs,
|
||||
}
|
||||
if sys_text:
|
||||
# Prefer system as a plain string (proxy compatibility)
|
||||
d["system"] = sys_text
|
||||
return d
|
||||
|
||||
def normalize_segment(self, x: Any) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Совместимо с [_as_claude_messages()](agentui/pipeline/executor.py:2602).
|
||||
"""
|
||||
msgs: List[Dict[str, Any]] = []
|
||||
try:
|
||||
if isinstance(x, dict):
|
||||
# Dict with messages (OpenAI-like)
|
||||
if isinstance(x.get("messages"), list):
|
||||
x = x.get("messages") or []
|
||||
# fallthrough to list mapping below
|
||||
elif isinstance(x.get("contents"), list):
|
||||
# Gemini -> Claude
|
||||
for c in (x.get("contents") or []):
|
||||
if not isinstance(c, dict):
|
||||
continue
|
||||
role_raw = str(c.get("role") or "user")
|
||||
role = "assistant" if role_raw == "model" else ("user" if role_raw not in {"user", "assistant"} else role_raw)
|
||||
parts = c.get("parts") or []
|
||||
text = "\n".join([str(p.get("text")) for p in parts if isinstance(p, dict) and isinstance(p.get("text"), str)]).strip()
|
||||
msgs.append({"role": role, "content": [{"type": "text", "text": text}]})
|
||||
return msgs
|
||||
|
||||
if isinstance(x, list):
|
||||
# Gemini contents list -> Claude messages
|
||||
if all(isinstance(c, dict) and "parts" in c for c in x):
|
||||
for c in x:
|
||||
role_raw = str(c.get("role") or "user")
|
||||
role = "assistant" if role_raw == "model" else ("user" if role_raw not in {"user", "assistant"} else role_raw)
|
||||
blocks: List[Dict[str, Any]] = []
|
||||
for p in (c.get("parts") or []):
|
||||
if isinstance(p, dict) and isinstance(p.get("text"), str):
|
||||
txt = p.get("text").strip()
|
||||
if txt:
|
||||
blocks.append({"type": "text", "text": txt})
|
||||
msgs.append({"role": role, "content": blocks or [{"type": "text", "text": ""}]})
|
||||
return msgs
|
||||
# OpenAI messages list -> Claude
|
||||
if all(isinstance(m, dict) and "content" in m for m in x):
|
||||
out: List[Dict[str, Any]] = []
|
||||
for m in x:
|
||||
role = m.get("role", "user")
|
||||
cont = m.get("content")
|
||||
blocks: List[Dict[str, Any]] = []
|
||||
if isinstance(cont, str):
|
||||
blocks.append({"type": "text", "text": cont})
|
||||
elif isinstance(cont, list):
|
||||
for p in cont:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
if p.get("type") == "text":
|
||||
blocks.append({"type": "text", "text": str(p.get("text") or "")})
|
||||
elif p.get("type") in {"image_url", "image"}:
|
||||
url = ""
|
||||
if isinstance(p.get("image_url"), dict):
|
||||
url = str((p.get("image_url") or {}).get("url") or "")
|
||||
elif "url" in p:
|
||||
url = str(p.get("url") or "")
|
||||
if url:
|
||||
blocks.append({"type": "image", "source": {"type": "url", "url": url}})
|
||||
else:
|
||||
blocks.append({"type": "text", "text": json.dumps(cont, ensure_ascii=False)})
|
||||
out.append({"role": role if role in {"user", "assistant"} else "user", "content": blocks})
|
||||
return out
|
||||
# Fallback
|
||||
return [{"role": "user", "content": [{"type": "text", "text": json.dumps(x, ensure_ascii=False)}]}]
|
||||
|
||||
if isinstance(x, str):
|
||||
try_obj = _try_json(x)
|
||||
if try_obj is not None:
|
||||
return self.normalize_segment(try_obj)
|
||||
return [{"role": "user", "content": [{"type": "text", "text": x}]}]
|
||||
return [{"role": "user", "content": [{"type": "text", "text": json.dumps(x, ensure_ascii=False)}]}]
|
||||
except Exception:
|
||||
return [{"role": "user", "content": [{"type": "text", "text": str(x)}]}]
|
||||
|
||||
def filter_items(self, arr: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Совместимо с [_filter_claude()](agentui/pipeline/executor.py:2820).
|
||||
"""
|
||||
out: List[Dict[str, Any]] = []
|
||||
for m in (arr or []):
|
||||
if not isinstance(m, dict):
|
||||
continue
|
||||
blocks = m.get("content")
|
||||
if isinstance(blocks, list):
|
||||
norm = []
|
||||
for b in blocks:
|
||||
if isinstance(b, dict) and b.get("type") == "text":
|
||||
txt = str(b.get("text") or "")
|
||||
if txt.strip():
|
||||
norm.append({"type": "text", "text": txt})
|
||||
if norm:
|
||||
out.append({"role": m.get("role", "user"), "content": norm})
|
||||
return out
|
||||
|
||||
def extract_system_text_from_obj(self, x: Any, render_ctx: Dict[str, Any]) -> Optional[str]:
|
||||
"""
|
||||
Поведение совместимо с [_extract_sys_text_from_obj()](agentui/pipeline/executor.py:2676).
|
||||
"""
|
||||
try:
|
||||
# Dict objects
|
||||
if isinstance(x, dict):
|
||||
# Gemini systemInstruction
|
||||
if "systemInstruction" in x:
|
||||
si = x.get("systemInstruction")
|
||||
|
||||
def _parts_to_text(siobj: Any) -> str:
|
||||
try:
|
||||
parts = siobj.get("parts") or []
|
||||
texts = [
|
||||
str(p.get("text") or "")
|
||||
for p in parts
|
||||
if isinstance(p, dict) and isinstance(p.get("text"), str) and p.get("text").strip()
|
||||
]
|
||||
return "\n".join([t for t in texts if t]).strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
if isinstance(si, dict):
|
||||
t = _parts_to_text(si)
|
||||
if t:
|
||||
return t
|
||||
if isinstance(si, list):
|
||||
texts = []
|
||||
for p in si:
|
||||
if isinstance(p, dict) and isinstance(p.get("text"), str) and p.get("text").strip():
|
||||
texts.append(p.get("text").strip())
|
||||
t = "\n".join(texts).strip()
|
||||
if t:
|
||||
return t
|
||||
if isinstance(si, str) and si.strip():
|
||||
return si.strip()
|
||||
# Claude system (string or blocks)
|
||||
if "system" in x and not ("messages" in x and isinstance(x.get("messages"), list)):
|
||||
sysv = x.get("system")
|
||||
if isinstance(sysv, str) and sysv.strip():
|
||||
return sysv.strip()
|
||||
if isinstance(sysv, list):
|
||||
texts = [
|
||||
str(b.get("text") or "")
|
||||
for b in sysv
|
||||
if isinstance(b, dict)
|
||||
and (b.get("type") == "text")
|
||||
and isinstance(b.get("text"), str)
|
||||
and b.get("text").strip()
|
||||
]
|
||||
t = "\n".join([t for t in texts if t]).strip()
|
||||
if t:
|
||||
return t
|
||||
# OpenAI messages with role=system
|
||||
if isinstance(x.get("messages"), list):
|
||||
sys_msgs = []
|
||||
for m in (x.get("messages") or []):
|
||||
try:
|
||||
if (str(m.get("role") or "").lower().strip() == "system"):
|
||||
cont = m.get("content")
|
||||
if isinstance(cont, str) and cont.strip():
|
||||
sys_msgs.append(cont.strip())
|
||||
elif isinstance(cont, list):
|
||||
for p in cont:
|
||||
if isinstance(p, dict) and p.get("type") == "text" and isinstance(p.get("text"), str) and p.get("text").strip():
|
||||
sys_msgs.append(p.get("text").strip())
|
||||
except Exception:
|
||||
continue
|
||||
if sys_msgs:
|
||||
return "\n\n".join(sys_msgs).strip()
|
||||
|
||||
# List objects
|
||||
if isinstance(x, list):
|
||||
# OpenAI messages list with role=system
|
||||
if all(isinstance(m, dict) and "role" in m for m in x):
|
||||
sys_msgs = []
|
||||
for m in x:
|
||||
try:
|
||||
if (str(m.get("role") or "").lower().strip() == "system"):
|
||||
cont = m.get("content")
|
||||
if isinstance(cont, str) and cont.strip():
|
||||
sys_msgs.append(cont.strip())
|
||||
elif isinstance(cont, list):
|
||||
for p in cont:
|
||||
if isinstance(p, dict) and p.get("type") == "text" and isinstance(p.get("text"), str) and p.get("text").strip():
|
||||
sys_msgs.append(p.get("text").strip())
|
||||
except Exception:
|
||||
continue
|
||||
if sys_msgs:
|
||||
return "\n\n".join(sys_msgs).strip()
|
||||
# Gemini 'contents' list: попробуем прочитать systemInstruction из входящего snapshot
|
||||
if all(isinstance(c, dict) and "parts" in c for c in x):
|
||||
try:
|
||||
inc = (render_ctx.get("incoming") or {}).get("json") or {}
|
||||
si = inc.get("systemInstruction")
|
||||
if si is not None:
|
||||
return self.extract_system_text_from_obj({"systemInstruction": si}, render_ctx)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def combine_segments(
|
||||
self,
|
||||
blocks_struct: Dict[str, Any],
|
||||
pre_segments_raw: List[Dict[str, Any]],
|
||||
raw_segs: List[str],
|
||||
render_ctx: Dict[str, Any],
|
||||
pre_var_paths: set[str],
|
||||
render_template_simple_fn,
|
||||
var_macro_fullmatch_re,
|
||||
detect_vendor_fn,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Повторяет ветку provider=='claude' из prompt_combine
|
||||
([ProviderCallNode.run()](agentui/pipeline/executor.py:2998)).
|
||||
"""
|
||||
built3: List[Dict[str, Any]] = []
|
||||
sys_texts: List[str] = []
|
||||
|
||||
# Нода-конфиг (для claude_no_system) передан через render_ctx['_node_config'], см. интеграцию
|
||||
node_cfg = {}
|
||||
try:
|
||||
nc = render_ctx.get("_node_config")
|
||||
if isinstance(nc, dict):
|
||||
node_cfg = nc
|
||||
except Exception:
|
||||
node_cfg = {}
|
||||
claude_no_system = False
|
||||
try:
|
||||
claude_no_system = bool(node_cfg.get("claude_no_system", False))
|
||||
except Exception:
|
||||
claude_no_system = False
|
||||
|
||||
# Пред‑сегменты
|
||||
for _pre in (pre_segments_raw or []):
|
||||
try:
|
||||
_obj = _pre.get("obj")
|
||||
items = self.normalize_segment(_obj)
|
||||
items = self.filter_items(items)
|
||||
built3 = insert_items(built3, items, _pre.get("pos"))
|
||||
try:
|
||||
sx = self.extract_system_text_from_obj(_obj, render_ctx)
|
||||
if isinstance(sx, str) and sx.strip():
|
||||
sys_texts.append(sx.strip())
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Основные сегменты
|
||||
for raw_seg in (raw_segs or []):
|
||||
body_seg, pos_spec = split_pos_spec(raw_seg)
|
||||
if body_seg == "[[PROMPT]]":
|
||||
items = self.filter_items(list(blocks_struct.get("messages", []) or []))
|
||||
built3 = insert_items(built3, items, pos_spec)
|
||||
continue
|
||||
m_pre = var_macro_fullmatch_re.fullmatch(body_seg)
|
||||
if m_pre:
|
||||
_p = (m_pre.group(1) or "").strip()
|
||||
try:
|
||||
if _p in pre_var_paths:
|
||||
# Skip duplicate var segment - already inserted via prompt_preprocess (filtered)
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
resolved = render_template_simple_fn(body_seg, render_ctx, render_ctx.get("OUT") or {})
|
||||
obj = _try_json(resolved)
|
||||
try:
|
||||
pg = detect_vendor_fn(obj if isinstance(obj, dict) else {})
|
||||
print(f"DEBUG: prompt_combine seg provider_guess={pg} -> target=claude pos={pos_spec}")
|
||||
except Exception:
|
||||
pass
|
||||
items = self.normalize_segment(obj if obj is not None else resolved)
|
||||
items = self.filter_items(items)
|
||||
built3 = insert_items(built3, items, pos_spec)
|
||||
try:
|
||||
sx = self.extract_system_text_from_obj(obj, render_ctx) if obj is not None else None
|
||||
if isinstance(sx, str) and sx.strip():
|
||||
sys_texts.append(sx.strip())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not built3:
|
||||
built3 = self.filter_items(list(blocks_struct.get("messages", []) or []))
|
||||
|
||||
# Merge system blocks from PROMPT blocks + gathered sys_texts
|
||||
existing_sys = blocks_struct.get("system") or []
|
||||
sys_blocks: List[Dict[str, Any]] = []
|
||||
if isinstance(existing_sys, list):
|
||||
sys_blocks.extend(existing_sys)
|
||||
st0 = blocks_struct.get("system_text") or ""
|
||||
# Ensure PROMPT system_text from blocks is included as a Claude system block
|
||||
if isinstance(st0, str) and st0.strip():
|
||||
sys_blocks.append({"type": "text", "text": st0})
|
||||
for s in sys_texts:
|
||||
sys_blocks.append({"type": "text", "text": s})
|
||||
st = "\n\n".join([t for t in [st0] + sys_texts if isinstance(t, str) and t.strip()])
|
||||
|
||||
if claude_no_system:
|
||||
# Prepend system text as a user message instead of top-level system
|
||||
if st:
|
||||
built3 = [{"role": "user", "content": [{"type": "text", "text": st}]}] + built3
|
||||
return {"messages": built3, "system_text": st}
|
||||
|
||||
pm_struct = {"messages": built3, "system_text": st}
|
||||
# Prefer array of system blocks when possible; fallback to single text block
|
||||
if sys_blocks:
|
||||
pm_struct["system"] = sys_blocks
|
||||
elif st:
|
||||
pm_struct["system"] = [{"type": "text", "text": st}]
|
||||
return pm_struct
|
||||
|
||||
def prompt_fragment(self, pm_struct: Dict[str, Any], node_config: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Совместимо с веткой provider=='claude' в построении [[PROMPT]]
|
||||
([ProviderCallNode.run()](agentui/pipeline/executor.py:3125)).
|
||||
"""
|
||||
parts: List[str] = []
|
||||
# Учитываем флаг совместимости: при claude_no_system не добавляем top-level "system"
|
||||
claude_no_system = False
|
||||
try:
|
||||
claude_no_system = bool((node_config or {}).get("claude_no_system", False))
|
||||
except Exception:
|
||||
claude_no_system = False
|
||||
|
||||
if not claude_no_system:
|
||||
# Предпочитаем массив блоков system, если он есть; иначе строковый system_text
|
||||
sys_val = pm_struct.get("system", None)
|
||||
if sys_val is None:
|
||||
sys_val = pm_struct.get("system_text")
|
||||
if sys_val:
|
||||
parts.append('"system": ' + json.dumps(sys_val, ensure_ascii=False))
|
||||
|
||||
msgs = pm_struct.get("messages")
|
||||
if msgs is not None:
|
||||
parts.append('"messages": ' + json.dumps(msgs, ensure_ascii=False))
|
||||
return ", ".join(parts)
|
||||
419
agentui/providers/adapters/gemini.py
Normal file
@@ -0,0 +1,419 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from agentui.providers.adapters.base import ( # [ProviderAdapter](agentui/providers/adapters/base.py:10)
|
||||
ProviderAdapter,
|
||||
insert_items,
|
||||
split_pos_spec,
|
||||
)
|
||||
|
||||
|
||||
def _is_data_url(u: str) -> bool: # [_is_data_url()](agentui/providers/adapters/gemini.py:14)
|
||||
return isinstance(u, str) and u.strip().lower().startswith("data:")
|
||||
|
||||
|
||||
def _split_data_url(u: str) -> tuple[str, str]: # [_split_data_url()](agentui/providers/adapters/gemini.py:18)
|
||||
"""
|
||||
Возвращает (mime, b64) для data URL.
|
||||
Поддерживаем форму: data:<mime>;base64,<b64>
|
||||
"""
|
||||
try:
|
||||
header, b64 = u.split(",", 1)
|
||||
mime = "application/octet-stream"
|
||||
if header.startswith("data:"):
|
||||
header2 = header[5:]
|
||||
if ";base64" in header2:
|
||||
mime = header2.split(";base64", 1)[0] or mime
|
||||
elif ";" in header2:
|
||||
mime = header2.split(";", 1)[0] or mime
|
||||
elif header2:
|
||||
mime = header2
|
||||
return mime, b64
|
||||
except Exception:
|
||||
return "application/octet-stream", ""
|
||||
|
||||
|
||||
def _try_json(s: str) -> Any: # [_try_json()](agentui/providers/adapters/gemini.py:38)
|
||||
try:
|
||||
obj = json.loads(s)
|
||||
except Exception:
|
||||
try:
|
||||
obj = json.loads(s, strict=False) # type: ignore[call-arg]
|
||||
except Exception:
|
||||
return None
|
||||
for _ in range(2):
|
||||
if isinstance(obj, str):
|
||||
st = obj.strip()
|
||||
if (st.startswith("{") and st.endswith("}")) or (st.startswith("[") and st.endswith("]")):
|
||||
try:
|
||||
obj = json.loads(st)
|
||||
continue
|
||||
except Exception:
|
||||
break
|
||||
break
|
||||
return obj
|
||||
|
||||
|
||||
class GeminiAdapter(ProviderAdapter): # [GeminiAdapter.__init__()](agentui/providers/adapters/gemini.py:56)
|
||||
name = "gemini"
|
||||
|
||||
# --- Дефолты HTTP ---
|
||||
def default_base_url(self) -> str:
|
||||
return "https://generativelanguage.googleapis.com"
|
||||
|
||||
def default_endpoint(self, model: str) -> str:
|
||||
# endpoint с шаблоном model (как в исходном коде)
|
||||
return "/v1beta/models/{{ model }}:generateContent"
|
||||
|
||||
# --- PROMPT: построение провайдерных структур ---
|
||||
|
||||
def blocks_struct_for_template(
|
||||
self,
|
||||
unified_messages: List[Dict[str, Any]],
|
||||
context: Dict[str, Any],
|
||||
node_config: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Совместимо с веткой provider in {'gemini','gemini_image'} из
|
||||
[ProviderCallNode._blocks_struct_for_template()](agentui/pipeline/executor.py:1981).
|
||||
"""
|
||||
def _text_from_msg(m: Dict[str, Any]) -> str:
|
||||
c = m.get("content")
|
||||
if isinstance(c, list):
|
||||
texts = [str(p.get("text") or "") for p in c if isinstance(p, dict) and p.get("type") == "text"]
|
||||
return "\n".join([t for t in texts if t])
|
||||
return str(c or "")
|
||||
|
||||
sys_text = "\n\n".join([_text_from_msg(m) for m in (unified_messages or []) if m.get("role") == "system"]).strip()
|
||||
|
||||
contents: List[Dict[str, Any]] = []
|
||||
for m in (unified_messages or []):
|
||||
if m.get("role") == "system":
|
||||
continue
|
||||
role = "model" if m.get("role") == "assistant" else "user"
|
||||
c = m.get("content")
|
||||
parts: List[Dict[str, Any]] = []
|
||||
if isinstance(c, list):
|
||||
for p in c:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
if p.get("type") == "text":
|
||||
parts.append({"text": str(p.get("text") or "")})
|
||||
elif p.get("type") in {"image_url", "image"}:
|
||||
url = str(p.get("url") or "")
|
||||
if _is_data_url(url):
|
||||
mime, b64 = _split_data_url(url)
|
||||
parts.append({"inline_data": {"mime_type": mime, "data": b64}})
|
||||
else:
|
||||
parts.append({"text": url})
|
||||
else:
|
||||
parts.append({"text": str(c or "")})
|
||||
contents.append({"role": role, "parts": parts})
|
||||
|
||||
d: Dict[str, Any] = {
|
||||
"contents": contents,
|
||||
"system_text": sys_text,
|
||||
}
|
||||
if sys_text:
|
||||
d["systemInstruction"] = {"parts": [{"text": sys_text}]}
|
||||
return d
|
||||
|
||||
def normalize_segment(self, x: Any) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Совместимо с [_as_gemini_contents()](agentui/pipeline/executor.py:2521).
|
||||
"""
|
||||
cnts: List[Dict[str, Any]] = []
|
||||
try:
|
||||
if isinstance(x, dict):
|
||||
if isinstance(x.get("contents"), list):
|
||||
return list(x.get("contents") or [])
|
||||
if isinstance(x.get("messages"), list):
|
||||
# OpenAI → Gemini
|
||||
for m in (x.get("messages") or []):
|
||||
if not isinstance(m, dict):
|
||||
continue
|
||||
role_raw = str(m.get("role") or "user")
|
||||
role = "model" if role_raw == "assistant" else "user"
|
||||
cont = m.get("content")
|
||||
parts: List[Dict[str, Any]] = []
|
||||
if isinstance(cont, str):
|
||||
parts = [{"text": cont}]
|
||||
elif isinstance(cont, list):
|
||||
for p in cont:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
if p.get("type") == "text":
|
||||
parts.append({"text": str(p.get("text") or "")})
|
||||
elif p.get("type") in {"image_url", "image"}:
|
||||
# Gemini не принимает внешние URL картинок как image — оставим как текстовую ссылку
|
||||
url = ""
|
||||
if isinstance(p.get("image_url"), dict):
|
||||
url = str((p.get("image_url") or {}).get("url") or "")
|
||||
elif "url" in p:
|
||||
url = str(p.get("url") or "")
|
||||
if url:
|
||||
parts.append({"text": url})
|
||||
else:
|
||||
parts = [{"text": json.dumps(cont, ensure_ascii=False)}]
|
||||
cnts.append({"role": role, "parts": parts})
|
||||
return cnts
|
||||
|
||||
if isinstance(x, list):
|
||||
# Gemini contents list already
|
||||
if all(isinstance(c, dict) and "parts" in c for c in x):
|
||||
return list(x)
|
||||
# OpenAI messages list -> Gemini
|
||||
if all(isinstance(m, dict) and "content" in m for m in x):
|
||||
out: List[Dict[str, Any]] = []
|
||||
for m in x:
|
||||
role_raw = str(m.get("role") or "user")
|
||||
role = "model" if role_raw == "assistant" else "user"
|
||||
cont = m.get("content")
|
||||
parts: List[Dict[str, Any]] = []
|
||||
if isinstance(cont, str):
|
||||
parts = [{"text": cont}]
|
||||
elif isinstance(cont, list):
|
||||
for p in cont:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
if p.get("type") == "text":
|
||||
parts.append({"text": str(p.get("text") or "")})
|
||||
elif p.get("type") in {"image_url", "image"}:
|
||||
url = ""
|
||||
if isinstance(p.get("image_url"), dict):
|
||||
url = str((p.get("image_url") or {}).get("url") or "")
|
||||
elif "url" in p:
|
||||
url = str(p.get("url") or "")
|
||||
if url:
|
||||
parts.append({"text": url})
|
||||
else:
|
||||
parts = [{"text": json.dumps(cont, ensure_ascii=False)}]
|
||||
out.append({"role": role, "parts": parts})
|
||||
return out
|
||||
# Fallback
|
||||
return [{"role": "user", "parts": [{"text": json.dumps(x, ensure_ascii=False)}]}]
|
||||
|
||||
if isinstance(x, str):
|
||||
try_obj = _try_json(x)
|
||||
if try_obj is not None:
|
||||
return self.normalize_segment(try_obj)
|
||||
return [{"role": "user", "parts": [{"text": x}]}]
|
||||
return [{"role": "user", "parts": [{"text": json.dumps(x, ensure_ascii=False)}]}]
|
||||
except Exception:
|
||||
return [{"role": "user", "parts": [{"text": str(x)}]}]
|
||||
|
||||
def filter_items(self, arr: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Совместимо с [_filter_gemini()](agentui/pipeline/executor.py:2782).
|
||||
Сохраняем inline_data/inlineData как есть; текстовые части — только непустые.
|
||||
"""
|
||||
out: List[Dict[str, Any]] = []
|
||||
for it in (arr or []):
|
||||
if not isinstance(it, dict):
|
||||
continue
|
||||
parts = it.get("parts") or []
|
||||
norm_parts = []
|
||||
for p in parts:
|
||||
if isinstance(p, dict):
|
||||
t = p.get("text")
|
||||
if isinstance(t, str) and t.strip():
|
||||
norm_parts.append({"text": t})
|
||||
elif "inline_data" in p or "inlineData" in p:
|
||||
norm_parts.append(p) # изображения пропускаем как есть
|
||||
if norm_parts:
|
||||
out.append({"role": it.get("role", "user"), "parts": norm_parts})
|
||||
return out
|
||||
|
||||
def extract_system_text_from_obj(self, x: Any, render_ctx: Dict[str, Any]) -> Optional[str]:
|
||||
"""
|
||||
Совместимо с [_extract_sys_text_from_obj()](agentui/pipeline/executor.py:2676) для Gemini.
|
||||
"""
|
||||
try:
|
||||
# Dict
|
||||
if isinstance(x, dict):
|
||||
if "systemInstruction" in x:
|
||||
si = x.get("systemInstruction")
|
||||
def _parts_to_text(siobj: Any) -> str:
|
||||
try:
|
||||
parts = siobj.get("parts") or []
|
||||
texts = [
|
||||
str(p.get("text") or "")
|
||||
for p in parts
|
||||
if isinstance(p, dict) and isinstance(p.get("text"), str) and p.get("text").strip()
|
||||
]
|
||||
return "\n".join([t for t in texts if t]).strip()
|
||||
except Exception:
|
||||
return ""
|
||||
if isinstance(si, dict):
|
||||
t = _parts_to_text(si)
|
||||
if t:
|
||||
return t
|
||||
if isinstance(si, list):
|
||||
texts = []
|
||||
for p in si:
|
||||
if isinstance(p, dict) and isinstance(p.get("text"), str) and p.get("text").strip():
|
||||
texts.append(p.get("text").strip())
|
||||
t = "\n".join(texts).strip()
|
||||
if t:
|
||||
return t
|
||||
if isinstance(si, str) and si.strip():
|
||||
return si.strip()
|
||||
# OpenAI system внутри messages
|
||||
if isinstance(x.get("messages"), list):
|
||||
sys_msgs = []
|
||||
for m in (x.get("messages") or []):
|
||||
try:
|
||||
if (str(m.get("role") or "").lower().strip() == "system"):
|
||||
cont = m.get("content")
|
||||
if isinstance(cont, str) and cont.strip():
|
||||
sys_msgs.append(cont.strip())
|
||||
elif isinstance(cont, list):
|
||||
for p in cont:
|
||||
if (
|
||||
isinstance(p, dict)
|
||||
and p.get("type") == "text"
|
||||
and isinstance(p.get("text"), str)
|
||||
and p.get("text").strip()
|
||||
):
|
||||
sys_msgs.append(p.get("text").strip())
|
||||
except Exception:
|
||||
continue
|
||||
if sys_msgs:
|
||||
return "\n\n".join(sys_msgs).strip()
|
||||
# List
|
||||
if isinstance(x, list):
|
||||
if all(isinstance(m, dict) and "role" in m for m in x):
|
||||
sys_msgs = []
|
||||
for m in x:
|
||||
try:
|
||||
if (str(m.get("role") or "").lower().strip() == "system"):
|
||||
cont = m.get("content")
|
||||
if isinstance(cont, str) and cont.strip():
|
||||
sys_msgs.append(cont.strip())
|
||||
elif isinstance(cont, list):
|
||||
for p in cont:
|
||||
if (
|
||||
isinstance(p, dict)
|
||||
and p.get("type") == "text"
|
||||
and isinstance(p.get("text"), str)
|
||||
and p.get("text").strip()
|
||||
):
|
||||
sys_msgs.append(p.get("text").strip())
|
||||
except Exception:
|
||||
continue
|
||||
if sys_msgs:
|
||||
return "\n\n".join(sys_msgs).strip()
|
||||
# Gemini contents list -> попробуем взять из входящего snapshot
|
||||
if all(isinstance(c, dict) and "parts" in c for c in x):
|
||||
try:
|
||||
inc = (render_ctx.get("incoming") or {}).get("json") or {}
|
||||
si = inc.get("systemInstruction")
|
||||
if si is not None:
|
||||
return self.extract_system_text_from_obj({"systemInstruction": si}, render_ctx)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def combine_segments(
|
||||
self,
|
||||
blocks_struct: Dict[str, Any],
|
||||
pre_segments_raw: List[Dict[str, Any]],
|
||||
raw_segs: List[str],
|
||||
render_ctx: Dict[str, Any],
|
||||
pre_var_paths: set[str],
|
||||
render_template_simple_fn,
|
||||
var_macro_fullmatch_re,
|
||||
detect_vendor_fn,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Повторяет ветку provider in {'gemini','gemini_image'} из prompt_combine
|
||||
([ProviderCallNode.run()](agentui/pipeline/executor.py:2874)).
|
||||
"""
|
||||
built: List[Dict[str, Any]] = []
|
||||
sys_texts: List[str] = []
|
||||
|
||||
# 1) Пред‑сегменты
|
||||
for _pre in (pre_segments_raw or []):
|
||||
try:
|
||||
_obj = _pre.get("obj")
|
||||
items = self.normalize_segment(_obj)
|
||||
items = self.filter_items(items)
|
||||
built = insert_items(built, items, _pre.get("pos"))
|
||||
try:
|
||||
sx = self.extract_system_text_from_obj(_obj, render_ctx)
|
||||
if isinstance(sx, str) and sx.strip():
|
||||
sys_texts.append(sx.strip())
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2) Основные сегменты
|
||||
for raw_seg in (raw_segs or []):
|
||||
body_seg, pos_spec = split_pos_spec(raw_seg)
|
||||
if body_seg == "[[PROMPT]]":
|
||||
items = self.filter_items(list(blocks_struct.get("contents", []) or []))
|
||||
built = insert_items(built, items, pos_spec)
|
||||
continue
|
||||
m_pre = var_macro_fullmatch_re.fullmatch(body_seg)
|
||||
if m_pre:
|
||||
_p = (m_pre.group(1) or "").strip()
|
||||
try:
|
||||
if _p in pre_var_paths:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
resolved = render_template_simple_fn(body_seg, render_ctx, render_ctx.get("OUT") or {})
|
||||
obj = _try_json(resolved)
|
||||
# debug provider guess
|
||||
try:
|
||||
pg = detect_vendor_fn(obj if isinstance(obj, dict) else {})
|
||||
print(f"DEBUG: prompt_combine seg provider_guess={pg} -> target=gemini pos={pos_spec}")
|
||||
except Exception:
|
||||
pass
|
||||
items = self.normalize_segment(obj if obj is not None else resolved)
|
||||
items = self.filter_items(items)
|
||||
built = insert_items(built, items, pos_spec)
|
||||
try:
|
||||
sx = self.extract_system_text_from_obj(obj, render_ctx) if obj is not None else None
|
||||
if isinstance(sx, str) and sx.strip():
|
||||
sys_texts.append(sx.strip())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not built:
|
||||
built = self.filter_items(list(blocks_struct.get("contents", []) or []))
|
||||
|
||||
# Merge systemInstruction: PROMPT blocks + gathered sys_texts
|
||||
existing_si = blocks_struct.get("systemInstruction")
|
||||
parts = []
|
||||
if isinstance(existing_si, dict) and isinstance(existing_si.get("parts"), list):
|
||||
parts = list(existing_si.get("parts") or [])
|
||||
for s in sys_texts:
|
||||
parts.append({"text": s})
|
||||
new_si = {"parts": parts} if parts else existing_si
|
||||
return {"contents": built, "systemInstruction": new_si, "system_text": blocks_struct.get("system_text")}
|
||||
|
||||
def prompt_fragment(self, pm_struct: Dict[str, Any], node_config: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Совместимо с веткой provider in {'gemini','gemini_image'} в построении [[PROMPT]]
|
||||
([ProviderCallNode.run()](agentui/pipeline/executor.py:3103)).
|
||||
"""
|
||||
parts = []
|
||||
contents = pm_struct.get("contents")
|
||||
if contents is not None:
|
||||
parts.append('"contents": ' + json.dumps(contents, ensure_ascii=False))
|
||||
sysi = pm_struct.get("systemInstruction")
|
||||
if sysi is not None:
|
||||
parts.append('"systemInstruction": ' + json.dumps(sysi, ensure_ascii=False))
|
||||
return ", ".join(parts)
|
||||
|
||||
|
||||
class GeminiImageAdapter(GeminiAdapter): # [GeminiImageAdapter.__init__()](agentui/providers/adapters/gemini.py:332)
|
||||
name = "gemini_image"
|
||||
|
||||
# Вся логика такая же, как у Gemini (generateContent), включая defaults.
|
||||
398
agentui/providers/adapters/openai.py
Normal file
@@ -0,0 +1,398 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from agentui.providers.adapters.base import ( # [ProviderAdapter](agentui/providers/adapters/base.py:10)
|
||||
ProviderAdapter,
|
||||
insert_items,
|
||||
split_pos_spec,
|
||||
)
|
||||
|
||||
|
||||
def _try_json(s: str) -> Any: # [_try_json()](agentui/providers/adapters/openai.py:16)
|
||||
"""
|
||||
Парсит JSON из строки. Пермиссивный режим и двукратная распаковка строк, как в старой логике.
|
||||
Возвращает dict/list/примитив или None при неудаче.
|
||||
"""
|
||||
try:
|
||||
obj = json.loads(s)
|
||||
except Exception:
|
||||
try:
|
||||
obj = json.loads(s, strict=False) # type: ignore[call-arg]
|
||||
except Exception:
|
||||
return None
|
||||
# Если это строка, которая сама похожа на JSON — пробуем распаковать до 2 раз
|
||||
for _ in range(2):
|
||||
if isinstance(obj, str):
|
||||
st = obj.strip()
|
||||
if (st.startswith("{") and st.endswith("}")) or (st.startswith("[") and st.endswith("]")):
|
||||
try:
|
||||
obj = json.loads(st)
|
||||
continue
|
||||
except Exception:
|
||||
break
|
||||
break
|
||||
return obj
|
||||
|
||||
|
||||
class OpenAIAdapter(ProviderAdapter): # [OpenAIAdapter.__init__()](agentui/providers/adapters/openai.py:39)
|
||||
name = "openai"
|
||||
|
||||
# --- Дефолты HTTP ---
|
||||
def default_base_url(self) -> str:
|
||||
return "https://api.openai.com"
|
||||
|
||||
def default_endpoint(self, model: str) -> str:
|
||||
return "/v1/chat/completions"
|
||||
|
||||
# --- PROMPT: построение провайдерных структур ---
|
||||
|
||||
def blocks_struct_for_template(
|
||||
self,
|
||||
unified_messages: List[Dict[str, Any]],
|
||||
context: Dict[str, Any],
|
||||
node_config: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Совместимо с веткой provider=='openai' из [ProviderCallNode._blocks_struct_for_template()](agentui/pipeline/executor.py:1958).
|
||||
"""
|
||||
def _map(m: Dict[str, Any]) -> Dict[str, Any]:
|
||||
c = m.get("content")
|
||||
if isinstance(c, list):
|
||||
parts = []
|
||||
for p in c:
|
||||
if isinstance(p, dict) and p.get("type") == "text":
|
||||
parts.append({"type": "text", "text": str(p.get("text") or "")})
|
||||
elif isinstance(p, dict) and p.get("type") in {"image_url", "image"}:
|
||||
url = str(p.get("url") or "")
|
||||
parts.append({"type": "image_url", "image_url": {"url": url}})
|
||||
return {"role": m.get("role", "user"), "content": parts}
|
||||
return {"role": m.get("role", "user"), "content": str(c or "")}
|
||||
|
||||
# system_text — склейка всех system-блоков (только текст, без картинок)
|
||||
sys_text = "\n\n".join(
|
||||
[
|
||||
str(m.get("content") or "")
|
||||
if not isinstance(m.get("content"), list)
|
||||
else "\n".join(
|
||||
[str(p.get("text") or "") for p in m.get("content") if isinstance(p, dict) and p.get("type") == "text"]
|
||||
)
|
||||
for m in (unified_messages or [])
|
||||
if m.get("role") == "system"
|
||||
]
|
||||
).strip()
|
||||
|
||||
return {
|
||||
"messages": [_map(m) for m in (unified_messages or [])],
|
||||
"system_text": sys_text,
|
||||
}
|
||||
|
||||
def normalize_segment(self, x: Any) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Совместимо с [_as_openai_messages()](agentui/pipeline/executor.py:2451).
|
||||
- Поддерживает dict with messages (openai)
|
||||
- Поддерживает dict/list в стиле Gemini.contents (склейка текстов частей)
|
||||
- Поддерживает list openai-like messages (нормализация parts)
|
||||
- Строки/прочее упаковываются как один user message
|
||||
"""
|
||||
msgs: List[Dict[str, Any]] = []
|
||||
try:
|
||||
# Dict inputs
|
||||
if isinstance(x, dict):
|
||||
if isinstance(x.get("messages"), list):
|
||||
return list(x.get("messages") or [])
|
||||
if isinstance(x.get("contents"), list):
|
||||
# Gemini -> OpenAI (text-only join)
|
||||
for c in (x.get("contents") or []):
|
||||
if not isinstance(c, dict):
|
||||
continue
|
||||
role_raw = str(c.get("role") or "user")
|
||||
role = "assistant" if role_raw == "model" else ("user" if role_raw not in {"user", "assistant"} else role_raw)
|
||||
parts = c.get("parts") or []
|
||||
text = "\n".join(
|
||||
[str(p.get("text")) for p in parts if isinstance(p, dict) and isinstance(p.get("text"), str)]
|
||||
).strip()
|
||||
msgs.append({"role": role, "content": text})
|
||||
return msgs
|
||||
|
||||
# List inputs
|
||||
if isinstance(x, list):
|
||||
# Gemini contents list -> OpenAI messages
|
||||
if all(isinstance(c, dict) and "parts" in c for c in x):
|
||||
for c in x:
|
||||
role_raw = str(c.get("role") or "user")
|
||||
role = "assistant" if role_raw == "model" else ("user" if role_raw not in {"user", "assistant"} else role_raw)
|
||||
parts = c.get("parts") or []
|
||||
text = "\n".join(
|
||||
[str(p.get("text")) for p in parts if isinstance(p, dict) and isinstance(p.get("text"), str)]
|
||||
).strip()
|
||||
msgs.append({"role": role, "content": text})
|
||||
return msgs
|
||||
# OpenAI messages list already — normalize parts if needed
|
||||
if all(isinstance(m, dict) and "content" in m for m in x):
|
||||
out: List[Dict[str, Any]] = []
|
||||
for m in x:
|
||||
role = m.get("role", "user")
|
||||
cont = m.get("content")
|
||||
if isinstance(cont, str):
|
||||
out.append({"role": role, "content": cont})
|
||||
elif isinstance(cont, list):
|
||||
parts2: List[Dict[str, Any]] = []
|
||||
for p in cont:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
if p.get("type") == "text":
|
||||
parts2.append({"type": "text", "text": str(p.get("text") or "")})
|
||||
elif p.get("type") in {"image_url", "image"}:
|
||||
url = ""
|
||||
if isinstance(p.get("image_url"), dict):
|
||||
url = str((p.get("image_url") or {}).get("url") or "")
|
||||
elif "url" in p:
|
||||
url = str(p.get("url") or "")
|
||||
if url:
|
||||
parts2.append({"type": "image_url", "image_url": {"url": url}})
|
||||
out.append({"role": role, "content": parts2 if parts2 else ""})
|
||||
return out
|
||||
# Fallback: dump JSON as a single user message
|
||||
return [{"role": "user", "content": json.dumps(x, ensure_ascii=False)}]
|
||||
|
||||
# Primitive inputs or embedded JSON string
|
||||
if isinstance(x, str):
|
||||
try_obj = _try_json(x)
|
||||
if try_obj is not None:
|
||||
return self.normalize_segment(try_obj)
|
||||
return [{"role": "user", "content": x}]
|
||||
return [{"role": "user", "content": json.dumps(x, ensure_ascii=False)}]
|
||||
except Exception:
|
||||
return [{"role": "user", "content": str(x)}]
|
||||
|
||||
def filter_items(self, arr: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Совместимо с [_filter_openai()](agentui/pipeline/executor.py:2801).
|
||||
"""
|
||||
out: List[Dict[str, Any]] = []
|
||||
for m in (arr or []):
|
||||
if not isinstance(m, dict):
|
||||
continue
|
||||
c = m.get("content")
|
||||
if isinstance(c, str) and c.strip():
|
||||
out.append({"role": m.get("role", "user"), "content": c})
|
||||
elif isinstance(c, list):
|
||||
parts = []
|
||||
for p in c:
|
||||
if isinstance(p, dict) and p.get("type") == "text":
|
||||
txt = str(p.get("text") or "")
|
||||
if txt.strip():
|
||||
parts.append({"type": "text", "text": txt})
|
||||
if parts:
|
||||
out.append({"role": m.get("role", "user"), "content": parts})
|
||||
return out
|
||||
|
||||
def extract_system_text_from_obj(self, x: Any, render_ctx: Dict[str, Any]) -> Optional[str]:
|
||||
"""
|
||||
Совместимо с [_extract_sys_text_from_obj()](agentui/pipeline/executor.py:2676).
|
||||
Умеет читать:
|
||||
- Gemini: systemInstruction.parts[].text
|
||||
- Claude: top-level system (string/list of blocks)
|
||||
- OpenAI: messages[*] with role=system (string content or parts[].text)
|
||||
- List форматы: openai messages list и gemini contents list (в последнем случае смотрит incoming.json.systemInstruction)
|
||||
"""
|
||||
try:
|
||||
# Dict objects
|
||||
if isinstance(x, dict):
|
||||
# Gemini systemInstruction
|
||||
if "systemInstruction" in x:
|
||||
si = x.get("systemInstruction")
|
||||
|
||||
def _parts_to_text(siobj: Any) -> str:
|
||||
try:
|
||||
parts = siobj.get("parts") or []
|
||||
texts = [
|
||||
str(p.get("text") or "")
|
||||
for p in parts
|
||||
if isinstance(p, dict) and isinstance(p.get("text"), str) and p.get("text").strip()
|
||||
]
|
||||
return "\n".join([t for t in texts if t]).strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
if isinstance(si, dict):
|
||||
t = _parts_to_text(si)
|
||||
if t:
|
||||
return t
|
||||
if isinstance(si, list):
|
||||
texts = []
|
||||
for p in si:
|
||||
if isinstance(p, dict) and isinstance(p.get("text"), str) and p.get("text").strip():
|
||||
texts.append(p.get("text").strip())
|
||||
t = "\n".join(texts).strip()
|
||||
if t:
|
||||
return t
|
||||
if isinstance(si, str) and si.strip():
|
||||
return si.strip()
|
||||
# Claude system (string or blocks)
|
||||
if "system" in x and not ("messages" in x and isinstance(x.get("messages"), list)):
|
||||
sysv = x.get("system")
|
||||
if isinstance(sysv, str) and sysv.strip():
|
||||
return sysv.strip()
|
||||
if isinstance(sysv, list):
|
||||
texts = [
|
||||
str(b.get("text") or "")
|
||||
for b in sysv
|
||||
if isinstance(b, dict) and (b.get("type") == "text") and isinstance(b.get("text"), str) and b.get("text").strip()
|
||||
]
|
||||
t = "\n".join([t for t in texts if t]).strip()
|
||||
if t:
|
||||
return t
|
||||
# OpenAI messages with role=system
|
||||
if isinstance(x.get("messages"), list):
|
||||
sys_msgs = []
|
||||
for m in (x.get("messages") or []):
|
||||
try:
|
||||
if (str(m.get("role") or "").lower().strip() == "system"):
|
||||
cont = m.get("content")
|
||||
if isinstance(cont, str) and cont.strip():
|
||||
sys_msgs.append(cont.strip())
|
||||
elif isinstance(cont, list):
|
||||
for p in cont:
|
||||
if (
|
||||
isinstance(p, dict)
|
||||
and p.get("type") == "text"
|
||||
and isinstance(p.get("text"), str)
|
||||
and p.get("text").strip()
|
||||
):
|
||||
sys_msgs.append(p.get("text").strip())
|
||||
except Exception:
|
||||
continue
|
||||
if sys_msgs:
|
||||
return "\n\n".join(sys_msgs).strip()
|
||||
|
||||
# List objects
|
||||
if isinstance(x, list):
|
||||
# OpenAI messages list with role=system
|
||||
if all(isinstance(m, dict) and "role" in m for m in x):
|
||||
sys_msgs = []
|
||||
for m in x:
|
||||
try:
|
||||
if (str(m.get("role") or "").lower().strip() == "system"):
|
||||
cont = m.get("content")
|
||||
if isinstance(cont, str) and cont.strip():
|
||||
sys_msgs.append(cont.strip())
|
||||
elif isinstance(cont, list):
|
||||
for p in cont:
|
||||
if (
|
||||
isinstance(p, dict)
|
||||
and p.get("type") == "text"
|
||||
and isinstance(p.get("text"), str)
|
||||
and p.get("text").strip()
|
||||
):
|
||||
sys_msgs.append(p.get("text").strip())
|
||||
except Exception:
|
||||
continue
|
||||
if sys_msgs:
|
||||
return "\n\n".join(sys_msgs).strip()
|
||||
# Gemini 'contents' list: try to read systemInstruction from incoming JSON snapshot
|
||||
if all(isinstance(c, dict) and "parts" in c for c in x):
|
||||
try:
|
||||
inc = (render_ctx.get("incoming") or {}).get("json") or {}
|
||||
si = inc.get("systemInstruction")
|
||||
if si is not None:
|
||||
# Рекурсивно используем себя
|
||||
return self.extract_system_text_from_obj({"systemInstruction": si}, render_ctx)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def combine_segments(
|
||||
self,
|
||||
blocks_struct: Dict[str, Any],
|
||||
pre_segments_raw: List[Dict[str, Any]],
|
||||
raw_segs: List[str],
|
||||
render_ctx: Dict[str, Any],
|
||||
pre_var_paths: set[str],
|
||||
render_template_simple_fn,
|
||||
var_macro_fullmatch_re,
|
||||
detect_vendor_fn,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Повторяет ветку provider=='openai' из prompt_combine в [ProviderCallNode.run()](agentui/pipeline/executor.py:2936).
|
||||
"""
|
||||
built: List[Dict[str, Any]] = []
|
||||
sys_texts: List[str] = []
|
||||
|
||||
# 1) Пред‑сегменты (prompt_preprocess)
|
||||
for _pre in (pre_segments_raw or []):
|
||||
try:
|
||||
_obj = _pre.get("obj")
|
||||
items = self.normalize_segment(_obj)
|
||||
items = self.filter_items(items)
|
||||
built = insert_items(built, items, _pre.get("pos"))
|
||||
try:
|
||||
sx = self.extract_system_text_from_obj(_obj, render_ctx)
|
||||
if isinstance(sx, str) and sx.strip():
|
||||
sys_texts.append(sx.strip())
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2) Основные сегменты (prompt_combine)
|
||||
for raw_seg in (raw_segs or []):
|
||||
body_seg, pos_spec = split_pos_spec(raw_seg)
|
||||
if body_seg == "[[PROMPT]]":
|
||||
items = self.filter_items(list(blocks_struct.get("messages", []) or []))
|
||||
built = insert_items(built, items, pos_spec)
|
||||
continue
|
||||
# Спрятать дубли plain [[VAR:path]] если уже вставляли этим путём в pre_var_overrides
|
||||
m_pre = var_macro_fullmatch_re.fullmatch(body_seg)
|
||||
if m_pre:
|
||||
_p = (m_pre.group(1) or "").strip()
|
||||
try:
|
||||
if _p in pre_var_paths:
|
||||
# Уже вставлено через prompt_preprocess с фильтрацией — пропускаем
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
resolved = render_template_simple_fn(body_seg, render_ctx, render_ctx.get("OUT") or {})
|
||||
obj = _try_json(resolved)
|
||||
# debug provider guess
|
||||
try:
|
||||
pg = detect_vendor_fn(obj if isinstance(obj, dict) else {})
|
||||
print(f"DEBUG: prompt_combine seg provider_guess={pg} -> target=openai pos={pos_spec}")
|
||||
except Exception:
|
||||
pass
|
||||
items = self.normalize_segment(obj if obj is not None else resolved)
|
||||
items = self.filter_items(items)
|
||||
built = insert_items(built, items, pos_spec)
|
||||
try:
|
||||
sx = self.extract_system_text_from_obj(obj, render_ctx) if obj is not None else None
|
||||
if isinstance(sx, str) and sx.strip():
|
||||
sys_texts.append(sx.strip())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Если ничего не собрали — берём исходные blocks
|
||||
if not built:
|
||||
built = self.filter_items(list(blocks_struct.get("messages", []) or []))
|
||||
|
||||
# Препендинг системных сообщений из sys_texts
|
||||
if sys_texts:
|
||||
sys_msgs = [{"role": "system", "content": s} for s in sys_texts if s]
|
||||
if sys_msgs:
|
||||
built = sys_msgs + built
|
||||
|
||||
# keep system_text for UI/debug
|
||||
st0 = blocks_struct.get("system_text") or ""
|
||||
st = "\n\n".join([t for t in [st0] + sys_texts if isinstance(t, str) and t.strip()])
|
||||
return {"messages": built, "system_text": st}
|
||||
|
||||
def prompt_fragment(self, pm_struct: Dict[str, Any], node_config: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Совместимо с веткой provider=='openai' в построении [[PROMPT]] из [ProviderCallNode.run()](agentui/pipeline/executor.py:3103).
|
||||
"""
|
||||
return '"messages": ' + json.dumps(pm_struct.get("messages", []), ensure_ascii=False)
|
||||
32
agentui/providers/adapters/registry.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from agentui.providers.adapters.base import ProviderAdapter, default_base_url_for as _default_base_url_for
|
||||
from agentui.providers.adapters.openai import OpenAIAdapter
|
||||
try:
|
||||
from agentui.providers.adapters.gemini import GeminiAdapter, GeminiImageAdapter
|
||||
except Exception:
|
||||
GeminiAdapter = None # type: ignore
|
||||
GeminiImageAdapter = None # type: ignore
|
||||
try:
|
||||
from agentui.providers.adapters.claude import ClaudeAdapter
|
||||
except Exception:
|
||||
ClaudeAdapter = None # type: ignore
|
||||
|
||||
|
||||
def get_adapter(provider: str) -> Optional[ProviderAdapter]:
|
||||
p = (provider or "").strip().lower()
|
||||
if p == "openai":
|
||||
return OpenAIAdapter()
|
||||
if p == "gemini" and GeminiAdapter:
|
||||
return GeminiAdapter() # type: ignore[operator]
|
||||
if p == "gemini_image" and GeminiImageAdapter:
|
||||
return GeminiImageAdapter() # type: ignore[operator]
|
||||
if p == "claude" and ClaudeAdapter:
|
||||
return ClaudeAdapter() # type: ignore[operator]
|
||||
return None
|
||||
|
||||
|
||||
def default_base_url_for(vendor: str) -> Optional[str]:
|
||||
return _default_base_url_for(vendor)
|
||||
@@ -1,14 +1,57 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
from typing import Optional, Dict
|
||||
from agentui.config import build_httpx_proxies
|
||||
from typing import Optional, Dict, Union
|
||||
import os
|
||||
from agentui.config import build_httpx_proxies, get_tls_verify, is_verify_explicit
|
||||
|
||||
|
||||
def _mask_proxy(url: str) -> str:
|
||||
"""Маскируем часть с логином/паролем в URL прокси, чтобы не утекла в логи."""
|
||||
try:
|
||||
if "://" in url and "@" in url:
|
||||
prefix, rest = url.split("://", 1)
|
||||
auth, host = rest.split("@", 1)
|
||||
return f"{prefix}://***@{host}"
|
||||
return url
|
||||
except Exception:
|
||||
return "<masked>"
|
||||
|
||||
|
||||
def build_client(timeout: float = 60.0) -> httpx.AsyncClient:
|
||||
proxies: Optional[Dict[str, str]] = build_httpx_proxies()
|
||||
verify: Union[bool, str] = get_tls_verify()
|
||||
|
||||
explicit = is_verify_explicit()
|
||||
# По умолчанию при наличии прокси отключаем проверку сертификатов,
|
||||
# но не трогаем, если пользователь явно задал verify или CA.
|
||||
if proxies and (verify is True) and (not explicit):
|
||||
verify = False
|
||||
|
||||
if os.getenv("AGENTUI_DEBUG", "").lower() in ("1", "true", "on", "yes"):
|
||||
masked = {k: _mask_proxy(v) for k, v in (proxies or {}).items()}
|
||||
print("[agentui.http_client] proxies=", masked, " verify=", verify)
|
||||
|
||||
# httpx сам понимает схемы socks://, socks5:// при установленном extras [socks]
|
||||
client = httpx.AsyncClient(timeout=timeout, proxies=proxies, follow_redirects=True)
|
||||
try:
|
||||
client = httpx.AsyncClient(
|
||||
timeout=timeout,
|
||||
proxies=proxies,
|
||||
follow_redirects=True,
|
||||
verify=verify,
|
||||
)
|
||||
except TypeError:
|
||||
if proxies:
|
||||
try:
|
||||
masked = {k: _mask_proxy(v) for k, v in proxies.items()}
|
||||
except Exception:
|
||||
masked = proxies
|
||||
print(f"[agentui.http_client] WARNING: proxies not supported in httpx.AsyncClient, skipping proxies={masked}")
|
||||
client = httpx.AsyncClient(
|
||||
timeout=timeout,
|
||||
follow_redirects=True,
|
||||
verify=verify,
|
||||
)
|
||||
return client
|
||||
|
||||
|
||||
|
||||
@@ -1,328 +1,665 @@
|
||||
# Переменные и макросы НадTavern
|
||||
|
||||
Краткая, человеко‑понятная шпаргалка по тому, какие переменные и макросы доступны в шаблонах (в том числе в Prompt Blocks), как они устроены и как их правильно использовать. Док ниже соответствует текущему коду.
|
||||
НАДTAVERN VARIABLES — ГАЙД ДЛЯ ТЕХ, КТО СЕГОДНЯ «НА МИНИМАЛКАХ» И ВСЁ РАВНО ХОЧЕТ, ЧТОБЫ РАБОТАЛО
|
||||
|
||||
Реализация формирует единый «контекст» переменных для всех нод пайплайна, дополняет его выходами уже выполненных нод, а узел 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)
|
||||
- Источники истины (это значит «код, который реально решает», а не чаты):
|
||||
- Исполнение пайплайна: [PipelineExecutor.run()](agentui/pipeline/executor.py:402)
|
||||
- Нода SetVars — выражения и функции: [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:1290)
|
||||
- Нода ProviderCall — вызов провайдера и PROMPT: [ProviderCallNode.run()](agentui/pipeline/executor.py:2084)
|
||||
- Нода RawForward — прямой прокси: [RawForwardNode.run()](agentui/pipeline/executor.py:3547)
|
||||
- Нода Return — формат финального ответа: [ReturnNode.run()](agentui/pipeline/executor.py:3930)
|
||||
- Нода If — парсер условий: [IfNode.run()](agentui/pipeline/executor.py:4024)
|
||||
- While-обёртка для ProviderCall: [_providercall_run_with_while()](agentui/pipeline/executor.py:4075)
|
||||
- While-обёртка для RawForward: [_rawforward_run_with_while()](agentui/pipeline/executor.py:4243)
|
||||
- Шаблоны: [[...]] и {{ ... }} здесь: [render_template_simple()](agentui/pipeline/templating.py:205)
|
||||
- Условия if/while (&&, ||, contains, скобочки): [eval_condition_expr()](agentui/pipeline/templating.py:391)
|
||||
- JSONPath (упрощённый, но хватит): [_json_path_extract()](agentui/pipeline/executor.py:1569)
|
||||
- Склейка текста при JSONPath: [_stringify_join()](agentui/pipeline/executor.py:1610)
|
||||
- UI инспектор Prompt Blocks: [PM.setupProviderCallPMUI()](static/js/pm-ui.js:9)
|
||||
- Экспорт/импорт пайплайна в редакторе: [AgentUISer.toPipelineJSON()](static/js/serialization.js:104), [AgentUISer.fromPipelineJSON()](static/js/serialization.js:286)
|
||||
- Веб-редактор: [editor.html](static/editor.html)
|
||||
|
||||
---
|
||||
Перед началом (термины в скобках — это определение, не морщи нос):
|
||||
- «Пайплайн» (pipeline — схема исполнения из «узлов»).
|
||||
- «Узел» (node — прямоугольный блок на канвасе).
|
||||
- «Порт» (port — кружок входа/выхода узла).
|
||||
- «Гейт» (gate — ветка выхода If.true/If.false; влияет на порядок выполнения ребёнка).
|
||||
- «STORE» (перманентное хранилище переменных на диск, одна запись на каждый pipeline.id).
|
||||
- «PROMPT» (специальный JSON-фрагмент сообщений, который подставляется в шаблон запроса провайдера).
|
||||
- «OUTx» (короткая ссылка на текст из выхода ноды nX).
|
||||
- «incoming» (снимок входящего HTTP-запроса: метод, URL, заголовки, JSON и т.д.).
|
||||
|
||||
## 1) Общие переменные контекста (для всех нод)
|
||||
|
||||
Эти переменные доступны в шаблонах любой ноды. Они добавляются на стороне сервера при обработке входящего HTTP‑запроса.
|
||||
РАЗДЕЛ 1 — НОДЫ: КТО ЕСТЬ КТО (КРАТКО, ШУТКИ В СТОРОНУ)
|
||||
|
||||
- model — строка с именем модели.
|
||||
Пример: "gpt-4o-mini"
|
||||
- vendor_format — вендор/протокол запроса: "openai" | "gemini" | "claude" | "unknown"
|
||||
- system — «системный» текст, если он был во входящем запросе; иначе пустая строка.
|
||||
1) SetVars (заводит твои переменные)
|
||||
- Входы: нет (только depends).
|
||||
- Выходы: vars — словарь новых переменных.
|
||||
- Поведение: для каждой переменной задаёшь name и mode (string или expr). В режиме string значение обрабатывается шаблоном ([[...]] и {{ ... }}), в режиме expr — безопасным мини-диалектом выражений.
|
||||
- Где смотреть реализацию: [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:1197).
|
||||
|
||||
- params — стандартные параметры генерации (можно использовать как дефолты)
|
||||
- params.temperature — число с плавающей точкой (по умолчанию 0.7)
|
||||
- params.max_tokens — целое или null
|
||||
- params.top_p — число (по умолчанию 1.0)
|
||||
- params.stop — массив строк или null
|
||||
2) If (ветвление по условию)
|
||||
- Входы: depends.
|
||||
- Выходы: true, false (гейты для «детей» по условию).
|
||||
- Поведение: expr парсится как булево выражение (&&, ||, !, ==, !=, <, <=, >, >=, contains, скобочки). Внутри можно использовать [[...]] и {{ ... }}.
|
||||
- Реализация парсера: [eval_condition_expr()](agentui/pipeline/templating.py:391), обёртка ноды: [IfNode.run()](agentui/pipeline/executor.py:3538).
|
||||
|
||||
- chat — сведения о чате во входящем запросе
|
||||
- chat.last_user — последнее сообщение пользователя (строка)
|
||||
- chat.messages — массив сообщений в унифицированной форме:
|
||||
- role — "system" | "user" | "assistant" | "tool"
|
||||
- content — содержимое (обычно строка)
|
||||
- name — опционально, строка
|
||||
- tool_call_id — опционально
|
||||
3) ProviderCall (отправка к провайдеру OpenAI/Gemini/Claude)
|
||||
- Входы: depends.
|
||||
- Выходы: result (сырой JSON ответа), response_text (извлечённый «текст»).
|
||||
- Ключи: provider, provider_configs (base_url, endpoint, headers, template), blocks (Prompt Blocks), prompt_combine (DSL &), while_expr/while_max_iters/ignore_errors, text_extract_*.
|
||||
- Реализация: [ProviderCallNode.run()](agentui/pipeline/executor.py:1991).
|
||||
|
||||
- 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
|
||||
4) RawForward (прямой прокси)
|
||||
- Входы: depends.
|
||||
- Выходы: result, response_text.
|
||||
- Ключи: base_url (может автоопределяться по входящему JSON-вендору), override_path, passthrough_headers, extra_headers, while_expr.
|
||||
- Реализация: [RawForwardNode.run()](agentui/pipeline/executor.py:3105).
|
||||
|
||||
Пример использования в шаблоне:
|
||||
- [[VAR:incoming.api_keys.key]] — возьмёт ключ из строки запроса (?key=...).
|
||||
- [[VAR:incoming.headers.x-api-key]] — возьмёт ключ из заголовка x-api-key (типично для Anthropic).
|
||||
- {{ params.temperature|default(0.7) }} — безопасно подставит число, если не задано во входящих данных.
|
||||
5) Return (оформление финального ответа для клиента)
|
||||
- Входы: depends.
|
||||
- Выходы: result (в формате openai/gemini/claude/auto), response_text (то, что вставили).
|
||||
- Ключи: target_format (auto/openai/gemini/claude), text_template (по умолчанию [[OUT1]]).
|
||||
- Реализация: [ReturnNode.run()](agentui/pipeline/executor.py:3444).
|
||||
|
||||
---
|
||||
Под капотом все узлы гоняет исполнитель «волнами» или итеративно:
|
||||
- Главная точка входа: [PipelineExecutor.run()](agentui/pipeline/executor.py:316).
|
||||
- И есть режим retry/циклов в узлах ProviderCall/RawForward — см. while в Разделе 5.
|
||||
|
||||
## 2) Выходы нод (OUT) и ссылки на них
|
||||
|
||||
Во время исполнения пайплайна результаты предыдущих нод собираются в снапшот OUT и доступны при рендере шаблонов следующих нод:
|
||||
РАЗДЕЛ 2 — ПЕРЕМЕННЫЕ И МАКРОСЫ ([[...]] ПРОТИВ {{ ... }}) С 12 ПРИМЕРАМИ
|
||||
|
||||
- OUT — словарь выходов нод, ключи — id нод в пайплайне (например, "n1", "n2").
|
||||
- OUT.n1, OUT.n2, ... — объект результата соответствующей ноды.
|
||||
Смысл (запомни, ладно?):
|
||||
- [[...]] (квадратные макросы) — текстовая подстановка со строкификацией (всегда превращается в строку, объекты — в JSON-строку).
|
||||
- {{ ... }} (фигурные вставки) — типобезопасная подстановка «как есть» (числа остаются числами, объекты — объектами), а ещё есть фильтр |default(...).
|
||||
|
||||
Формы доступа:
|
||||
- Полная форма: [[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)).
|
||||
Доступные макросы (см. [render_template_simple()](agentui/pipeline/templating.py:205)):
|
||||
- [[VAR:путь]] — берёт значение по пути из контекста (context/incoming/params/...).
|
||||
- [[OUT:nodeId(.path)*]] — берёт из выходов ноды (сырой JSON).
|
||||
- [[OUTx]] — короткая форма текста из ноды nX (best-effort).
|
||||
- [[STORE:путь]] — читает из стойкого хранилища (store.*).
|
||||
- [[NAME]] — «голая» переменная: сперва ищется в пользовательских переменных (SetVars), иначе в контексте по пути.
|
||||
- [[PROMPT]] — провайдерный JSON-фрагмент сообщений (см. Раздел 6).
|
||||
- Доп. сахар: img(mime)[[...]] → «data:mime;base64,ЗНАЧЕНИЕ» (см. [templating._IMG_WRAPPER_RE](agentui/pipeline/templating.py:41)).
|
||||
|
||||
Что возвращают встроенные ноды:
|
||||
- ProviderCall:
|
||||
- OUT.nX.result — сырой JSON ответа провайдера
|
||||
- OUT.nX.response_text — уже извлечённый «лучший текст» (строка)
|
||||
- RawForward:
|
||||
- OUT.nX.result — JSON, как пришёл от апстрима (или {"error": "...", "text": "..."} при не‑JSON ответе)
|
||||
Фигурные {{ ... }}:
|
||||
- {{ OUT.n2.result.choices.0.message.content }} — доступ к JSON как к полям.
|
||||
- {{ путь|default(значение) }} — цепочки дефолтов, поддерживает вложенность и JSON-литералы в default(...).
|
||||
|
||||
Подсказка по короткой форме [[OUTx]]:
|
||||
- OpenAI: вернёт choices[0].message.content
|
||||
- Gemini: вернёт candidates[0].content.parts[0].text
|
||||
- Claude: склеит content[].text
|
||||
- Если явных полей нет — выполнит «глубокий поиск» по ключам "text"/"content"
|
||||
12 примеров (пониже пояса — для тех, кто любит копипасту):
|
||||
1) Заголовок авторизации в JSON-строке:
|
||||
{"Authorization":"Bearer [[VAR:incoming.headers.authorization]]"}
|
||||
Объяснение: [[VAR:...]] берёт заголовок из входа (incoming.headers.authorization).
|
||||
|
||||
---
|
||||
2) Провайдерная модель «как пришла» (фигурные):
|
||||
"model": "{{ model }}"
|
||||
Объяснение: {{ ... }} вставляет строку без кавычек лишний раз.
|
||||
|
||||
## 3) Макросы подстановки и синтаксис
|
||||
3) Число по умолчанию:
|
||||
"temperature": {{ incoming.json.temperature|default(0.7) }}
|
||||
Объяснение: default(0.7) сработает, если температуры нет.
|
||||
|
||||
В шаблонах доступны обе формы подстановки:
|
||||
4) Лист по умолчанию:
|
||||
"stop": {{ incoming.json.stop|default([]) }}
|
||||
Объяснение: вставляет [] как настоящий массив.
|
||||
|
||||
1) Квадратные скобки [[ ... ]] — простая подстановка
|
||||
- [[VAR:путь]] — взять значение из контекста по точечному пути
|
||||
Пример: [[VAR:incoming.json.max_tokens]]
|
||||
- [[OUT:путь]] — взять значение из OUT (см. раздел выше)
|
||||
Пример: [[OUT:n1.result.choices.0.message.content]]
|
||||
- [[OUT1]] / [[OUT2]] — короткая форма «просто текст»
|
||||
- [[PROMPT]] — специальный JSON‑фрагмент из Prompt Blocks (см. ниже)
|
||||
5) Короткая вытяжка текста из ноды n2:
|
||||
"note": "[[OUT2]]"
|
||||
Объяснение: [[OUT2]] — best-effort текст из ответа.
|
||||
|
||||
2) Фигурные скобки {{ ... }} — «джинджа‑лайт»
|
||||
- {{ путь }} — взять значение по пути из контекста (или из OUT.* если начать с OUT.)
|
||||
Пример: {{ OUT.n1.result }}
|
||||
- Фильтр по умолчанию: {{ что-то|default(значение) }}
|
||||
Примеры:
|
||||
- {{ params.temperature|default(0.7) }}
|
||||
- {{ incoming.json.stop|default([]) }}
|
||||
- {{ anthropic_version|default('2023-06-01') }} — см. «Опциональные поля» ниже
|
||||
- Фигурные скобки удобны там, где нужно вставить внутрь JSON не строку, а ЧИСЛО/ОБЪЕКТ/МАССИВ без кавычек и/или задать дефолт.
|
||||
6) Точное поле из результата:
|
||||
"[[OUT:n2.result.choices.0.message.content]]"
|
||||
Объяснение: берёт конкретную ветку JSON из OUT ноды n2.
|
||||
|
||||
---
|
||||
7) «Голая» переменная SetVars:
|
||||
"key": "[[MyOpenAiKey]]"
|
||||
Объяснение: имя без VAR/OUT — сперва ищется среди переменных.
|
||||
|
||||
## 4) ProviderCall: Prompt Blocks, pm.* и [[PROMPT]]
|
||||
8) STORE (между прогонами):
|
||||
"{{ STORE.KEEP|default('miss') }}"
|
||||
Объяснение: из стойкого хранилища (если clear_var_store=False).
|
||||
|
||||
Узел ProviderCall собирает ваши Prompt Blocks (блоки вида: роль/текст/вкл‑выкл/порядок) в стандартные «сообщения» и превращает их в структуру для конкретного провайдера.
|
||||
9) Прокинуть запрос как есть в Gemini:
|
||||
[[VAR:incoming.json.contents]]
|
||||
Объяснение: квадратные дадут строку (для template это ок: JSON-строка без лишних кавычек — см. PROMPT).
|
||||
|
||||
Внутри шаблонов этого узла доступны:
|
||||
- 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:"..."}] }
|
||||
10) JSON-путь с фигурными:
|
||||
{{ OUT.n1.result.obj.value|default(0) }}
|
||||
Объяснение: берёт число или 0.
|
||||
|
||||
- [[PROMPT]] — готовый JSON‑фрагмент на основе pm, безопасный для вставки внутрь шаблона:
|
||||
- OpenAI → подставит: "messages": [...]
|
||||
- Gemini → подставит: "contents": [...], "systemInstruction": {...}
|
||||
- Claude → подставит: "system": "...", "messages": [...]
|
||||
11) Картинка из base64 переменной (img()):
|
||||
"image": "[[IMG_B64]])"
|
||||
Объяснение: заменится на data:image/jpeg;base64,....
|
||||
|
||||
Зачем это нужно?
|
||||
- Чтобы 1) удобно собирать промпт из визуальных блоков, 2) не «сломать» JSON руками.
|
||||
Вы можете вручную использовать {{ pm.* }}, но [[PROMPT]] — рекомендуемый и самый безопасный вариант.
|
||||
12) Сложная строка с несколькими макросами:
|
||||
"msg": "User=[[VAR:chat.last_user]] | Echo=[[OUT1]]"
|
||||
Объяснение: комбинируй сколько хочешь, лишь бы JSON остался валидным.
|
||||
|
||||
---
|
||||
|
||||
## 5) Частые сценарии и примеры
|
||||
РАЗДЕЛ 3 — SETVARS: ВЫРАЖЕНИЯ, РАЗРЕШЁННЫЕ ФУНКЦИИ, ОПАСНО НЕ БУДЕМ (10+ ПРИМЕРОВ)
|
||||
|
||||
Примеры ниже можно вклеивать в поле «template» ноды ProviderCall. Они уже используют [[PROMPT]] и аккуратные дефолты.
|
||||
Где код: [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:1197). Он парсит мини-язык через AST, ничего небезопасного не позволит.
|
||||
|
||||
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([])) }}
|
||||
}
|
||||
```
|
||||
Разрешено:
|
||||
- Литералы: числа, строки, true/false/null (JSON-стиль), списки [...], объекты {...}.
|
||||
- Операции: + - * / // % и унарные + -, сравнения == != < <= > >=, логика and/or.
|
||||
- Вызовы ТОЛЬКО упомянутых функций (без kwargs, без *args):
|
||||
- rand() → float [0,1)
|
||||
- randint(a,b) → int в [a,b]
|
||||
- choice(list) → элемент списка/кортежа
|
||||
- from_json(x) → распарсить строку JSON
|
||||
- jp(value, path, join_sep="\n") → извлечь по JSONPath (см. Раздел 7)
|
||||
- jp_text(value, path, join_sep="\n") → JSONPath + склейка строк
|
||||
- file_b64(path) → прочитать файл и вернуть base64-строку
|
||||
- data_url(b64, mime) → "data:mime;base64,b64"
|
||||
- file_data_url(path, mime?) → прочитать файл и собрать data URL
|
||||
Подсказка: аргументы функций прогоняются через шаблон рендера, так что внутрь jp/… можно передавать строки с [[...]]/{{...}} — они сначала развернутся.
|
||||
|
||||
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]]
|
||||
Нельзя:
|
||||
- Любые имена/доступы к атрибутам/индексации вне списка/словаря литералом.
|
||||
- Любые другие функции, чем перечисленные.
|
||||
- kwargs/starargs.
|
||||
|
||||
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.
|
||||
10+ примеров SetVars (mode=expr):
|
||||
1) Чистая математика:
|
||||
128 + 64
|
||||
|
||||
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') }}"}`
|
||||
2) Случайное число:
|
||||
rand()
|
||||
|
||||
---
|
||||
3) Случайное из списка:
|
||||
choice(["red","green","blue"])
|
||||
|
||||
## 6) Опциональные/редкие поля, о которых стоит знать
|
||||
4) Безопасный int-диапазон:
|
||||
randint(10, 20)
|
||||
|
||||
- anthropic_version — используется как HTTP‑заголовок для Claude ("anthropic-version"). В тело запроса не вставляется.
|
||||
Если нужен дефолт, задавайте его в headers (например, в конфиге ноды/шаблоне заголовков). В шаблонах тела используйте [[PROMPT]]/pm.* без anthropic_version.
|
||||
5) from_json + доступ через jp:
|
||||
jp(from_json("{\"a\":{\"b\":[{\"x\":1},{\"x\":2}]}}"), "a.b.1.x") → 2
|
||||
|
||||
- stream — в MVP стриминг отключён, сервер принудительно не стримит ответ.
|
||||
В шаблонах можно встретить поля stream, но по умолчанию они не включены.
|
||||
6) jp_text (склейка строк через « | »):
|
||||
jp_text(from_json("{\"items\":[{\"t\":\"A\"},{\"t\":\"B\"},{\"t\":\"C\"}]}"), "items.*.t", " | ") → "A | B | C"
|
||||
|
||||
---
|
||||
7) Вытянуть из OUT (с шаблонной подстановкой):
|
||||
jp({{ OUT.n2.result }}, "choices.0.message.content") → текст первого ответа
|
||||
|
||||
## 7) Когда использовать [[...]] и когда {{ ... }}
|
||||
8) Собрать data URL из файла:
|
||||
file_data_url("./img/cat.png", "image/png")
|
||||
|
||||
- Внутрь JSON как ОБЪЕКТ/МАССИВ/ЧИСЛО: используйте {{ ... }}
|
||||
(фигурные скобки вставляют «как есть», без кавычек, и умеют |default(...))
|
||||
- Для строк/URL/заголовков/простых значений: можно использовать [[...]]
|
||||
(квадратные скобки удобны и короче писать)
|
||||
9) Ручная сборка data URL из base64:
|
||||
data_url([[IMG_B64]], "image/jpeg")
|
||||
|
||||
10) Преобразование строки JSON:
|
||||
from_json("[1,2,3]") → список [1,2,3]
|
||||
|
||||
11) Комбо с логикой:
|
||||
(rand() > 0.5) and "HEADS" or "TAILS"
|
||||
|
||||
12) Вложенные вызовы:
|
||||
choice(jp(from_json("[{\"v\":10},{\"v\":20}]"), "*.v")) → 10 или 20
|
||||
|
||||
Результат SetVars попадает в:
|
||||
- Текущие «user vars» (сразу доступны как [[NAME]] и {{ NAME }}).
|
||||
- STORE (персистентно) — см. Раздел 8, если clear_var_store=False.
|
||||
|
||||
|
||||
РАЗДЕЛ 4 — IF: ВЫРАЖЕНИЯ, ОПЕРАТОРЫ, 12 ГРОМКИХ ПРИМЕРОВ
|
||||
|
||||
Парсер условий: [eval_condition_expr()](agentui/pipeline/templating.py:391). Он превращает видимые тобой токены в безопасное AST и вычисляет.
|
||||
|
||||
Операторы:
|
||||
- Логика: && (and), || (or), ! (not)
|
||||
- Сравнение: ==, !=, <, <=, >, >=
|
||||
- Специальный contains (как функция contains(a,b)): для строк — подстрока; для списков — membership.
|
||||
- Скобки ( ... )
|
||||
- Литералы: числа, "строки" или 'строки' (без экранирования внутри), true/false/null (через макросы из контекста).
|
||||
- Макросы: [[...]] и {{ ... }} допустимы прямо внутри выражения (они сначала раскрываются в значения).
|
||||
|
||||
12 примеров (да-да, трижды проверено, хватит ныть):
|
||||
1) Проверить, что [[OUT1]] содержит «ok»:
|
||||
[[OUT1]] contains "ok"
|
||||
|
||||
2) Проверка статуса:
|
||||
{{ OUT.n2.result.status|default(0) }} >= 200 && {{ OUT.n2.result.status|default(0) }} < 300
|
||||
|
||||
3) Инверсия:
|
||||
!([[OUT3]] contains "error")
|
||||
|
||||
4) Сравнить переменную:
|
||||
[[LANG]] == "ru"
|
||||
|
||||
5) Двойная логика:
|
||||
([[MSG]] contains "Hello") || ([[MSG]] contains "Привет")
|
||||
|
||||
6) Цепочка со скобками:
|
||||
( [[CITY]] == "Moscow" && {{ params.max_tokens|default(0) }} > 0 ) || [[FALLBACK]] == "yes"
|
||||
|
||||
7) Списки и contains:
|
||||
contains(["a","b","c"], "b")
|
||||
|
||||
8) Числа и сравнения:
|
||||
{{ OUT.n1.result.value|default(0) }} >= 10
|
||||
|
||||
9) Пустые значения:
|
||||
{{ missing|default("") }} == ""
|
||||
|
||||
10) Комбо macOS:
|
||||
contains([[VAR:incoming.url]], "/v1/") && ([[VAR:incoming.method]] == "POST")
|
||||
|
||||
11) Несколько слоёв default():
|
||||
{{ incoming.json.limit|default(params.limit|default(100)) }} > 50
|
||||
|
||||
12) Сложное условие с OUT пути:
|
||||
[[OUT:n2.result.choices.0.message.content]] contains "done"
|
||||
|
||||
Помни: If только выставляет флаги true/false на выходах. «Дети» с входом depends="nIf.true" запустятся только если условие истинно.
|
||||
|
||||
РАЗДЕЛ 4.1 — СПРАВОЧНИК ОПЕРАТОРОВ IF/WHILE (ПРОСТЫМИ СЛОВАМИ)
|
||||
|
||||
- !A — «не A» (инверсия). Пример: !( [[OUT3]] contains "err" ) → «строка из [[OUT3]] НЕ содержит "err"».
|
||||
- A != B — «A не равно B». Пример: [[MODEL]] != "gemini-2.5-pro".
|
||||
- A && B — «A и B одновременно».
|
||||
- A || B — «A или B» (достаточно одного истинного).
|
||||
- contains(A, B) — специальный оператор:
|
||||
- если A — список/множество, это membership: contains(["a","b"], "a") → true
|
||||
- иначе — проверка подстроки: contains("abc", "b") → true
|
||||
- Запись "X contains Y" эквивалентна contains(X, Y).
|
||||
- Скобки управляют приоритетами: !(A || B) отличается от (!A || B).
|
||||
|
||||
Где какой «язык» используется:
|
||||
- Строковые поля (template, headers/extra_headers, base_url/override_path, Return.text_template, строки prompt_preprocess, сегменты prompt_combine) — это шаблоны с подстановками [[...]] и {{ ... }}.
|
||||
- If.expr и while_expr — булевы выражения (&&, ||, !, ==, !=, <, <=, >, >=, contains, скобки) и допускают макросы [[...]] / {{ ... }} внутри.
|
||||
- SetVars (mode=expr) — отдельный безопасный мини-язык (арифметика + - * / // %, and/or, сравнения) и whitelisted-функции: rand, randint, choice, from_json, jp, jp_text, file_b64, data_url, file_data_url.
|
||||
|
||||
Диагностика:
|
||||
- В логах If/While печатается expanded — строковое раскрытие макросов — и result (true/false).
|
||||
- Ошибка парсера (например, несбалансированные скобки) выводится как if_error/while_error и приводит к result=false.
|
||||
|
||||
|
||||
РАЗДЕЛ 5 — WHILE В НОДАХ PROVIDERCALL/RAWFORWARD (РЕТРАЙ, ЦИКЛЫ). МОЖНО ЛОМАЕТЬ IF В БОЛЬШИНСТВЕ СЛУЧАЕВ (12 ПАТТЕРНОВ)
|
||||
|
||||
Где логика:
|
||||
- Для ProviderCall: [_providercall_run_with_while()](agentui/pipeline/executor.py:3589)
|
||||
- Для RawForward: [_rawforward_run_with_while()](agentui/pipeline/executor.py:3741)
|
||||
- Обёртка делает «do-while»: первая итерация выполняется всегда, потом условие проверяется перед следующей.
|
||||
|
||||
Ключи конфигурации у ноды:
|
||||
- while_expr (строка условие как в If)
|
||||
- while_max_iters (safety, по умолчанию 50)
|
||||
- ignore_errors (True — не падать на исключениях, а возвращать result={"error":"..."} и продолжать цикл)
|
||||
|
||||
Добавочные локальные переменные и семантика внутри цикла:
|
||||
- [[cycleindex]] (int, 0..N) — индекс текущей итерации.
|
||||
- [[WAS_ERROR]] (bool) — при проверке while_expr на i>0 равен «была ли ошибка на предыдущей итерации». Внутри самой итерации на старте содержит то же значение и обновляется для следующей проверки по факту результата.
|
||||
- Подсказка: для ретраев по ошибкам используйте «WAS_ERROR» (а не «!WAS_ERROR»); включайте ignore_errors:true, чтобы исключения не прерывали цикл.
|
||||
|
||||
Глобальные переменные, которые нода выставит после цикла для «детей»:
|
||||
- [[WAS_ERROR__nX]] — была ли ошибка на последней итерации
|
||||
- [[CYCLEINDEX__nX]] — последний индекс итерации (например 2 если были 0,1,2)
|
||||
|
||||
12 паттернов:
|
||||
1) Повтори до 3 раз:
|
||||
while_expr: "cycleindex < 3"
|
||||
|
||||
2) Повтори, пока OUT3 не содержит «ok»:
|
||||
while_expr: "!([[OUT3]] contains \"ok\") && cycleindex < 10"
|
||||
|
||||
3) Ретраи на ошибках сети:
|
||||
ignore_errors: true
|
||||
while_expr: "WAS_ERROR || ({{ OUT.n4.result.status|default(0) }} >= 500)"
|
||||
|
||||
4) Комбо с внешним If — заменяем If:
|
||||
Вместо If.true/false делай while_expr, который набивает нужный результат (например, пока не получишь 2xx от апстрима).
|
||||
|
||||
5) Изменение запроса по итерации:
|
||||
Используй [[cycleindex]] внутри template (например, «page»: {{ vars.page_start|default(1) }} + cycleindex).
|
||||
|
||||
6) Дожидаться готовности ресурса:
|
||||
while_expr: "!([[OUT4]] contains \"READY\") && cycleindex < 30"
|
||||
|
||||
7) Прерывание на плохих данных:
|
||||
while_expr: "!([[OUT7]] contains \"fatal\") && cycleindex < 5"
|
||||
|
||||
8) Backoff вручную (временную задержку делай sleep_ms):
|
||||
sleep_ms: {{ cycleindex }} * 500
|
||||
|
||||
9) Прокси-ретрай RawForward по тексту ответа:
|
||||
ignore_errors: true
|
||||
while_expr: "([[OUT:n1.result.text]] contains \"try again\") && cycleindex < 4"
|
||||
|
||||
10) Gemini «Stream failed to …» из коробки:
|
||||
while_expr: "([[OUT3]] contains "Stream failed to") || ([[OUT3]] contains "gemini-2.5-pro")"
|
||||
(ровно как в твоих пресетах)
|
||||
Добавь " || WAS_ERROR" если хочешь ретраить также на исключениях (при ignore_errors: true).
|
||||
|
||||
11) Проверка флага из STORE:
|
||||
while_expr: "{{ STORE.SHALL_CONTINUE|default(false) }} && cycleindex < 10"
|
||||
|
||||
12) Сложный сценарий: first success wins
|
||||
while_expr: "!([[OUT7]] contains \"success\") && cycleindex < 5"
|
||||
Пояснение: крути пока не словишь success, но не более 5.
|
||||
|
||||
Эти while позволяют чаще не городить отдельный If-гейт — ты просто делаешь один узел, который сам повторяет себя, пока условие не «устаканится». Ну и не забудь выставить ignore_errors там, где ретраи — оправдано.
|
||||
|
||||
|
||||
РАЗДЕЛ 5.1 — WAS_ERROR В WHILE ПОСЛЕ ОБНОВЛЕНИЯ (ПОВЕДЕНИЕ И РЕЦЕПТЫ)
|
||||
|
||||
- Семантика do-while:
|
||||
- Итерация i=0 выполняется всегда.
|
||||
- Начиная с i>0, перед проверкой while_expr двигатель подставляет в [[WAS_ERROR]] значение «была ли ошибка (исключение) на предыдущей итерации».
|
||||
- Как учитываются исключения:
|
||||
- При ignore_errors: true исключения внутри итерации не прерывают ноду; результат оформляется как result={"error":"..."}.
|
||||
- Такое событие считается ошибкой и установит [[WAS_ERROR]]=true для следующей проверки условия.
|
||||
- Рецепты:
|
||||
- Ретраить только при ошибке (до 5 раз): while_expr: "WAS_ERROR && (cycleindex < 5)"
|
||||
- Ретраить при ошибке ИЛИ по признаку в ответе: while_expr: "WAS_ERROR || ([[OUT3]] contains "Stream failed to") || ({{ OUT.n3.result.status|default(0) }} >= 500)"
|
||||
- NB: "!WAS_ERROR" означает «продолжать, если ошибки НЕ было» — это обратное «ретраю при ошибке».
|
||||
- Диагностика:
|
||||
- В логах видны строки вида TRACE while: ... expr='...' expanded='...' index=i result=true/false.
|
||||
- Ошибка парсера (например, несбалансированные скобки) логируется как while_error и приводит к result=false.
|
||||
|
||||
РАЗДЕЛ 6 — PROMPT_COMBINE (DSL «&»): ВЫ ТАМ ЛЮБИТЕ МАГИЮ? ВОТ ОНА, ЧТОБЫ НЕ ЛЕПИТЬ РУКАМИ (12 ПРИМЕРОВ)
|
||||
|
||||
Где реализовано: в [ProviderCallNode.run()](agentui/pipeline/executor.py:1991) — см. кусок обработки combine_raw.
|
||||
|
||||
Идея:
|
||||
- Поле prompt_combine — строка вида «СЕГМЕНТ1 & СЕГМЕНТ2 & ...».
|
||||
- СЕГМЕНТ — это либо [[PROMPT]] (спец сегмент текущих Prompt Blocks), либо любая строка/JSON/список сообщений, либо [[VAR:incoming.*]] и т.п.
|
||||
- Для каждой цели (provider) всё приводится к нужным структурам:
|
||||
- openai → messages: [...]
|
||||
- gemini → contents: [...] (+ systemInstruction)
|
||||
- claude → messages: [...] (+ system)
|
||||
- Системный текст (из openai.system / claude.system / gemini.systemInstruction) автоматически извлекается и объединяется.
|
||||
|
||||
Позиционирование:
|
||||
- Можно добавить директиву @pos=prepend | append | N | -1
|
||||
- Она управляет тем, куда вставить элементы из сегмента внутри собираемого массива сообщений/контента. -1 — вставить перед последним.
|
||||
|
||||
Фильтрация:
|
||||
- Пустые сообщения выкидываются (без пустых текстов).
|
||||
- Изображения (inlineData и т.п.) сохраняются.
|
||||
|
||||
12 примеров (разные таргеты и трюки):
|
||||
1) Классика: входящие Gemini contents + твой PROMPT (OpenAI target)
|
||||
"[[VAR:incoming.json.contents]] & [[PROMPT]]"
|
||||
Результат: messages содержит и конвертированные входящие (model→assistant), и твои blocks.
|
||||
|
||||
2) PROMPT первым (OpenAI):
|
||||
"[[PROMPT]]@pos=prepend & [[VAR:incoming.json.contents]]"
|
||||
Результат: system из PROMPT — в самом начале messages.
|
||||
|
||||
3) Вставка в конкретный индекс (OpenAI):
|
||||
"[[VAR:incoming.json.messages]] & [[PROMPT]]@pos=1"
|
||||
Результат: вторым элементом окажутся твои блоки.
|
||||
|
||||
4) Негативный индекс (OpenAI):
|
||||
"[[VAR:incoming.json.messages]] & [[PROMPT]]@pos=-1"
|
||||
Результат: перед самым последним.
|
||||
|
||||
5) Для Gemini: openai.messages + PROMPT
|
||||
"[[VAR:incoming.json.messages]] & [[PROMPT]]"
|
||||
Результат: contents и systemInstruction соберутся; system из incoming и PROMPT сольются.
|
||||
|
||||
6) Для Claude: openai.messages + PROMPT
|
||||
"[[VAR:incoming.json.messages]] & [[PROMPT]]"
|
||||
Результат: messages + top-level system (как строка/блоки).
|
||||
|
||||
7) Сырый JSON-строковый сегмент:
|
||||
"{\"messages\": [{\"role\":\"user\",\"content\":\"Hi\"}] } & [[PROMPT]]"
|
||||
Результат: корректно распарсится и слепится.
|
||||
|
||||
8) Списковая форма сегмента:
|
||||
"[{\"role\":\"user\",\"content\":\"A\"},{\"role\":\"assistant\",\"content\":\"B\"}] & [[PROMPT]]"
|
||||
Результат: нормализуется под целевой провайдер.
|
||||
|
||||
9) Системные тексты из разных форматов — сольются:
|
||||
"[{\"messages\":[{\"role\":\"system\",\"content\":\"SYS IN\"}]}] & [[PROMPT]]"
|
||||
Результат: system_text включает обе части.
|
||||
|
||||
10) Подмешать внешнюю систему в Claude без top-level system (claude_no_system):
|
||||
В конфиге ноды поставь claude_no_system=true — тогда system из PROMPT положим первым user-сообщением.
|
||||
|
||||
11) Очистка пустых:
|
||||
Если твой сегмент даёт пустые тексты — они выкинутся и JSON не сломается. Не плачь.
|
||||
|
||||
12) Микс строк + JSON:
|
||||
"Просто строка & [[PROMPT]]"
|
||||
Результат: «Просто строка» упакуется корректно (как user/text) в нужную структуру провайдера.
|
||||
|
||||
И да, это позволяет не писать руками половину «склейки» в template — ты описываешь, откуда что привнести, а движок доведёт до провайдерного формата.
|
||||
|
||||
|
||||
РАЗДЕЛ 6.1 — PROMPT_PREPROCESS (pre‑merge DSL: фильтрация/позиционирование сегментов ДО prompt_combine)
|
||||
|
||||
Где реализовано: в [ProviderCallNode.run()](agentui/pipeline/executor.py:2083). Это выполняется перед сборкой [[PROMPT]]/prompt_combine. Поле конфигурации ноды: prompt_preprocess (многострочное).
|
||||
|
||||
Идея:
|
||||
- Каждая строка prompt_preprocess задаёт «пред‑сегмент», который будет вставлен в будущий массив сообщений/контента до обработки [prompt_combine (DSL &)](agentui/pipeline/executor.py:2230).
|
||||
- Эти пред‑сегменты конвертируются под целевого провайдера (openai/gemini/claude) так же, как и сегменты prompt_combine, и «вплетаются» первыми.
|
||||
- Если prompt_combine пуст — используются только пред‑сегменты (и при отсутствии пред‑сегментов — исходные Prompt Blocks как раньше).
|
||||
|
||||
Синтаксис строки:
|
||||
SEGMENT [delKeyContains "needle"] [delpos=prepend|append|N|-1] [case=ci|cs] [pruneEmpty]
|
||||
|
||||
- SEGMENT — строка/JSON/список, допускаются макросы [[...]] и {{ ... }}.
|
||||
- delKeyContains "needle" — удалить ключи в любом месте объекта, если строковое представление их значения содержит needle (поддерживаются несколько delKeyContains).
|
||||
- case=ci|cs — управление регистром для contains; по умолчанию case=ci (без учёта регистра).
|
||||
- pruneEmpty — удалять опустевшие {} / [] после чистки (кроме корня); по умолчанию выключено.
|
||||
- delpos=... — позиция вставки элементов пред‑сегмента в массив (как @pos у prompt_combine): prepend | append | N | -1; по умолчанию append.
|
||||
|
||||
Поведение:
|
||||
- Для каждого SEGMENT рендерятся макросы, затем выполняется попытка json.loads() (в т.ч. для двойной JSON‑строки).
|
||||
- После этого применяется фильтрация delKeyContains (если задана), с учётом case и pruneEmpty.
|
||||
- Итог вставляется в текущий собираемый массив сообщений/контента в позицию delpos (prepend/append/индекс/отрицательный индекс).
|
||||
- Системный текст, присутствующий внутри сегмента (Gemini systemInstruction / OpenAI role=system / Claude system), автоматически извлекается и сольётся, как в prompt_combine.
|
||||
|
||||
Примеры:
|
||||
- {{ pm.contents }} — вставит массив как настоящий массив (без кавычек)
|
||||
- {{ params.temperature|default(0.7) }} — безопасный дефолт для числа
|
||||
- [[VAR:incoming.api_keys.authorization]] — быстро подставить строку Authorization
|
||||
1) Удалить поля, где значение содержит «Текст», и вставить перед последним:
|
||||
[[VAR:incoming.json.contents]] delKeyContains "Текст" delpos=-1
|
||||
|
||||
---
|
||||
2) Удалить «debug» с учётом регистра и подчистить пустые контейнеры:
|
||||
[[VAR:incoming.json.messages]] delKeyContains "debug" case=cs pruneEmpty
|
||||
|
||||
## 8) Отладка и рекомендации
|
||||
3) Несколько подстрок + вставка в начало:
|
||||
[[VAR:incoming.json]] delKeyContains "кеш" delKeyContains "cache" delpos=prepend
|
||||
|
||||
- ProviderCall печатает в консоль DEBUG сведения: выбранный провайдер, конечный URL, первые символы тела запроса — удобно для проверки корректности шаблона.
|
||||
- Если «ничего не подставилось»:
|
||||
1) Проверьте, что вы НЕ передаёте сырое входное тело напрямую в ProviderCall (узел строит тело из шаблона и Prompt Blocks).
|
||||
2) Убедитесь, что итоговый JSON валиден (закрывающие скобки, запятые).
|
||||
3) Проверьте точность путей в макросах (OUT vs OUTx, правильные id нод n1/n2/...).
|
||||
- Для ссылок на выходы предыдущих нод используйте [[OUT1]] как «просто текст», либо полные пути [[OUT:n1...]] для точного фрагмента.
|
||||
4) Смешанный пайплайн: сначала пред‑сегменты, затем:
|
||||
prompt_combine: "[[VAR:incoming.json.messages]] & [[PROMPT]]@pos=1"
|
||||
|
||||
---
|
||||
Диагностика:
|
||||
- В логи (SSE) слать событие "prompt_preprocess" с полями lines/used/removed_keys. Смотри [ProviderCallNode.run()](agentui/pipeline/executor.py:2211).
|
||||
|
||||
## 9) Быстрая памятка по ключам доступа
|
||||
Ограничения и заметки:
|
||||
- Это локальная предобработка именно сегментов для промпта (не глобальная фильтрация всего тела запроса).
|
||||
- Если пред‑сегменты и prompt_combine пусты — результат совпадает с классическим [[PROMPT]] (Prompt Blocks).
|
||||
РАЗДЕЛ 7 — JSON PATH (НАШ ПРОСТОЙ ДИАЛЕКТ) + 12 ПРИМЕРОВ
|
||||
|
||||
- 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_path_extract()](agentui/pipeline/executor.py:1475).
|
||||
|
||||
---
|
||||
Синтаксис (очень простой):
|
||||
- Путь вида a.b.c — точки для полей объектов.
|
||||
- Числовой индекс для массивов: items.0.title
|
||||
- Шаг «*» разворачивает все значения словаря или все элементы списка: items.*.title
|
||||
- Если на каком-то шаге ничего не найдено — вернёт None.
|
||||
- jp(...) → отдаёт найденное значение или список значений, jp_text(...) → склеит строки через join_sep (см. [_stringify_join()](agentui/pipeline/executor.py:1517)).
|
||||
|
||||
## 10) Ссылки на реализацию (для интересующихся деталями)
|
||||
12 примеров:
|
||||
1) Обычный путь:
|
||||
"a.b.c" на {"a":{"b":{"c":10}}} → 10
|
||||
|
||||
- Контекст (переменные): [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)
|
||||
2) Индекс массива:
|
||||
"items.1" на {"items":[10,20,30]} → 20
|
||||
|
||||
Удачного редактирования!
|
||||
---
|
||||
## Пользовательские переменные (SetVars) — «для людей»
|
||||
3) Вложено:
|
||||
"items.1.title" на items=[{title:"A"},{title:"B"}] → "B"
|
||||
|
||||
Задача: в начале пайплайна положить свои значения и потом использовать их в шаблонах одной строкой — например [[MY_KEY]] или {{ MAX_TOKENS }}.
|
||||
4) Звёздочка по массиву:
|
||||
"items.*.title" на items=[{title:"A"},{title:"B"}] → ["A","B"]
|
||||
|
||||
Где это в UI
|
||||
- В левой панели добавьте ноду SetVars и откройте её в инспекторе.
|
||||
- Жмите «Добавить переменную», у каждой переменной есть три поля:
|
||||
- name — имя переменной (латинские буквы/цифры/подчёркивание, не с цифры): MY_KEY, REGION, MAX_TOKENS
|
||||
- mode — режим обработки значения:
|
||||
- string — строка, в которой работают макросы ([[...]] и {{ ... }})
|
||||
- expr — «мини‑формула» без макросов (подробнее ниже)
|
||||
- value — собственно значение
|
||||
5) Звёздочка по объекту:
|
||||
"*.*.name" на {"x":{"name":"X"}, "y":{"name":"Y"}} → ["X","Y"]
|
||||
|
||||
Как потом вставлять переменные
|
||||
- Для строк (URL/заголовки/текст) — квадратные скобки: [[MY_KEY]]
|
||||
- Для чисел/массивов/объектов — фигурные скобки: {{ MAX_TOKENS }}, {{ GEN_CFG }}
|
||||
6) Смешанный:
|
||||
"candidates.0.content.parts.*.text" (Gemini) → все тексты
|
||||
|
||||
Примеры «как надо»
|
||||
- Переменная-строка (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 }}
|
||||
7) Несуществующее поле:
|
||||
"obj.miss" → None
|
||||
|
||||
Важно про два режима
|
||||
- string — это «шаблон». Внутри работают все макросы ([[VAR:...]], [[OUT:...]], [[PROMPT]], {{ ... }}). Значение прогоняется через рендер [render_template_simple()](agentui/pipeline/templating.py:184).
|
||||
- expr — это «мини‑формула». Внутри НЕТ макросов и НЕТ доступа к контексту; только литералы и операции (см. ниже). Вычисляет значение безопасно — без eval, на белом списке AST (реализация: [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:291)).
|
||||
8) Склейка текстов (jp_text):
|
||||
jp_text(value, "items.*.desc", " | ") → "a | b | c"
|
||||
|
||||
Что умеет 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 и т.п.
|
||||
- Запрещено: функции (кроме специально разрешённых ниже), доступ к переменным/контексту, атрибуты/индексация/условные выражения.
|
||||
9) Взять base64 из inlineData:
|
||||
"candidates.0.content.parts.1.inlineData.data"
|
||||
|
||||
Рандом в 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 нет — каждый запуск выдаёт новое значение.
|
||||
10) Несколько уровней массивов:
|
||||
"a.*.b.*.c"
|
||||
|
||||
«Почему в expr нельзя подставлять переменные/макросы?»
|
||||
- Для безопасности и предсказуемости: expr — это закрытый мини‑язык без окружения.
|
||||
- Если нужно использовать другие переменные/макросы — делайте это в режиме string (там всё рендерится шаблонизатором).
|
||||
- Технические детали: защита реализована в [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:291), а вставка string‑значений — через [render_template_simple()](agentui/pipeline/templating.py:184).
|
||||
11) Индекс вне границ:
|
||||
"items.99" → None
|
||||
|
||||
Как это работает внутри (если интересно)
|
||||
- SetVars исполняется как обычная нода пайплайна и отдаёт {"vars": {...}}.
|
||||
- Исполнитель добавляет эти значения в контекст для последующих нод как context.vars (см. [PipelineExecutor.run()](agentui/pipeline/executor.py:131)).
|
||||
- При рендере шаблонов:
|
||||
- [[NAME]] и {{ NAME }} подставляются с приоритетом из пользовательских переменных (см. обработку в [render_template_simple()](agentui/pipeline/templating.py:184)).
|
||||
- Сам SetVars считает переменные в порядке списка и возвращает их одним пакетом (внутри одной ноды значения не зависят друг от друга).
|
||||
12) Список/объект → строка (jp_text сам постарается найти текст глубже):
|
||||
jp_text(value, "response", "\n")
|
||||
|
||||
Частые вопросы
|
||||
- «Хочу собрать строку с частями из внешнего запроса»: делайте 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)).
|
||||
|
||||
Ссылки на реализацию (для любопытных)
|
||||
- Нода переменных: [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)
|
||||
РАЗДЕЛ 8 — OUTx, ИЗВЛЕЧЕНИЕ ТЕКСТА, ПРЕСЕТЫ, ГЛОБАЛЬНЫЕ ОПЦИИ
|
||||
|
||||
Откуда [[OUTx]] берёт текст:
|
||||
- Универсальный алгоритм (см. [templating._best_text_from_outputs()](agentui/pipeline/templating.py:133)) ищет:
|
||||
- OpenAI: choices[0].message.content
|
||||
- Gemini: candidates[].content.parts[].text
|
||||
- Claude: content[].text
|
||||
- Иначе — глубокий поиск текстовых полей.
|
||||
- Для ProviderCall/RawForward нода сама пишет response_text и отдаёт в OUT.
|
||||
|
||||
Настройка, если тебе нужно «не обобщённо, а вот отсюда»:
|
||||
- Глобальная meta (в «Запуск»): text_extract_strategy (auto|deep|jsonpath|openai|gemini|claude), text_extract_json_path, text_join_sep.
|
||||
- Пресеты в «Запуск → Пресеты парсинга OUTx»: создаёшь набор id/json_path, затем в ноде выбираешь preset по id (text_extract_preset_id).
|
||||
- На уровне ноды можно переопределить: text_extract_strategy, text_extract_json_path, text_join_sep.
|
||||
|
||||
Пример (пер-нодовый пресет):
|
||||
- В «Запуск» добавь JSONPath: candidates.0.content.parts.*.text
|
||||
- В ноде ProviderCall выбери этот preset — и [[OUTn]] станет строго вытягивать по нему.
|
||||
|
||||
Пример (жёсткий путь в ноде):
|
||||
- text_extract_strategy: "jsonpath"
|
||||
- text_extract_json_path: "result.echo.payload.parts.*.text"
|
||||
- text_join_sep: " | "
|
||||
|
||||
|
||||
РАЗДЕЛ 9 — ПАНЕЛЬ «ПЕРЕМЕННЫЕ» (STORE), КОПИРОВАНИЕ МАКРОСОВ
|
||||
|
||||
Где посмотреть в UI: [editor.html](static/editor.html) — кнопка «ПЕРЕМЕННЫЕ».
|
||||
- Там список ключей:
|
||||
- vars (текущие пользовательские переменные из SetVars)
|
||||
- snapshot (снимок последнего запуска: incoming, params, model, vendor_format, system, OUT, OUT_TEXT, LAST_NODE, алиасы OUT1/OUT2/…)
|
||||
- По клику копируется готовый макрос:
|
||||
- Для vars → [[NAME]] или {{ NAME }}
|
||||
- Для snapshot.OUT_TEXT.nX → [[OUTx]] или {{ OUT.nX.response_text }}
|
||||
- Для snapshot.OUT.nX.something → [[OUT:nX.something]] или {{ OUT.nX.something }}
|
||||
- Для прочего контекста → [[VAR:path]] или {{ path }}
|
||||
- Переключатель «фигурные» — управляет, в какой форме скопируется (квадратные или фигурные).
|
||||
|
||||
STORE (персистентность между прогонами):
|
||||
- Если pipeline.clear_var_store=false, содержимое не очищается между запуском.
|
||||
- Примеры макросов:
|
||||
- [[STORE:KEEP]]
|
||||
- {{ STORE.KEEP|default('none') }}
|
||||
|
||||
|
||||
РАЗДЕЛ 10 — ЦВЕТА КОННЕКТОВ И КТО О ЧЁМ ШЕПОЧЕТ
|
||||
|
||||
Выглядит мило, да. Это не просто так, это сигнализация (см. [editor.css](static/editor.css)):
|
||||
|
||||
- If.true (зелёный, пунктир): ветка истинности — класс .conn-if-true
|
||||
- If.false (сланцево-серый, пунктир): ветка ложности — .conn-if-false
|
||||
- ProviderCall (приглушённый синий): .conn-provider
|
||||
- RawForward (мягкий фиолетовый): .conn-raw
|
||||
- SetVars (мятный): .conn-setvars
|
||||
- Return (холодный серо-синий): .conn-return
|
||||
- Входящие к узлу с ошибкой подсвечиваются красным: .conn-upstream-err
|
||||
|
||||
А ещё стрелочки направления рисуются поверх линий, и лейблы «true/false» к If-веткам, так что перестань путаться, пожалуйста.
|
||||
|
||||
|
||||
РАЗДЕЛ 11 — ЧАСТЫЕ ПАТТЕРНЫ (РЕЦЕПТЫ НА 1 МИНУТУ)
|
||||
|
||||
1) «Прокинуть, но если 502 — подёргать ещё»
|
||||
- RawForward:
|
||||
- ignore_errors: true
|
||||
- while_expr: "{{ OUT.n3.result.status|default(0) }} == 502 && cycleindex < 3"
|
||||
|
||||
2) «Gemini: взять входные contents, добавить свой system и отправить в OpenAI»
|
||||
- ProviderCall (openai):
|
||||
- prompt_combine: "[[VAR:incoming.json.contents]] & [[PROMPT]]@pos=prepend"
|
||||
|
||||
3) «Сделать язык вывода в зависимости от заголовка X-Lang»
|
||||
- SetVars:
|
||||
- LANG (string): "[[VAR:incoming.headers.X-Lang|default('en')]]"
|
||||
- If:
|
||||
- expr: "[[LANG]] == 'ru'"
|
||||
- Return:
|
||||
- text_template: "[[OUT2]]" (где n2 — твоя ветка для RU)
|
||||
|
||||
4) «Доставать base64 из ответа и вставлять картинкой куда нужно»
|
||||
- jp_text(OUT, "candidates.*.content.parts.*.inlineData.data", "")
|
||||
- Либо сразу data URL через img(png)[[...]] на канвасе.
|
||||
|
||||
5) «Стабильно вытягивать текст из Claude»
|
||||
- Настраиваешь пресет: json_path="content.*.text", join="\n"
|
||||
- В ноде ProviderCall выбираешь этот preset.
|
||||
|
||||
|
||||
РАЗДЕЛ 12 — БЕЗОПАСНОСТЬ И НЕ ПАЛИ КЛЮЧИ
|
||||
|
||||
- Никогда не вписывай реальные ключи в presets/pipeline.json. Никогда — слышишь?
|
||||
- Передавай ключи из клиента заголовками:
|
||||
- OpenAI: Authorization: Bearer X
|
||||
- Anthropic: x-api-key: X
|
||||
- Gemini: ?key=... в URL
|
||||
- В шаблонах юзай [[VAR:incoming.headers.authorization]], [[VAR:incoming.headers.x-api-key]], [[VAR:incoming.api_keys.key]]
|
||||
- Убедись, что логирование не льёт секреты в проде (маскируй, см. сервер).
|
||||
|
||||
|
||||
РАЗДЕЛ 13 — ТРОБЛШУТИНГ (КАК НЕ ПЛАКАТЬ)
|
||||
|
||||
- JSON «не валидный» в ProviderCall:
|
||||
- В template лишняя запятая вокруг [[PROMPT]] или ты вставил строкой не-JSON. Проверь печать «rendered_template» в консоли (см. [ProviderCallNode.run()](agentui/pipeline/executor.py:2055)).
|
||||
|
||||
- Линии «исчезают» в редакторе:
|
||||
- Жми «Загрузить пайплайн» ещё раз — отложенные порты и наблюдатели синхронизируются.
|
||||
|
||||
- [[OUTx]] пустой:
|
||||
- Настрой пресет извлечения OUTx (Раздел 8) либо задействуй явный json_path.
|
||||
|
||||
- While завис навечно:
|
||||
- Проверь while_max_iters и само условие. Помни: это do-while, первая итерация — всегда.
|
||||
|
||||
- Claude system вдруг не где надо:
|
||||
- Смотри флаг claude_no_system (нода ProviderCall) — он переносит system в user.
|
||||
|
||||
|
||||
ПРИЛОЖЕНИЕ — ПОЛНЫЙ ЧЕК-ЛИСТ ПРИ СБОРКЕ НОДЫ PROVIDERCALL
|
||||
|
||||
1) Выбери provider (openai/gemini/claude) в инспекторе.
|
||||
2) Заполни provider_configs.{provider}.(base_url, endpoint, headers, template).
|
||||
- Подстановки в headers/template — через [[...]] / {{ ... }} (см. [render_template_simple()](agentui/pipeline/templating.py:205))
|
||||
3) Заполни Prompt Blocks (system/user/assistant/tool) — они в [[PROMPT]].
|
||||
4) Если нужно смешать с входящим payload — используй prompt_combine (Раздел 6).
|
||||
5) Если нужно ретраить — поставь while_expr/ignore_errors/sleep_ms.
|
||||
6) Если нужно извлекать текст особым образом — выбери preset или text_extract_*.
|
||||
7) Соедини depends по порядку и посмотри цвета проводов (Раздел 10).
|
||||
8) Готово. Без косяков? Правда? Ну, посмотрим.
|
||||
|
||||
СЛИШКОМ ДЛИННО, НЕ ЧИТАЛ:
|
||||
- [[...]] — текстовая подстановка.
|
||||
- {{ ... }} — типобезопасная подстановка (числа/объекты).
|
||||
- SetVars expr — только whitelist функций (rand, randint, choice, from_json, jp, jp_text, file_b64, data_url, file_data_url) и операции + - * / // % and/or/сравнения.
|
||||
- If — && || !, contains, скобки, макросы внутри.
|
||||
- While — do-while в ProviderCall/RawForward, есть cycleindex и WAS_ERROR; можно заменить If в ретраях.
|
||||
- prompt_combine — склейка сообщений из разных форматов с @pos=… и автоконвертацией под провайдера.
|
||||
- JSONPath — a.b.0.*.x, звёздочка и индексы; jp/jp_text.
|
||||
- Цвета линий — true/false — пунктир, по типу ноды — разные цвета; ошибка — красные upstream.
|
||||
- Не пались: ключи только через incoming.headers/URL.
|
||||
|
||||
Если ты дошёл досюда — ну… я не впечатлена. Просто запомни и не ломи моя нервная система, ладно? Хмф.
|
||||
|
||||
|
||||
1034
editor.html
BIN
favicon_io_saya/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
favicon_io_saya/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 337 KiB |
BIN
favicon_io_saya/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
favicon_io_saya/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 973 B |
BIN
favicon_io_saya/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
favicon_io_saya/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
favicon_io_saya/saya1.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
1
favicon_io_saya/site.webmanifest
Normal file
@@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
278
pipeline.json
@@ -6,139 +6,51 @@
|
||||
"loop_max_iters": 1000,
|
||||
"loop_time_budget_ms": 999999999999,
|
||||
"clear_var_store": true,
|
||||
"http_timeout_sec": 999.0,
|
||||
"text_extract_strategy": "auto",
|
||||
"text_extract_json_path": "",
|
||||
"text_join_sep": "\n",
|
||||
"text_extract_presets": [
|
||||
{
|
||||
"id": "pmfqonx6fvcubc09k4ep",
|
||||
"name": "candidates.0.content.parts.1.inlineData.data",
|
||||
"strategy": "jsonpath",
|
||||
"json_path": "candidates.0.content.parts.1.inlineData.data",
|
||||
"join_sep": "\n"
|
||||
},
|
||||
{
|
||||
"id": "pmfqrelw6wu9rutnzk1",
|
||||
"name": "candidates.0.content.parts.1.inlineData",
|
||||
"strategy": "jsonpath",
|
||||
"json_path": "candidates.0.content.parts.1.inlineData",
|
||||
"join_sep": "\n"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n1",
|
||||
"type": "RawForward",
|
||||
"pos_x": 450,
|
||||
"pos_y": 346,
|
||||
"config": {
|
||||
"passthrough_headers": true,
|
||||
"extra_headers": "{}",
|
||||
"_origId": "n1"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n5.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n2",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 662,
|
||||
"pos_y": 52,
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"Bearer [[MyOpenAiKey]]\"}",
|
||||
"template": "{\n \"model\": \"gpt-5-chat-latest\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": 500,\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]",
|
||||
"headers": "{}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://api.anthropic.com",
|
||||
"endpoint": "/v1/messages",
|
||||
"headers": "{\"x-api-key\":\"[[VAR:incoming.headers.x-api-key]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}"
|
||||
}
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "bmfchnynm",
|
||||
"name": "Сделай [[OUT1]] красивее",
|
||||
"role": "user",
|
||||
"prompt": "Сделай [[OUT1]] красивее",
|
||||
"enabled": true,
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"_origId": "n2"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n1.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n3",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 660.2222222222222,
|
||||
"pos_y": 561,
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"Bearer [[MyOpenAiKey]]\"}",
|
||||
"template": "{\n \"model\": \"gpt-5-chat-latest\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": 500,\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]",
|
||||
"headers": "{}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://api.anthropic.com",
|
||||
"endpoint": "/v1/messages",
|
||||
"headers": "{\"x-api-key\":\"[[VAR:incoming.headers.x-api-key]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}"
|
||||
}
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "bmfchn1hq",
|
||||
"name": "Сделай [[OUT1]] красивее",
|
||||
"role": "user",
|
||||
"prompt": "Сделай [[OUT1]] красивее",
|
||||
"enabled": true,
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"_origId": "n3"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n1.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n4",
|
||||
"type": "Return",
|
||||
"pos_x": 1277,
|
||||
"pos_y": 139,
|
||||
"config": {
|
||||
"target_format": "auto",
|
||||
"text_template": "[[OUT6]] [[Test]]",
|
||||
"_origId": "n4"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n7.true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n5",
|
||||
"type": "SetVars",
|
||||
"pos_x": 180,
|
||||
"pos_y": 477,
|
||||
"pos_x": 300,
|
||||
"pos_y": 720,
|
||||
"config": {
|
||||
"variables": [
|
||||
{
|
||||
"id": "vmfche3wn",
|
||||
"name": "Test",
|
||||
"id": "vmfi99ftc",
|
||||
"name": "Clod",
|
||||
"mode": "string",
|
||||
"value": "Быбра"
|
||||
"value": "igrovik"
|
||||
},
|
||||
{
|
||||
"id": "vmfchjpw4",
|
||||
"id": "vmfi99gjw",
|
||||
"name": "MyOpenAiKey",
|
||||
"mode": "string",
|
||||
"value": "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"
|
||||
},
|
||||
{
|
||||
"id": "vmfjkn09i",
|
||||
"name": "NAMETest",
|
||||
"mode": "expr",
|
||||
"value": "128 + 64"
|
||||
}
|
||||
],
|
||||
"_origId": "n5"
|
||||
@@ -146,18 +58,51 @@
|
||||
"in": {}
|
||||
},
|
||||
{
|
||||
"id": "n6",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 902,
|
||||
"pos_y": 320,
|
||||
"id": "n2",
|
||||
"type": "Return",
|
||||
"pos_x": 1344,
|
||||
"pos_y": 756,
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"target_format": "auto",
|
||||
"text_template": "[[OUT7]]",
|
||||
"_origId": "n2"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n7.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n3",
|
||||
"type": "RawForward",
|
||||
"pos_x": 552,
|
||||
"pos_y": 696,
|
||||
"config": {
|
||||
"passthrough_headers": true,
|
||||
"extra_headers": "{\"connection\": \"close\"}",
|
||||
"_origId": "n3",
|
||||
"while_expr": "([[OUT3]] contains \"Stream failed to\") || ([[OUT3]] contains \"gemini-2.5-pro\") ) || [[WAS_ERROR]]",
|
||||
"ignore_errors": true,
|
||||
"while_max_iters": 50,
|
||||
"override_path": "",
|
||||
"base_url": ""
|
||||
},
|
||||
"in": {
|
||||
"depends": "n5.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n4",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 780,
|
||||
"pos_y": 672,
|
||||
"config": {
|
||||
"provider": "gemini",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"Bearer [[MyOpenAiKey]]\"}",
|
||||
"template": "{\n \"model\": \"gpt-5-chat-latest\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": 500,\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
"headers": "{\"Authorization\":\"[[VAR:incoming.headers.authorization]]\"}",
|
||||
"template": "{\n \"model\": \"gpt-5-chat-latest\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_completion_tokens\": {{ incoming.json.max_completion_tokens|default(params.max_tokens|default(256)) }},\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
@@ -165,6 +110,12 @@
|
||||
"headers": "{}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"gemini_image": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent",
|
||||
"headers": "{\"x-goog-api-key\":\"[[VAR:incoming.api_keys.key]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]]\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://api.anthropic.com",
|
||||
"endpoint": "/v1/messages",
|
||||
@@ -174,35 +125,78 @@
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "bmfdyczbd",
|
||||
"name": "Объедени [[OUT3]], [[OUT4]] сделай более красиво.",
|
||||
"id": "bmfwy94ev",
|
||||
"name": "Твой ответ недостаточно хорош",
|
||||
"role": "user",
|
||||
"prompt": "Объедени [ [[OUT3]], [[OUT4]] ] сделай более красиво. напиши слово \"Красиво\" в конце.",
|
||||
"prompt": "Твой ответ:\n```\n[[OUT3]]\n```\nнедостаточно хорош, при его написании ты не следовал инструкциям. переделай исходя из инструкций, найди недостатки разобрав каждое действие оценив его логичность и следование истории от 0до10, перепиши эти моменты на нормальные.",
|
||||
"enabled": true,
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"_origId": "n6"
|
||||
"_origId": "n4",
|
||||
"prompt_preprocess": "[[VAR:incoming.json.contents]] delKeyContains \"Okie!\"",
|
||||
"prompt_combine": "[[VAR:incoming.json.contents]] & [[PROMPT]]@pos=append",
|
||||
"while_expr": "([[OUT4]] contains \"Stream failed to\") || ([[OUT4]] contains \"gemini-2.5-pro\") || ([[WAS_ERROR]] == true)",
|
||||
"ignore_errors": true,
|
||||
"while_max_iters": 50,
|
||||
"sleep_ms": 555555000
|
||||
},
|
||||
"in": {
|
||||
"depends": [
|
||||
"n2.done",
|
||||
"n3.done",
|
||||
"n7.false"
|
||||
]
|
||||
"depends": "n3.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n7",
|
||||
"type": "If",
|
||||
"pos_x": 1313,
|
||||
"pos_y": 566,
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 1080,
|
||||
"pos_y": 600,
|
||||
"config": {
|
||||
"expr": "[[OUT6]] contains \"Красиво\"",
|
||||
"_origId": "n7"
|
||||
"provider": "gemini",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"Bearer [[MyOpenAiKey]]\"}",
|
||||
"template": "{\n \"model\": \"gpt-5-chat-latest\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_completion_tokens\": {{ incoming.json.max_completion_tokens|default(params.max_tokens|default(256)) }},\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]",
|
||||
"headers": "{}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"gemini_image": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent",
|
||||
"headers": "{\"x-goog-api-key\":\"[[VAR:incoming.api_keys.key]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]]\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://proxy.malepreg.lol/proxy/aws/claude",
|
||||
"endpoint": "/v1/messages",
|
||||
"headers": "{\"x-api-key\":\"igrovik\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}",
|
||||
"template": "{\n \"model\": \"claude-opus-4-20250514\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}"
|
||||
}
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "bmfzvzpl7",
|
||||
"name": "Может содержать такие конструкции",
|
||||
"role": "user",
|
||||
"prompt": "Твой ответ:\n```\n[[OUT4]]\n```\nМожет содержать такие конструкции:\n**'Not X, but Y'** narrative structure. This includes any and all variations of stating what something *is not* in order to emphasize what it *is*. Нужно заменить места на нормальный нарратив.",
|
||||
"enabled": true,
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"_origId": "n7",
|
||||
"prompt_combine": "[[VAR:incoming.json.contents]] & [[PROMPT]]@pos=-1",
|
||||
"claude_no_system": true,
|
||||
"while_expr": "([[OUT7]] contains \"Stream failed to\") || ([[OUT7]] contains \"gemini-2.5-pro\") || [[WAS_ERROR]] == true",
|
||||
"ignore_errors": true,
|
||||
"while_max_iters": 50
|
||||
},
|
||||
"in": {
|
||||
"depends": "n6.done"
|
||||
"depends": "n4.done"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
237
presets/123123123.json
Normal file
@@ -0,0 +1,237 @@
|
||||
{
|
||||
"id": "pipeline_editor",
|
||||
"name": "Edited Pipeline",
|
||||
"parallel_limit": 8,
|
||||
"loop_mode": "iterative",
|
||||
"loop_max_iters": 1000,
|
||||
"loop_time_budget_ms": 999999999999,
|
||||
"clear_var_store": true,
|
||||
"http_timeout_sec": 999,
|
||||
"text_extract_strategy": "auto",
|
||||
"text_extract_json_path": "",
|
||||
"text_join_sep": "\n",
|
||||
"text_extract_presets": [
|
||||
{
|
||||
"id": "pmfqonx6fvcubc09k4ep",
|
||||
"name": "candidates.0.content.parts.1.inlineData.data",
|
||||
"strategy": "jsonpath",
|
||||
"json_path": "candidates.0.content.parts.1.inlineData.data",
|
||||
"join_sep": "\n"
|
||||
},
|
||||
{
|
||||
"id": "pmfqrelw6wu9rutnzk1",
|
||||
"name": "candidates.0.content.parts.1.inlineData",
|
||||
"strategy": "jsonpath",
|
||||
"json_path": "candidates.0.content.parts.1.inlineData",
|
||||
"join_sep": "\n"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n5",
|
||||
"type": "SetVars",
|
||||
"pos_x": 12,
|
||||
"pos_y": 780,
|
||||
"config": {
|
||||
"variables": [
|
||||
{
|
||||
"id": "vmfi99ftc",
|
||||
"name": "Clod",
|
||||
"mode": "string",
|
||||
"value": "igrovik"
|
||||
},
|
||||
{
|
||||
"id": "vmfi99gjw",
|
||||
"name": "MyOpenAiKey",
|
||||
"mode": "string",
|
||||
"value": "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"
|
||||
},
|
||||
{
|
||||
"id": "vmfjkn09i",
|
||||
"name": "NAMETest",
|
||||
"mode": "expr",
|
||||
"value": "128 + 64"
|
||||
}
|
||||
],
|
||||
"_origId": "n5"
|
||||
},
|
||||
"in": {}
|
||||
},
|
||||
{
|
||||
"id": "n2",
|
||||
"type": "Return",
|
||||
"pos_x": 1344,
|
||||
"pos_y": 756,
|
||||
"config": {
|
||||
"target_format": "auto",
|
||||
"text_template": "[[OUT7]]",
|
||||
"_origId": "n2"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n8.false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n3",
|
||||
"type": "RawForward",
|
||||
"pos_x": 564,
|
||||
"pos_y": 660,
|
||||
"config": {
|
||||
"passthrough_headers": true,
|
||||
"extra_headers": "{\"connection\": \"close\"}",
|
||||
"_origId": "n3"
|
||||
},
|
||||
"in": {
|
||||
"depends": [
|
||||
"n5.done",
|
||||
"n1.true"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n1",
|
||||
"type": "If",
|
||||
"pos_x": 564,
|
||||
"pos_y": 888,
|
||||
"config": {
|
||||
"expr": "([[OUT3]] contains \"Stream failed to\") || ([[OUT3]] contains \"gemini-2.5-pro\")",
|
||||
"_origId": "n1"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n3.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n4",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 792,
|
||||
"pos_y": 624,
|
||||
"config": {
|
||||
"provider": "gemini",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"Bearer [[VAR:incoming.headers.authorization]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"max_completion_tokens\": {{ incoming.json.max_completion_tokens|default(params.max_tokens|default(256)) }},\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]",
|
||||
"headers": "{}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"gemini_image": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent",
|
||||
"headers": "{\"x-goog-api-key\":\"[[VAR:incoming.api_keys.key]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]]\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://api.anthropic.com",
|
||||
"endpoint": "/v1/messages",
|
||||
"headers": "{\"x-api-key\":\"[[VAR:incoming.headers.x-api-key]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}"
|
||||
}
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "bmfwy94ev",
|
||||
"name": "Твой ответ недостаточно хорош",
|
||||
"role": "user",
|
||||
"prompt": "Твой ответ:\n```\n[[OUT3]]\n```\nнедостаточно хорош, при его написании ты не следовал инструкциям. переделай исходя из инструкций, найди недостатк1.",
|
||||
"enabled": true,
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"_origId": "n4",
|
||||
"prompt_combine": "[[VAR:incoming.json.contents]] & [[PROMPT]]@pos=-1"
|
||||
},
|
||||
"in": {
|
||||
"depends": [
|
||||
"n6.true",
|
||||
"n1.false"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n6",
|
||||
"type": "If",
|
||||
"pos_x": 792,
|
||||
"pos_y": 876,
|
||||
"config": {
|
||||
"expr": "([[OUT4]] contains \"Stream failed to\") || ([[OUT4]] contains \"gemini-2.5-pro\")",
|
||||
"_origId": "n6"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n4.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n7",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 1068,
|
||||
"pos_y": 540,
|
||||
"config": {
|
||||
"provider": "gemini",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"Bearer [[VAR:incoming.headers.authorization]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"max_completion_tokens\": {{ incoming.json.max_completion_tokens|default(params.max_tokens|default(256)) }},\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]",
|
||||
"headers": "{}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"gemini_image": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent",
|
||||
"headers": "{\"x-goog-api-key\":\"[[VAR:incoming.api_keys.key]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]]\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://api.anthropic.com",
|
||||
"endpoint": "/v1/messages",
|
||||
"headers": "{\"x-api-key\":\"[[VAR:incoming.headers.x-api-key]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}"
|
||||
}
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "bmfzvzpl7",
|
||||
"name": "Может содержать такие конструкции",
|
||||
"role": "user",
|
||||
"prompt": "Твой ответ:\n```\n[[OUT4]]\n```\nМожет содержать такие конструкции:\n**'Not X, but Y'** narrative structure. This includes any and all variations of stating what something *is not* in order to emphasize what it *is*. Нужно заменить места на нормальный нарратив.",
|
||||
"enabled": true,
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"_origId": "n7",
|
||||
"prompt_combine": "[[VAR:incoming.json.contents]] & [[PROMPT]]@pos=-1"
|
||||
},
|
||||
"in": {
|
||||
"depends": [
|
||||
"n6.false",
|
||||
"n8.true"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n8",
|
||||
"type": "If",
|
||||
"pos_x": 1068,
|
||||
"pos_y": 876,
|
||||
"config": {
|
||||
"expr": "([[OUT7]] contains \"Stream failed to\") || ([[OUT7]] contains \"gemini-2.5-pro\")",
|
||||
"_origId": "n8"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n7.done"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
191
presets/imgtests.json
Normal file
@@ -0,0 +1,191 @@
|
||||
{
|
||||
"id": "pipeline_editor",
|
||||
"name": "Edited Pipeline",
|
||||
"parallel_limit": 8,
|
||||
"loop_mode": "iterative",
|
||||
"loop_max_iters": 1000,
|
||||
"loop_time_budget_ms": 999999999999,
|
||||
"clear_var_store": true,
|
||||
"http_timeout_sec": 999,
|
||||
"text_extract_strategy": "auto",
|
||||
"text_extract_json_path": "",
|
||||
"text_join_sep": "\n",
|
||||
"text_extract_presets": [
|
||||
{
|
||||
"id": "pmfqonx6fvcubc09k4ep",
|
||||
"name": "candidates.0.content.parts.1.inlineData.data",
|
||||
"strategy": "jsonpath",
|
||||
"json_path": "candidates.0.content.parts.1.inlineData.data",
|
||||
"join_sep": "\n"
|
||||
},
|
||||
{
|
||||
"id": "pmfqrelw6wu9rutnzk1",
|
||||
"name": "candidates.0.content.parts.1.inlineData",
|
||||
"strategy": "jsonpath",
|
||||
"json_path": "candidates.0.content.parts.1.inlineData",
|
||||
"join_sep": "\n"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n5",
|
||||
"type": "SetVars",
|
||||
"pos_x": -603,
|
||||
"pos_y": 637,
|
||||
"config": {
|
||||
"variables": [
|
||||
{
|
||||
"id": "vmfi99ftc",
|
||||
"name": "Clod",
|
||||
"mode": "string",
|
||||
"value": "igrovik"
|
||||
},
|
||||
{
|
||||
"id": "vmfi99gjw",
|
||||
"name": "MyOpenAiKey",
|
||||
"mode": "string",
|
||||
"value": "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"
|
||||
},
|
||||
{
|
||||
"id": "vmfjkn09i",
|
||||
"name": "NAMETest",
|
||||
"mode": "expr",
|
||||
"value": "128 + 64"
|
||||
}
|
||||
],
|
||||
"_origId": "n5"
|
||||
},
|
||||
"in": {}
|
||||
},
|
||||
{
|
||||
"id": "n2",
|
||||
"type": "Return",
|
||||
"pos_x": 509,
|
||||
"pos_y": 459,
|
||||
"config": {
|
||||
"target_format": "auto",
|
||||
"text_template": "[[OUT3]]",
|
||||
"_origId": "n2"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n1.false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n3",
|
||||
"type": "RawForward",
|
||||
"pos_x": 45,
|
||||
"pos_y": 750,
|
||||
"config": {
|
||||
"passthrough_headers": true,
|
||||
"extra_headers": "{\"connection\": \"close\"}",
|
||||
"_origId": "n3"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n1.true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n1",
|
||||
"type": "If",
|
||||
"pos_x": 344,
|
||||
"pos_y": 730,
|
||||
"config": {
|
||||
"expr": "([[OUT3]] contains \"Stream failed to\") || ([[OUT3]] contains \"gemini-2.5-pro\")",
|
||||
"_origId": "n1"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n3.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n4",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": -185.88888888888889,
|
||||
"pos_y": 523,
|
||||
"config": {
|
||||
"provider": "gemini_image",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"Bearer [[VAR:incoming.headers.authorization]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"max_completion_tokens\": {{ incoming.json.max_completion_tokens|default(params.max_tokens|default(256)) }},\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]",
|
||||
"headers": "{}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"gemini_image": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/gemini-2.5-flash-image-preview:generateContent",
|
||||
"headers": "{\"x-goog-api-key\":\"[[VAR:incoming.api_keys.key]]\"}",
|
||||
"template": "{\n \"model\": \"gemini-2.5-flash-image-preview\",\n [[OUT3]]\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://api.anthropic.com",
|
||||
"endpoint": "/v1/messages",
|
||||
"headers": "{\"x-api-key\":\"[[VAR:incoming.headers.x-api-key]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}"
|
||||
}
|
||||
},
|
||||
"blocks": [],
|
||||
"_origId": "n4"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n6.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n6",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": -391,
|
||||
"pos_y": 648,
|
||||
"config": {
|
||||
"provider": "gemini",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"Bearer [[VAR:incoming.headers.authorization]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"max_completion_tokens\": {{ incoming.json.max_completion_tokens|default(params.max_tokens|default(256)) }},\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]",
|
||||
"headers": "{}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[VAR:incoming.json.contents]],\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"gemini_image": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent",
|
||||
"headers": "{\"x-goog-api-key\":\"[[VAR:incoming.api_keys.key]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]]\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://api.anthropic.com",
|
||||
"endpoint": "/v1/messages",
|
||||
"headers": "{\"x-api-key\":\"[[VAR:incoming.headers.x-api-key]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}"
|
||||
}
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "bmfuw6ayo",
|
||||
"name": "Создание промпта",
|
||||
"role": "user",
|
||||
"prompt": "Создай промпт для генерации изображения исходя из последнего действие {{user}}. Промпт должен быть лаконичный, простенький, без сложных формулировок. В ответе не пиши ничего кроме промпта.",
|
||||
"enabled": true,
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"_origId": "n6"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n5.done"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
171
presets/prepprst.json
Normal file
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"id": "pipeline_editor",
|
||||
"name": "Edited Pipeline",
|
||||
"parallel_limit": 8,
|
||||
"loop_mode": "iterative",
|
||||
"loop_max_iters": 1000,
|
||||
"loop_time_budget_ms": 999999999999,
|
||||
"clear_var_store": true,
|
||||
"http_timeout_sec": 999,
|
||||
"text_extract_strategy": "auto",
|
||||
"text_extract_json_path": "",
|
||||
"text_join_sep": "\n",
|
||||
"text_extract_presets": [
|
||||
{
|
||||
"id": "pmfqonx6fvcubc09k4ep",
|
||||
"name": "candidates.0.content.parts.1.inlineData.data",
|
||||
"strategy": "jsonpath",
|
||||
"json_path": "candidates.0.content.parts.1.inlineData.data",
|
||||
"join_sep": "\n"
|
||||
},
|
||||
{
|
||||
"id": "pmfqrelw6wu9rutnzk1",
|
||||
"name": "candidates.0.content.parts.1.inlineData",
|
||||
"strategy": "jsonpath",
|
||||
"json_path": "candidates.0.content.parts.1.inlineData",
|
||||
"join_sep": "\n"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n5",
|
||||
"type": "SetVars",
|
||||
"pos_x": -125,
|
||||
"pos_y": 561,
|
||||
"config": {
|
||||
"variables": [
|
||||
{
|
||||
"id": "vmfi99ftc",
|
||||
"name": "Clod",
|
||||
"mode": "string",
|
||||
"value": "igrovik"
|
||||
},
|
||||
{
|
||||
"id": "vmfi99gjw",
|
||||
"name": "MyOpenAiKey",
|
||||
"mode": "string",
|
||||
"value": "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"
|
||||
},
|
||||
{
|
||||
"id": "vmfjkn09i",
|
||||
"name": "NAMETest",
|
||||
"mode": "expr",
|
||||
"value": "128 + 64"
|
||||
}
|
||||
],
|
||||
"_origId": "n5"
|
||||
},
|
||||
"in": {}
|
||||
},
|
||||
{
|
||||
"id": "n2",
|
||||
"type": "Return",
|
||||
"pos_x": 954,
|
||||
"pos_y": 564,
|
||||
"config": {
|
||||
"target_format": "auto",
|
||||
"text_template": "[[OUT4]]",
|
||||
"_origId": "n2"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n6.false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n3",
|
||||
"type": "RawForward",
|
||||
"pos_x": 74,
|
||||
"pos_y": 450.5,
|
||||
"config": {
|
||||
"passthrough_headers": true,
|
||||
"extra_headers": "{\"connection\": \"close\"}",
|
||||
"_origId": "n3"
|
||||
},
|
||||
"in": {
|
||||
"depends": [
|
||||
"n5.done",
|
||||
"n1.true"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n1",
|
||||
"type": "If",
|
||||
"pos_x": 75,
|
||||
"pos_y": 909,
|
||||
"config": {
|
||||
"expr": "([[OUT3]] contains \"Stream failed to\") || ([[OUT3]] contains \"gemini-2.5-pro\")",
|
||||
"_origId": "n1"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n3.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n4",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 663,
|
||||
"pos_y": 335,
|
||||
"config": {
|
||||
"provider": "gemini",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"Bearer [[VAR:incoming.headers.authorization]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"max_completion_tokens\": {{ incoming.json.max_completion_tokens|default(params.max_tokens|default(256)) }},\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]",
|
||||
"headers": "{}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"gemini_image": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent",
|
||||
"headers": "{\"x-goog-api-key\":\"[[VAR:incoming.api_keys.key]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]]\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://api.anthropic.com",
|
||||
"endpoint": "/v1/messages",
|
||||
"headers": "{\"x-api-key\":\"[[VAR:incoming.headers.x-api-key]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}"
|
||||
}
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "bmfwy94ev",
|
||||
"name": "Твой ответ недостаточно хорош",
|
||||
"role": "user",
|
||||
"prompt": "Твой ответ:\n```\n[[OUT3]]\n```\nнедостаточно хорош, при его написании ты не следовал инструкциям. переделай исходя из инструкций, найди недостатки.",
|
||||
"enabled": true,
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"_origId": "n4",
|
||||
"prompt_combine": "[[VAR:incoming.json.contents]] & [[PROMPT]]@pos=-1"
|
||||
},
|
||||
"in": {
|
||||
"depends": [
|
||||
"n6.true",
|
||||
"n1.false"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n6",
|
||||
"type": "If",
|
||||
"pos_x": 675,
|
||||
"pos_y": 882.25,
|
||||
"config": {
|
||||
"expr": "([[OUT4]] contains \"Stream failed to\") || ([[OUT4]] contains \"gemini-2.5-pro\")",
|
||||
"_origId": "n6"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n4.done"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
105
presets/retry.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"id": "pipeline_editor",
|
||||
"name": "Edited Pipeline",
|
||||
"parallel_limit": 8,
|
||||
"loop_mode": "iterative",
|
||||
"loop_max_iters": 1000,
|
||||
"loop_time_budget_ms": 999999999999,
|
||||
"clear_var_store": true,
|
||||
"http_timeout_sec": 999,
|
||||
"text_extract_strategy": "auto",
|
||||
"text_extract_json_path": "",
|
||||
"text_join_sep": "\n",
|
||||
"text_extract_presets": [
|
||||
{
|
||||
"id": "pmfqonx6fvcubc09k4ep",
|
||||
"name": "candidates.0.content.parts.1.inlineData.data",
|
||||
"strategy": "jsonpath",
|
||||
"json_path": "candidates.0.content.parts.1.inlineData.data",
|
||||
"join_sep": "\n"
|
||||
},
|
||||
{
|
||||
"id": "pmfqrelw6wu9rutnzk1",
|
||||
"name": "candidates.0.content.parts.1.inlineData",
|
||||
"strategy": "jsonpath",
|
||||
"json_path": "candidates.0.content.parts.1.inlineData",
|
||||
"join_sep": "\n"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n5",
|
||||
"type": "SetVars",
|
||||
"pos_x": -125,
|
||||
"pos_y": 561,
|
||||
"config": {
|
||||
"variables": [
|
||||
{
|
||||
"id": "vmfi99ftc",
|
||||
"name": "Clod",
|
||||
"mode": "string",
|
||||
"value": "igrovik"
|
||||
},
|
||||
{
|
||||
"id": "vmfi99gjw",
|
||||
"name": "MyOpenAiKey",
|
||||
"mode": "string",
|
||||
"value": "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"
|
||||
},
|
||||
{
|
||||
"id": "vmfjkn09i",
|
||||
"name": "NAMETest",
|
||||
"mode": "expr",
|
||||
"value": "128 + 64"
|
||||
}
|
||||
],
|
||||
"_origId": "n5"
|
||||
},
|
||||
"in": {}
|
||||
},
|
||||
{
|
||||
"id": "n2",
|
||||
"type": "Return",
|
||||
"pos_x": 507,
|
||||
"pos_y": 459,
|
||||
"config": {
|
||||
"target_format": "auto",
|
||||
"text_template": "[[OUT3]]",
|
||||
"_origId": "n2"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n1.false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n3",
|
||||
"type": "RawForward",
|
||||
"pos_x": 114,
|
||||
"pos_y": 425,
|
||||
"config": {
|
||||
"passthrough_headers": true,
|
||||
"extra_headers": "{\"connection\": \"close\"}",
|
||||
"_origId": "n3"
|
||||
},
|
||||
"in": {
|
||||
"depends": [
|
||||
"n5.done",
|
||||
"n1.true"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n1",
|
||||
"type": "If",
|
||||
"pos_x": 344,
|
||||
"pos_y": 730,
|
||||
"config": {
|
||||
"expr": "([[OUT3]] contains \"Stream failed to\") || ([[OUT3]] contains \"gemini-2.5-pro\")",
|
||||
"_origId": "n1"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n3.done"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
278
presets/test2.json
Normal file
@@ -0,0 +1,278 @@
|
||||
{
|
||||
"id": "pipeline_editor",
|
||||
"name": "Edited Pipeline",
|
||||
"parallel_limit": 8,
|
||||
"loop_mode": "iterative",
|
||||
"loop_max_iters": 1000,
|
||||
"loop_time_budget_ms": 999999999999,
|
||||
"clear_var_store": true,
|
||||
"http_timeout_sec": 999,
|
||||
"text_extract_strategy": "auto",
|
||||
"text_extract_json_path": "",
|
||||
"text_join_sep": "\n",
|
||||
"text_extract_presets": [
|
||||
{
|
||||
"id": "pmfipb98aywtx6jepd5",
|
||||
"name": "ввв",
|
||||
"strategy": "jsonpath",
|
||||
"json_path": "ввв",
|
||||
"join_sep": "\n"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n1",
|
||||
"type": "RawForward",
|
||||
"pos_x": 441,
|
||||
"pos_y": 354,
|
||||
"config": {
|
||||
"passthrough_headers": true,
|
||||
"extra_headers": "{}",
|
||||
"_origId": "n1"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n5.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n2",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 652,
|
||||
"pos_y": 46,
|
||||
"config": {
|
||||
"provider": "gemini",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"Bearer [[MyOpenAiKey]]\"}",
|
||||
"template": "{\n \"model\": \"gpt-5-chat-latest\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": 500,\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]",
|
||||
"headers": "{}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://proxy.malepreg.lol/proxy/aws/claude",
|
||||
"endpoint": "/v1/messages",
|
||||
"headers": "{\"x-api-key\":\"[[Clod]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"output-128k-2025-02-19\"}",
|
||||
"template": "{\n \"model\": \"claude-opus-4-20250514\",\n [[PROMPT]],\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('enabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(3000) }}\n }\n}"
|
||||
},
|
||||
"gemini_image": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent",
|
||||
"headers": "{\"x-goog-api-key\":\"[[VAR:incoming.api_keys.key]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]]\n}"
|
||||
}
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "bmfmstojw",
|
||||
"name": "Great assustant",
|
||||
"role": "system",
|
||||
"prompt": "You are Great assustant",
|
||||
"enabled": true,
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"id": "bmfchnynm",
|
||||
"name": "Сделай [[OUT1]] красивее",
|
||||
"role": "user",
|
||||
"prompt": "Сделай [[OUT1]] красивее",
|
||||
"enabled": true,
|
||||
"order": 1
|
||||
}
|
||||
],
|
||||
"_origId": "n2"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n1.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n3",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 654,
|
||||
"pos_y": 566,
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"Bearer [[MyOpenAiKey]]\"}",
|
||||
"template": "{\n \"model\": \"gpt-5-chat-latest\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": 500,\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]",
|
||||
"headers": "{}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://api.anthropic.com",
|
||||
"endpoint": "/v1/messages",
|
||||
"headers": "{\"x-api-key\":\"[[VAR:incoming.headers.x-api-key]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}"
|
||||
},
|
||||
"gemini_image": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent",
|
||||
"headers": "{\"x-goog-api-key\":\"[[VAR:incoming.api_keys.key]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]]\n}"
|
||||
}
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "bmfchn1hq",
|
||||
"name": "Сделай [[OUT1]] красивее",
|
||||
"role": "user",
|
||||
"prompt": "Сделай [[OUT1]] красивее",
|
||||
"enabled": true,
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"_origId": "n3"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n1.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n4",
|
||||
"type": "Return",
|
||||
"pos_x": 1193,
|
||||
"pos_y": 314,
|
||||
"config": {
|
||||
"target_format": "auto",
|
||||
"text_template": "[[OUT6]] [[Test]]",
|
||||
"_origId": "n4"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n7.true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n5",
|
||||
"type": "SetVars",
|
||||
"pos_x": 171,
|
||||
"pos_y": 487,
|
||||
"config": {
|
||||
"variables": [
|
||||
{
|
||||
"id": "vmfi99ftc",
|
||||
"name": "Clod",
|
||||
"mode": "string",
|
||||
"value": "igrovik"
|
||||
},
|
||||
{
|
||||
"id": "vmfi99gjw",
|
||||
"name": "MyOpenAiKey",
|
||||
"mode": "string",
|
||||
"value": "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"
|
||||
},
|
||||
{
|
||||
"id": "vmfjkn09i",
|
||||
"name": "NAMETest",
|
||||
"mode": "expr",
|
||||
"value": "128 + 64"
|
||||
}
|
||||
],
|
||||
"_origId": "n5"
|
||||
},
|
||||
"in": {}
|
||||
},
|
||||
{
|
||||
"id": "n6",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 923,
|
||||
"pos_y": 345,
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"Bearer [[MyOpenAiKey]]\"}",
|
||||
"template": "{\n \"model\": \"gpt-5-chat-latest\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": 500,\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]",
|
||||
"headers": "{}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://api.anthropic.com",
|
||||
"endpoint": "/v1/messages",
|
||||
"headers": "{\"x-api-key\":\"[[VAR:incoming.headers.x-api-key]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}"
|
||||
},
|
||||
"gemini_image": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent",
|
||||
"headers": "{\"x-goog-api-key\":\"[[VAR:incoming.api_keys.key]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]]\n}"
|
||||
}
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "bmfmk7g4a",
|
||||
"name": "New Block",
|
||||
"role": "system",
|
||||
"prompt": "",
|
||||
"enabled": true,
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"id": "bmfdyczbd",
|
||||
"name": "Объедени [[OUT3]], [[OUT4]] сделай более красиво.",
|
||||
"role": "user",
|
||||
"prompt": "Объедени [ [[OUT3]], [[OUT2]] ] сделай более красиво. напиши слово \"Красиво\" в конце.",
|
||||
"enabled": true,
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": "bmfh98jkh",
|
||||
"name": "New Block1",
|
||||
"role": "system",
|
||||
"prompt": "1",
|
||||
"enabled": true,
|
||||
"order": 2
|
||||
},
|
||||
{
|
||||
"id": "bmfmk74yz",
|
||||
"name": "New Block",
|
||||
"role": "assistant",
|
||||
"prompt": "fuf",
|
||||
"enabled": true,
|
||||
"order": 3
|
||||
}
|
||||
],
|
||||
"_origId": "n6"
|
||||
},
|
||||
"in": {
|
||||
"depends": [
|
||||
"n2.done",
|
||||
"n3.done",
|
||||
"n7.false"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n7",
|
||||
"type": "If",
|
||||
"pos_x": 1311,
|
||||
"pos_y": 566,
|
||||
"config": {
|
||||
"expr": "[[OUT6]] contains \"Красиво\"",
|
||||
"_origId": "n7"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n6.done"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,34 +1,53 @@
|
||||
{
|
||||
"id": "pipeline_editor",
|
||||
"name": "Edited Pipeline",
|
||||
"parallel_limit": 8,
|
||||
"loop_mode": "iterative",
|
||||
"loop_max_iters": 1000,
|
||||
"loop_time_budget_ms": 999999999999,
|
||||
"clear_var_store": true,
|
||||
"http_timeout_sec": 999,
|
||||
"text_extract_strategy": "auto",
|
||||
"text_extract_json_path": "",
|
||||
"text_join_sep": "\n",
|
||||
"text_extract_presets": [
|
||||
{
|
||||
"id": "pmfipb98aywtx6jepd5",
|
||||
"name": "ввв",
|
||||
"strategy": "jsonpath",
|
||||
"json_path": "ввв",
|
||||
"join_sep": "\n"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n1",
|
||||
"type": "RawForward",
|
||||
"pos_x": 427,
|
||||
"pos_y": 363.5,
|
||||
"pos_x": 450,
|
||||
"pos_y": 352,
|
||||
"config": {
|
||||
"passthrough_headers": true,
|
||||
"extra_headers": "{}",
|
||||
"_origId": "n1"
|
||||
"_origId": "n1",
|
||||
"sleep_ms": 5000
|
||||
},
|
||||
"in": {
|
||||
"depends": "n6.done"
|
||||
"depends": "n5.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n2",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 659,
|
||||
"pos_y": 89,
|
||||
"pos_x": 653,
|
||||
"pos_y": 51,
|
||||
"config": {
|
||||
"provider": "gemini",
|
||||
"provider": "claude",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"Bearer [[VAR:incoming.headers.authorization]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"max_completion_tokens\": {{ incoming.json.max_completion_tokens|default(params.max_tokens|default(256)) }},\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
"headers": "{\"Authorization\":\"Bearer [[MyOpenAiKey]]\"}",
|
||||
"template": "{\n \"model\": \"gpt-5-chat-latest\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": 500,\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
@@ -37,10 +56,10 @@
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://api.anthropic.com",
|
||||
"base_url": "https://proxy.malepreg.lol/proxy/aws/claude",
|
||||
"endpoint": "/v1/messages",
|
||||
"headers": "{\"x-api-key\":\"[[VAR:incoming.headers.x-api-key]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}"
|
||||
"headers": "{\"x-api-key\":\"[[Clod]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"output-128k-2025-02-19\"}",
|
||||
"template": "{\n \"model\": \"claude-opus-4-20250514\",\n [[PROMPT]],\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('enabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(6000) }}\n }\n}"
|
||||
}
|
||||
},
|
||||
"blocks": [
|
||||
@@ -62,8 +81,8 @@
|
||||
{
|
||||
"id": "n3",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 673,
|
||||
"pos_y": 455,
|
||||
"pos_x": 658,
|
||||
"pos_y": 564,
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"provider_configs": {
|
||||
@@ -104,9 +123,53 @@
|
||||
},
|
||||
{
|
||||
"id": "n4",
|
||||
"type": "Return",
|
||||
"pos_x": 1277,
|
||||
"pos_y": 139,
|
||||
"config": {
|
||||
"target_format": "auto",
|
||||
"text_template": "[[OUT6]] [[Test]]",
|
||||
"_origId": "n4"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n7.true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n5",
|
||||
"type": "SetVars",
|
||||
"pos_x": 171,
|
||||
"pos_y": 489,
|
||||
"config": {
|
||||
"variables": [
|
||||
{
|
||||
"id": "vmfi99ftc",
|
||||
"name": "Clod",
|
||||
"mode": "string",
|
||||
"value": "igrovik"
|
||||
},
|
||||
{
|
||||
"id": "vmfi99gjw",
|
||||
"name": "MyOpenAiKey",
|
||||
"mode": "string",
|
||||
"value": "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"
|
||||
},
|
||||
{
|
||||
"id": "vmfi99h1v",
|
||||
"name": "Test",
|
||||
"mode": "string",
|
||||
"value": "Ьыыы"
|
||||
}
|
||||
],
|
||||
"_origId": "n5"
|
||||
},
|
||||
"in": {}
|
||||
},
|
||||
{
|
||||
"id": "n6",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 929.5,
|
||||
"pos_y": 233.5,
|
||||
"pos_x": 923,
|
||||
"pos_y": 345,
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"provider_configs": {
|
||||
@@ -131,73 +194,43 @@
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "bmfchm54f",
|
||||
"name": "Объедени [[OUT3]], [[OUT2]] сделай более красиво.",
|
||||
"id": "bmfdyczbd",
|
||||
"name": "Объедени [[OUT3]], [[OUT4]] сделай более красиво.",
|
||||
"role": "user",
|
||||
"prompt": "Объедени [[OUT3]], [[OUT4]] сделай более красиво. [[TEST]]",
|
||||
"prompt": "Объедени [ [[OUT3]], [[OUT2]] ] сделай более красиво. напиши слово \"Красиво\" в конце.",
|
||||
"enabled": true,
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"_origId": "n4"
|
||||
},
|
||||
"in": {
|
||||
"depends": [
|
||||
"n3.done",
|
||||
"n2.done",
|
||||
"n7.true"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n5",
|
||||
"type": "Return",
|
||||
"pos_x": 1281,
|
||||
"pos_y": 139.5,
|
||||
"config": {
|
||||
"target_format": "auto",
|
||||
"text_template": "[[OUT4]] [[Test]]",
|
||||
"_origId": "n5"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n7.true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n6",
|
||||
"type": "SetVars",
|
||||
"pos_x": 180,
|
||||
"pos_y": 477,
|
||||
"config": {
|
||||
"variables": [
|
||||
{
|
||||
"id": "vmfche3wn",
|
||||
"name": "Test",
|
||||
"mode": "string",
|
||||
"value": "Быбра"
|
||||
},
|
||||
{
|
||||
"id": "vmfchjpw4",
|
||||
"name": "MyOpenAiKey",
|
||||
"mode": "string",
|
||||
"value": "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"
|
||||
"id": "bmfh98jkh",
|
||||
"name": "New Block1",
|
||||
"role": "system",
|
||||
"prompt": "1",
|
||||
"enabled": true,
|
||||
"order": 1
|
||||
}
|
||||
],
|
||||
"_origId": "n6"
|
||||
},
|
||||
"in": {}
|
||||
"in": {
|
||||
"depends": [
|
||||
"n2.done",
|
||||
"n3.done",
|
||||
"n7.false"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n7",
|
||||
"type": "If",
|
||||
"pos_x": 1145,
|
||||
"pos_y": 463,
|
||||
"pos_x": 1313,
|
||||
"pos_y": 566,
|
||||
"config": {
|
||||
"expr": "[[OUT4]] contains \"красиво\"",
|
||||
"expr": "[[OUT6]] contains \"Красиво\"",
|
||||
"_origId": "n7"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n4.done"
|
||||
"depends": "n6.done"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
171
presets/testtesttt.json
Normal file
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"id": "pipeline_editor",
|
||||
"name": "Edited Pipeline",
|
||||
"parallel_limit": 8,
|
||||
"loop_mode": "iterative",
|
||||
"loop_max_iters": 1000,
|
||||
"loop_time_budget_ms": 999999999999,
|
||||
"clear_var_store": true,
|
||||
"http_timeout_sec": 999,
|
||||
"text_extract_strategy": "auto",
|
||||
"text_extract_json_path": "",
|
||||
"text_join_sep": "\n",
|
||||
"text_extract_presets": [
|
||||
{
|
||||
"id": "pmfqonx6fvcubc09k4ep",
|
||||
"name": "candidates.0.content.parts.1.inlineData.data",
|
||||
"strategy": "jsonpath",
|
||||
"json_path": "candidates.0.content.parts.1.inlineData.data",
|
||||
"join_sep": "\n"
|
||||
},
|
||||
{
|
||||
"id": "pmfqrelw6wu9rutnzk1",
|
||||
"name": "candidates.0.content.parts.1.inlineData",
|
||||
"strategy": "jsonpath",
|
||||
"json_path": "candidates.0.content.parts.1.inlineData",
|
||||
"join_sep": "\n"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n5",
|
||||
"type": "SetVars",
|
||||
"pos_x": -125,
|
||||
"pos_y": 561,
|
||||
"config": {
|
||||
"variables": [
|
||||
{
|
||||
"id": "vmfi99ftc",
|
||||
"name": "Clod",
|
||||
"mode": "string",
|
||||
"value": "igrovik"
|
||||
},
|
||||
{
|
||||
"id": "vmfi99gjw",
|
||||
"name": "MyOpenAiKey",
|
||||
"mode": "string",
|
||||
"value": "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"
|
||||
},
|
||||
{
|
||||
"id": "vmfjkn09i",
|
||||
"name": "NAMETest",
|
||||
"mode": "expr",
|
||||
"value": "128 + 64"
|
||||
}
|
||||
],
|
||||
"_origId": "n5"
|
||||
},
|
||||
"in": {}
|
||||
},
|
||||
{
|
||||
"id": "n2",
|
||||
"type": "Return",
|
||||
"pos_x": 954,
|
||||
"pos_y": 564,
|
||||
"config": {
|
||||
"target_format": "auto",
|
||||
"text_template": "[[OUT4]]",
|
||||
"_origId": "n2"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n6.false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n3",
|
||||
"type": "RawForward",
|
||||
"pos_x": 72,
|
||||
"pos_y": 444,
|
||||
"config": {
|
||||
"passthrough_headers": true,
|
||||
"extra_headers": "{\"connection\": \"close\"}",
|
||||
"_origId": "n3"
|
||||
},
|
||||
"in": {
|
||||
"depends": [
|
||||
"n5.done",
|
||||
"n1.true"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n1",
|
||||
"type": "If",
|
||||
"pos_x": 75,
|
||||
"pos_y": 909,
|
||||
"config": {
|
||||
"expr": "([[OUT3]] contains \"Stream failed to\") || ([[OUT3]] contains \"gemini-2.5-pro\")",
|
||||
"_origId": "n1"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n3.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n4",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 663,
|
||||
"pos_y": 335,
|
||||
"config": {
|
||||
"provider": "gemini",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"Bearer [[VAR:incoming.headers.authorization]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"max_completion_tokens\": {{ incoming.json.max_completion_tokens|default(params.max_tokens|default(256)) }},\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]",
|
||||
"headers": "{}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"gemini_image": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent",
|
||||
"headers": "{\"x-goog-api-key\":\"[[VAR:incoming.api_keys.key]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]]\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://api.anthropic.com",
|
||||
"endpoint": "/v1/messages",
|
||||
"headers": "{\"x-api-key\":\"[[VAR:incoming.headers.x-api-key]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}"
|
||||
}
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "bmfwy94ev",
|
||||
"name": "Твой ответ недостаточно хорош",
|
||||
"role": "user",
|
||||
"prompt": "Твой ответ:\n```\n[[OUT3]]\n```\nнедостаточно хорош, при его написании ты не следовал инструкциям. переделай исходя из инструкций, найди недостатк.",
|
||||
"enabled": true,
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"_origId": "n4",
|
||||
"prompt_combine": "[[VAR:incoming.json.contents]] & [[PROMPT]]@pos=-1"
|
||||
},
|
||||
"in": {
|
||||
"depends": [
|
||||
"n6.true",
|
||||
"n1.false"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n6",
|
||||
"type": "If",
|
||||
"pos_x": 675,
|
||||
"pos_y": 882.25,
|
||||
"config": {
|
||||
"expr": "([[OUT4]] contains \"Stream failed to\") || ([[OUT4]] contains \"gemini-2.5-pro\")",
|
||||
"_origId": "n6"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n4.done"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
285
presets/tttttt.json
Normal file
@@ -0,0 +1,285 @@
|
||||
{
|
||||
"id": "pipeline_editor",
|
||||
"name": "Edited Pipeline",
|
||||
"parallel_limit": 8,
|
||||
"loop_mode": "iterative",
|
||||
"loop_max_iters": 1000,
|
||||
"loop_time_budget_ms": 999999999999,
|
||||
"clear_var_store": true,
|
||||
"http_timeout_sec": 999,
|
||||
"text_extract_strategy": "auto",
|
||||
"text_extract_json_path": "",
|
||||
"text_join_sep": "\n",
|
||||
"text_extract_presets": [
|
||||
{
|
||||
"id": "pmfqonx6fvcubc09k4ep",
|
||||
"name": "candidates.0.content.parts.1.inlineData.data",
|
||||
"strategy": "jsonpath",
|
||||
"json_path": "candidates.0.content.parts.1.inlineData.data",
|
||||
"join_sep": "\n"
|
||||
},
|
||||
{
|
||||
"id": "pmfqrelw6wu9rutnzk1",
|
||||
"name": "candidates.0.content.parts.1.inlineData",
|
||||
"strategy": "jsonpath",
|
||||
"json_path": "candidates.0.content.parts.1.inlineData",
|
||||
"join_sep": "\n"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n5",
|
||||
"type": "SetVars",
|
||||
"pos_x": 12,
|
||||
"pos_y": 780,
|
||||
"config": {
|
||||
"variables": [
|
||||
{
|
||||
"id": "vmfi99ftc",
|
||||
"name": "Clod",
|
||||
"mode": "string",
|
||||
"value": "igrovik"
|
||||
},
|
||||
{
|
||||
"id": "vmfi99gjw",
|
||||
"name": "MyOpenAiKey",
|
||||
"mode": "string",
|
||||
"value": "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"
|
||||
},
|
||||
{
|
||||
"id": "vmfjkn09i",
|
||||
"name": "NAMETest",
|
||||
"mode": "expr",
|
||||
"value": "128 + 64"
|
||||
}
|
||||
],
|
||||
"_origId": "n5"
|
||||
},
|
||||
"in": {}
|
||||
},
|
||||
{
|
||||
"id": "n2",
|
||||
"type": "Return",
|
||||
"pos_x": 1344,
|
||||
"pos_y": 756,
|
||||
"config": {
|
||||
"target_format": "auto",
|
||||
"text_template": "[[OUT7]]",
|
||||
"_origId": "n2"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n8.false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n3",
|
||||
"type": "RawForward",
|
||||
"pos_x": 588,
|
||||
"pos_y": 624,
|
||||
"config": {
|
||||
"passthrough_headers": true,
|
||||
"extra_headers": "{\"connection\": \"close\"}",
|
||||
"_origId": "n3"
|
||||
},
|
||||
"in": {
|
||||
"depends": [
|
||||
"n5.done",
|
||||
"n1.true"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n1",
|
||||
"type": "If",
|
||||
"pos_x": 564,
|
||||
"pos_y": 876,
|
||||
"config": {
|
||||
"expr": "([[OUT3]] contains \"Stream failed to\") || ([[OUT3]] contains \"gemini-2.5-pro\")",
|
||||
"_origId": "n1"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n3.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n4",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 792,
|
||||
"pos_y": 624,
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"[[VAR:incoming.headers.authorization]]\"}",
|
||||
"template": "{\n \"model\": \"gpt-5-chat-latest\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_completion_tokens\": {{ incoming.json.max_completion_tokens|default(params.max_tokens|default(256)) }},\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]",
|
||||
"headers": "{}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"gemini_image": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent",
|
||||
"headers": "{\"x-goog-api-key\":\"[[VAR:incoming.api_keys.key]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]]\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://api.anthropic.com",
|
||||
"endpoint": "/v1/messages",
|
||||
"headers": "{\"x-api-key\":\"[[VAR:incoming.headers.x-api-key]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}"
|
||||
}
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "bmfwy94ev",
|
||||
"name": "Твой ответ недостаточно хорош",
|
||||
"role": "user",
|
||||
"prompt": "Твой ответ:\n```\n[[OUT3]]\n```\nнедостаточно хорош, при его написании ты не следовал инструкциям. переделай исходя из инструкций, найди недостатк1.",
|
||||
"enabled": true,
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"_origId": "n4",
|
||||
"prompt_combine": "[[VAR:incoming.json.messages]] & [[PROMPT]]"
|
||||
},
|
||||
"in": {
|
||||
"depends": [
|
||||
"n6.true",
|
||||
"n1.false"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n6",
|
||||
"type": "If",
|
||||
"pos_x": 792,
|
||||
"pos_y": 876,
|
||||
"config": {
|
||||
"expr": "([[OUT4]] contains \"Stream failed to\") || ([[OUT4]] contains \"gemini-2.5-pro\")",
|
||||
"_origId": "n6"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n4.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n7",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 1056,
|
||||
"pos_y": 624,
|
||||
"config": {
|
||||
"provider": "claude",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"Bearer [[VAR:incoming.headers.authorization]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"max_completion_tokens\": {{ incoming.json.max_completion_tokens|default(params.max_tokens|default(256)) }},\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]",
|
||||
"headers": "{}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"gemini_image": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent",
|
||||
"headers": "{\"x-goog-api-key\":\"[[VAR:incoming.api_keys.key]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]]\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://proxy.malepreg.lol/proxy/aws/claude",
|
||||
"endpoint": "/v1/messages",
|
||||
"headers": "{\"x-api-key\":\"igrovik\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}",
|
||||
"template": "{\n \"model\": \"claude-opus-4-20250514\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}"
|
||||
}
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "bmfzvzpl7",
|
||||
"name": "Может содержать такие конструкции",
|
||||
"role": "user",
|
||||
"prompt": "Твой ответ:\n```\n[[OUT4]]\n```\nМожет содержать такие конструкции:\n**'Not X, but Y'** narrative structure. This includes any and all variations of stating what something *is not* in order to emphasize what it *is*. Нужно заменить места на нормальный нарратив.",
|
||||
"enabled": true,
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"_origId": "n7",
|
||||
"prompt_combine": "[[VAR:incoming.json.messages]] & [[PROMPT]]",
|
||||
"claude_no_system": true
|
||||
},
|
||||
"in": {
|
||||
"depends": [
|
||||
"n6.false",
|
||||
"n8.true"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n8",
|
||||
"type": "If",
|
||||
"pos_x": 1068,
|
||||
"pos_y": 876,
|
||||
"config": {
|
||||
"expr": "([[OUT7]] contains \"Stream failed to\") || ([[OUT7]] contains \"gemini-2.5-pro\")",
|
||||
"_origId": "n8"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n7.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n9",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 1104,
|
||||
"pos_y": 456,
|
||||
"config": {
|
||||
"provider": "claude",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"Bearer [[VAR:incoming.headers.authorization]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"max_completion_tokens\": {{ incoming.json.max_completion_tokens|default(params.max_tokens|default(256)) }},\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]",
|
||||
"headers": "{}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"gemini_image": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent",
|
||||
"headers": "{\"x-goog-api-key\":\"[[VAR:incoming.api_keys.key]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]]\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://proxy.malepreg.lol/proxy/aws/claude",
|
||||
"endpoint": "/v1/messages",
|
||||
"headers": "{\"x-api-key\":\"igrovik\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}",
|
||||
"template": "{\n \"model\": \"claude-opus-4-20250514\",\n [[PROMPT]],\n \"top_p\": 1,\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}"
|
||||
}
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "bmg26nusx",
|
||||
"name": "New Block",
|
||||
"role": "user",
|
||||
"prompt": "Hey",
|
||||
"enabled": true,
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"_origId": "n9"
|
||||
},
|
||||
"in": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
247
presets/tttttt1.json
Normal file
@@ -0,0 +1,247 @@
|
||||
{
|
||||
"id": "pipeline_editor",
|
||||
"name": "Edited Pipeline",
|
||||
"parallel_limit": 8,
|
||||
"loop_mode": "iterative",
|
||||
"loop_max_iters": 1000,
|
||||
"loop_time_budget_ms": 999999999999,
|
||||
"clear_var_store": true,
|
||||
"http_timeout_sec": 999,
|
||||
"text_extract_strategy": "auto",
|
||||
"text_extract_json_path": "",
|
||||
"text_join_sep": "\n",
|
||||
"text_extract_presets": [
|
||||
{
|
||||
"id": "pmfqonx6fvcubc09k4ep",
|
||||
"name": "candidates.0.content.parts.1.inlineData.data",
|
||||
"strategy": "jsonpath",
|
||||
"json_path": "candidates.0.content.parts.1.inlineData.data",
|
||||
"join_sep": "\n"
|
||||
},
|
||||
{
|
||||
"id": "pmfqrelw6wu9rutnzk1",
|
||||
"name": "candidates.0.content.parts.1.inlineData",
|
||||
"strategy": "jsonpath",
|
||||
"json_path": "candidates.0.content.parts.1.inlineData",
|
||||
"join_sep": "\n"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n5",
|
||||
"type": "SetVars",
|
||||
"pos_x": 12,
|
||||
"pos_y": 780,
|
||||
"config": {
|
||||
"variables": [
|
||||
{
|
||||
"id": "vmfi99ftc",
|
||||
"name": "Clod",
|
||||
"mode": "string",
|
||||
"value": "igrovik"
|
||||
},
|
||||
{
|
||||
"id": "vmfi99gjw",
|
||||
"name": "MyOpenAiKey",
|
||||
"mode": "string",
|
||||
"value": "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"
|
||||
},
|
||||
{
|
||||
"id": "vmfjkn09i",
|
||||
"name": "NAMETest",
|
||||
"mode": "expr",
|
||||
"value": "128 + 64"
|
||||
}
|
||||
],
|
||||
"_origId": "n5"
|
||||
},
|
||||
"in": {}
|
||||
},
|
||||
{
|
||||
"id": "n2",
|
||||
"type": "Return",
|
||||
"pos_x": 1344,
|
||||
"pos_y": 756,
|
||||
"config": {
|
||||
"target_format": "auto",
|
||||
"text_template": "[[OUT7]]",
|
||||
"_origId": "n2"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n8.false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n3",
|
||||
"type": "RawForward",
|
||||
"pos_x": 588,
|
||||
"pos_y": 624,
|
||||
"config": {
|
||||
"passthrough_headers": true,
|
||||
"extra_headers": "{\"connection\": \"close\"}",
|
||||
"_origId": "n3",
|
||||
"while_expr": "([[OUT3]] contains \"Stream failed to\") || ([[OUT3]] contains \"gemini-2.5-pro\")",
|
||||
"ignore_errors": false,
|
||||
"while_max_iters": 50
|
||||
},
|
||||
"in": {
|
||||
"depends": [
|
||||
"n5.done",
|
||||
"n1.true"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n1",
|
||||
"type": "If",
|
||||
"pos_x": 600,
|
||||
"pos_y": 876,
|
||||
"config": {
|
||||
"expr": "([[OUT3]] contains \"Stream failed to\") || ([[OUT3]] contains \"gemini-2.5-pro\")",
|
||||
"_origId": "n1"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n3.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n4",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 792,
|
||||
"pos_y": 624,
|
||||
"config": {
|
||||
"provider": "gemini",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"[[VAR:incoming.headers.authorization]]\"}",
|
||||
"template": "{\n \"model\": \"gpt-5-chat-latest\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_completion_tokens\": {{ incoming.json.max_completion_tokens|default(params.max_tokens|default(256)) }},\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]",
|
||||
"headers": "{}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"gemini_image": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent",
|
||||
"headers": "{\"x-goog-api-key\":\"[[VAR:incoming.api_keys.key]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]]\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://api.anthropic.com",
|
||||
"endpoint": "/v1/messages",
|
||||
"headers": "{\"x-api-key\":\"[[VAR:incoming.headers.x-api-key]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}"
|
||||
}
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "bmfwy94ev",
|
||||
"name": "Твой ответ недостаточно хорош",
|
||||
"role": "user",
|
||||
"prompt": "Твой ответ:\n```\n[[OUT3]]\n```\nнедостаточно хорош, при его написании ты не следовал инструкциям. переделай исходя из инструкций, найди недостатки разобрав каждое действие оценив его логичность и следование истории от 0до10, перепиши эти моменты на нормальные.",
|
||||
"enabled": true,
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"_origId": "n4",
|
||||
"prompt_combine": "[[VAR:incoming.json.contents]] & [[PROMPT]]@pos=-1",
|
||||
"while_expr": "([[OUT3]] contains \"Stream failed to\") || ([[OUT3]] contains \"gemini-2.5-pro\")",
|
||||
"ignore_errors": false,
|
||||
"while_max_iters": 50
|
||||
},
|
||||
"in": {
|
||||
"depends": [
|
||||
"n6.true",
|
||||
"n1.false"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n6",
|
||||
"type": "If",
|
||||
"pos_x": 852,
|
||||
"pos_y": 960,
|
||||
"config": {
|
||||
"expr": "([[OUT4]] contains \"Stream failed to\") || ([[OUT4]] contains \"gemini-2.5-pro\")",
|
||||
"_origId": "n6"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n4.done"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n7",
|
||||
"type": "ProviderCall",
|
||||
"pos_x": 1080,
|
||||
"pos_y": 624,
|
||||
"config": {
|
||||
"provider": "gemini",
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"headers": "{\"Authorization\":\"Bearer [[MyOpenAiKey]]\"}",
|
||||
"template": "{\n \"model\": \"gpt-5-chat-latest\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_completion_tokens\": {{ incoming.json.max_completion_tokens|default(params.max_tokens|default(256)) }},\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}"
|
||||
},
|
||||
"gemini": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]",
|
||||
"headers": "{}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}"
|
||||
},
|
||||
"gemini_image": {
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"endpoint": "/v1beta/models/{{ model }}:generateContent",
|
||||
"headers": "{\"x-goog-api-key\":\"[[VAR:incoming.api_keys.key]]\"}",
|
||||
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]]\n}"
|
||||
},
|
||||
"claude": {
|
||||
"base_url": "https://proxy.malepreg.lol/proxy/aws/claude",
|
||||
"endpoint": "/v1/messages",
|
||||
"headers": "{\"x-api-key\":\"igrovik\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}",
|
||||
"template": "{\n \"model\": \"claude-opus-4-20250514\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}"
|
||||
}
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"id": "bmfzvzpl7",
|
||||
"name": "Может содержать такие конструкции",
|
||||
"role": "user",
|
||||
"prompt": "Твой ответ:\n```\n[[OUT4]]\n```\nМожет содержать такие конструкции:\n**'Not X, but Y'** narrative structure. This includes any and all variations of stating what something *is not* in order to emphasize what it *is*. Нужно заменить места на нормальный нарратив.",
|
||||
"enabled": true,
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"_origId": "n7",
|
||||
"prompt_combine": "[[VAR:incoming.json.contents]] & [[PROMPT]]@pos=-1",
|
||||
"claude_no_system": true,
|
||||
"while_expr": "([[OUT3]] contains \"Stream failed to\") || ([[OUT3]] contains \"gemini-2.5-pro\")",
|
||||
"ignore_errors": false,
|
||||
"while_max_iters": 50
|
||||
},
|
||||
"in": {
|
||||
"depends": [
|
||||
"n6.false",
|
||||
"n8.true"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n8",
|
||||
"type": "If",
|
||||
"pos_x": 1068,
|
||||
"pos_y": 876,
|
||||
"config": {
|
||||
"expr": "([[OUT7]] contains \"Stream failed to\") || ([[OUT7]] contains \"gemini-2.5-pro\")",
|
||||
"_origId": "n8"
|
||||
},
|
||||
"in": {
|
||||
"depends": "n7.done"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
fastapi==0.112.2
|
||||
fastapi==0.115.2
|
||||
uvicorn==0.30.6
|
||||
pydantic==2.8.2
|
||||
httpx==0.27.0
|
||||
starlette==0.38.2
|
||||
httpx[socks]==0.27.0
|
||||
|
||||
starlette==0.40.0
|
||||
|
||||
brotlicffi
|
||||
brotli
|
||||
@@ -1,27 +1,51 @@
|
||||
@echo off
|
||||
setlocal
|
||||
chcp 65001 >NUL
|
||||
set PORT=7860
|
||||
echo Installing dependencies...
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
REM -------- Config --------
|
||||
if "%PORT%"=="" set PORT=7860
|
||||
if "%HOST%"=="" set HOST=127.0.0.1
|
||||
REM ------------------------
|
||||
|
||||
echo [НадTavern] Preparing virtual environment...
|
||||
|
||||
REM Pick Python launcher
|
||||
where py >NUL 2>&1
|
||||
if %ERRORLEVEL%==0 (
|
||||
set PY=py
|
||||
) else (
|
||||
set PY=python
|
||||
)
|
||||
|
||||
REM Create venv if missing
|
||||
if not exist ".venv\Scripts\python.exe" (
|
||||
%PY% -m venv .venv
|
||||
if errorlevel 1 goto :fail
|
||||
)
|
||||
|
||||
set "VENV_PY=.venv\Scripts\python.exe"
|
||||
|
||||
echo [НадTavern] Upgrading pip...
|
||||
"%VENV_PY%" -m pip install --upgrade pip
|
||||
if errorlevel 1 goto :fail
|
||||
pip install -r requirements.txt
|
||||
|
||||
echo [НадTavern] Installing dependencies from requirements.txt...
|
||||
"%VENV_PY%" -m pip install -r requirements.txt
|
||||
if errorlevel 1 goto :fail
|
||||
echo Starting НадTavern on http://127.0.0.1:%PORT%/
|
||||
|
||||
echo [НадTavern] Starting on http://%HOST%:%PORT%/
|
||||
timeout /t 1 /nobreak >NUL
|
||||
start "" "http://127.0.0.1:%PORT%/ui/editor.html"
|
||||
python -m uvicorn agentui.api.server:app --host 127.0.0.1 --port %PORT% --log-level info
|
||||
start "" "http://%HOST%:%PORT%/ui/editor.html"
|
||||
|
||||
"%VENV_PY%" -m uvicorn agentui.api.server:app --host %HOST% --port %PORT% --log-level info
|
||||
if errorlevel 1 goto :fail
|
||||
goto :end
|
||||
|
||||
:fail
|
||||
echo.
|
||||
echo Server failed with errorlevel %errorlevel%.
|
||||
echo [НадTavern] Server failed with errorlevel %errorlevel%.
|
||||
echo Check the console output above and the file agentui.log for details.
|
||||
pause
|
||||
|
||||
:end
|
||||
pause
|
||||
endlocal
|
||||
|
||||
|
||||
|
||||
46
run_agentui.sh
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# НадTavern Linux/macOS launcher with local .venv bootstrap
|
||||
# Usage:
|
||||
# chmod +x ./run_agentui.sh
|
||||
# ./run_agentui.sh
|
||||
# Optional env: HOST=0.0.0.0 PORT=7860
|
||||
|
||||
# Go to repo root (script location)
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
PORT="${PORT:-7860}"
|
||||
HOST="${HOST:-127.0.0.1}"
|
||||
|
||||
# Pick python
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
PY=python3
|
||||
else
|
||||
PY=python
|
||||
fi
|
||||
|
||||
# Create venv if missing
|
||||
if [ ! -f ".venv/bin/python" ]; then
|
||||
echo "[НадTavern] Creating .venv ..."
|
||||
"$PY" -m venv .venv
|
||||
fi
|
||||
|
||||
VENV_PY=".venv/bin/python"
|
||||
|
||||
echo "[НадTavern] Upgrading pip ..."
|
||||
"$VENV_PY" -m pip install --upgrade pip
|
||||
|
||||
echo "[НадTavern] Installing deps from requirements.txt ..."
|
||||
"$VENV_PY" -m pip install -r requirements.txt
|
||||
|
||||
echo "[НадTavern] Starting on http://$HOST:$PORT/"
|
||||
|
||||
# Try to open UI editor in default browser (non-fatal if fails)
|
||||
if command -v xdg-open >/dev/null 2>&1; then
|
||||
xdg-open "http://$HOST:$PORT/ui/editor.html" >/dev/null 2>&1 || true
|
||||
elif command -v open >/dev/null 2>&1; then
|
||||
open "http://$HOST:$PORT/ui/editor.html" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
exec "$VENV_PY" -m uvicorn agentui.api.server:app --host "$HOST" --port "$PORT" --log-level info
|
||||
1315
static/editor.css
4133
static/editor.html
@@ -4,6 +4,12 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>НадTavern</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 24px; }
|
||||
textarea { width: 100%; height: 200px; }
|
||||
|
||||
@@ -21,10 +21,15 @@
|
||||
// Готовим новые данные с глубокой копией blocks
|
||||
const newData = { ...(n.data || {}), blocks: Array.isArray(d2.blocks) ? d2.blocks.map(b => ({ ...b })) : [] };
|
||||
// 1) Обновляем внутреннее состояние Drawflow, чтобы export() возвращал актуальные данные
|
||||
try { editor.updateNodeDataFromId(id, newData); } catch (e) {}
|
||||
// 2) Обновляем DOM-отражение (источник правды для toPipelineJSON)
|
||||
const el2 = document.querySelector(`#node-${id}`);
|
||||
if (el2) el2.__data = JSON.parse(JSON.stringify(newData));
|
||||
try {
|
||||
if (w.AU && typeof w.AU.updateNodeDataAndDom === 'function') {
|
||||
w.AU.updateNodeDataAndDom(editor, id, newData);
|
||||
} else {
|
||||
editor.updateNodeDataFromId(id, newData);
|
||||
const el2 = document.querySelector(`#node-${id}`);
|
||||
if (el2) el2.__data = JSON.parse(JSON.stringify(newData));
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) {}
|
||||
}
|
||||
// Initial sync to attach blocks into __data for toPipelineJSON
|
||||
|
||||
158
static/js/providerTemplates.js
Normal file
@@ -0,0 +1,158 @@
|
||||
/* global window */
|
||||
(function (w) {
|
||||
'use strict';
|
||||
|
||||
// Centralized registry for provider-specific defaults (base_url, endpoint, headers, template)
|
||||
// Exposes window.ProviderTemplates with:
|
||||
// .register(name, { defaultConfig: () => ({ base_url, endpoint, headers, template }) })
|
||||
// .defaults(provider)
|
||||
// .ensureConfigs(nodeData)
|
||||
// .getActiveProv(nodeData)
|
||||
// .getActiveCfg(nodeData)
|
||||
// .providers()
|
||||
|
||||
const PT = {};
|
||||
const _registry = new Map();
|
||||
|
||||
function norm(p) {
|
||||
return String(p == null ? 'openai' : p).toLowerCase().trim();
|
||||
}
|
||||
|
||||
PT.register = function register(name, def) {
|
||||
const key = norm(name);
|
||||
if (!def || typeof def.defaultConfig !== 'function') {
|
||||
throw new Error('ProviderTemplates.register: def.defaultConfig() required');
|
||||
}
|
||||
_registry.set(key, { defaultConfig: def.defaultConfig });
|
||||
};
|
||||
|
||||
PT.providers = function providers() {
|
||||
return Array.from(_registry.keys());
|
||||
};
|
||||
|
||||
PT.defaults = function defaults(provider) {
|
||||
const key = norm(provider);
|
||||
const rec = _registry.get(key);
|
||||
if (rec && typeof rec.defaultConfig === 'function') {
|
||||
try { return rec.defaultConfig(); } catch (_) {}
|
||||
}
|
||||
return { base_url: '', endpoint: '', headers: `{}`, template: `{}` };
|
||||
};
|
||||
|
||||
PT.ensureConfigs = function ensureConfigs(d) {
|
||||
if (!d) return;
|
||||
if (!d.provider) d.provider = 'openai';
|
||||
if (!d.provider_configs || typeof d.provider_configs !== 'object') d.provider_configs = {};
|
||||
for (const p of PT.providers()) {
|
||||
if (!d.provider_configs[p]) d.provider_configs[p] = PT.defaults(p);
|
||||
}
|
||||
};
|
||||
|
||||
PT.getActiveProv = function getActiveProv(d) {
|
||||
return norm(d && d.provider);
|
||||
};
|
||||
|
||||
PT.getActiveCfg = function getActiveCfg(d) {
|
||||
PT.ensureConfigs(d);
|
||||
const p = PT.getActiveProv(d);
|
||||
return d && d.provider_configs ? (d.provider_configs[p] || {}) : {};
|
||||
};
|
||||
|
||||
// --- Built-in providers (default presets) ---
|
||||
// Templates mirror original editor.html logic; use macros [[...]] and {{ ... }} as-is.
|
||||
function T_OPENAI() { return `{
|
||||
"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)) }},
|
||||
"max_completion_tokens": {{ incoming.json.max_completion_tokens|default(params.max_tokens|default(256)) }},
|
||||
"presence_penalty": {{ incoming.json.presence_penalty|default(0) }},
|
||||
"frequency_penalty": {{ incoming.json.frequency_penalty|default(0) }},
|
||||
"stop": {{ incoming.json.stop|default(params.stop|default([])) }},
|
||||
"stream": {{ incoming.json.stream|default(false) }}
|
||||
}`; }
|
||||
|
||||
function T_GEMINI() { return `{
|
||||
"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([])) }},
|
||||
"candidateCount": {{ incoming.json.generationConfig.candidateCount|default(1) }},
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},
|
||||
"thinkingBudget": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}
|
||||
}
|
||||
}
|
||||
}`; }
|
||||
|
||||
function T_GEMINI_IMAGE() { return `{
|
||||
"model": "{{ model }}",
|
||||
[[PROMPT]]
|
||||
}`; }
|
||||
|
||||
function T_CLAUDE() { return `{
|
||||
"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_sequences": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},
|
||||
"stream": {{ incoming.json.stream|default(false) }},
|
||||
"thinking": {
|
||||
"type": "{{ incoming.json.thinking.type|default('disabled') }}",
|
||||
"budget_tokens": {{ incoming.json.thinking.budget_tokens|default(0) }}
|
||||
},
|
||||
"anthropic_version": "{{ anthropic_version|default('2023-06-01') }}"
|
||||
}`; }
|
||||
|
||||
// Register built-ins
|
||||
PT.register('openai', {
|
||||
defaultConfig: () => ({
|
||||
base_url: 'https://api.openai.com',
|
||||
endpoint: '/v1/chat/completions',
|
||||
headers: `{"Authorization":"Bearer [[VAR:incoming.headers.authorization]]"}`,
|
||||
template: T_OPENAI()
|
||||
})
|
||||
});
|
||||
PT.register('gemini', {
|
||||
defaultConfig: () => ({
|
||||
base_url: 'https://generativelanguage.googleapis.com',
|
||||
endpoint: '/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]',
|
||||
headers: `{}`,
|
||||
template: T_GEMINI()
|
||||
})
|
||||
});
|
||||
PT.register('gemini_image', {
|
||||
defaultConfig: () => ({
|
||||
base_url: 'https://generativelanguage.googleapis.com',
|
||||
endpoint: '/v1beta/models/{{ model }}:generateContent',
|
||||
headers: `{"x-goog-api-key":"[[VAR:incoming.api_keys.key]]"}`,
|
||||
template: T_GEMINI_IMAGE()
|
||||
})
|
||||
});
|
||||
PT.register('claude', {
|
||||
defaultConfig: () => ({
|
||||
base_url: 'https://api.anthropic.com',
|
||||
endpoint: '/v1/messages',
|
||||
headers: `{"x-api-key":"[[VAR:incoming.headers.x-api-key]]","anthropic-version":"2023-06-01","anthropic-beta":"[[VAR:incoming.headers.anthropic-beta]]"}`,
|
||||
template: T_CLAUDE()
|
||||
})
|
||||
});
|
||||
|
||||
try { console.debug('[ProviderTemplates] providers:', PT.providers()); } catch (_) {}
|
||||
|
||||
// Export globals and compatibility shims
|
||||
try {
|
||||
w.ProviderTemplates = PT;
|
||||
// Back-compat shims so existing code can call global helpers
|
||||
w.providerDefaults = PT.defaults;
|
||||
w.ensureProviderConfigs = PT.ensureConfigs;
|
||||
w.getActiveProv = PT.getActiveProv;
|
||||
w.getActiveCfg = PT.getActiveCfg;
|
||||
} catch (_) {}
|
||||
})(window);
|
||||
@@ -12,7 +12,8 @@
|
||||
|
||||
// Top-level pipeline meta kept in memory and included into JSON on save.
|
||||
// Allows UI to edit loop parameters without manual JSON edits.
|
||||
let _pipelineMeta = {
|
||||
// DRY: единый источник дефолтов и нормализации meta
|
||||
const MetaDefaults = Object.freeze({
|
||||
id: 'pipeline_editor',
|
||||
name: 'Edited Pipeline',
|
||||
parallel_limit: 8,
|
||||
@@ -20,7 +21,74 @@
|
||||
loop_max_iters: 1000,
|
||||
loop_time_budget_ms: 10000,
|
||||
clear_var_store: true,
|
||||
};
|
||||
http_timeout_sec: 60,
|
||||
text_extract_strategy: 'auto',
|
||||
text_extract_json_path: '',
|
||||
text_join_sep: '\n',
|
||||
// v2: коллекция пресетов извлечения текста, управляется в "Запуск"
|
||||
// [{ id, name, strategy, json_path, join_sep }]
|
||||
text_extract_presets: [],
|
||||
});
|
||||
|
||||
let _pipelineMeta = { ...MetaDefaults };
|
||||
|
||||
// Нормализатор meta: приводит типы, поддерживает синонимы ключей, заполняет дефолты
|
||||
function ensureMeta(p) {
|
||||
const src = (p && typeof p === 'object') ? p : {};
|
||||
const out = { ...MetaDefaults };
|
||||
|
||||
// helpers
|
||||
const toInt = (v, def) => {
|
||||
try {
|
||||
const n = parseInt(v, 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : def;
|
||||
} catch { return def; }
|
||||
};
|
||||
const toNum = (v, def) => {
|
||||
try {
|
||||
const n = parseFloat(v);
|
||||
return !Number.isNaN(n) && n > 0 ? n : def;
|
||||
} catch { return def; }
|
||||
};
|
||||
|
||||
// базовые поля
|
||||
try { out.id = String((src.id ?? out.id) || out.id); } catch {}
|
||||
try { out.name = String((src.name ?? out.name) || out.name); } catch {}
|
||||
|
||||
out.parallel_limit = toInt(src.parallel_limit, out.parallel_limit);
|
||||
out.loop_mode = String((src.loop_mode ?? out.loop_mode) || out.loop_mode);
|
||||
out.loop_max_iters = toInt(src.loop_max_iters, out.loop_max_iters);
|
||||
out.loop_time_budget_ms = toInt(src.loop_time_budget_ms, out.loop_time_budget_ms);
|
||||
out.clear_var_store = (typeof src.clear_var_store === 'boolean') ? !!src.clear_var_store : out.clear_var_store;
|
||||
out.http_timeout_sec = toNum(src.http_timeout_sec, out.http_timeout_sec);
|
||||
out.text_extract_strategy = String((src.text_extract_strategy ?? out.text_extract_strategy) || out.text_extract_strategy);
|
||||
out.text_extract_json_path = String((src.text_extract_json_path ?? out.text_extract_json_path) || out.text_extract_json_path);
|
||||
|
||||
// поддержка синонимов text_join_sep (регистр и вариации)
|
||||
let joinSep = out.text_join_sep;
|
||||
try {
|
||||
for (const k of Object.keys(src)) {
|
||||
if (String(k).toLowerCase() === 'text_join_sep') { joinSep = src[k]; break; }
|
||||
}
|
||||
} catch {}
|
||||
out.text_join_sep = String((joinSep ?? src.text_join_sep ?? out.text_join_sep) || out.text_join_sep);
|
||||
|
||||
// коллекция пресетов
|
||||
try {
|
||||
const arr = Array.isArray(src.text_extract_presets) ? src.text_extract_presets : [];
|
||||
out.text_extract_presets = arr
|
||||
.filter(it => it && typeof it === 'object')
|
||||
.map((it, idx) => ({
|
||||
id: String((it.id ?? '') || ('p' + Date.now().toString(36) + Math.random().toString(36).slice(2) + idx)),
|
||||
name: String(it.name ?? (it.json_path || 'Preset')),
|
||||
strategy: String(it.strategy ?? 'auto'),
|
||||
json_path: String(it.json_path ?? ''),
|
||||
join_sep: String(it.join_sep ?? '\n'),
|
||||
}));
|
||||
} catch { out.text_extract_presets = []; }
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function getPipelineMeta() {
|
||||
return { ..._pipelineMeta };
|
||||
@@ -28,19 +96,8 @@
|
||||
|
||||
function updatePipelineMeta(p) {
|
||||
if (!p || typeof p !== 'object') return;
|
||||
const keys = ['id','name','parallel_limit','loop_mode','loop_max_iters','loop_time_budget_ms','clear_var_store'];
|
||||
for (const k of keys) {
|
||||
if (Object.prototype.hasOwnProperty.call(p, k) && p[k] !== undefined && p[k] !== null && (k === 'clear_var_store' ? true : p[k] !== '')) {
|
||||
if (k === 'parallel_limit' || k === 'loop_max_iters' || k === 'loop_time_budget_ms') {
|
||||
const v = parseInt(p[k], 10);
|
||||
if (!Number.isNaN(v) && v > 0) _pipelineMeta[k] = v;
|
||||
} else if (k === 'clear_var_store') {
|
||||
_pipelineMeta[k] = !!p[k];
|
||||
} else {
|
||||
_pipelineMeta[k] = String(p[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// DRY: единая точка нормализации
|
||||
_pipelineMeta = ensureMeta({ ..._pipelineMeta, ...p });
|
||||
}
|
||||
|
||||
// Drawflow -> pipeline JSON
|
||||
@@ -60,11 +117,24 @@
|
||||
const wantIds = {}; // drawflow id -> желаемый/финальный nX
|
||||
const isValidNid = (s) => typeof s === 'string' && /^n\d+$/i.test(s.trim());
|
||||
|
||||
// Helper: вернуть исключительно «живые» данные ноды из редактора (если доступны).
|
||||
// Это исключает расхождения между DOM.__data и editor.getNodeFromId(..).data.
|
||||
function mergedNodeData(df, el, dfid) {
|
||||
try {
|
||||
const nid = parseInt(dfid, 10);
|
||||
const n = (w.editor && typeof w.editor.getNodeFromId === 'function') ? w.editor.getNodeFromId(nid) : null;
|
||||
if (n && n.data) return n.data;
|
||||
} catch (_) {}
|
||||
if (df && df.data) return df.data;
|
||||
// как последний fallback — DOM.__data (почти не используется после этого изменения)
|
||||
return (el && el.__data) ? el.__data : {};
|
||||
}
|
||||
|
||||
// Первый проход: резервируем существующие валидные _origId
|
||||
for (const id in dfNodes) {
|
||||
const df = dfNodes[id];
|
||||
const el = document.querySelector(`#node-${id}`);
|
||||
const datacopySrc = el && el.__data ? el.__data : (df.data || {});
|
||||
const datacopySrc = mergedNodeData(df, el, id);
|
||||
const tmp = typeof w.applyNodeDefaults === 'function'
|
||||
? w.applyNodeDefaults(df.name, JSON.parse(JSON.stringify(datacopySrc)))
|
||||
: (JSON.parse(JSON.stringify(datacopySrc)));
|
||||
@@ -95,11 +165,22 @@
|
||||
for (const id in dfNodes) {
|
||||
const df = dfNodes[id];
|
||||
const el = document.querySelector(`#node-${id}`);
|
||||
const datacopySrc = el && el.__data ? el.__data : (df.data || {});
|
||||
const datacopySrc = mergedNodeData(df, el, id);
|
||||
const datacopy = typeof w.applyNodeDefaults === 'function'
|
||||
? w.applyNodeDefaults(df.name, JSON.parse(JSON.stringify(datacopySrc)))
|
||||
: (JSON.parse(JSON.stringify(datacopySrc)));
|
||||
try { datacopy._origId = idMap[id]; } catch (e) {}
|
||||
|
||||
// Спец-обработка SetVars: гарантированно берём свежие variables из живых данных редактора
|
||||
try {
|
||||
if (String(df.name) === 'SetVars') {
|
||||
const nid = parseInt(id, 10);
|
||||
const nLive = (w.editor && typeof w.editor.getNodeFromId === 'function') ? w.editor.getNodeFromId(nid) : null;
|
||||
const v = nLive && nLive.data && Array.isArray(nLive.data.variables) ? nLive.data.variables : (Array.isArray(datacopy.variables) ? datacopy.variables : []);
|
||||
datacopy.variables = v.map(x => ({ ...(x || {}) })); // глубокая копия
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
nodes.push({
|
||||
id: idMap[id],
|
||||
type: df.name,
|
||||
@@ -195,18 +276,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Собираем итоговый pipeline JSON с метаданными
|
||||
const meta = getPipelineMeta();
|
||||
return {
|
||||
id: meta.id || 'pipeline_editor',
|
||||
name: meta.name || 'Edited Pipeline',
|
||||
parallel_limit: (typeof meta.parallel_limit === 'number' ? meta.parallel_limit : 8),
|
||||
loop_mode: (meta.loop_mode || 'dag'),
|
||||
loop_max_iters: (typeof meta.loop_max_iters === 'number' ? meta.loop_max_iters : 1000),
|
||||
loop_time_budget_ms: (typeof meta.loop_time_budget_ms === 'number' ? meta.loop_time_budget_ms : 10000),
|
||||
clear_var_store: (typeof meta.clear_var_store === 'boolean' ? meta.clear_var_store : true),
|
||||
nodes
|
||||
};
|
||||
// 3) Собираем итоговый pipeline JSON с метаданными (нормализованными)
|
||||
const meta = ensureMeta(getPipelineMeta());
|
||||
try { console.debug('[AgentUISer.toPipelineJSON] meta_keys', Object.keys(meta || {})); } catch (e) {}
|
||||
return { ...meta, nodes };
|
||||
}
|
||||
|
||||
// pipeline JSON -> Drawflow
|
||||
@@ -214,19 +287,25 @@
|
||||
ensureDeps();
|
||||
const editor = w.editor;
|
||||
const NODE_IO = w.NODE_IO;
|
||||
|
||||
// Сохраняем метаданные пайплайна для UI
|
||||
try {
|
||||
updatePipelineMeta({
|
||||
id: p && p.id ? p.id : 'pipeline_editor',
|
||||
name: p && p.name ? p.name : 'Edited Pipeline',
|
||||
parallel_limit: (p && typeof p.parallel_limit === 'number') ? p.parallel_limit : 8,
|
||||
loop_mode: p && p.loop_mode ? p.loop_mode : 'dag',
|
||||
loop_max_iters: (p && typeof p.loop_max_iters === 'number') ? p.loop_max_iters : 1000,
|
||||
loop_time_budget_ms: (p && typeof p.loop_time_budget_ms === 'number') ? p.loop_time_budget_ms : 10000,
|
||||
clear_var_store: (p && typeof p.clear_var_store === 'boolean') ? p.clear_var_store : true,
|
||||
});
|
||||
} catch (e) {}
|
||||
// Сохраняем метаданные пайплайна для UI (сквозная нормализация)
|
||||
try {
|
||||
updatePipelineMeta(p || {});
|
||||
// Диагностический лог состава meta для подтверждения DRY-рефакторинга
|
||||
try {
|
||||
const metaKeys = ["id","name","parallel_limit","loop_mode","loop_max_iters","loop_time_budget_ms","clear_var_store","http_timeout_sec","text_extract_strategy","text_extract_json_path","text_join_sep","text_extract_presets"];
|
||||
const incomingKeys = metaKeys.filter(k => (p && Object.prototype.hasOwnProperty.call(p, k)));
|
||||
const currentMeta = (typeof getPipelineMeta === 'function') ? getPipelineMeta() : {};
|
||||
console.debug('[AgentUISer.fromPipelineJSON] meta_keys', {
|
||||
incomingKeys,
|
||||
resultKeys: Object.keys(currentMeta || {}),
|
||||
metaPreview: {
|
||||
id: currentMeta && currentMeta.id,
|
||||
loop_mode: currentMeta && currentMeta.loop_mode,
|
||||
http_timeout_sec: currentMeta && currentMeta.http_timeout_sec
|
||||
}
|
||||
});
|
||||
} catch (_) {}
|
||||
} catch (e) {}
|
||||
|
||||
editor.clear();
|
||||
let x = 100; let y = 120; // Fallback
|
||||
|
||||
213
static/js/utils.js
Normal file
@@ -0,0 +1,213 @@
|
||||
/* global window */
|
||||
// AgentUI common UI utilities (DRY helpers shared by editor.html and pm-ui.js)
|
||||
(function (w) {
|
||||
'use strict';
|
||||
|
||||
const AU = {};
|
||||
|
||||
// HTML escaping for safe text/attribute insertion
|
||||
AU.escapeHtml = function escapeHtml(s) {
|
||||
const str = String(s ?? '');
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
};
|
||||
|
||||
// Attribute-safe escape (keeps quotes escaped; conservative)
|
||||
AU.escAttr = function escAttr(v) {
|
||||
const s = String(v ?? '');
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
};
|
||||
|
||||
// Text-node escape (keeps quotes as-is for readability)
|
||||
AU.escText = function escText(v) {
|
||||
const s = String(v ?? '');
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
};
|
||||
|
||||
// DRY helper: sync Drawflow node data + mirror into DOM.__data with deep copy
|
||||
AU.updateNodeDataAndDom = function updateNodeDataAndDom(editor, id, data) {
|
||||
try { editor && typeof editor.updateNodeDataFromId === 'function' && editor.updateNodeDataFromId(id, data); } catch (_) {}
|
||||
try {
|
||||
const el = document.querySelector('#node-' + id);
|
||||
if (el) el.__data = JSON.parse(JSON.stringify(data));
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
// Double rAF helper: waits for two animation frames; returns Promise or accepts callback
|
||||
AU.nextRaf2 = function nextRaf2(cb) {
|
||||
try {
|
||||
if (typeof requestAnimationFrame === 'function') {
|
||||
if (typeof cb === 'function') {
|
||||
requestAnimationFrame(() => { requestAnimationFrame(() => { try { cb(); } catch (_) {} }); });
|
||||
return;
|
||||
}
|
||||
return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
||||
} else {
|
||||
if (typeof cb === 'function') { setTimeout(() => { try { cb(); } catch (_) {} }, 32); return; }
|
||||
return new Promise((resolve) => setTimeout(resolve, 32));
|
||||
}
|
||||
} catch (_) {
|
||||
if (typeof cb === 'function') { try { cb(); } catch (__ ) {} }
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
// Heuristic: looks like long base64 payload
|
||||
AU.isProbablyBase64 = function isProbablyBase64(s) {
|
||||
try {
|
||||
if (typeof s !== 'string') return false;
|
||||
if (s.length < 64) return false;
|
||||
return /^[A-Za-z0-9+/=\r\n]+$/.test(s);
|
||||
} catch { return false; }
|
||||
};
|
||||
|
||||
AU.trimBase64 = function trimBase64(s, maxLen = 180) {
|
||||
try {
|
||||
const str = String(s ?? '');
|
||||
if (str.length > maxLen) {
|
||||
return str.slice(0, maxLen) + `... (trimmed ${str.length - maxLen})`;
|
||||
}
|
||||
return str;
|
||||
} catch { return String(s ?? ''); }
|
||||
};
|
||||
|
||||
// Flatten JSON-like object into [path, stringValue] pairs
|
||||
// Includes special handling for backend preview objects: { "__truncated__": true, "preview": "..." }
|
||||
AU.flattenObject = function flattenObject(obj, prefix = '') {
|
||||
const out = [];
|
||||
if (obj == null) return out;
|
||||
if (typeof obj !== 'object') {
|
||||
out.push([prefix, String(obj)]);
|
||||
return out;
|
||||
}
|
||||
try {
|
||||
const entries = Object.entries(obj);
|
||||
for (const [k, v] of entries) {
|
||||
const p = prefix ? `${prefix}.${k}` : k;
|
||||
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
||||
// Special preview shape from backend
|
||||
if (Object.prototype.hasOwnProperty.call(v, '__truncated__') && Object.prototype.hasOwnProperty.call(v, 'preview')) {
|
||||
out.push([p, String(v.preview ?? '')]);
|
||||
continue;
|
||||
}
|
||||
out.push(...AU.flattenObject(v, p));
|
||||
} else {
|
||||
try {
|
||||
const s = (typeof v === 'string') ? v : JSON.stringify(v, null, 0);
|
||||
out.push([p, s]);
|
||||
} catch {
|
||||
out.push([p, String(v)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fallback best-effort
|
||||
try { out.push([prefix, JSON.stringify(obj)]); } catch { out.push([prefix, String(obj)]); }
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
// Format headers dictionary into text lines "Key: Value"
|
||||
AU.fmtHeaders = function fmtHeaders(h) {
|
||||
try {
|
||||
const keys = Object.keys(h || {});
|
||||
return keys.map(k => `${k}: ${String(h[k])}`).join('\n');
|
||||
} catch { return ''; }
|
||||
};
|
||||
|
||||
// Build HTTP request preview text
|
||||
AU.buildReqText = function buildReqText(x) {
|
||||
if (!x) return '';
|
||||
const head = `${x.method || 'POST'} ${x.url || '/'} HTTP/1.1`;
|
||||
const host = (() => {
|
||||
try { const u = new URL(x.url); return `Host: ${u.host}`; } catch { return ''; }
|
||||
})();
|
||||
const hs = AU.fmtHeaders(x.headers || {});
|
||||
const body = String(x.body_text || '').trim();
|
||||
return [head, host, hs, '', body].filter(Boolean).join('\n');
|
||||
};
|
||||
|
||||
// Build HTTP response preview text
|
||||
AU.buildRespText = function buildRespText(x) {
|
||||
if (!x) return '';
|
||||
const head = `HTTP/1.1 ${x.status || 0}`;
|
||||
const hs = AU.fmtHeaders(x.headers || {});
|
||||
const body = String(x.body_text || '').trim();
|
||||
return [head, hs, '', body].filter(Boolean).join('\n');
|
||||
};
|
||||
|
||||
// Unified fetch helper with timeout and JSON handling
|
||||
AU.apiFetch = async function apiFetch(url, opts) {
|
||||
const t0 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
|
||||
const o = opts || {};
|
||||
const method = String(o.method || 'GET').toUpperCase();
|
||||
const expectJson = (o.expectJson !== false); // default true
|
||||
const headers = Object.assign({}, o.headers || {});
|
||||
let body = o.body;
|
||||
const timeoutMs = Number.isFinite(o.timeoutMs) ? o.timeoutMs : 15000;
|
||||
|
||||
const hasAbort = (typeof AbortController !== 'undefined');
|
||||
const ctrl = hasAbort ? new AbortController() : null;
|
||||
let to = null;
|
||||
if (ctrl) {
|
||||
try { to = setTimeout(() => { try { ctrl.abort(); } catch(_){} }, timeoutMs); } catch(_) {}
|
||||
}
|
||||
|
||||
try {
|
||||
if (expectJson) {
|
||||
if (!headers['Accept'] && !headers['accept']) headers['Accept'] = 'application/json';
|
||||
}
|
||||
if (body != null) {
|
||||
const isForm = (typeof FormData !== 'undefined' && body instanceof FormData);
|
||||
const isBlob = (typeof Blob !== 'undefined' && body instanceof Blob);
|
||||
if (typeof body === 'object' && !isForm && !isBlob) {
|
||||
body = JSON.stringify(body);
|
||||
if (!headers['Content-Type'] && !headers['content-type']) headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(url, { method, headers, body, signal: ctrl ? ctrl.signal : undefined });
|
||||
const ct = String(res.headers && res.headers.get ? (res.headers.get('Content-Type') || '') : '');
|
||||
const isJsonCt = /application\/json/i.test(ct);
|
||||
|
||||
let data = null;
|
||||
if (expectJson || isJsonCt) {
|
||||
try { data = await res.json(); } catch (_) { data = null; }
|
||||
} else {
|
||||
try { data = await res.text(); } catch (_) { data = null; }
|
||||
}
|
||||
|
||||
const t1 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
|
||||
try { console.debug('[AU.apiFetch]', { method, url, status: res.status, ms: Math.round(t1 - t0) }); } catch(_) {}
|
||||
|
||||
if (!res.ok) {
|
||||
const msg = (data && typeof data === 'object' && data.error) ? String(data.error) : `HTTP ${res.status}`;
|
||||
const err = new Error(`apiFetch: ${msg}`);
|
||||
err.status = res.status;
|
||||
err.data = data;
|
||||
err.url = url;
|
||||
throw err;
|
||||
}
|
||||
|
||||
return data;
|
||||
} finally {
|
||||
if (to) { try { clearTimeout(to); } catch(_) {} }
|
||||
}
|
||||
};
|
||||
|
||||
// Expose
|
||||
try { w.AU = AU; } catch (_) {}
|
||||
try { w.nextRaf2 = AU.nextRaf2; } catch (_) {}
|
||||
})(window);
|
||||
@@ -4,6 +4,12 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>НадTavern — Pipeline Editor (JSON)</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 24px; }
|
||||
textarea { width: 100%; height: 70vh; }
|
||||
|
||||
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Make tests a package so imports like "from tests.utils import ..." work.
|
||||
199
tests/test_cancel_modes.py
Normal file
@@ -0,0 +1,199 @@
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any, Dict
|
||||
|
||||
from agentui.pipeline.executor import PipelineExecutor, ExecutionError
|
||||
from agentui.common.cancel import request_cancel, clear_cancel
|
||||
import agentui.providers.http_client as hc
|
||||
import agentui.pipeline.executor as ex
|
||||
from tests.utils import ctx as _ctx
|
||||
|
||||
|
||||
class DummyResponse:
|
||||
def __init__(self, status: int, json_obj: Dict[str, Any]) -> None:
|
||||
self.status_code = status
|
||||
self._json = json_obj
|
||||
self.headers = {}
|
||||
try:
|
||||
self.content = json.dumps(json_obj, ensure_ascii=False).encode("utf-8")
|
||||
except Exception:
|
||||
self.content = b"{}"
|
||||
try:
|
||||
self.text = json.dumps(json_obj, ensure_ascii=False)
|
||||
except Exception:
|
||||
self.text = "{}"
|
||||
|
||||
def json(self) -> Dict[str, Any]:
|
||||
return self._json
|
||||
|
||||
|
||||
class DummyClient:
|
||||
"""
|
||||
Async client with artificial delay to simulate in-flight HTTP that can be cancelled.
|
||||
Provides .post() and .request() compatible with executor usage.
|
||||
"""
|
||||
def __init__(self, delay: float = 0.3, status_code: int = 200) -> None:
|
||||
self._delay = delay
|
||||
self._status = status_code
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
async def post(self, url: str, content: bytes, headers: Dict[str, str]):
|
||||
# Artificial delay to allow cancel/abort to happen while awaiting
|
||||
await asyncio.sleep(self._delay)
|
||||
try:
|
||||
payload = json.loads(content.decode("utf-8"))
|
||||
except Exception:
|
||||
payload = {"_raw": content.decode("utf-8", errors="ignore")}
|
||||
return DummyResponse(self._status, {"echo": payload})
|
||||
|
||||
async def request(self, method: str, url: str, headers: Dict[str, str], content: bytes | None):
|
||||
return await self.post(url, content or b"{}", headers)
|
||||
|
||||
|
||||
def _patch_http_client(delay: float = 0.3):
|
||||
"""
|
||||
Patch both providers.http_client.build_client and executor.build_client
|
||||
to return our DummyClient with a given delay.
|
||||
"""
|
||||
orig_hc = hc.build_client
|
||||
orig_ex = ex.build_client
|
||||
hc.build_client = lambda timeout=60.0: DummyClient(delay=delay) # type: ignore[assignment]
|
||||
ex.build_client = lambda timeout=60.0: DummyClient(delay=delay) # type: ignore[assignment]
|
||||
return orig_hc, orig_ex
|
||||
|
||||
|
||||
def _restore_http_client(orig_hc, orig_ex) -> None:
|
||||
hc.build_client = orig_hc
|
||||
ex.build_client = orig_ex
|
||||
|
||||
|
||||
def test_graceful_cancel_while_providercall():
|
||||
"""
|
||||
Expectation:
|
||||
- Cancel(mode=graceful) during in-flight HTTP should NOT interrupt the current request.
|
||||
- While-wrapper should stop before starting next iteration.
|
||||
- Final CYCLEINDEX__n2 == 0 (only first iteration finished), WAS_ERROR__n2 is False/absent.
|
||||
"""
|
||||
async def main():
|
||||
p = {
|
||||
"id": "p_cancel_soft",
|
||||
"name": "ProviderCall graceful cancel",
|
||||
"loop_mode": "dag",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n2",
|
||||
"type": "ProviderCall",
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"while_expr": "cycleindex < 5",
|
||||
"while_max_iters": 10,
|
||||
# ignore_errors not needed for graceful (no interruption of in-flight)
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "http://dummy.local",
|
||||
"headers": "{}",
|
||||
"template": "{}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"in": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
pid = p["id"]
|
||||
orig_hc, orig_ex = _patch_http_client(delay=0.3)
|
||||
try:
|
||||
ctx = _ctx()
|
||||
exr = PipelineExecutor(p)
|
||||
task = asyncio.create_task(exr.run(ctx))
|
||||
# Give the node time to start HTTP, then request graceful cancel
|
||||
await asyncio.sleep(0.05)
|
||||
request_cancel(pid, mode="graceful")
|
||||
out = await task
|
||||
finally:
|
||||
_restore_http_client(orig_hc, orig_ex)
|
||||
try:
|
||||
clear_cancel(pid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
assert isinstance(out, dict)
|
||||
vars_map = out.get("vars") or {}
|
||||
assert isinstance(vars_map, dict)
|
||||
# Only first iteration should have finished; last index = 0
|
||||
assert vars_map.get("CYCLEINDEX__n2") == 0
|
||||
# No error expected on graceful (we didn't interrupt the in-flight HTTP)
|
||||
assert vars_map.get("WAS_ERROR__n2") in (False, None)
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
def test_abort_cancel_inflight_providercall():
|
||||
"""
|
||||
Expectation:
|
||||
- Cancel(mode=abort) during in-flight HTTP cancels the await with ExecutionError.
|
||||
- While-wrapper with ignore_errors=True converts it into {"result":{"error":...}}.
|
||||
- Final CYCLEINDEX__n2 == 0 and WAS_ERROR__n2 == True; error mentions 'Cancelled by user (abort)'.
|
||||
"""
|
||||
async def main():
|
||||
p = {
|
||||
"id": "p_cancel_abort",
|
||||
"name": "ProviderCall abort cancel",
|
||||
"loop_mode": "dag",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n2",
|
||||
"type": "ProviderCall",
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"while_expr": "cycleindex < 5",
|
||||
"while_max_iters": 10,
|
||||
"ignore_errors": True, # convert cancellation exception into error payload
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "http://dummy.local",
|
||||
"headers": "{}",
|
||||
"template": "{}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"in": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
pid = p["id"]
|
||||
orig_hc, orig_ex = _patch_http_client(delay=0.3)
|
||||
try:
|
||||
ctx = _ctx()
|
||||
exr = PipelineExecutor(p)
|
||||
task = asyncio.create_task(exr.run(ctx))
|
||||
# Let HTTP start, then trigger hard abort
|
||||
await asyncio.sleep(0.05)
|
||||
request_cancel(pid, mode="abort")
|
||||
out = await task
|
||||
finally:
|
||||
_restore_http_client(orig_hc, orig_ex)
|
||||
try:
|
||||
clear_cancel(pid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
assert isinstance(out, dict)
|
||||
vars_map = out.get("vars") or {}
|
||||
assert isinstance(vars_map, dict)
|
||||
# First iteration was started; after abort it is considered errored and loop stops
|
||||
assert vars_map.get("CYCLEINDEX__n2") == 0
|
||||
assert vars_map.get("WAS_ERROR__n2") is True
|
||||
|
||||
# Error propagated into node's result (ignore_errors=True path)
|
||||
res = out.get("result") or {}
|
||||
assert isinstance(res, dict)
|
||||
err = res.get("error")
|
||||
assert isinstance(err, str) and "Cancelled by user (abort)" in err
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -1,25 +1,9 @@
|
||||
import asyncio
|
||||
import json
|
||||
from agentui.pipeline.executor import PipelineExecutor, ExecutionError, Node, NODE_REGISTRY
|
||||
from tests.utils import pp as _pp, base_ctx as _base_ctx
|
||||
|
||||
# Helper to pretty print short JSON safely
|
||||
def _pp(obj, max_len=800):
|
||||
try:
|
||||
s = json.dumps(obj, ensure_ascii=False, indent=2)
|
||||
except Exception:
|
||||
s = str(obj)
|
||||
if len(s) > max_len:
|
||||
return s[:max_len] + "...<truncated>"
|
||||
return s
|
||||
|
||||
def _base_ctx(vendor="openai"):
|
||||
return {
|
||||
"model": "gpt-x",
|
||||
"vendor_format": vendor,
|
||||
"params": {"temperature": 0.1},
|
||||
"chat": {"last_user": "hi"},
|
||||
"OUT": {},
|
||||
}
|
||||
|
||||
async def scenario_if_single_quotes_ok():
|
||||
print("\n=== SCENARIO 1: If with single quotes ===")
|
||||
|
||||
@@ -1,33 +1,8 @@
|
||||
import asyncio
|
||||
import json
|
||||
from agentui.pipeline.executor import PipelineExecutor
|
||||
from agentui.pipeline.storage import clear_var_store
|
||||
from tests.utils import pp as _pp, ctx as _ctx
|
||||
|
||||
def _pp(obj, max_len=800):
|
||||
try:
|
||||
s = json.dumps(obj, ensure_ascii=False, indent=2)
|
||||
except Exception:
|
||||
s = str(obj)
|
||||
if len(s) > max_len:
|
||||
return s[:max_len] + "...<truncated>"
|
||||
return s
|
||||
|
||||
def _ctx(vendor="openai", incoming=None, params=None):
|
||||
return {
|
||||
"model": "gpt-x",
|
||||
"vendor_format": vendor,
|
||||
"params": params or {"temperature": 0.25},
|
||||
"chat": {"last_user": "Привет"},
|
||||
"OUT": {},
|
||||
"incoming": incoming or {
|
||||
"method": "POST",
|
||||
"url": "http://localhost/test",
|
||||
"path": "/test",
|
||||
"query": "",
|
||||
"headers": {"x": "X-HEADER"},
|
||||
"json": {},
|
||||
},
|
||||
}
|
||||
|
||||
async def scenario_bare_vars_and_braces():
|
||||
print("\n=== MACROS 1: Bare [[NAME]] и {{ NAME }} + числа/объекты без кавычек ===")
|
||||
@@ -63,6 +38,7 @@ async def scenario_bare_vars_and_braces():
|
||||
out = await PipelineExecutor(p).run(_ctx())
|
||||
print("OUT:", _pp(out))
|
||||
|
||||
|
||||
async def scenario_var_path_and_defaults():
|
||||
print("\n=== MACROS 2: [[VAR:path]] и {{ ...|default(...) }} (вложенные и JSON-литералы) ===")
|
||||
incoming = {
|
||||
@@ -101,6 +77,7 @@ async def scenario_var_path_and_defaults():
|
||||
out = await PipelineExecutor(p).run(_ctx(incoming=incoming, params={"temperature": 0.2}))
|
||||
print("OUT:", _pp(out))
|
||||
|
||||
|
||||
async def scenario_out_macros_full_and_short():
|
||||
print("\n=== MACROS 3: [[OUT:nX...]] и короткая форма [[OUTx]] ===")
|
||||
p = {
|
||||
@@ -142,6 +119,7 @@ async def scenario_out_macros_full_and_short():
|
||||
out = await PipelineExecutor(p).run(_ctx())
|
||||
print("OUT:", _pp(out))
|
||||
|
||||
|
||||
async def scenario_store_macros_two_runs():
|
||||
print("\n=== MACROS 4: [[STORE:key]] и {{ STORE.key }} между запусками (clear_var_store=False) ===")
|
||||
pid = "p_macros_4_store"
|
||||
@@ -198,6 +176,7 @@ async def scenario_store_macros_two_runs():
|
||||
out2 = await PipelineExecutor(p2).run(_ctx())
|
||||
print("RUN2:", _pp(out2))
|
||||
|
||||
|
||||
async def scenario_pm_prompt_blocks_to_provider_structs():
|
||||
print("\n=== MACROS 5: Prompt Blocks ([[PROMPT]]) → provider-structures (OpenAI) ===")
|
||||
# Проверяем, что [[PROMPT]] со списком блоков превращается в "messages":[...]
|
||||
@@ -232,6 +211,7 @@ async def scenario_pm_prompt_blocks_to_provider_structs():
|
||||
out = await PipelineExecutor(p).run(_ctx())
|
||||
print("OUT:", _pp(out))
|
||||
|
||||
|
||||
def run_all():
|
||||
async def main():
|
||||
await scenario_bare_vars_and_braces()
|
||||
@@ -242,5 +222,6 @@ def run_all():
|
||||
print("\n=== MACROS VARS SUITE: DONE ===")
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_all()
|
||||
249
tests/test_prompt_combine.py
Normal file
@@ -0,0 +1,249 @@
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from agentui.pipeline.executor import PipelineExecutor
|
||||
import agentui.providers.http_client as hc
|
||||
from tests.utils import ctx as _ctx, pp as _pp
|
||||
|
||||
|
||||
# Capture of all outbound ProviderCall HTTP requests (one per run)
|
||||
CAPTURED: List[Dict[str, Any]] = []
|
||||
|
||||
|
||||
class DummyResponse:
|
||||
def __init__(self, status_code: int = 200, body: Dict[str, Any] | None = None):
|
||||
self.status_code = status_code
|
||||
self._json = body if body is not None else {"ok": True}
|
||||
self.headers = {}
|
||||
try:
|
||||
self.content = json.dumps(self._json, ensure_ascii=False).encode("utf-8")
|
||||
except Exception:
|
||||
self.content = b"{}"
|
||||
try:
|
||||
self.text = json.dumps(self._json, ensure_ascii=False)
|
||||
except Exception:
|
||||
self.text = "{}"
|
||||
|
||||
def json(self) -> Any:
|
||||
return self._json
|
||||
|
||||
|
||||
class DummyClient:
|
||||
def __init__(self, capture: List[Dict[str, Any]], status_code: int = 200):
|
||||
self._capture = capture
|
||||
self._status = status_code
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
async def post(self, url: str, content: bytes, headers: Dict[str, str]):
|
||||
try:
|
||||
payload = json.loads(content.decode("utf-8"))
|
||||
except Exception:
|
||||
payload = {"_raw": content.decode("utf-8", errors="ignore")}
|
||||
rec = {"url": url, "headers": headers, "payload": payload}
|
||||
self._capture.append(rec)
|
||||
# Echo payload back to keep extractor happy but not tied to vendor formats
|
||||
return DummyResponse(self._status, {"echo": rec})
|
||||
|
||||
# RawForward may use .request, but we don't need it here
|
||||
async def request(self, method: str, url: str, headers: Dict[str, str], content: bytes | None):
|
||||
return await self.post(url, content or b"{}", headers)
|
||||
|
||||
|
||||
def _patch_http_client():
|
||||
"""Monkeypatch build_client used by ProviderCall to our dummy."""
|
||||
hc.build_client = lambda timeout=60.0: DummyClient(CAPTURED, 200) # type: ignore[assignment]
|
||||
# Также патчим символ, импортированный внутрь executor, чтобы ProviderCall использовал DummyClient
|
||||
import agentui.pipeline.executor as ex # type: ignore
|
||||
ex.build_client = lambda timeout=60.0: DummyClient(CAPTURED, 200) # type: ignore
|
||||
|
||||
|
||||
def _mk_pipeline(provider: str, prompt_combine: str) -> Dict[str, Any]:
|
||||
"""Build a minimal ProviderCall-only pipeline for a given provider and combine spec."""
|
||||
provider = provider.lower().strip()
|
||||
if provider not in {"openai", "gemini", "claude"}:
|
||||
raise AssertionError(f"Unsupported provider in test: {provider}")
|
||||
base_url = "http://mock.local"
|
||||
if provider == "openai":
|
||||
endpoint = "/v1/chat/completions"
|
||||
template = '{ "model": "{{ model }}", [[PROMPT]] }'
|
||||
elif provider == "gemini":
|
||||
endpoint = "/v1beta/models/{{ model }}:generateContent"
|
||||
template = '{ "model": "{{ model }}", [[PROMPT]] }'
|
||||
else: # claude
|
||||
endpoint = "/v1/messages"
|
||||
template = '{ "model": "{{ model }}", [[PROMPT]] }'
|
||||
p = {
|
||||
"id": f"p_prompt_combine_{provider}",
|
||||
"name": f"prompt_combine to {provider}",
|
||||
"loop_mode": "dag",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n1",
|
||||
"type": "ProviderCall",
|
||||
"config": {
|
||||
"provider": provider,
|
||||
"provider_configs": {
|
||||
provider: {
|
||||
"base_url": base_url,
|
||||
"endpoint": endpoint,
|
||||
"headers": "{}",
|
||||
"template": template,
|
||||
}
|
||||
},
|
||||
# Key under test:
|
||||
"prompt_combine": prompt_combine,
|
||||
# Prompt Blocks (PROMPT)
|
||||
"blocks": [
|
||||
{"id": "b1", "name": "sys", "role": "system", "prompt": "Ты — Narrator-chan.", "enabled": True, "order": 0},
|
||||
{"id": "b2", "name": "user", "role": "user", "prompt": "как лела", "enabled": True, "order": 1},
|
||||
],
|
||||
},
|
||||
"in": {},
|
||||
}
|
||||
],
|
||||
}
|
||||
return p
|
||||
|
||||
|
||||
def _ctx_with_incoming(incoming_json: Dict[str, Any], vendor: str = "openai") -> Dict[str, Any]:
|
||||
base = _ctx(vendor=vendor)
|
||||
inc = dict(base["incoming"])
|
||||
inc["json"] = incoming_json
|
||||
base["incoming"] = inc
|
||||
return base
|
||||
|
||||
|
||||
async def scenario_openai_target_from_gemini_contents():
|
||||
print("\n=== PROMPT_COMBINE 1: target=openai, incoming=gemini.contents & PROMPT ===")
|
||||
_patch_http_client()
|
||||
CAPTURED.clear()
|
||||
|
||||
# Incoming JSON in Gemini shape
|
||||
incoming_json = {
|
||||
"contents": [
|
||||
{"role": "user", "parts": [{"text": "Прив"}]},
|
||||
{"role": "model", "parts": [{"text": "И тебе привет!"}]},
|
||||
]
|
||||
}
|
||||
p = _mk_pipeline("openai", "[[VAR:incoming.json.contents]] & [[PROMPT]]")
|
||||
out = await PipelineExecutor(p).run(_ctx_with_incoming(incoming_json, vendor="gemini"))
|
||||
print("PIPE OUT:", _pp(out))
|
||||
assert CAPTURED, "No HTTP request captured"
|
||||
req = CAPTURED[-1]
|
||||
payload = req["payload"]
|
||||
# Validate OpenAI body
|
||||
assert "messages" in payload, "OpenAI payload must contain messages"
|
||||
msgs = payload["messages"]
|
||||
# Expected: 2 (converted Gemini) + 2 (PROMPT blocks system+user) = 4
|
||||
assert isinstance(msgs, list) and len(msgs) == 4
|
||||
roles = [m.get("role") for m in msgs]
|
||||
# Gemini model -> OpenAI assistant
|
||||
assert "assistant" in roles and "user" in roles
|
||||
# PROMPT system+user present (system may be not first without @pos; we just ensure existence)
|
||||
assert any(m.get("role") == "system" for m in msgs), "System message from PROMPT must be present"
|
||||
|
||||
|
||||
async def scenario_gemini_target_from_openai_messages():
|
||||
print("\n=== PROMPT_COMBINE 2: target=gemini, incoming=openai.messages & PROMPT ===")
|
||||
_patch_http_client()
|
||||
CAPTURED.clear()
|
||||
|
||||
incoming_json = {
|
||||
"messages": [
|
||||
{"role": "system", "content": "Системный-тест из входящего"},
|
||||
{"role": "user", "content": "Its just me.."},
|
||||
{"role": "assistant", "content": "Reply from model"},
|
||||
]
|
||||
}
|
||||
p = _mk_pipeline("gemini", "[[VAR:incoming.json.messages]] & [[PROMPT]]")
|
||||
out = await PipelineExecutor(p).run(_ctx_with_incoming(incoming_json, vendor="openai"))
|
||||
print("PIPE OUT:", _pp(out))
|
||||
assert CAPTURED, "No HTTP request captured"
|
||||
payload = CAPTURED[-1]["payload"]
|
||||
# Validate Gemini body
|
||||
assert "contents" in payload, "Gemini payload must contain contents"
|
||||
cnts = payload["contents"]
|
||||
assert isinstance(cnts, list)
|
||||
# PROMPT system goes to systemInstruction, user block goes to contents
|
||||
assert "systemInstruction" in payload, "Gemini payload must contain systemInstruction when system text exists"
|
||||
si = payload["systemInstruction"]
|
||||
# SystemInstruction.parts[].text must include both incoming system and PROMPT system merged
|
||||
si_texts = []
|
||||
try:
|
||||
for prt in si.get("parts", []):
|
||||
t = prt.get("text")
|
||||
if isinstance(t, str) and t.strip():
|
||||
si_texts.append(t.strip())
|
||||
except Exception:
|
||||
pass
|
||||
joined = "\n".join(si_texts)
|
||||
assert "Системный-тест из входящего" in joined, "Incoming system must be merged into systemInstruction"
|
||||
assert "Narrator-chan" in joined, "PROMPT system must be merged into systemInstruction"
|
||||
|
||||
|
||||
async def scenario_claude_target_from_openai_messages():
|
||||
print("\n=== PROMPT_COMBINE 3: target=claude, incoming=openai.messages & PROMPT ===")
|
||||
_patch_http_client()
|
||||
CAPTURED.clear()
|
||||
|
||||
incoming_json = {
|
||||
"messages": [
|
||||
{"role": "system", "content": "Системный-тест CLAUDE"},
|
||||
{"role": "user", "content": "Прив"},
|
||||
{"role": "assistant", "content": "Привет!"},
|
||||
]
|
||||
}
|
||||
p = _mk_pipeline("claude", "[[VAR:incoming.json.messages]] & [[PROMPT]]")
|
||||
out = await PipelineExecutor(p).run(_ctx_with_incoming(incoming_json, vendor="openai"))
|
||||
print("PIPE OUT:", _pp(out))
|
||||
assert CAPTURED, "No HTTP request captured"
|
||||
payload = CAPTURED[-1]["payload"]
|
||||
# Validate Claude body
|
||||
assert "messages" in payload, "Claude payload must contain messages"
|
||||
assert "system" in payload, "Claude payload must contain system blocks"
|
||||
sys_blocks = payload["system"]
|
||||
# system must be array of blocks with type=text
|
||||
assert isinstance(sys_blocks, list) and any(isinstance(b, dict) and b.get("type") == "text" for b in sys_blocks)
|
||||
sys_text_join = "\n".join([b.get("text") for b in sys_blocks if isinstance(b, dict) and isinstance(b.get("text"), str)])
|
||||
assert "Системный-тест CLAUDE" in sys_text_join, "Incoming system should be present"
|
||||
assert "Narrator-chan" in sys_text_join, "PROMPT system should be present"
|
||||
|
||||
|
||||
async def scenario_prepend_positioning_openai():
|
||||
print("\n=== PROMPT_COMBINE 4: target=openai, PROMPT@pos=prepend & incoming.contents ===")
|
||||
_patch_http_client()
|
||||
CAPTURED.clear()
|
||||
|
||||
incoming_json = {
|
||||
"contents": [
|
||||
{"role": "user", "parts": [{"text": "A"}]},
|
||||
{"role": "model", "parts": [{"text": "B"}]},
|
||||
]
|
||||
}
|
||||
# Put PROMPT first; ensure system message becomes first in messages
|
||||
p = _mk_pipeline("openai", "[[PROMPT]]@pos=prepend & [[VAR:incoming.json.contents]]")
|
||||
out = await PipelineExecutor(p).run(_ctx_with_incoming(incoming_json, vendor="gemini"))
|
||||
print("PIPE OUT:", _pp(out))
|
||||
assert CAPTURED, "No HTTP request captured"
|
||||
payload = CAPTURED[-1]["payload"]
|
||||
msgs = payload.get("messages", [])
|
||||
assert isinstance(msgs, list) and len(msgs) >= 2
|
||||
first = msgs[0]
|
||||
# Expect first to be system (from PROMPT) due to prepend
|
||||
assert first.get("role") == "system", f"Expected system as first message, got {first}"
|
||||
|
||||
|
||||
def test_prompt_combine_all():
|
||||
async def main():
|
||||
await scenario_openai_target_from_gemini_contents()
|
||||
await scenario_gemini_target_from_openai_messages()
|
||||
await scenario_claude_target_from_openai_messages()
|
||||
await scenario_prepend_positioning_openai()
|
||||
print("\n=== PROMPT_COMBINE: DONE ===")
|
||||
asyncio.run(main())
|
||||
23
tests/test_pytest_wrapper.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Pytest-обёртка для существующих сценариев, которые сами себя запускают через run_all()/run_checks()
|
||||
# Позволяет запускать все тесты одной командой: python -m pytest -q
|
||||
# Не меняем исходные файлы, просто вызываем их публичные функции из pytest-тестов.
|
||||
|
||||
def test_executor_iterative():
|
||||
# tests/test_executor_iterative.py содержит run_checks() (внутри сам asyncio.run)
|
||||
from tests.test_executor_iterative import run_checks
|
||||
run_checks()
|
||||
|
||||
def test_edge_cases():
|
||||
# tests/test_edge_cases.py содержит run_all() (внутри сам asyncio.run)
|
||||
from tests.test_edge_cases import run_all
|
||||
run_all()
|
||||
|
||||
def test_macros_and_vars():
|
||||
# tests/test_macros_vars.py содержит run_all() (внутри сам asyncio.run)
|
||||
from tests.test_macros_vars import run_all
|
||||
run_all()
|
||||
|
||||
def test_while_nodes():
|
||||
# наш новый набор сценариев; внутри есть run_all() со своим asyncio.run
|
||||
from tests.test_while_nodes import run_all
|
||||
run_all()
|
||||
97
tests/test_setvars_jp.py
Normal file
@@ -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")
|
||||
134
tests/test_while_nodes.py
Normal file
@@ -0,0 +1,134 @@
|
||||
import asyncio
|
||||
from agentui.pipeline.executor import PipelineExecutor
|
||||
from tests.utils import ctx as _ctx
|
||||
|
||||
|
||||
async def scenario_providercall_while_ignore():
|
||||
# ProviderCall with while loop and ignore_errors enabled.
|
||||
# No base_url is provided to force ExecutionError inside node.run();
|
||||
# wrapper will catch it and expose {"error": "..."} plus vars.
|
||||
p = {
|
||||
"id": "p_pc_while_ignore",
|
||||
"name": "ProviderCall while+ignore",
|
||||
"loop_mode": "dag",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n2",
|
||||
"type": "ProviderCall",
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
# while: 3 iterations (0,1,2)
|
||||
"while_expr": "cycleindex < 3",
|
||||
"while_max_iters": 10,
|
||||
"ignore_errors": True,
|
||||
# no base_url / provider_configs to trigger error safely
|
||||
},
|
||||
"in": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
out = await PipelineExecutor(p).run(_ctx())
|
||||
assert isinstance(out, dict)
|
||||
# Wrapper returns final out with .vars merged by executor into STORE as well, but we assert on node out.
|
||||
vars_map = out.get("vars") or {}
|
||||
assert isinstance(vars_map, dict)
|
||||
# Final iteration index should be 2
|
||||
assert vars_map.get("WAS_ERROR__n2") is True
|
||||
assert vars_map.get("CYCLEINDEX__n2") == 2
|
||||
|
||||
|
||||
async def scenario_rawforward_while_ignore():
|
||||
# RawForward with while loop and ignore_errors enabled.
|
||||
# No base_url and incoming.json is a plain string -> detect_vendor=unknown -> ExecutionError,
|
||||
# wrapper catches and returns {"error": "..."} with vars set.
|
||||
p = {
|
||||
"id": "p_rf_while_ignore",
|
||||
"name": "RawForward while+ignore",
|
||||
"loop_mode": "dag",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n1",
|
||||
"type": "RawForward",
|
||||
"config": {
|
||||
"while_expr": "cycleindex < 2",
|
||||
"while_max_iters": 10,
|
||||
"ignore_errors": True,
|
||||
# no base_url; vendor detect will fail on plain text
|
||||
},
|
||||
"in": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
ctx = _ctx()
|
||||
# Provide incoming as plain text-like JSON so detect_vendor returns unknown
|
||||
ctx["incoming"] = {
|
||||
"method": "POST",
|
||||
"url": "http://example.local/test",
|
||||
"path": "/test",
|
||||
"query": "",
|
||||
"headers": {"content-type": "text/plain"},
|
||||
"json": "raw-plain-body-simulated"
|
||||
}
|
||||
out = await PipelineExecutor(p).run(ctx)
|
||||
assert isinstance(out, dict)
|
||||
vars_map = out.get("vars") or {}
|
||||
assert isinstance(vars_map, dict)
|
||||
# Final iteration index should be 1 (0 and 1)
|
||||
assert vars_map.get("WAS_ERROR__n1") is True
|
||||
assert vars_map.get("CYCLEINDEX__n1") == 1
|
||||
|
||||
|
||||
async def scenario_providercall_while_with_out_macro():
|
||||
# SetVars -> ProviderCall while uses OUT from n1 in expression
|
||||
# Expression: ([[OUT:n1.vars.MSG]] contains "123") && (cycleindex < 2)
|
||||
# Ignore errors to bypass real HTTP
|
||||
p = {
|
||||
"id": "p_pc_while_out_macro",
|
||||
"name": "ProviderCall while with OUT macro",
|
||||
"loop_mode": "iterative",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n1",
|
||||
"type": "SetVars",
|
||||
"config": {
|
||||
"variables": [
|
||||
{"id": "v1", "name": "MSG", "mode": "string", "value": "abc123xyz"}
|
||||
]
|
||||
},
|
||||
"in": {}
|
||||
},
|
||||
{
|
||||
"id": "n2",
|
||||
"type": "ProviderCall",
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"while_expr": "([[OUT:n1.vars.MSG]] contains \"123\") && (cycleindex < 2)",
|
||||
"while_max_iters": 10,
|
||||
"ignore_errors": True
|
||||
},
|
||||
"in": {
|
||||
"depends": "n1.done"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
out = await PipelineExecutor(p).run(_ctx())
|
||||
assert isinstance(out, dict)
|
||||
vars_map = out.get("vars") or {}
|
||||
assert isinstance(vars_map, dict)
|
||||
# Since MSG contains "123" and cycleindex < 2, two iterations (0,1)
|
||||
assert vars_map.get("WAS_ERROR__n2") is True
|
||||
assert vars_map.get("CYCLEINDEX__n2") == 1
|
||||
|
||||
|
||||
def run_all():
|
||||
async def main():
|
||||
await scenario_providercall_while_ignore()
|
||||
await scenario_rawforward_while_ignore()
|
||||
await scenario_providercall_while_with_out_macro()
|
||||
print("\n=== WHILE_NODES: DONE ===")
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_all()
|
||||
52
tests/utils.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
def pp(obj: Any, max_len: int = 800) -> str:
|
||||
"""
|
||||
Pretty-print JSON-like objects in tests with length guard.
|
||||
"""
|
||||
try:
|
||||
s = json.dumps(obj, ensure_ascii=False, indent=2)
|
||||
except Exception:
|
||||
s = str(obj)
|
||||
if len(s) > max_len:
|
||||
return s[:max_len] + "...<truncated>"
|
||||
return s
|
||||
|
||||
|
||||
def base_ctx(vendor: str = "openai") -> Dict[str, Any]:
|
||||
"""
|
||||
Base context used by edge-case tests (mirrors previous _base_ctx).
|
||||
"""
|
||||
return {
|
||||
"model": "gpt-x",
|
||||
"vendor_format": vendor,
|
||||
"params": {"temperature": 0.1},
|
||||
"chat": {"last_user": "hi"},
|
||||
"OUT": {},
|
||||
}
|
||||
|
||||
|
||||
def ctx(vendor: str = "openai", incoming: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
General context used by macros/vars tests (mirrors previous _ctx).
|
||||
"""
|
||||
return {
|
||||
"model": "gpt-x",
|
||||
"vendor_format": vendor,
|
||||
"params": params or {"temperature": 0.25},
|
||||
"chat": {"last_user": "Привет"},
|
||||
"OUT": {},
|
||||
"incoming": incoming
|
||||
or {
|
||||
"method": "POST",
|
||||
"url": "http://localhost/test",
|
||||
"path": "/test",
|
||||
"query": "",
|
||||
"headers": {"x": "X-HEADER"},
|
||||
"json": {},
|
||||
},
|
||||
}
|
||||