Compare commits
12 Commits
74a3f14094
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 86182c0808 | |||
| 2abfbb4b1a | |||
| 135c393eda | |||
| d155ffa74c | |||
| 0e39250c3c | |||
| 46d2fb8173 | |||
| 563663f9f1 | |||
| 338e65624f | |||
| 81014d26f8 | |||
| 11a0535712 | |||
| 3c77c3dc2e | |||
| 02725378bb |
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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
55
.agentui/vars/p_gate_only.json
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"FLAG": "yes",
|
||||||
|
"snapshot": {
|
||||||
|
"incoming": null,
|
||||||
|
"params": {
|
||||||
|
"temperature": 0.1
|
||||||
|
},
|
||||||
|
"model": "gpt-x",
|
||||||
|
"vendor_format": "openai",
|
||||||
|
"system": "",
|
||||||
|
"OUT": {
|
||||||
|
"n1": {
|
||||||
|
"vars": {
|
||||||
|
"FLAG": "yes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nIf": {
|
||||||
|
"result": true,
|
||||||
|
"true": true,
|
||||||
|
"false": false
|
||||||
|
},
|
||||||
|
"nThen": {
|
||||||
|
"result": {
|
||||||
|
"id": "ret_mock_123",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"model": "gpt-x",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "then-branch"
|
||||||
|
},
|
||||||
|
"finish_reason": "stop"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 1,
|
||||||
|
"total_tokens": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response_text": "then-branch"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OUT_TEXT": {
|
||||||
|
"n1": "yes",
|
||||||
|
"nIf": "",
|
||||||
|
"nThen": "then-branch"
|
||||||
|
},
|
||||||
|
"LAST_NODE": "nThen",
|
||||||
|
"OUT1": "yes",
|
||||||
|
"EXEC_TRACE": "n1(SetVars) -> If (#nIf) [[FLAG]] == 'yes' => true -> nThen(Return)"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
.agentui/vars/p_if_bad_string.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"MSG": "Hello"
|
||||||
|
}
|
||||||
55
.agentui/vars/p_if_single_quotes.json
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"MSG": "Hello",
|
||||||
|
"snapshot": {
|
||||||
|
"incoming": null,
|
||||||
|
"params": {
|
||||||
|
"temperature": 0.1
|
||||||
|
},
|
||||||
|
"model": "gpt-x",
|
||||||
|
"vendor_format": "openai",
|
||||||
|
"system": "",
|
||||||
|
"OUT": {
|
||||||
|
"n1": {
|
||||||
|
"vars": {
|
||||||
|
"MSG": "Hello"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nIf": {
|
||||||
|
"result": true,
|
||||||
|
"true": true,
|
||||||
|
"false": false
|
||||||
|
},
|
||||||
|
"nRet": {
|
||||||
|
"result": {
|
||||||
|
"id": "ret_mock_123",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"model": "gpt-x",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "ok"
|
||||||
|
},
|
||||||
|
"finish_reason": "stop"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 1,
|
||||||
|
"total_tokens": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response_text": "ok"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OUT_TEXT": {
|
||||||
|
"n1": "Hello",
|
||||||
|
"nIf": "",
|
||||||
|
"nRet": "ok"
|
||||||
|
},
|
||||||
|
"LAST_NODE": "nRet",
|
||||||
|
"OUT1": "Hello",
|
||||||
|
"EXEC_TRACE": "n1(SetVars) -> If (#nIf) [[MSG]] contains 'Hello' => true -> nRet(Return)"
|
||||||
|
}
|
||||||
|
}
|
||||||
75
.agentui/vars/p_macros_1.json
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"STR": "строка",
|
||||||
|
"NUM": 42,
|
||||||
|
"OBJ": {
|
||||||
|
"x": 1,
|
||||||
|
"y": [
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"STR": "строка",
|
||||||
|
"NUM": 42,
|
||||||
|
"OBJ": {
|
||||||
|
"x": 1,
|
||||||
|
"y": [
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"n2": {
|
||||||
|
"result": {
|
||||||
|
"id": "ret_mock_123",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"model": "gpt-x",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "строка | 42 | {\"x\": 1, \"y\": [2, 3]}"
|
||||||
|
},
|
||||||
|
"finish_reason": "stop"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 9,
|
||||||
|
"total_tokens": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response_text": "строка | 42 | {\"x\": 1, \"y\": [2, 3]}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OUT_TEXT": {
|
||||||
|
"n1": "строка",
|
||||||
|
"n2": "строка | 42 | {\"x\": 1, \"y\": [2, 3]}"
|
||||||
|
},
|
||||||
|
"LAST_NODE": "n2",
|
||||||
|
"OUT1": "строка",
|
||||||
|
"OUT2": "строка | 42 | {\"x\": 1, \"y\": [2, 3]}",
|
||||||
|
"EXEC_TRACE": "n1(SetVars) -> n2(Return)"
|
||||||
|
}
|
||||||
|
}
|
||||||
54
.agentui/vars/p_macros_2.json
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"snapshot": {
|
||||||
|
"incoming": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://localhost/test?foo=bar",
|
||||||
|
"path": "/test",
|
||||||
|
"query": "foo=bar",
|
||||||
|
"headers": {
|
||||||
|
"authorization": "Bearer X",
|
||||||
|
"x-api-key": "Y"
|
||||||
|
},
|
||||||
|
"json": {
|
||||||
|
"a": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"temperature": 0.2
|
||||||
|
},
|
||||||
|
"model": "gpt-x",
|
||||||
|
"vendor_format": "openai",
|
||||||
|
"system": "",
|
||||||
|
"OUT": {
|
||||||
|
"n1": {
|
||||||
|
"result": {
|
||||||
|
"id": "ret_mock_123",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"model": "gpt-x",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "auth=Bearer X | xkey=Y | num=123 | num2=456 | lit_list=[1, 2, 3] | lit_obj={\"k\": 10}"
|
||||||
|
},
|
||||||
|
"finish_reason": "stop"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 15,
|
||||||
|
"total_tokens": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response_text": "auth=Bearer X | xkey=Y | num=123 | num2=456 | lit_list=[1, 2, 3] | lit_obj={\"k\": 10}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OUT_TEXT": {
|
||||||
|
"n1": "auth=Bearer X | xkey=Y | num=123 | num2=456 | lit_list=[1, 2, 3] | lit_obj={\"k\": 10}"
|
||||||
|
},
|
||||||
|
"LAST_NODE": "n1",
|
||||||
|
"OUT1": "auth=Bearer X | xkey=Y | num=123 | num2=456 | lit_list=[1, 2, 3] | lit_obj={\"k\": 10}",
|
||||||
|
"EXEC_TRACE": "n1(Return)"
|
||||||
|
}
|
||||||
|
}
|
||||||
59
.agentui/vars/p_macros_3.json
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"MSG": "hello",
|
||||||
|
"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": "hello"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"n2": {
|
||||||
|
"result": {
|
||||||
|
"id": "ret_mock_123",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"model": "gpt-x",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "hello"
|
||||||
|
},
|
||||||
|
"finish_reason": "stop"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 1,
|
||||||
|
"total_tokens": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response_text": "hello"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OUT_TEXT": {
|
||||||
|
"n1": "hello",
|
||||||
|
"n2": "hello"
|
||||||
|
},
|
||||||
|
"LAST_NODE": "n2",
|
||||||
|
"OUT1": "hello",
|
||||||
|
"OUT2": "hello",
|
||||||
|
"EXEC_TRACE": "n1(SetVars) -> n2(Return)"
|
||||||
|
}
|
||||||
|
}
|
||||||
52
.agentui/vars/p_macros_4_store.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"KEEP": "persist-me",
|
||||||
|
"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": {
|
||||||
|
"result": {
|
||||||
|
"id": "ret_mock_123",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"model": "gpt-x",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "persist-me | persist-me"
|
||||||
|
},
|
||||||
|
"finish_reason": "stop"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 3,
|
||||||
|
"total_tokens": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response_text": "persist-me | persist-me"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OUT_TEXT": {
|
||||||
|
"n1": "persist-me | persist-me"
|
||||||
|
},
|
||||||
|
"LAST_NODE": "n1",
|
||||||
|
"OUT1": "persist-me | persist-me",
|
||||||
|
"EXEC_TRACE": "n1(Return)"
|
||||||
|
}
|
||||||
|
}
|
||||||
54
.agentui/vars/p_macros_5.json
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"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": {
|
||||||
|
"result": {
|
||||||
|
"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": "https://api.openai.com/v1/chat/completions"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OUT_TEXT": {
|
||||||
|
"n1": "https://api.openai.com/v1/chat/completions"
|
||||||
|
},
|
||||||
|
"LAST_NODE": "n1",
|
||||||
|
"OUT1": "https://api.openai.com/v1/chat/completions",
|
||||||
|
"EXEC_TRACE": "n1(ProviderCall)"
|
||||||
|
}
|
||||||
|
}
|
||||||
77
.agentui/vars/p_multi_depends.json
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"A": "foo",
|
||||||
|
"B": "bar",
|
||||||
|
"snapshot": {
|
||||||
|
"incoming": null,
|
||||||
|
"params": {
|
||||||
|
"temperature": 0.1
|
||||||
|
},
|
||||||
|
"model": "gpt-x",
|
||||||
|
"vendor_format": "openai",
|
||||||
|
"system": "",
|
||||||
|
"OUT": {
|
||||||
|
"n1": {
|
||||||
|
"vars": {
|
||||||
|
"A": "foo",
|
||||||
|
"B": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"n3": {
|
||||||
|
"result": {
|
||||||
|
"id": "ret_mock_123",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"model": "gpt-x",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "bar"
|
||||||
|
},
|
||||||
|
"finish_reason": "stop"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 1,
|
||||||
|
"total_tokens": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response_text": "bar"
|
||||||
|
},
|
||||||
|
"n2": {
|
||||||
|
"result": {
|
||||||
|
"id": "ret_mock_123",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"model": "gpt-x",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "foo"
|
||||||
|
},
|
||||||
|
"finish_reason": "stop"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 1,
|
||||||
|
"total_tokens": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response_text": "foo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OUT_TEXT": {
|
||||||
|
"n1": "foo",
|
||||||
|
"n3": "bar",
|
||||||
|
"n2": "foo"
|
||||||
|
},
|
||||||
|
"LAST_NODE": "n3",
|
||||||
|
"OUT1": "foo",
|
||||||
|
"OUT3": "bar",
|
||||||
|
"OUT2": "foo",
|
||||||
|
"EXEC_TRACE": "n1(SetVars) -> n3(Return) -> n2(Return)"
|
||||||
|
}
|
||||||
|
}
|
||||||
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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
36
.agentui/vars/p_prompt_empty.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"snapshot": {
|
||||||
|
"incoming": null,
|
||||||
|
"params": {
|
||||||
|
"temperature": 0.1
|
||||||
|
},
|
||||||
|
"model": "gpt-x",
|
||||||
|
"vendor_format": "openai",
|
||||||
|
"system": "",
|
||||||
|
"OUT": {
|
||||||
|
"n1": {
|
||||||
|
"result": {
|
||||||
|
"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": "https://api.openai.com/v1/chat/completions"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OUT_TEXT": {
|
||||||
|
"n1": "https://api.openai.com/v1/chat/completions"
|
||||||
|
},
|
||||||
|
"LAST_NODE": "n1",
|
||||||
|
"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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
.agentui/vars/pipeline_editor.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"Clod": "igrovik",
|
||||||
|
"MyOpenAiKey": "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",
|
||||||
|
"NAMETest": 192,
|
||||||
|
"WAS_ERROR__n3": true,
|
||||||
|
"CYCLEINDEX__n3": 0
|
||||||
|
}
|
||||||
48
.agentui/vars/pipeline_test_iter_1.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"MSG": "Hello",
|
||||||
|
"snapshot": {
|
||||||
|
"incoming": null,
|
||||||
|
"params": {},
|
||||||
|
"model": "gpt-x",
|
||||||
|
"vendor_format": "openai",
|
||||||
|
"system": "",
|
||||||
|
"OUT": {
|
||||||
|
"n1": {
|
||||||
|
"vars": {
|
||||||
|
"MSG": "Hello"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"n2": {
|
||||||
|
"result": {
|
||||||
|
"id": "ret_mock_123",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"model": "gpt-x",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Hello"
|
||||||
|
},
|
||||||
|
"finish_reason": "stop"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 1,
|
||||||
|
"total_tokens": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response_text": "Hello"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OUT_TEXT": {
|
||||||
|
"n1": "Hello",
|
||||||
|
"n2": "Hello"
|
||||||
|
},
|
||||||
|
"LAST_NODE": "n2",
|
||||||
|
"OUT1": "Hello",
|
||||||
|
"OUT2": "Hello",
|
||||||
|
"EXEC_TRACE": "n1(SetVars) -> n2(Return)"
|
||||||
|
}
|
||||||
|
}
|
||||||
53
.agentui/vars/pipeline_test_iter_2.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"MSG": "Hello world",
|
||||||
|
"snapshot": {
|
||||||
|
"incoming": null,
|
||||||
|
"params": {},
|
||||||
|
"model": "gpt-x",
|
||||||
|
"vendor_format": "openai",
|
||||||
|
"system": "",
|
||||||
|
"OUT": {
|
||||||
|
"n1": {
|
||||||
|
"vars": {
|
||||||
|
"MSG": "Hello world"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nIf": {
|
||||||
|
"result": true,
|
||||||
|
"true": true,
|
||||||
|
"false": false
|
||||||
|
},
|
||||||
|
"nRet": {
|
||||||
|
"result": {
|
||||||
|
"id": "ret_mock_123",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"model": "gpt-x",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Hello world ok"
|
||||||
|
},
|
||||||
|
"finish_reason": "stop"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 3,
|
||||||
|
"total_tokens": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response_text": "Hello world ok"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OUT_TEXT": {
|
||||||
|
"n1": "Hello world",
|
||||||
|
"nIf": "",
|
||||||
|
"nRet": "Hello world ok"
|
||||||
|
},
|
||||||
|
"LAST_NODE": "nRet",
|
||||||
|
"OUT1": "Hello world",
|
||||||
|
"EXEC_TRACE": "n1(SetVars) -> If (#nIf) [[MSG]] contains \"Hello\" => true -> nRet(Return)"
|
||||||
|
}
|
||||||
|
}
|
||||||
56
.agentui/vars/pipeline_test_iter_3.json
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"MSG": "Hello OUT",
|
||||||
|
"X_MSG": "Hello OUT",
|
||||||
|
"snapshot": {
|
||||||
|
"incoming": null,
|
||||||
|
"params": {},
|
||||||
|
"model": "gpt-x",
|
||||||
|
"vendor_format": "openai",
|
||||||
|
"system": "",
|
||||||
|
"OUT": {
|
||||||
|
"n1": {
|
||||||
|
"vars": {
|
||||||
|
"MSG": "Hello OUT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"n2": {
|
||||||
|
"vars": {
|
||||||
|
"X_MSG": "Hello OUT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"n3": {
|
||||||
|
"result": {
|
||||||
|
"id": "ret_mock_123",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"model": "gpt-x",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Hello OUT"
|
||||||
|
},
|
||||||
|
"finish_reason": "stop"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 2,
|
||||||
|
"total_tokens": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response_text": "Hello OUT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OUT_TEXT": {
|
||||||
|
"n1": "Hello OUT",
|
||||||
|
"n2": "Hello OUT",
|
||||||
|
"n3": "Hello OUT"
|
||||||
|
},
|
||||||
|
"LAST_NODE": "n3",
|
||||||
|
"OUT1": "Hello OUT",
|
||||||
|
"OUT2": "Hello OUT",
|
||||||
|
"OUT3": "Hello OUT",
|
||||||
|
"EXEC_TRACE": "n1(SetVars) -> n2(Probe) -> n3(Return)"
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
9
.gitignore
vendored
@@ -41,18 +41,13 @@ Thumbs.db
|
|||||||
*.log
|
*.log
|
||||||
agentui.log
|
agentui.log
|
||||||
|
|
||||||
# proxy
|
|
||||||
proxy.txt
|
|
||||||
|
|
||||||
# Local config
|
# Local config
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
*.env
|
*.env
|
||||||
|
#
|
||||||
# Project-specific runtime files
|
|
||||||
presets/
|
|
||||||
pipeline.json
|
|
||||||
|
|
||||||
# Node
|
# Node
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
|||||||
@@ -1,80 +1,200 @@
|
|||||||
# AgentUI Project Overview
|
# НадTavern (AgentUI): объясняем просто
|
||||||
|
|
||||||
## Цель проекта
|
Это инструмент, который помогает слать запросы к ИИ (OpenAI, Gemini, Claude) через простой конвейер из блоков (узлов). Вы кликаете мышкой, соединяете блоки — и получаете ответ в том же формате, в каком спрашивали.
|
||||||
AgentUI — это прокси‑сервер с визуальным редактором пайплайнов (на базе Drawflow), который нормализует запросы от различных клиентов в единый формат и выполняет их через цепочку узлов (nodes). Это позволяет гибко собирать пайплайны обработки текстовых/LLM‑запросов без необходимости вручную интегрировать каждый провайдер.
|
|
||||||
|
Коротко: было сложно — стало просто.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Основные компоненты
|
Что вы можете сделать
|
||||||
|
|
||||||
### Фронтенд: Визуальный редактор
|
- Принять запрос от клиента как есть (OpenAI/Gemini/Claude).
|
||||||
- Построен на **Drawflow**.
|
- При желании подменить его или дополнить.
|
||||||
- Поддерживает узлы, входы/выходы и соединения.
|
- Отправить к нужному провайдеру (или прямо «пробросить» как есть).
|
||||||
- Реализована надёжная сериализация/десериализация:
|
- Склеить финальный ответ в том формате, который ждёт клиент.
|
||||||
- `toPipelineJSON()` сохраняет структуру + все соединения.
|
|
||||||
- `fromPipelineJSON()` восстанавливает узлы и соединения с учётом времени рендера DOM (retry‑логика).
|
|
||||||
- Исправлены баги исчезающих соединений.
|
|
||||||
- В инспекторе узлов отображается оригинальный ID узла, а не runtime ID от Drawflow.
|
|
||||||
- UI подсказки: макрохинты в синтаксисе `[[...]]` (например `[[VAR:system.prompt]]`, `[[OUT:node1.text]]`).
|
|
||||||
|
|
||||||
### Бэкенд: Исполнение пайплайна
|
|
||||||
- Основной код: [`agentui/pipeline/executor.py`](agentui/pipeline/executor.py).
|
|
||||||
- Выполняется **топологическая сортировка** графа для правильного порядка исполнения и предотвращения циклов.
|
|
||||||
- Узлы:
|
|
||||||
- **RawForwardNode**:
|
|
||||||
- Прямой HTTP‑форвардинг с макросами в `base_url`, `override_path`, `headers`.
|
|
||||||
- Автоопределение провайдера.
|
|
||||||
- **ProviderCallNode**:
|
|
||||||
- Унифицированный вызов LLM‑провайдеров.
|
|
||||||
- Преобразует внутренний формат сообщений в специфический формат для OpenAI, Gemini, Anthropic.
|
|
||||||
- Поддерживает параметры `temperature`, `max_tokens`, `top_p`, `stop` (или аналоги).
|
|
||||||
- Поддержка **макросов**:
|
|
||||||
- `{{ path }}` — Jinja‑подобный.
|
|
||||||
- `[[VAR:...]]` — доступ к данным контекста (system, chat, params).
|
|
||||||
- `[[OUT:nodeId(.attr)]]` — ссылки на вывод других узлов.
|
|
||||||
- `{{ OUT.node.* }}` — альтернативная форма.
|
|
||||||
|
|
||||||
### API сервер (`agentui/api/server.py`)
|
|
||||||
- Нормализует запросы под форматы `/v1/chat/completions`, Gemini, Anthropic.
|
|
||||||
- Формирует контекст макросов (vendor, model, params, incoming).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Текущий прогресс
|
Как это работает в 5 шагов
|
||||||
- Исправлены баги сериализации соединений во фронтенде.
|
|
||||||
- Добавлены подсказки по макросам.
|
1) Клиент шлёт HTTP‑запрос на ваш сервер.
|
||||||
- Реализована топологическая сортировка исполнения.
|
2) Сервер понимает формат (OpenAI/Gemini/Claude) и делает «унифицированный» вид.
|
||||||
- Создан универсальный рендер макросов `render_template_simple`.
|
3) Загружается ваш пайплайн (схема из узлов) из файла pipeline.json.
|
||||||
- Интегрирован RawForward с макроподстановкой.
|
4) Узлы запускаются по очереди «волнами» и обмениваются результатами.
|
||||||
- ProviderCall теперь преобразует сообщения под формат конкретного провайдера.
|
5) Последний узел отдаёт ответ клиенту в нужном формате.
|
||||||
|
|
||||||
|
Если страшно — не бойтесь: всё это уже настроено из коробки.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Текущая задача (для нового разработчика)
|
Быстрый старт
|
||||||
|
|
||||||
В проекте мы начинаем реализацию **Prompt Manager**, который станет частью узла `ProviderCall`.
|
Вариант A (Windows, авто‑настройка .venv):
|
||||||
|
- Запустите [run_agentui.bat](run_agentui.bat) двойным кликом или из консоли.
|
||||||
|
- Скрипт сам:
|
||||||
|
- создаст локальное окружение .venv в каталоге проекта;
|
||||||
|
- обновит pip;
|
||||||
|
- установит зависимости из [requirements.txt](requirements.txt);
|
||||||
|
- поднимет сервер и откроет редактор в браузере.
|
||||||
|
- Переменные окружения (опционально перед запуском): HOST=127.0.0.1 PORT=7860
|
||||||
|
|
||||||
**Что уже решено:**
|
Вариант B (Linux/macOS, авто‑настройка .venv):
|
||||||
- Архитектура пайплайна, сериализация/десериализация, макросная система, базовые конвертеры форматов.
|
- Сделайте исполняемым и запустите:
|
||||||
|
- chmod +x [run_agentui.sh](run_agentui.sh)
|
||||||
|
- ./run_agentui.sh
|
||||||
|
- Скрипт сделает то же самое: .venv + установка зависимостей + старт сервера.
|
||||||
|
|
||||||
**Что нужно сделать:**
|
Вариант C (ручной запуск, если хотите контролировать шаги):
|
||||||
- [ ] Спроектировать структуру prompt‑менеджера: массив блоков `{ name, role, prompt, enabled, order }`.
|
- Установите Python 3.10+.
|
||||||
- [ ] Добавить универсальный рендер макросов, который применяется ко всем блокам перед конвертацией.
|
- Создайте и активируйте .venv:
|
||||||
- [ ] Доработать конвертеры форматов под OpenAI, Gemini, Anthropic, чтобы они учитывали эти блоки.
|
- Windows (cmd): py -m venv .venv && .\.venv\Scripts\activate
|
||||||
- [ ] Интегрировать prompt‑менеджер в `ProviderCallNode`:
|
- 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
|
||||||
- [ ] Реализовать UI prompt‑менеджера во фронтенде:
|
|
||||||
- CRUD операций над блоками.
|
Откройте в браузере:
|
||||||
- Drag&Drop сортировку.
|
- http://127.0.0.1:7860/ui/editor.html — визуальный редактор узлов
|
||||||
- Возможность включать/выключать блок.
|
- http://127.0.0.1:7860/ui/pipeline.html — редактор «сырых» JSON настроек пайплайна
|
||||||
- Выбор роли (`user`, `system`, `assistant`, `tool`).
|
- http://127.0.0.1:7860/ — простая страница с примером запроса
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Важные файлы
|
Где лежат важные файлы
|
||||||
- [`static/editor.html`](static/editor.html) — визуальный редактор пайплайнов.
|
|
||||||
- [`agentui/pipeline/executor.py`](agentui/pipeline/executor.py) — логика исполнения пайплайнов, макросов и узлов.
|
- API сервер: [`agentui/api/server.py`](agentui/api/server.py)
|
||||||
- [`agentui/api/server.py`](agentui/api/server.py) — REST API для внешних клиентов.
|
- Исполнитель узлов: [`agentui/pipeline/executor.py`](agentui/pipeline/executor.py)
|
||||||
- [`pipeline.json`](pipeline.json) — сохранённый пайплайн по умолчанию.
|
- Шаблоны и макросы: [`agentui/pipeline/templating.py`](agentui/pipeline/templating.py)
|
||||||
|
- Определение провайдера (OpenAI/Gemini/Claude): [`agentui/common/vendors.py`](agentui/common/vendors.py)
|
||||||
|
- Активный пайплайн: [`pipeline.json`](pipeline.json)
|
||||||
|
- Пресеты (готовые пайплайны): папка [`presets`](presets/)
|
||||||
|
- Визуальный редактор (страница): [`static/editor.html`](static/editor.html)
|
||||||
|
- Логика сериализации/соединений: [`static/js/serialization.js`](static/js/serialization.js)
|
||||||
|
- Настройка Prompt Manager UI: [`static/js/pm-ui.js`](static/js/pm-ui.js)
|
||||||
|
- Справка по переменным: [`docs/VARIABLES.md`](docs/VARIABLES.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Что такое «узлы»
|
||||||
|
|
||||||
|
Представьте конструктор Лего. Узел — это кубик. Соединяете кубики — получаете конвейер (пайплайн).
|
||||||
|
В проекте есть 4 базовых узла:
|
||||||
|
|
||||||
|
1) SetVars — завести свои переменные.
|
||||||
|
- Пример: MY_KEY, REGION, MAX_TOKENS.
|
||||||
|
- Потом в шаблонах вы можете писать [[MY_KEY]] или {{ MAX_TOKENS }}.
|
||||||
|
|
||||||
|
2) RawForward — «пробросить» входящий запрос как есть дальше (reverse‑proxy).
|
||||||
|
- Полезно, когда вы не хотите ничего менять.
|
||||||
|
|
||||||
|
3) ProviderCall — аккуратно собрать JSON для провайдера (OpenAI/Gemini/Claude) из «кусочков текста» (Prompt Blocks) и отправить.
|
||||||
|
- Это удобно, если вы хотите дописать системное сообщение, переписать текст пользователя и т.п.
|
||||||
|
|
||||||
|
4) Return — оформить финальный ответ под тот формат, который ждёт клиент.
|
||||||
|
- Если клиент прислал в стиле OpenAI, вернём в стиле OpenAI; можно принудительно выбрать формат.
|
||||||
|
|
||||||
|
Узлы соединяются линиями «из выхода в вход». Так вы задаёте порядок.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Простые «заклинания» (макросы), которые работают в шаблонах
|
||||||
|
|
||||||
|
- [[VAR:путь]] — взять значение из входящего запроса.
|
||||||
|
- Например: [[VAR:incoming.headers.authorization]]
|
||||||
|
- [[OUT:n1.что‑то]] — взять кусочек результата из узла n1.
|
||||||
|
- [[OUT1]] — взять «самый понятный текст» из узла n1 (короткая форма).
|
||||||
|
- [[PROMPT]] — умный фрагмент JSON, который автоматически соберётся из ваших Prompt Blocks для выбранного провайдера.
|
||||||
|
- {{ путь }} — вставка без кавычек (подходит для чисел/массивов/объектов).
|
||||||
|
- {{ путь|default(значение) }} — вставка с безопасным дефолтом.
|
||||||
|
|
||||||
|
Пример 1 (OpenAI, фрагмент шаблона тела запроса):
|
||||||
|
{
|
||||||
|
"model": "{{ model }}",
|
||||||
|
[[PROMPT]],
|
||||||
|
"temperature": {{ incoming.json.temperature|default(0.7) }}
|
||||||
|
}
|
||||||
|
|
||||||
|
Пример 2 (вставим токен из заголовка в заголовки запроса):
|
||||||
|
{"Authorization":"Bearer [[VAR:incoming.headers.authorization]]"}
|
||||||
|
|
||||||
|
Подробная шпаргалка — в файле [`docs/VARIABLES.md`](docs/VARIABLES.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Первый рабочий пайплайн за 3 минуты
|
||||||
|
|
||||||
|
Цель: взять сообщение пользователя, немного «пригладить» его и вернуть ответ в стиле OpenAI.
|
||||||
|
|
||||||
|
1) Добавьте узел SetVars (можно пропустить).
|
||||||
|
2) Добавьте узел ProviderCall.
|
||||||
|
- В инспекторе выберите провайдера (например, openai).
|
||||||
|
- В Prompt Blocks создайте блоки:
|
||||||
|
- system: «Ты — помощник. Отвечай коротко.»
|
||||||
|
- user: «[[VAR:chat.last_user]] — перепиши текст красивее.»
|
||||||
|
- В шаблоне тела (template) не трогайте структуру — там уже есть [[PROMPT]].
|
||||||
|
3) Добавьте узел Return.
|
||||||
|
- Оставьте «auto», чтобы формат ответа совпал с входящим.
|
||||||
|
4) Соедините ProviderCall → Return.
|
||||||
|
5) Нажмите «Сохранить пайплайн».
|
||||||
|
6) Отправьте запрос на POST /v1/chat/completions (можно со страницы `/`).
|
||||||
|
|
||||||
|
Если что‑то не работает — смотрите журнал (консоль) и всплывающие подсказки в UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Полезные адреса (админка)
|
||||||
|
|
||||||
|
- GET /admin/pipeline — получить текущий пайплайн.
|
||||||
|
- POST /admin/pipeline — сохранить пайплайн.
|
||||||
|
- GET /admin/presets — список пресетов.
|
||||||
|
- GET/POST /admin/presets/{name} — загрузить/сохранить пресет.
|
||||||
|
- GET /admin/trace/stream — «живой» поток событий исполнения. Редактор подсвечивает узлы (начал/успешно/ошибка).
|
||||||
|
|
||||||
|
Это уже настроено в [`agentui/api/server.py`](agentui/api/server.py).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Где смотреть логи
|
||||||
|
|
||||||
|
- Сервер пишет в консоль шаги узлов и ответы провайдеров.
|
||||||
|
- В UI редакторе видно подсветку состояний узлов (события из /admin/trace/stream).
|
||||||
|
- При необходимости включите/отключите подробность логов в коде узлов [`agentui/pipeline/executor.py`](agentui/pipeline/executor.py).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Важная безопасность (прочитайте!)
|
||||||
|
|
||||||
|
- Никогда не храните реальные ключи в файлах в репозитории (pipeline.json, пресеты).
|
||||||
|
- Передавайте ключи через заголовки запроса клиента:
|
||||||
|
- OpenAI: Authorization: Bearer XXXXXX
|
||||||
|
- Anthropic: x-api-key: XXXXXX
|
||||||
|
- Gemini: ?key=XXXXXX в URL
|
||||||
|
- В шаблонах используйте подстановки из входящего запроса: [[VAR:incoming.headers.authorization]], [[VAR:incoming.headers.x-api-key]], [[VAR:incoming.api_keys.key]].
|
||||||
|
- В логах при разработке печатаются заголовки и тело. Для продакшена выключайте или маскируйте их.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Частые ошибки и быстрые решения
|
||||||
|
|
||||||
|
- «Соединения между узлами пропадают»
|
||||||
|
- Нажмите «Загрузить пайплайн» ещё раз: редактор терпеливо дождётся DOM и восстановит линии.
|
||||||
|
- «Шаблон JSON ругается на запятые/скобки»
|
||||||
|
- Убедитесь, что [[PROMPT]] стоит без лишних запятых вокруг; числа/массивы вставляйте через {{ ... }}.
|
||||||
|
- «Нет ответа от провайдера»
|
||||||
|
- Проверьте ключ и URL/endpoint в конфигурации узла ProviderCall (инспектор справа).
|
||||||
|
- «Нужен просто прокси без изменений»
|
||||||
|
- Используйте узел RawForward первым, а потом Return.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Для любопытных (необязательно)
|
||||||
|
|
||||||
|
Код устроен так:
|
||||||
|
- Сервер создаётся здесь: [`agentui/api/server.py`](agentui/api/server.py)
|
||||||
|
- Исполнение узлов и «волны» — здесь: [`PipelineExecutor.run()`](agentui/pipeline/executor.py:136)
|
||||||
|
- Провайдерный вызов и Prompt Blocks — здесь: [`ProviderCallNode.run()`](agentui/pipeline/executor.py:650)
|
||||||
|
- Простой шаблонизатор (две скобки): [`render_template_simple()`](agentui/pipeline/templating.py:187)
|
||||||
|
|
||||||
|
Этого достаточно, чтобы понимать, куда заглянуть, если захотите кое‑что подкрутить.
|
||||||
|
|
||||||
|
Удачи! Запускайте редактор, соединяйте узлы и получайте ответы без боли.
|
||||||
|
|||||||
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 pathlib import Path
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
def _parse_proxy_line(line: str) -> Optional[str]:
|
def _parse_proxy_line(line: str) -> Optional[str]:
|
||||||
@@ -39,6 +40,9 @@ def _read_proxy_from_file() -> Optional[str]:
|
|||||||
line = raw.strip()
|
line = raw.strip()
|
||||||
if not line or line.startswith("#"):
|
if not line or line.startswith("#"):
|
||||||
continue
|
continue
|
||||||
|
# поддержим дополнительные ключи вида key=value в этом же файле (разберём ниже)
|
||||||
|
if "=" in line:
|
||||||
|
continue
|
||||||
url = _parse_proxy_line(line)
|
url = _parse_proxy_line(line)
|
||||||
if url:
|
if url:
|
||||||
return 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
|
||||||
|
|||||||
@@ -4,10 +4,18 @@ from typing import Any, Dict
|
|||||||
def default_pipeline() -> Dict[str, Any]:
|
def default_pipeline() -> Dict[str, Any]:
|
||||||
# Минимальный дефолт без устаревших нод.
|
# Минимальный дефолт без устаревших нод.
|
||||||
# Если пайплайн пустой, сервер вернёт echo-ответ (см. server.execute_pipeline_echo).
|
# Если пайплайн пустой, сервер вернёт echo-ответ (см. server.execute_pipeline_echo).
|
||||||
|
# Добавлены поля управления режимом исполнения:
|
||||||
|
# - loop_mode: "dag" | "iterative" (по умолчанию "dag")
|
||||||
|
# - loop_max_iters: максимальное число запусков задач (safety)
|
||||||
|
# - loop_time_budget_ms: ограничение по времени (safety)
|
||||||
return {
|
return {
|
||||||
"id": "pipeline_default",
|
"id": "pipeline_default",
|
||||||
"name": "Default Chat Pipeline",
|
"name": "Default Chat Pipeline",
|
||||||
"parallel_limit": 8,
|
"parallel_limit": 8,
|
||||||
|
"loop_mode": "dag",
|
||||||
|
"loop_max_iters": 1000,
|
||||||
|
"loop_time_budget_ms": 10000,
|
||||||
|
"clear_var_store": True,
|
||||||
"nodes": []
|
"nodes": []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,19 +8,96 @@ from agentui.pipeline.defaults import default_pipeline
|
|||||||
|
|
||||||
PIPELINE_FILE = Path("pipeline.json")
|
PIPELINE_FILE = Path("pipeline.json")
|
||||||
PRESETS_DIR = Path("presets")
|
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]:
|
def load_pipeline() -> Dict[str, Any]:
|
||||||
if PIPELINE_FILE.exists():
|
if PIPELINE_FILE.exists():
|
||||||
try:
|
try:
|
||||||
return json.loads(PIPELINE_FILE.read_text(encoding="utf-8"))
|
data = json.loads(PIPELINE_FILE.read_text(encoding="utf-8"))
|
||||||
except Exception:
|
return normalize_pipeline(data)
|
||||||
pass
|
except Exception:
|
||||||
return default_pipeline()
|
pass
|
||||||
|
return normalize_pipeline(default_pipeline())
|
||||||
|
|
||||||
|
|
||||||
def save_pipeline(pipeline: Dict[str, Any]) -> None:
|
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]:
|
def list_presets() -> List[str]:
|
||||||
@@ -42,3 +119,51 @@ def save_preset(name: str, pipeline: Dict[str, Any]) -> None:
|
|||||||
path.write_text(json.dumps(pipeline, ensure_ascii=False, indent=2), encoding="utf-8")
|
path.write_text(json.dumps(pipeline, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- Variable Store (per-pipeline) ----------------
|
||||||
|
|
||||||
|
def _var_store_path(pipeline_id: str) -> Path:
|
||||||
|
pid = pipeline_id or "pipeline_editor"
|
||||||
|
VARS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
# normalize to safe filename
|
||||||
|
safe = "".join(ch if ch.isalnum() or ch in ("-", "_", ".") else "_" for ch in str(pid))
|
||||||
|
return VARS_DIR / f"{safe}.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load_var_store(pipeline_id: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Load variable store dictionary for given pipeline id.
|
||||||
|
Returns {} if not exists or invalid.
|
||||||
|
"""
|
||||||
|
path = _var_store_path(pipeline_id)
|
||||||
|
if not path.exists():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
return data if isinstance(data, dict) else {}
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def save_var_store(pipeline_id: str, data: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Save variable store dictionary for given pipeline id.
|
||||||
|
"""
|
||||||
|
path = _var_store_path(pipeline_id)
|
||||||
|
try:
|
||||||
|
VARS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
path.write_text(json.dumps(data or {}, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def clear_var_store(pipeline_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Delete/reset variable store for given pipeline id.
|
||||||
|
"""
|
||||||
|
path = _var_store_path(pipeline_id)
|
||||||
|
try:
|
||||||
|
if path.exists():
|
||||||
|
path.unlink()
|
||||||
|
except Exception:
|
||||||
|
# ignore failures
|
||||||
|
pass
|
||||||
|
|||||||
@@ -17,11 +17,14 @@ __all__ = [
|
|||||||
"_deep_find_text",
|
"_deep_find_text",
|
||||||
"_best_text_from_outputs",
|
"_best_text_from_outputs",
|
||||||
"render_template_simple",
|
"render_template_simple",
|
||||||
|
"eval_condition_expr",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Regex-макросы (общие для бэка)
|
# Regex-макросы (общие для бэка)
|
||||||
_OUT_MACRO_RE = re.compile(r"\[\[\s*OUT\s*[:\s]\s*([^\]]+?)\s*\]\]", re.IGNORECASE)
|
_OUT_MACRO_RE = re.compile(r"\[\[\s*OUT\s*[:\s]\s*([^\]]+?)\s*\]\]", re.IGNORECASE)
|
||||||
_VAR_MACRO_RE = re.compile(r"\[\[\s*VAR\s*[:\s]\s*([^\]]+?)\s*\]\]", re.IGNORECASE)
|
_VAR_MACRO_RE = re.compile(r"\[\[\s*VAR\s*[:\s]\s*([^\]]+?)\s*\]\]", re.IGNORECASE)
|
||||||
|
# STORE: постоянное хранилище переменных (пер-пайплайн)
|
||||||
|
_STORE_MACRO_RE = re.compile(r"\[\[\s*STORE\s*[:\s]\s*([^\]]+?)\s*\]\]", re.IGNORECASE)
|
||||||
# Единый фрагмент PROMPT (провайдеро-специфичный JSON-фрагмент)
|
# Единый фрагмент PROMPT (провайдеро-специфичный JSON-фрагмент)
|
||||||
_PROMPT_MACRO_RE = re.compile(r"\[\[\s*PROMPT\s*\]\]", re.IGNORECASE)
|
_PROMPT_MACRO_RE = re.compile(r"\[\[\s*PROMPT\s*\]\]", re.IGNORECASE)
|
||||||
# Короткая форма: [[OUT1]] — best-effort текст из ноды n1
|
# Короткая форма: [[OUT1]] — best-effort текст из ноды n1
|
||||||
@@ -29,7 +32,13 @@ _OUT_SHORT_RE = re.compile(r"\[\[\s*OUT\s*(\d+)\s*\]\]", re.IGNORECASE)
|
|||||||
# Голые переменные: [[NAME]] или [[path.to.value]] — сначала ищем в vars, затем в контексте
|
# Голые переменные: [[NAME]] или [[path.to.value]] — сначала ищем в vars, затем в контексте
|
||||||
_BARE_MACRO_RE = re.compile(r"\[\[\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[^\]]+?)?)\s*\]\]")
|
_BARE_MACRO_RE = re.compile(r"\[\[\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[^\]]+?)?)\s*\]\]")
|
||||||
# Подстановки {{ ... }} (включая простейший фильтр |default(...))
|
# Подстановки {{ ... }} (включая простейший фильтр |default(...))
|
||||||
_BRACES_RE = re.compile(r"\{\{\s*([^}]+?)\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]:
|
def _split_path(path: str) -> List[str]:
|
||||||
@@ -160,12 +169,21 @@ def _best_text_from_outputs(node_out: Any) -> str:
|
|||||||
# Gemini
|
# Gemini
|
||||||
try:
|
try:
|
||||||
if isinstance(base, dict):
|
if isinstance(base, dict):
|
||||||
cand0 = (base.get("candidates") or [{}])[0]
|
cands = base.get("candidates") or []
|
||||||
content = cand0.get("content") or {}
|
texts: List[str] = []
|
||||||
parts0 = (content.get("parts") or [{}])[0]
|
for cand in cands:
|
||||||
t = parts0.get("text")
|
try:
|
||||||
if isinstance(t, str):
|
content = cand.get("content") or {}
|
||||||
return t
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -192,13 +210,55 @@ def render_template_simple(template: str, context: Dict[str, Any], out_map: Dict
|
|||||||
value может быть числом, строкой ('..'/".."), массивом/объектом в виде литерала.
|
value может быть числом, строкой ('..'/".."), массивом/объектом в виде литерала.
|
||||||
- [[VAR:path]] — берёт из context
|
- [[VAR:path]] — берёт из context
|
||||||
- [[OUT:nodeId(.path)*]] — берёт из out_map
|
- [[OUT:nodeId(.path)*]] — берёт из out_map
|
||||||
|
- [[STORE:path]] — берёт из постоянного хранилища (context.store)
|
||||||
Возвращает строку.
|
Возвращает строку.
|
||||||
"""
|
"""
|
||||||
if template is None:
|
if template is None:
|
||||||
return ""
|
return ""
|
||||||
s = str(template)
|
s = str(template)
|
||||||
|
|
||||||
# 1) Макросы [[VAR:...]] и [[OUT:...]]
|
# 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:
|
def repl_var(m: re.Match) -> str:
|
||||||
path = m.group(1).strip()
|
path = m.group(1).strip()
|
||||||
val = _get_by_path(context, path)
|
val = _get_by_path(context, path)
|
||||||
@@ -214,8 +274,15 @@ def render_template_simple(template: str, context: Dict[str, Any], out_map: Dict
|
|||||||
val = out_map.get(body)
|
val = out_map.get(body)
|
||||||
return _stringify_for_template(val)
|
return _stringify_for_template(val)
|
||||||
|
|
||||||
|
def repl_store(m: re.Match) -> str:
|
||||||
|
path = m.group(1).strip()
|
||||||
|
store = context.get("store") or {}
|
||||||
|
val = _get_by_path(store, path)
|
||||||
|
return _stringify_for_template(val)
|
||||||
|
|
||||||
s = _VAR_MACRO_RE.sub(repl_var, s)
|
s = _VAR_MACRO_RE.sub(repl_var, s)
|
||||||
s = _OUT_MACRO_RE.sub(repl_out, s)
|
s = _OUT_MACRO_RE.sub(repl_out, s)
|
||||||
|
s = _STORE_MACRO_RE.sub(repl_store, s)
|
||||||
|
|
||||||
# [[OUT1]] → текст из ноды n1 (best-effort)
|
# [[OUT1]] → текст из ноды n1 (best-effort)
|
||||||
def repl_out_short(m: re.Match) -> str:
|
def repl_out_short(m: re.Match) -> str:
|
||||||
@@ -250,7 +317,7 @@ def render_template_simple(template: str, context: Dict[str, Any], out_map: Dict
|
|||||||
# 2) Подстановки {{ ... }} (+ simple default filter)
|
# 2) Подстановки {{ ... }} (+ simple default filter)
|
||||||
def repl_braces(m: re.Match) -> str:
|
def repl_braces(m: re.Match) -> str:
|
||||||
expr = m.group(1).strip()
|
expr = m.group(1).strip()
|
||||||
|
|
||||||
def eval_path(p: str) -> Any:
|
def eval_path(p: str) -> Any:
|
||||||
p = p.strip()
|
p = p.strip()
|
||||||
# Приоритет пользовательских переменных для простых идентификаторов {{ NAME }}
|
# Приоритет пользовательских переменных для простых идентификаторов {{ NAME }}
|
||||||
@@ -264,8 +331,13 @@ def render_template_simple(template: str, context: Dict[str, Any], out_map: Dict
|
|||||||
node_val = out_map.get(node_id)
|
node_val = out_map.get(node_id)
|
||||||
return _get_by_path(node_val, rest)
|
return _get_by_path(node_val, rest)
|
||||||
return out_map.get(body)
|
return out_map.get(body)
|
||||||
|
# STORE.* — из постоянного хранилища
|
||||||
|
if p.startswith("STORE.") or p.startswith("store."):
|
||||||
|
body = p.split(".", 1)[1] if "." in p else ""
|
||||||
|
store = context.get("store") or {}
|
||||||
|
return _get_by_path(store, body)
|
||||||
return _get_by_path(context, p)
|
return _get_by_path(context, p)
|
||||||
|
|
||||||
default_match = re.match(r"([^|]+)\|\s*default\((.*)\)\s*$", expr)
|
default_match = re.match(r"([^|]+)\|\s*default\((.*)\)\s*$", expr)
|
||||||
if default_match:
|
if default_match:
|
||||||
base_path = default_match.group(1).strip()
|
base_path = default_match.group(1).strip()
|
||||||
@@ -305,4 +377,431 @@ def render_template_simple(template: str, context: Dict[str, Any], out_map: Dict
|
|||||||
return _stringify_for_template(val)
|
return _stringify_for_template(val)
|
||||||
|
|
||||||
s = _BRACES_RE.sub(repl_braces, s)
|
s = _BRACES_RE.sub(repl_braces, s)
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
# --- Boolean condition evaluator for If-node ---------------------------------
|
||||||
|
# Поддерживает:
|
||||||
|
# - Операторы: &&, ||, !, ==, !=, <, <=, >, >=, contains
|
||||||
|
# - Скобки (...)
|
||||||
|
# - Токены-литералы: числа (int/float), строки "..." (без escape-сложностей)
|
||||||
|
# - Макросы: [[VAR:...]], [[OUT:...]], [[OUT1]], [[NAME]] (vars/context),
|
||||||
|
# {{ path }} и {{ path|default(...) }} — типобезопасно (числа остаются числами)
|
||||||
|
# Возвращает bool. Бросает ValueError при синтаксической/семантической ошибке.
|
||||||
|
def eval_condition_expr(expr: str, context: Dict[str, Any], out_map: Dict[str, Any]) -> bool:
|
||||||
|
import ast
|
||||||
|
|
||||||
|
if expr is None:
|
||||||
|
return False
|
||||||
|
s = str(expr)
|
||||||
|
|
||||||
|
# Tokenize into a flat list of tokens and build value bindings for macros/braces.
|
||||||
|
tokens, bindings = _tokenize_condition_expr(s, context, out_map)
|
||||||
|
|
||||||
|
# Transform infix "contains" into function form contains(a,b)
|
||||||
|
tokens = _transform_contains(tokens)
|
||||||
|
|
||||||
|
# Join into python-like boolean expression and map logical ops.
|
||||||
|
py_expr = _tokens_to_python_expr(tokens)
|
||||||
|
|
||||||
|
# Evaluate safely via AST with strict whitelist
|
||||||
|
result = _safe_eval_bool(py_expr, bindings)
|
||||||
|
return bool(result)
|
||||||
|
|
||||||
|
|
||||||
|
def _tokens_to_python_expr(tokens: List[str]) -> str:
|
||||||
|
# Уже нормализовано на этапе токенизации, просто склеиваем с пробелами
|
||||||
|
return " ".join(tokens)
|
||||||
|
|
||||||
|
|
||||||
|
def _transform_contains(tokens: List[str]) -> List[str]:
|
||||||
|
# Заменяет "... A contains B ..." на "contains(A, B)" с учётом скобок.
|
||||||
|
i = 0
|
||||||
|
out: List[str] = tokens[:] # копия
|
||||||
|
# Итерируем, пока встречается 'contains'
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
idx = out.index("contains")
|
||||||
|
except ValueError:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Левая часть
|
||||||
|
lstart = idx - 1
|
||||||
|
if lstart >= 0 and out[lstart] == ")":
|
||||||
|
# найти соответствующую открывающую "("
|
||||||
|
bal = 0
|
||||||
|
j = lstart
|
||||||
|
while j >= 0:
|
||||||
|
if out[j] == ")":
|
||||||
|
bal += 1
|
||||||
|
elif out[j] == "(":
|
||||||
|
bal -= 1
|
||||||
|
if bal == 0:
|
||||||
|
lstart = j
|
||||||
|
break
|
||||||
|
j -= 1
|
||||||
|
if bal != 0:
|
||||||
|
# несбалансированные скобки
|
||||||
|
raise ValueError("Unbalanced parentheses around left operand of contains")
|
||||||
|
# Правая часть
|
||||||
|
rend = idx + 1
|
||||||
|
if rend < len(out) and out[rend] == "(":
|
||||||
|
bal = 0
|
||||||
|
j = rend
|
||||||
|
while j < len(out):
|
||||||
|
if out[j] == "(":
|
||||||
|
bal += 1
|
||||||
|
elif out[j] == ")":
|
||||||
|
bal -= 1
|
||||||
|
if bal == 0:
|
||||||
|
rend = j
|
||||||
|
break
|
||||||
|
j += 1
|
||||||
|
if bal != 0:
|
||||||
|
raise ValueError("Unbalanced parentheses around right operand of contains")
|
||||||
|
# Если нет скобок — однотокенный операнд
|
||||||
|
left_tokens = out[lstart:idx]
|
||||||
|
right_tokens = out[idx + 1:rend + 1] if (idx + 1 < len(out) and out[idx + 1] == "(") else out[idx + 1:idx + 2]
|
||||||
|
if not left_tokens or not right_tokens:
|
||||||
|
raise ValueError("contains requires two operands")
|
||||||
|
|
||||||
|
left_str = " ".join(left_tokens)
|
||||||
|
right_str = " ".join(right_tokens)
|
||||||
|
|
||||||
|
# Синтезируем вызов и заменяем диапазон
|
||||||
|
new_tok = f"contains({left_str}, {right_str})"
|
||||||
|
out = out[:lstart] + [new_tok] + out[(rend + 1) if (idx + 1 < len(out) and out[idx + 1] == "(") else (idx + 2):]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _tokenize_condition_expr(expr: str, context: Dict[str, Any], out_map: Dict[str, Any]) -> tuple[List[str], Dict[str, Any]]:
|
||||||
|
tokens: List[str] = []
|
||||||
|
bindings: Dict[str, Any] = {}
|
||||||
|
i = 0
|
||||||
|
n = len(expr)
|
||||||
|
vcount = 0
|
||||||
|
|
||||||
|
def add_binding(val: Any) -> str:
|
||||||
|
nonlocal vcount
|
||||||
|
name = f"__v{vcount}"
|
||||||
|
vcount += 1
|
||||||
|
bindings[name] = val
|
||||||
|
return name
|
||||||
|
|
||||||
|
while i < n:
|
||||||
|
ch = expr[i]
|
||||||
|
# Пробелы
|
||||||
|
if ch.isspace():
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Операторы двойные
|
||||||
|
if expr.startswith("&&", i):
|
||||||
|
tokens.append("and")
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
if expr.startswith("||", i):
|
||||||
|
tokens.append("or")
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
if expr.startswith(">=", i) or expr.startswith("<=", i) or expr.startswith("==", i) or expr.startswith("!=", i):
|
||||||
|
tokens.append(expr[i:i+2])
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Одинарные операторы
|
||||||
|
if ch in "()<>":
|
||||||
|
tokens.append(ch)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if ch == "!":
|
||||||
|
# уже обработали "!=" как двойной
|
||||||
|
tokens.append("not")
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Строковые литералы "...." и '....' (простая версия: без экранирования)
|
||||||
|
if ch == '"':
|
||||||
|
j = i + 1
|
||||||
|
while j < n and expr[j] != '"':
|
||||||
|
# простая версия: без экранирования
|
||||||
|
j += 1
|
||||||
|
if j >= n:
|
||||||
|
raise ValueError('Unterminated string literal')
|
||||||
|
content = expr[i+1:j]
|
||||||
|
# Конвертируем в безопасный Python-литерал
|
||||||
|
tokens.append(repr(content))
|
||||||
|
i = j + 1
|
||||||
|
continue
|
||||||
|
# Одинарные кавычки
|
||||||
|
if ch == "'":
|
||||||
|
j = i + 1
|
||||||
|
while j < n and expr[j] != "'":
|
||||||
|
# простая версия: без экранирования
|
||||||
|
j += 1
|
||||||
|
if j >= n:
|
||||||
|
raise ValueError('Unterminated string literal')
|
||||||
|
content = expr[i+1:j]
|
||||||
|
tokens.append(repr(content))
|
||||||
|
i = j + 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Макросы [[...]]
|
||||||
|
if expr.startswith("[[", i):
|
||||||
|
j = expr.find("]]", i + 2)
|
||||||
|
if j < 0:
|
||||||
|
raise ValueError("Unterminated [[...]] macro")
|
||||||
|
body = expr[i+2:j]
|
||||||
|
val = _resolve_square_macro_value(body, context, out_map)
|
||||||
|
name = add_binding(val)
|
||||||
|
tokens.append(name)
|
||||||
|
i = j + 2
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Скобки {{ ... }}
|
||||||
|
if expr.startswith("{{", i):
|
||||||
|
j = expr.find("}}", i + 2)
|
||||||
|
if j < 0:
|
||||||
|
raise ValueError("Unterminated {{ ... }} expression")
|
||||||
|
body = expr[i+2:j]
|
||||||
|
val = _resolve_braces_value(body, context, out_map)
|
||||||
|
name = add_binding(val)
|
||||||
|
tokens.append(name)
|
||||||
|
i = j + 2
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ключевое слово contains
|
||||||
|
if expr[i:i+8].lower() == "contains":
|
||||||
|
tokens.append("contains")
|
||||||
|
i += 8
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Число
|
||||||
|
if ch.isdigit():
|
||||||
|
j = i + 1
|
||||||
|
dot_seen = False
|
||||||
|
while j < n and (expr[j].isdigit() or (expr[j] == "." and not dot_seen)):
|
||||||
|
if expr[j] == ".":
|
||||||
|
dot_seen = True
|
||||||
|
j += 1
|
||||||
|
tokens.append(expr[i:j])
|
||||||
|
i = j
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Идентификатор (на всякий — пропускаем последовательность букв/подчёрк/цифр)
|
||||||
|
if ch.isalpha() or ch == "_":
|
||||||
|
j = i + 1
|
||||||
|
while j < n and (expr[j].isalnum() or expr[j] in "._"):
|
||||||
|
j += 1
|
||||||
|
word = expr[i:j]
|
||||||
|
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
|
||||||
|
|
||||||
|
# Иное — ошибка
|
||||||
|
raise ValueError(f"Unexpected character in expression: {ch!r}")
|
||||||
|
|
||||||
|
return tokens, bindings
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_square_macro_value(body: str, context: Dict[str, Any], out_map: Dict[str, Any]) -> Any:
|
||||||
|
# Тело без [[...]]
|
||||||
|
b = str(body or "").strip()
|
||||||
|
# [[OUT1]]
|
||||||
|
m = re.fullmatch(r"(?is)OUT\s*(\d+)", b)
|
||||||
|
if m:
|
||||||
|
try:
|
||||||
|
num = int(m.group(1))
|
||||||
|
node_id = f"n{num}"
|
||||||
|
node_out = out_map.get(node_id)
|
||||||
|
return _best_text_from_outputs(node_out)
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# [[VAR: ...]]
|
||||||
|
m = re.fullmatch(r"(?is)VAR\s*[:]\s*(.+)", b)
|
||||||
|
if m:
|
||||||
|
path = m.group(1).strip()
|
||||||
|
return _get_by_path(context, path)
|
||||||
|
|
||||||
|
# [[OUT: node.path]]
|
||||||
|
m = re.fullmatch(r"(?is)OUT\s*[:]\s*(.+)", b)
|
||||||
|
if m:
|
||||||
|
body2 = m.group(1).strip()
|
||||||
|
if "." in body2:
|
||||||
|
node_id, rest = body2.split(".", 1)
|
||||||
|
node_val = out_map.get(node_id.strip())
|
||||||
|
return _get_by_path(node_val, rest.strip())
|
||||||
|
return out_map.get(body2)
|
||||||
|
|
||||||
|
# [[STORE: path]]
|
||||||
|
m = re.fullmatch(r"(?is)STORE\s*[:]\s*(.+)", b)
|
||||||
|
if m:
|
||||||
|
path = m.group(1).strip()
|
||||||
|
store = context.get("store") or {}
|
||||||
|
return _get_by_path(store, path)
|
||||||
|
|
||||||
|
# [[NAME]] — «голая» переменная: сначала vars, потом context по пути/ключу
|
||||||
|
name = b
|
||||||
|
vmap = context.get("vars") or {}
|
||||||
|
if isinstance(vmap, dict) and (name in vmap):
|
||||||
|
return vmap.get(name)
|
||||||
|
return _get_by_path(context, name)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_braces_value(body: str, context: Dict[str, Any], out_map: Dict[str, Any]) -> Any:
|
||||||
|
# Логика совместима с {{ path|default(value) }}, возврат — типобезопасный
|
||||||
|
expr = str(body or "").strip()
|
||||||
|
|
||||||
|
def eval_path(p: str) -> Any:
|
||||||
|
p = p.strip()
|
||||||
|
vmap = context.get("vars") or {}
|
||||||
|
# Простой идентификатор — сначала в vars
|
||||||
|
if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", p) and isinstance(vmap, dict) and p in vmap:
|
||||||
|
return vmap.get(p)
|
||||||
|
if p.startswith("OUT."):
|
||||||
|
body2 = p[4:].strip()
|
||||||
|
if "." in body2:
|
||||||
|
node_id, rest = body2.split(".", 1)
|
||||||
|
node_val = out_map.get(node_id.strip())
|
||||||
|
return _get_by_path(node_val, rest.strip())
|
||||||
|
return out_map.get(body2)
|
||||||
|
return _get_by_path(context, p)
|
||||||
|
|
||||||
|
m = re.match(r"([^|]+)\|\s*default\((.*)\)\s*$", expr)
|
||||||
|
if m:
|
||||||
|
base_path = m.group(1).strip()
|
||||||
|
fallback_raw = m.group(2).strip()
|
||||||
|
|
||||||
|
def eval_default(raw: str) -> Any:
|
||||||
|
raw = raw.strip()
|
||||||
|
dm = re.match(r"([^|]+)\|\s*default\((.*)\)\s*$", raw)
|
||||||
|
if dm:
|
||||||
|
base2 = dm.group(1).strip()
|
||||||
|
fb2 = dm.group(2).strip()
|
||||||
|
v2 = eval_path(base2)
|
||||||
|
if v2 not in (None, ""):
|
||||||
|
return v2
|
||||||
|
return eval_default(fb2)
|
||||||
|
# Пробуем как путь
|
||||||
|
v = eval_path(raw)
|
||||||
|
if v not in (None, ""):
|
||||||
|
return v
|
||||||
|
# Строка в кавычках
|
||||||
|
if len(raw) >= 2 and ((raw[0] == '"' and raw[-1] == '"') or (raw[0] == "'" and raw[-1] == "'")):
|
||||||
|
return raw[1:-1]
|
||||||
|
# JSON литерал
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except Exception:
|
||||||
|
return raw
|
||||||
|
|
||||||
|
raw_val = eval_path(base_path)
|
||||||
|
return raw_val if raw_val not in (None, "") else eval_default(fallback_raw)
|
||||||
|
else:
|
||||||
|
return eval_path(expr)
|
||||||
|
|
||||||
|
|
||||||
|
def _stringify_for_contains(val: Any) -> str:
|
||||||
|
# Для contains на строках — совместим со строкификацией шаблона
|
||||||
|
return _stringify_for_template(val)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_eval_bool(py_expr: str, bindings: Dict[str, Any]) -> bool:
|
||||||
|
import ast
|
||||||
|
import operator as op
|
||||||
|
|
||||||
|
def contains_fn(a: Any, b: Any) -> bool:
|
||||||
|
# Семантика: список/кортеж/множество — membership, иначе — подстрока по строковому представлению
|
||||||
|
if isinstance(a, (list, tuple, set)):
|
||||||
|
return b in a
|
||||||
|
sa = _stringify_for_contains(a)
|
||||||
|
sb = _stringify_for_contains(b)
|
||||||
|
return sb in sa
|
||||||
|
|
||||||
|
allowed_boolops = {ast.And, ast.Or}
|
||||||
|
allowed_unary = {ast.Not}
|
||||||
|
allowed_cmp = {ast.Eq: op.eq, ast.NotEq: op.ne, ast.Lt: op.lt, ast.LtE: op.le, ast.Gt: op.gt, ast.GtE: op.ge}
|
||||||
|
|
||||||
|
def eval_node(node: ast.AST) -> Any:
|
||||||
|
if isinstance(node, ast.Expression):
|
||||||
|
return eval_node(node.body)
|
||||||
|
if isinstance(node, ast.Constant):
|
||||||
|
return node.value
|
||||||
|
if isinstance(node, ast.Name):
|
||||||
|
if node.id == "contains":
|
||||||
|
# Возврат специальной метки, реально обрабатывается в Call
|
||||||
|
return ("__fn__", "contains")
|
||||||
|
if node.id in bindings:
|
||||||
|
return bindings[node.id]
|
||||||
|
# Неизвестные имена запрещены
|
||||||
|
raise ValueError(f"Unknown name: {node.id}")
|
||||||
|
if isinstance(node, ast.UnaryOp) and isinstance(node.op, tuple(allowed_unary)):
|
||||||
|
val = bool(eval_node(node.operand))
|
||||||
|
if isinstance(node.op, ast.Not):
|
||||||
|
return (not val)
|
||||||
|
if isinstance(node, ast.BoolOp) and isinstance(node.op, tuple(allowed_boolops)):
|
||||||
|
# Короткое замыкание:
|
||||||
|
# AND — при первом False прекращаем и возвращаем False; иначе True
|
||||||
|
# OR — при первом True прекращаем и возвращаем True; иначе False
|
||||||
|
if isinstance(node.op, ast.And):
|
||||||
|
for v in node.values:
|
||||||
|
if not bool(eval_node(v)):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
if isinstance(node.op, ast.Or):
|
||||||
|
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):
|
||||||
|
if type(opnode) not in allowed_cmp:
|
||||||
|
raise ValueError("Unsupported comparison operator")
|
||||||
|
right = eval_node(comparator)
|
||||||
|
if not allowed_cmp[type(opnode)](left, right):
|
||||||
|
return False
|
||||||
|
left = right
|
||||||
|
return True
|
||||||
|
if isinstance(node, ast.Call):
|
||||||
|
# Разрешаем только contains(a,b)
|
||||||
|
if node.keywords or len(node.args) != 2:
|
||||||
|
raise ValueError("Only contains(a,b) call is allowed")
|
||||||
|
fn = node.func
|
||||||
|
# Форма contains(...) может прийти как Name('contains') или как ("__fn__","contains")
|
||||||
|
if isinstance(fn, ast.Name) and fn.id == "contains":
|
||||||
|
a = eval_node(node.args[0])
|
||||||
|
b = eval_node(node.args[1])
|
||||||
|
return contains_fn(a, b)
|
||||||
|
# Дополнительно: если парсер вернул константу-маркер
|
||||||
|
if isinstance(fn, ast.Constant) and fn.value == ("__fn__", "contains"):
|
||||||
|
a = eval_node(node.args[0])
|
||||||
|
b = eval_node(node.args[1])
|
||||||
|
return contains_fn(a, b)
|
||||||
|
raise ValueError("Function calls are not allowed")
|
||||||
|
# Запрещаем имена, атрибуты, индексации и прочее
|
||||||
|
raise ValueError("Expression construct not allowed")
|
||||||
|
|
||||||
|
try:
|
||||||
|
tree = ast.parse(py_expr, mode="eval")
|
||||||
|
except Exception as exc:
|
||||||
|
raise ValueError(f"Condition parse error: {exc}") from exc
|
||||||
|
return bool(eval_node(tree))
|
||||||
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from typing import Optional, Dict
|
from typing import Optional, Dict, Union
|
||||||
from agentui.config import build_httpx_proxies
|
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:
|
def build_client(timeout: float = 60.0) -> httpx.AsyncClient:
|
||||||
proxies: Optional[Dict[str, str]] = build_httpx_proxies()
|
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]
|
# 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
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,328 +1,665 @@
|
|||||||
# Переменные и макросы НадTavern
|
|
||||||
|
|
||||||
Краткая, человеко‑понятная шпаргалка по тому, какие переменные и макросы доступны в шаблонах (в том числе в Prompt Blocks), как они устроены и как их правильно использовать. Док ниже соответствует текущему коду.
|
НАДTAVERN VARIABLES — ГАЙД ДЛЯ ТЕХ, КТО СЕГОДНЯ «НА МИНИМАЛКАХ» И ВСЁ РАВНО ХОЧЕТ, ЧТОБЫ РАБОТАЛО
|
||||||
|
|
||||||
Реализация формирует единый «контекст» переменных для всех нод пайплайна, дополняет его выходами уже выполненных нод, а узел ProviderCall добавляет свои служебные структуры для удобной сборки промпта.
|
Смотри сюда, слабак. Я — твой наидобрейший цун-энциклопедист, и сейчас я очень терпеливо (фрр) объясню так, чтобы даже ты не накосячил. Прочитаешь до конца — и у тебя получится. Может быть. Если постараешься. М-м… не думай, что я делаю это ради тебя!
|
||||||
|
|
||||||
Ссылки на код:
|
- Источники истины (это значит «код, который реально решает», а не чаты):
|
||||||
- Формирование контекста запроса: [build_macro_context()](agentui/api/server.py:142)
|
- Исполнение пайплайна: [PipelineExecutor.run()](agentui/pipeline/executor.py:402)
|
||||||
- Исполнитель пайплайна и снапшот OUT: [PipelineExecutor.run()](agentui/pipeline/executor.py:136)
|
- Нода SetVars — выражения и функции: [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:1290)
|
||||||
- Узел провайдера (Prompt Blocks → provider payload): [ProviderCallNode.run()](agentui/pipeline/executor.py:650)
|
- Нода ProviderCall — вызов провайдера и PROMPT: [ProviderCallNode.run()](agentui/pipeline/executor.py:2084)
|
||||||
- Шаблоны/макросы ([[...]] и {{ ... }}): [render_template_simple()](agentui/pipeline/templating.py:187)
|
- Нода RawForward — прямой прокси: [RawForwardNode.run()](agentui/pipeline/executor.py:3547)
|
||||||
- Короткая форма [[OUTx]] (извлечение текста): [_best_text_from_outputs()](agentui/pipeline/templating.py:124)
|
- Нода Return — формат финального ответа: [ReturnNode.run()](agentui/pipeline/executor.py:3930)
|
||||||
- Прямой форвард запросов: [RawForwardNode.run()](agentui/pipeline/executor.py:833)
|
- Нода 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 — строка с именем модели.
|
1) SetVars (заводит твои переменные)
|
||||||
Пример: "gpt-4o-mini"
|
- Входы: нет (только depends).
|
||||||
- vendor_format — вендор/протокол запроса: "openai" | "gemini" | "claude" | "unknown"
|
- Выходы: vars — словарь новых переменных.
|
||||||
- system — «системный» текст, если он был во входящем запросе; иначе пустая строка.
|
- Поведение: для каждой переменной задаёшь name и mode (string или expr). В режиме string значение обрабатывается шаблоном ([[...]] и {{ ... }}), в режиме expr — безопасным мини-диалектом выражений.
|
||||||
|
- Где смотреть реализацию: [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:1197).
|
||||||
|
|
||||||
- params — стандартные параметры генерации (можно использовать как дефолты)
|
2) If (ветвление по условию)
|
||||||
- params.temperature — число с плавающей точкой (по умолчанию 0.7)
|
- Входы: depends.
|
||||||
- params.max_tokens — целое или null
|
- Выходы: true, false (гейты для «детей» по условию).
|
||||||
- params.top_p — число (по умолчанию 1.0)
|
- Поведение: expr парсится как булево выражение (&&, ||, !, ==, !=, <, <=, >, >=, contains, скобочки). Внутри можно использовать [[...]] и {{ ... }}.
|
||||||
- params.stop — массив строк или null
|
- Реализация парсера: [eval_condition_expr()](agentui/pipeline/templating.py:391), обёртка ноды: [IfNode.run()](agentui/pipeline/executor.py:3538).
|
||||||
|
|
||||||
- chat — сведения о чате во входящем запросе
|
3) ProviderCall (отправка к провайдеру OpenAI/Gemini/Claude)
|
||||||
- chat.last_user — последнее сообщение пользователя (строка)
|
- Входы: depends.
|
||||||
- chat.messages — массив сообщений в унифицированной форме:
|
- Выходы: result (сырой JSON ответа), response_text (извлечённый «текст»).
|
||||||
- role — "system" | "user" | "assistant" | "tool"
|
- Ключи: provider, provider_configs (base_url, endpoint, headers, template), blocks (Prompt Blocks), prompt_combine (DSL &), while_expr/while_max_iters/ignore_errors, text_extract_*.
|
||||||
- content — содержимое (обычно строка)
|
- Реализация: [ProviderCallNode.run()](agentui/pipeline/executor.py:1991).
|
||||||
- name — опционально, строка
|
|
||||||
- tool_call_id — опционально
|
|
||||||
|
|
||||||
- incoming — детали ВХОДЯЩЕГО HTTP‑запроса
|
4) RawForward (прямой прокси)
|
||||||
- incoming.method — метод ("POST" и т.п.)
|
- Входы: depends.
|
||||||
- incoming.url — полный URL (в query ключи маскируются для логов)
|
- Выходы: result, response_text.
|
||||||
- incoming.path — путь (например, /v1/chat/completions)
|
- Ключи: base_url (может автоопределяться по входящему JSON-вендору), override_path, passthrough_headers, extra_headers, while_expr.
|
||||||
- incoming.query — строка query без вопросительного знака
|
- Реализация: [RawForwardNode.run()](agentui/pipeline/executor.py:3105).
|
||||||
- incoming.query_params — объект со всеми query‑параметрами
|
|
||||||
- incoming.headers — объект всех заголовков запроса
|
|
||||||
- incoming.json — сырой JSON тела запроса, как прислал клиент
|
|
||||||
- incoming.api_keys — удобные «срезы» ключей
|
|
||||||
- incoming.api_keys.authorization — значение из заголовка Authorization (если есть)
|
|
||||||
- incoming.api_keys.key — значение из query (?key=...) — удобно для Gemini
|
|
||||||
|
|
||||||
Пример использования в шаблоне:
|
5) Return (оформление финального ответа для клиента)
|
||||||
- [[VAR:incoming.api_keys.key]] — возьмёт ключ из строки запроса (?key=...).
|
- Входы: depends.
|
||||||
- [[VAR:incoming.headers.x-api-key]] — возьмёт ключ из заголовка x-api-key (типично для Anthropic).
|
- Выходы: result (в формате openai/gemini/claude/auto), response_text (то, что вставили).
|
||||||
- {{ params.temperature|default(0.7) }} — безопасно подставит число, если не задано во входящих данных.
|
- Ключи: 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(...).
|
||||||
|
|
||||||
Формы доступа:
|
Доступные макросы (см. [render_template_simple()](agentui/pipeline/templating.py:205)):
|
||||||
- Полная форма: [[OUT:n1.result.choices.0.message.content]]
|
- [[VAR:путь]] — берёт значение по пути из контекста (context/incoming/params/...).
|
||||||
(или фигурными скобками: {{ OUT.n1.result.choices.0.message.content }})
|
- [[OUT:nodeId(.path)*]] — берёт из выходов ноды (сырой JSON).
|
||||||
- Короткая форма «просто текст»: [[OUT1]], [[OUT2]], ...
|
- [[OUTx]] — короткая форма текста из ноды nX (best-effort).
|
||||||
Это эвристика: берётся самое вероятное «текстовое» поле из результата (см. [_best_text_from_outputs()](agentui/pipeline/templating.py:121)).
|
- [[STORE:путь]] — читает из стойкого хранилища (store.*).
|
||||||
|
- [[NAME]] — «голая» переменная: сперва ищется в пользовательских переменных (SetVars), иначе в контексте по пути.
|
||||||
|
- [[PROMPT]] — провайдерный JSON-фрагмент сообщений (см. Раздел 6).
|
||||||
|
- Доп. сахар: img(mime)[[...]] → «data:mime;base64,ЗНАЧЕНИЕ» (см. [templating._IMG_WRAPPER_RE](agentui/pipeline/templating.py:41)).
|
||||||
|
|
||||||
Что возвращают встроенные ноды:
|
Фигурные {{ ... }}:
|
||||||
- ProviderCall:
|
- {{ OUT.n2.result.choices.0.message.content }} — доступ к JSON как к полям.
|
||||||
- OUT.nX.result — сырой JSON ответа провайдера
|
- {{ путь|default(значение) }} — цепочки дефолтов, поддерживает вложенность и JSON-литералы в default(...).
|
||||||
- OUT.nX.response_text — уже извлечённый «лучший текст» (строка)
|
|
||||||
- RawForward:
|
|
||||||
- OUT.nX.result — JSON, как пришёл от апстрима (или {"error": "...", "text": "..."} при не‑JSON ответе)
|
|
||||||
|
|
||||||
Подсказка по короткой форме [[OUTx]]:
|
12 примеров (пониже пояса — для тех, кто любит копипасту):
|
||||||
- OpenAI: вернёт choices[0].message.content
|
1) Заголовок авторизации в JSON-строке:
|
||||||
- Gemini: вернёт candidates[0].content.parts[0].text
|
{"Authorization":"Bearer [[VAR:incoming.headers.authorization]]"}
|
||||||
- Claude: склеит content[].text
|
Объяснение: [[VAR:...]] берёт заголовок из входа (incoming.headers.authorization).
|
||||||
- Если явных полей нет — выполнит «глубокий поиск» по ключам "text"/"content"
|
|
||||||
|
|
||||||
---
|
2) Провайдерная модель «как пришла» (фигурные):
|
||||||
|
"model": "{{ model }}"
|
||||||
|
Объяснение: {{ ... }} вставляет строку без кавычек лишний раз.
|
||||||
|
|
||||||
## 3) Макросы подстановки и синтаксис
|
3) Число по умолчанию:
|
||||||
|
"temperature": {{ incoming.json.temperature|default(0.7) }}
|
||||||
|
Объяснение: default(0.7) сработает, если температуры нет.
|
||||||
|
|
||||||
В шаблонах доступны обе формы подстановки:
|
4) Лист по умолчанию:
|
||||||
|
"stop": {{ incoming.json.stop|default([]) }}
|
||||||
|
Объяснение: вставляет [] как настоящий массив.
|
||||||
|
|
||||||
1) Квадратные скобки [[ ... ]] — простая подстановка
|
5) Короткая вытяжка текста из ноды n2:
|
||||||
- [[VAR:путь]] — взять значение из контекста по точечному пути
|
"note": "[[OUT2]]"
|
||||||
Пример: [[VAR:incoming.json.max_tokens]]
|
Объяснение: [[OUT2]] — best-effort текст из ответа.
|
||||||
- [[OUT:путь]] — взять значение из OUT (см. раздел выше)
|
|
||||||
Пример: [[OUT:n1.result.choices.0.message.content]]
|
|
||||||
- [[OUT1]] / [[OUT2]] — короткая форма «просто текст»
|
|
||||||
- [[PROMPT]] — специальный JSON‑фрагмент из Prompt Blocks (см. ниже)
|
|
||||||
|
|
||||||
2) Фигурные скобки {{ ... }} — «джинджа‑лайт»
|
6) Точное поле из результата:
|
||||||
- {{ путь }} — взять значение по пути из контекста (или из OUT.* если начать с OUT.)
|
"[[OUT:n2.result.choices.0.message.content]]"
|
||||||
Пример: {{ OUT.n1.result }}
|
Объяснение: берёт конкретную ветку JSON из OUT ноды n2.
|
||||||
- Фильтр по умолчанию: {{ что-то|default(значение) }}
|
|
||||||
Примеры:
|
|
||||||
- {{ params.temperature|default(0.7) }}
|
|
||||||
- {{ incoming.json.stop|default([]) }}
|
|
||||||
- {{ anthropic_version|default('2023-06-01') }} — см. «Опциональные поля» ниже
|
|
||||||
- Фигурные скобки удобны там, где нужно вставить внутрь JSON не строку, а ЧИСЛО/ОБЪЕКТ/МАССИВ без кавычек и/или задать дефолт.
|
|
||||||
|
|
||||||
---
|
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).
|
||||||
|
|
||||||
Внутри шаблонов этого узла доступны:
|
10) JSON-путь с фигурными:
|
||||||
- pm — «сырьевые» структуры из Prompt Blocks
|
{{ OUT.n1.result.obj.value|default(0) }}
|
||||||
- Для OpenAI:
|
Объяснение: берёт число или 0.
|
||||||
- pm.messages — массив { role, content, name? }
|
|
||||||
- pm.system_text — один большой текст из всех system‑блоков
|
|
||||||
- Для Gemini:
|
|
||||||
- pm.contents — массив { role: "user"|"model", parts: [{text}] }
|
|
||||||
- pm.systemInstruction — объект вида { parts: [{text}] } или пустой {}
|
|
||||||
- pm.system_text — строка
|
|
||||||
- Для Claude:
|
|
||||||
- pm.system_text — строка
|
|
||||||
- pm.system — то же самое (удобно подставлять в поле "system")
|
|
||||||
- pm.messages — массив { role: "user"|"assistant", content: [{type:"text", text:"..."}] }
|
|
||||||
|
|
||||||
- [[PROMPT]] — готовый JSON‑фрагмент на основе pm, безопасный для вставки внутрь шаблона:
|
11) Картинка из base64 переменной (img()):
|
||||||
- OpenAI → подставит: "messages": [...]
|
"image": "[[IMG_B64]])"
|
||||||
- Gemini → подставит: "contents": [...], "systemInstruction": {...}
|
Объяснение: заменится на data:image/jpeg;base64,....
|
||||||
- Claude → подставит: "system": "...", "messages": [...]
|
|
||||||
|
|
||||||
Зачем это нужно?
|
12) Сложная строка с несколькими макросами:
|
||||||
- Чтобы 1) удобно собирать промпт из визуальных блоков, 2) не «сломать» JSON руками.
|
"msg": "User=[[VAR:chat.last_user]] | Echo=[[OUT1]]"
|
||||||
Вы можете вручную использовать {{ pm.* }}, но [[PROMPT]] — рекомендуемый и самый безопасный вариант.
|
Объяснение: комбинируй сколько хочешь, лишь бы JSON остался валидным.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5) Частые сценарии и примеры
|
РАЗДЕЛ 3 — SETVARS: ВЫРАЖЕНИЯ, РАЗРЕШЁННЫЕ ФУНКЦИИ, ОПАСНО НЕ БУДЕМ (10+ ПРИМЕРОВ)
|
||||||
|
|
||||||
Примеры ниже можно вклеивать в поле «template» ноды ProviderCall. Они уже используют [[PROMPT]] и аккуратные дефолты.
|
Где код: [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:1197). Он парсит мини-язык через AST, ничего небезопасного не позволит.
|
||||||
|
|
||||||
OpenAI (POST /v1/chat/completions):
|
Разрешено:
|
||||||
```
|
- Литералы: числа, строки, true/false/null (JSON-стиль), списки [...], объекты {...}.
|
||||||
{
|
- Операции: + - * / // % и унарные + -, сравнения == != < <= > >=, логика and/or.
|
||||||
"model": "{{ model }}",
|
- Вызовы ТОЛЬКО упомянутых функций (без kwargs, без *args):
|
||||||
[[PROMPT]],
|
- rand() → float [0,1)
|
||||||
"temperature": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},
|
- randint(a,b) → int в [a,b]
|
||||||
"top_p": {{ incoming.json.top_p|default(params.top_p|default(1)) }},
|
- choice(list) → элемент списка/кортежа
|
||||||
"max_tokens": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},
|
- from_json(x) → распарсить строку JSON
|
||||||
"stop": {{ incoming.json.stop|default(params.stop|default([])) }}
|
- 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 }}",
|
- kwargs/starargs.
|
||||||
[[PROMPT]],
|
|
||||||
"safetySettings": {{ incoming.json.safetySettings|default([]) }},
|
|
||||||
"generationConfig": {
|
|
||||||
"temperature": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},
|
|
||||||
"topP": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},
|
|
||||||
"maxOutputTokens": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},
|
|
||||||
"stopSequences": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Подсказка: ключ Gemini удобно брать из строки запроса:
|
|
||||||
в endpoint используйте …?key=[[VAR:incoming.api_keys.key]]
|
|
||||||
|
|
||||||
Claude (POST /v1/messages):
|
10+ примеров SetVars (mode=expr):
|
||||||
```
|
1) Чистая математика:
|
||||||
{
|
128 + 64
|
||||||
"model": "{{ model }}",
|
|
||||||
[[PROMPT]],
|
|
||||||
"temperature": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},
|
|
||||||
"top_p": {{ incoming.json.top_p|default(params.top_p|default(1)) }},
|
|
||||||
"max_tokens": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},
|
|
||||||
"system": {{ pm.system|default("") }}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Подсказка: ключ Anthropic обычно передают в заголовке x-api-key.
|
|
||||||
В UI‑пресете это поле уже есть в headers.
|
|
||||||
|
|
||||||
RawForward (прямой форвард входящего запроса):
|
2) Случайное число:
|
||||||
- Поля конфигурации base_url, override_path, extra_headers проходят через те же макросы, поэтому можно подставлять динамику:
|
rand()
|
||||||
- base_url: https://generativelanguage.googleapis.com
|
|
||||||
- override_path: [[VAR:incoming.path]] (или задать свой)
|
|
||||||
- extra_headers (JSON): `{"X-Trace":"req-{{ incoming.query_params.session|default('no-session') }}"}`
|
|
||||||
|
|
||||||
---
|
3) Случайное из списка:
|
||||||
|
choice(["red","green","blue"])
|
||||||
|
|
||||||
## 6) Опциональные/редкие поля, о которых стоит знать
|
4) Безопасный int-диапазон:
|
||||||
|
randint(10, 20)
|
||||||
|
|
||||||
- anthropic_version — используется как HTTP‑заголовок для Claude ("anthropic-version"). В тело запроса не вставляется.
|
5) from_json + доступ через jp:
|
||||||
Если нужен дефолт, задавайте его в headers (например, в конфиге ноды/шаблоне заголовков). В шаблонах тела используйте [[PROMPT]]/pm.* без anthropic_version.
|
jp(from_json("{\"a\":{\"b\":[{\"x\":1},{\"x\":2}]}}"), "a.b.1.x") → 2
|
||||||
|
|
||||||
- stream — в MVP стриминг отключён, сервер принудительно не стримит ответ.
|
6) jp_text (склейка строк через « | »):
|
||||||
В шаблонах можно встретить поля stream, но по умолчанию они не включены.
|
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 как ОБЪЕКТ/МАССИВ/ЧИСЛО: используйте {{ ... }}
|
9) Ручная сборка data URL из base64:
|
||||||
(фигурные скобки вставляют «как есть», без кавычек, и умеют |default(...))
|
data_url([[IMG_B64]], "image/jpeg")
|
||||||
- Для строк/URL/заголовков/простых значений: можно использовать [[...]]
|
|
||||||
(квадратные скобки удобны и короче писать)
|
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 }} — вставит массив как настоящий массив (без кавычек)
|
1) Удалить поля, где значение содержит «Текст», и вставить перед последним:
|
||||||
- {{ params.temperature|default(0.7) }} — безопасный дефолт для числа
|
[[VAR:incoming.json.contents]] delKeyContains "Текст" delpos=-1
|
||||||
- [[VAR:incoming.api_keys.authorization]] — быстро подставить строку Authorization
|
|
||||||
|
|
||||||
---
|
2) Удалить «debug» с учётом регистра и подчистить пустые контейнеры:
|
||||||
|
[[VAR:incoming.json.messages]] delKeyContains "debug" case=cs pruneEmpty
|
||||||
|
|
||||||
## 8) Отладка и рекомендации
|
3) Несколько подстрок + вставка в начало:
|
||||||
|
[[VAR:incoming.json]] delKeyContains "кеш" delKeyContains "cache" delpos=prepend
|
||||||
|
|
||||||
- ProviderCall печатает в консоль DEBUG сведения: выбранный провайдер, конечный URL, первые символы тела запроса — удобно для проверки корректности шаблона.
|
4) Смешанный пайплайн: сначала пред‑сегменты, затем:
|
||||||
- Если «ничего не подставилось»:
|
prompt_combine: "[[VAR:incoming.json.messages]] & [[PROMPT]]@pos=1"
|
||||||
1) Проверьте, что вы НЕ передаёте сырое входное тело напрямую в ProviderCall (узел строит тело из шаблона и Prompt Blocks).
|
|
||||||
2) Убедитесь, что итоговый JSON валиден (закрывающие скобки, запятые).
|
|
||||||
3) Проверьте точность путей в макросах (OUT vs OUTx, правильные id нод n1/n2/...).
|
|
||||||
- Для ссылок на выходы предыдущих нод используйте [[OUT1]] как «просто текст», либо полные пути [[OUT:n1...]] для точного фрагмента.
|
|
||||||
|
|
||||||
---
|
Диагностика:
|
||||||
|
- В логи (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=...).
|
Где реализовано: [_json_path_extract()](agentui/pipeline/executor.py:1475).
|
||||||
- OpenAI: [[VAR:incoming.headers.authorization]] (или [[VAR:incoming.api_keys.authorization]]) — стандартный Bearer‑токен.
|
|
||||||
- Anthropic: [[VAR:incoming.headers.x-api-key]] — ключ в заголовке.
|
|
||||||
|
|
||||||
---
|
Синтаксис (очень простой):
|
||||||
|
- Путь вида 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)
|
2) Индекс массива:
|
||||||
- Исполнение пайплайна, зависимости, снапшоты OUT: [PipelineExecutor.run()](agentui/pipeline/executor.py:136)
|
"items.1" на {"items":[10,20,30]} → 20
|
||||||
- Узел провайдера (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)
|
|
||||||
|
|
||||||
Удачного редактирования!
|
3) Вложено:
|
||||||
---
|
"items.1.title" на items=[{title:"A"},{title:"B"}] → "B"
|
||||||
## Пользовательские переменные (SetVars) — «для людей»
|
|
||||||
|
|
||||||
Задача: в начале пайплайна положить свои значения и потом использовать их в шаблонах одной строкой — например [[MY_KEY]] или {{ MAX_TOKENS }}.
|
4) Звёздочка по массиву:
|
||||||
|
"items.*.title" на items=[{title:"A"},{title:"B"}] → ["A","B"]
|
||||||
|
|
||||||
Где это в UI
|
5) Звёздочка по объекту:
|
||||||
- В левой панели добавьте ноду SetVars и откройте её в инспекторе.
|
"*.*.name" на {"x":{"name":"X"}, "y":{"name":"Y"}} → ["X","Y"]
|
||||||
- Жмите «Добавить переменную», у каждой переменной есть три поля:
|
|
||||||
- name — имя переменной (латинские буквы/цифры/подчёркивание, не с цифры): MY_KEY, REGION, MAX_TOKENS
|
|
||||||
- mode — режим обработки значения:
|
|
||||||
- string — строка, в которой работают макросы ([[...]] и {{ ... }})
|
|
||||||
- expr — «мини‑формула» без макросов (подробнее ниже)
|
|
||||||
- value — собственно значение
|
|
||||||
|
|
||||||
Как потом вставлять переменные
|
6) Смешанный:
|
||||||
- Для строк (URL/заголовки/текст) — квадратные скобки: [[MY_KEY]]
|
"candidates.0.content.parts.*.text" (Gemini) → все тексты
|
||||||
- Для чисел/массивов/объектов — фигурные скобки: {{ MAX_TOKENS }}, {{ GEN_CFG }}
|
|
||||||
|
|
||||||
Примеры «как надо»
|
7) Несуществующее поле:
|
||||||
- Переменная-строка (mode=string):
|
"obj.miss" → None
|
||||||
- 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 }}
|
|
||||||
|
|
||||||
Важно про два режима
|
8) Склейка текстов (jp_text):
|
||||||
- string — это «шаблон». Внутри работают все макросы ([[VAR:...]], [[OUT:...]], [[PROMPT]], {{ ... }}). Значение прогоняется через рендер [render_template_simple()](agentui/pipeline/templating.py:184).
|
jp_text(value, "items.*.desc", " | ") → "a | b | c"
|
||||||
- expr — это «мини‑формула». Внутри НЕТ макросов и НЕТ доступа к контексту; только литералы и операции (см. ниже). Вычисляет значение безопасно — без eval, на белом списке AST (реализация: [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:291)).
|
|
||||||
|
|
||||||
Что умеет expr (мини‑формулы)
|
9) Взять base64 из inlineData:
|
||||||
- Числа и арифметика: 128 + 64, (5 * 60) + 30, 42 % 2, -5, 23 // 10
|
"candidates.0.content.parts.1.inlineData.data"
|
||||||
- Строки: "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 и т.п.
|
|
||||||
- Запрещено: функции (кроме специально разрешённых ниже), доступ к переменным/контексту, атрибуты/индексация/условные выражения.
|
|
||||||
|
|
||||||
Рандом в expr
|
10) Несколько уровней массивов:
|
||||||
- В expr доступны три простые функции случайности:
|
"a.*.b.*.c"
|
||||||
- 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 нет — каждый запуск выдаёт новое значение.
|
|
||||||
|
|
||||||
«Почему в expr нельзя подставлять переменные/макросы?»
|
11) Индекс вне границ:
|
||||||
- Для безопасности и предсказуемости: expr — это закрытый мини‑язык без окружения.
|
"items.99" → None
|
||||||
- Если нужно использовать другие переменные/макросы — делайте это в режиме string (там всё рендерится шаблонизатором).
|
|
||||||
- Технические детали: защита реализована в [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:291), а вставка string‑значений — через [render_template_simple()](agentui/pipeline/templating.py:184).
|
|
||||||
|
|
||||||
Как это работает внутри (если интересно)
|
12) Список/объект → строка (jp_text сам постарается найти текст глубже):
|
||||||
- SetVars исполняется как обычная нода пайплайна и отдаёт {"vars": {...}}.
|
jp_text(value, "response", "\n")
|
||||||
- Исполнитель добавляет эти значения в контекст для последующих нод как context.vars (см. [PipelineExecutor.run()](agentui/pipeline/executor.py:131)).
|
|
||||||
- При рендере шаблонов:
|
|
||||||
- [[NAME]] и {{ NAME }} подставляются с приоритетом из пользовательских переменных (см. обработку в [render_template_simple()](agentui/pipeline/templating.py:184)).
|
|
||||||
- Сам SetVars считает переменные в порядке списка и возвращает их одним пакетом (внутри одной ноды значения не зависят друг от друга).
|
|
||||||
|
|
||||||
Частые вопросы
|
|
||||||
- «Хочу собрать строку с частями из внешнего запроса»: делайте 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)).
|
|
||||||
|
|
||||||
Ссылки на реализацию (для любопытных)
|
РАЗДЕЛ 8 — OUTx, ИЗВЛЕЧЕНИЕ ТЕКСТА, ПРЕСЕТЫ, ГЛОБАЛЬНЫЕ ОПЦИИ
|
||||||
- Нода переменных: [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)
|
Откуда [[OUTx]] берёт текст:
|
||||||
- Шаблоны и макросы (включая «голые» [[NAME]]/{{ NAME }}): [render_template_simple()](agentui/pipeline/templating.py:184)
|
- Универсальный алгоритм (см. [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.
|
||||||
|
|
||||||
|
Если ты дошёл досюда — ну… я не впечатлена. Просто запомни и не ломи моя нервная система, ладно? Хмф.
|
||||||
|
|
||||||
|
|||||||
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"}
|
||||||
203
pipeline.json
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
{
|
||||||
|
"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.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": "n5",
|
||||||
|
"type": "SetVars",
|
||||||
|
"pos_x": 300,
|
||||||
|
"pos_y": 720,
|
||||||
|
"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": "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\":\"[[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_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": "n3.done"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n7",
|
||||||
|
"type": "ProviderCall",
|
||||||
|
"pos_x": 1080,
|
||||||
|
"pos_y": 600,
|
||||||
|
"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": "([[OUT7]] contains \"Stream failed to\") || ([[OUT7]] contains \"gemini-2.5-pro\") || [[WAS_ERROR]] == true",
|
||||||
|
"ignore_errors": true,
|
||||||
|
"while_max_iters": 50
|
||||||
|
},
|
||||||
|
"in": {
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
237
presets/test3.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": "pmfipb98aywtx6jepd5",
|
||||||
|
"name": "ввв",
|
||||||
|
"strategy": "jsonpath",
|
||||||
|
"json_path": "ввв",
|
||||||
|
"join_sep": "\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "n1",
|
||||||
|
"type": "RawForward",
|
||||||
|
"pos_x": 450,
|
||||||
|
"pos_y": 352,
|
||||||
|
"config": {
|
||||||
|
"passthrough_headers": true,
|
||||||
|
"extra_headers": "{}",
|
||||||
|
"_origId": "n1",
|
||||||
|
"sleep_ms": 5000
|
||||||
|
},
|
||||||
|
"in": {
|
||||||
|
"depends": "n5.done"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n2",
|
||||||
|
"type": "ProviderCall",
|
||||||
|
"pos_x": 653,
|
||||||
|
"pos_y": 51,
|
||||||
|
"config": {
|
||||||
|
"provider": "claude",
|
||||||
|
"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(6000) }}\n }\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": 658,
|
||||||
|
"pos_y": 564,
|
||||||
|
"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": 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": 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}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "bmfdyczbd",
|
||||||
|
"name": "Объедени [[OUT3]], [[OUT4]] сделай более красиво.",
|
||||||
|
"role": "user",
|
||||||
|
"prompt": "Объедени [ [[OUT3]], [[OUT2]] ] сделай более красиво. напиши слово \"Красиво\" в конце.",
|
||||||
|
"enabled": true,
|
||||||
|
"order": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bmfh98jkh",
|
||||||
|
"name": "New Block1",
|
||||||
|
"role": "system",
|
||||||
|
"prompt": "1",
|
||||||
|
"enabled": true,
|
||||||
|
"order": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"_origId": "n6"
|
||||||
|
},
|
||||||
|
"in": {
|
||||||
|
"depends": [
|
||||||
|
"n2.done",
|
||||||
|
"n3.done",
|
||||||
|
"n7.false"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n7",
|
||||||
|
"type": "If",
|
||||||
|
"pos_x": 1313,
|
||||||
|
"pos_y": 566,
|
||||||
|
"config": {
|
||||||
|
"expr": "[[OUT6]] contains \"Красиво\"",
|
||||||
|
"_origId": "n7"
|
||||||
|
},
|
||||||
|
"in": {
|
||||||
|
"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
|
uvicorn==0.30.6
|
||||||
pydantic==2.8.2
|
pydantic==2.8.2
|
||||||
httpx==0.27.0
|
httpx==0.27.0
|
||||||
starlette==0.38.2
|
starlette==0.40.0
|
||||||
httpx[socks]==0.27.0
|
|
||||||
|
|
||||||
|
|
||||||
|
brotlicffi
|
||||||
|
brotli
|
||||||
@@ -1,27 +1,51 @@
|
|||||||
@echo off
|
@echo off
|
||||||
setlocal
|
setlocal
|
||||||
chcp 65001 >NUL
|
chcp 65001 >NUL
|
||||||
set PORT=7860
|
|
||||||
echo Installing dependencies...
|
REM -------- Config --------
|
||||||
python -m pip install --upgrade pip
|
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
|
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
|
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
|
timeout /t 1 /nobreak >NUL
|
||||||
start "" "http://127.0.0.1:%PORT%/ui/editor.html"
|
start "" "http://%HOST%:%PORT%/ui/editor.html"
|
||||||
python -m uvicorn agentui.api.server:app --host 127.0.0.1 --port %PORT% --log-level info
|
|
||||||
|
"%VENV_PY%" -m uvicorn agentui.api.server:app --host %HOST% --port %PORT% --log-level info
|
||||||
if errorlevel 1 goto :fail
|
if errorlevel 1 goto :fail
|
||||||
goto :end
|
goto :end
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
echo.
|
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.
|
echo Check the console output above and the file agentui.log for details.
|
||||||
pause
|
pause
|
||||||
|
|
||||||
:end
|
:end
|
||||||
pause
|
|
||||||
endlocal
|
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
4319
static/editor.html
@@ -4,6 +4,12 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>НадTavern</title>
|
<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>
|
<style>
|
||||||
body { font-family: Arial, sans-serif; margin: 24px; }
|
body { font-family: Arial, sans-serif; margin: 24px; }
|
||||||
textarea { width: 100%; height: 200px; }
|
textarea { width: 100%; height: 200px; }
|
||||||
|
|||||||
@@ -21,10 +21,15 @@
|
|||||||
// Готовим новые данные с глубокой копией blocks
|
// Готовим новые данные с глубокой копией blocks
|
||||||
const newData = { ...(n.data || {}), blocks: Array.isArray(d2.blocks) ? d2.blocks.map(b => ({ ...b })) : [] };
|
const newData = { ...(n.data || {}), blocks: Array.isArray(d2.blocks) ? d2.blocks.map(b => ({ ...b })) : [] };
|
||||||
// 1) Обновляем внутреннее состояние Drawflow, чтобы export() возвращал актуальные данные
|
// 1) Обновляем внутреннее состояние Drawflow, чтобы export() возвращал актуальные данные
|
||||||
try { editor.updateNodeDataFromId(id, newData); } catch (e) {}
|
try {
|
||||||
// 2) Обновляем DOM-отражение (источник правды для toPipelineJSON)
|
if (w.AU && typeof w.AU.updateNodeDataAndDom === 'function') {
|
||||||
const el2 = document.querySelector(`#node-${id}`);
|
w.AU.updateNodeDataAndDom(editor, id, newData);
|
||||||
if (el2) el2.__data = JSON.parse(JSON.stringify(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) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
// Initial sync to attach blocks into __data for toPipelineJSON
|
// Initial sync to attach blocks into __data for toPipelineJSON
|
||||||
@@ -40,6 +45,17 @@
|
|||||||
const cancelBtn = document.getElementById('pm-cancel');
|
const cancelBtn = document.getElementById('pm-cancel');
|
||||||
let editingId = null;
|
let editingId = null;
|
||||||
|
|
||||||
|
// Безопасное экранирование HTML для вставок в UI
|
||||||
|
function pmEscapeHtml(s) {
|
||||||
|
const str = String(s ?? '');
|
||||||
|
return str
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
// Изменения блока применяются только по кнопке «Сохранить» внутри редактора блока.
|
// Изменения блока применяются только по кнопке «Сохранить» внутри редактора блока.
|
||||||
|
|
||||||
// Drag&Drop через SortableJS (если доступен)
|
// Drag&Drop через SortableJS (если доступен)
|
||||||
@@ -77,11 +93,13 @@
|
|||||||
li.style.alignItems = 'center';
|
li.style.alignItems = 'center';
|
||||||
li.style.gap = '6px';
|
li.style.gap = '6px';
|
||||||
li.style.padding = '4px 0';
|
li.style.padding = '4px 0';
|
||||||
|
const nameDisp = pmEscapeHtml(b.name || ('Block ' + (i + 1)));
|
||||||
|
const roleDisp = pmEscapeHtml(b.role || 'user');
|
||||||
li.innerHTML = `
|
li.innerHTML = `
|
||||||
<span class="pm-handle" style="cursor:grab;">☰</span>
|
<span class="pm-handle" style="cursor:grab;">☰</span>
|
||||||
<input type="checkbox" class="pm-enabled" ${b.enabled !== false ? 'checked' : ''} title="enabled"/>
|
<input type="checkbox" class="pm-enabled" ${b.enabled !== false ? 'checked' : ''} title="enabled"/>
|
||||||
<span class="pm-name" style="flex:1">${(b.name || ('Block ' + (i + 1))).replace(/</g, '<')}</span>
|
<span class="pm-name" style="flex:1">${nameDisp}</span>
|
||||||
<span class="pm-role" style="opacity:.8">${b.role || 'user'}</span>
|
<span class="pm-role" style="opacity:.8">${roleDisp}</span>
|
||||||
<button class="pm-edit" title="Редактировать">✎</button>
|
<button class="pm-edit" title="Редактировать">✎</button>
|
||||||
<button class="pm-del" title="Удалить">🗑</button>
|
<button class="pm-del" title="Удалить">🗑</button>
|
||||||
`;
|
`;
|
||||||
|
|||||||
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);
|
||||||
@@ -10,6 +10,96 @@
|
|||||||
if (!w.NODE_IO) throw new Error('AgentUISer: global NODE_IO is not available');
|
if (!w.NODE_IO) throw new Error('AgentUISer: global NODE_IO is not available');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Top-level pipeline meta kept in memory and included into JSON on save.
|
||||||
|
// Allows UI to edit loop parameters without manual JSON edits.
|
||||||
|
// DRY: единый источник дефолтов и нормализации meta
|
||||||
|
const MetaDefaults = Object.freeze({
|
||||||
|
id: 'pipeline_editor',
|
||||||
|
name: 'Edited Pipeline',
|
||||||
|
parallel_limit: 8,
|
||||||
|
loop_mode: 'dag',
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePipelineMeta(p) {
|
||||||
|
if (!p || typeof p !== 'object') return;
|
||||||
|
// DRY: единая точка нормализации
|
||||||
|
_pipelineMeta = ensureMeta({ ..._pipelineMeta, ...p });
|
||||||
|
}
|
||||||
|
|
||||||
// Drawflow -> pipeline JSON
|
// Drawflow -> pipeline JSON
|
||||||
function toPipelineJSON() {
|
function toPipelineJSON() {
|
||||||
ensureDeps();
|
ensureDeps();
|
||||||
@@ -22,20 +112,77 @@
|
|||||||
|
|
||||||
const dfNodes = (data && data.drawflow && data.drawflow.Home && data.drawflow.Home.data) ? data.drawflow.Home.data : {};
|
const dfNodes = (data && data.drawflow && data.drawflow.Home && data.drawflow.Home.data) ? data.drawflow.Home.data : {};
|
||||||
|
|
||||||
// 1) Собираем ноды
|
// 1) Собираем ноды с устойчивыми id на основе _origId (если валиден/уникален)
|
||||||
let idx = 1;
|
const usedIds = new Set();
|
||||||
|
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) {
|
for (const id in dfNodes) {
|
||||||
const df = dfNodes[id];
|
const df = dfNodes[id];
|
||||||
const genId = `n${idx++}`;
|
|
||||||
idMap[id] = genId;
|
|
||||||
const el = document.querySelector(`#node-${id}`);
|
const el = document.querySelector(`#node-${id}`);
|
||||||
// Берём источник правды из DOM.__data (куда жмём «Сохранить параметры») или из drawflow.data
|
const datacopySrc = mergedNodeData(df, el, id);
|
||||||
const datacopySrc = el && el.__data ? el.__data : (df.data || {});
|
const tmp = typeof w.applyNodeDefaults === 'function'
|
||||||
|
? w.applyNodeDefaults(df.name, JSON.parse(JSON.stringify(datacopySrc)))
|
||||||
|
: (JSON.parse(JSON.stringify(datacopySrc)));
|
||||||
|
let desired = (tmp && typeof tmp._origId === 'string') ? String(tmp._origId).trim() : '';
|
||||||
|
if (isValidNid(desired) && !usedIds.has(desired)) {
|
||||||
|
wantIds[id] = desired;
|
||||||
|
usedIds.add(desired);
|
||||||
|
} else {
|
||||||
|
wantIds[id] = null; // назначим позже
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Поиск ближайшего свободного nX
|
||||||
|
function nextFreeId() {
|
||||||
|
let x = 1;
|
||||||
|
while (usedIds.has('n' + x)) x += 1;
|
||||||
|
return 'n' + x;
|
||||||
|
}
|
||||||
|
// Второй проход: назначаем конфликты/пустые
|
||||||
|
for (const id in dfNodes) {
|
||||||
|
if (!wantIds[id]) {
|
||||||
|
const nid = nextFreeId();
|
||||||
|
wantIds[id] = nid;
|
||||||
|
usedIds.add(nid);
|
||||||
|
}
|
||||||
|
idMap[id] = wantIds[id];
|
||||||
|
}
|
||||||
|
// Финальный проход: формируем массив нод, синхронизируя _origId
|
||||||
|
for (const id in dfNodes) {
|
||||||
|
const df = dfNodes[id];
|
||||||
|
const el = document.querySelector(`#node-${id}`);
|
||||||
|
const datacopySrc = mergedNodeData(df, el, id);
|
||||||
const datacopy = typeof w.applyNodeDefaults === 'function'
|
const datacopy = typeof w.applyNodeDefaults === 'function'
|
||||||
? w.applyNodeDefaults(df.name, JSON.parse(JSON.stringify(datacopySrc)))
|
? w.applyNodeDefaults(df.name, JSON.parse(JSON.stringify(datacopySrc)))
|
||||||
: (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({
|
nodes.push({
|
||||||
id: genId,
|
id: idMap[id],
|
||||||
type: df.name,
|
type: df.name,
|
||||||
pos_x: df.pos_x,
|
pos_x: df.pos_x,
|
||||||
pos_y: df.pos_y,
|
pos_y: df.pos_y,
|
||||||
@@ -43,6 +190,7 @@
|
|||||||
in: {}
|
in: {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
try { console.debug('[AgentUISer.toPipelineJSON] idMap drawflowId->nX', idMap); } catch (e) {}
|
||||||
|
|
||||||
// 2) Восстанавливаем связи по входам (inputs)
|
// 2) Восстанавливаем связи по входам (inputs)
|
||||||
// В Drawflow v0.0.55 inputs/outputs — это объекты вида input_1/output_1
|
// В Drawflow v0.0.55 inputs/outputs — это объекты вида input_1/output_1
|
||||||
@@ -56,15 +204,15 @@
|
|||||||
const inputKey = `input_${i + 1}`;
|
const inputKey = `input_${i + 1}`;
|
||||||
const input = df.inputs && df.inputs[inputKey];
|
const input = df.inputs && df.inputs[inputKey];
|
||||||
if (!input || !Array.isArray(input.connections) || input.connections.length === 0) continue;
|
if (!input || !Array.isArray(input.connections) || input.connections.length === 0) continue;
|
||||||
|
|
||||||
// Собираем все связи этого входа и сохраняем строку либо массив строк (для depends поддерживаем мульти-коннекты)
|
// Собираем все связи этого входа и сохраняем строку либо массив строк (для depends поддерживаем мульти-коннекты)
|
||||||
const refs = [];
|
const refs = [];
|
||||||
for (const conn of (input.connections || [])) {
|
for (const conn of (input.connections || [])) {
|
||||||
if (!conn) continue;
|
if (!conn) continue;
|
||||||
const sourceDfId = String(conn.node);
|
const sourceDfId = String(conn.node);
|
||||||
const outKey = String(conn.output ?? '');
|
const outKey = String(conn.output ?? '');
|
||||||
|
|
||||||
// conn.output может быть "output_1", "1" (строкой), либо числом 1
|
// 1) Попробуем определить индекс выхода из conn.output
|
||||||
let sourceOutIdx = -1;
|
let sourceOutIdx = -1;
|
||||||
let m = outKey.match(/output_(\d+)/);
|
let m = outKey.match(/output_(\d+)/);
|
||||||
if (m) {
|
if (m) {
|
||||||
@@ -74,28 +222,64 @@
|
|||||||
} else if (typeof conn.output === 'number') {
|
} else if (typeof conn.output === 'number') {
|
||||||
sourceOutIdx = conn.output - 1;
|
sourceOutIdx = conn.output - 1;
|
||||||
}
|
}
|
||||||
if (!(sourceOutIdx >= 0)) sourceOutIdx = 0; // safety
|
|
||||||
|
|
||||||
const sourceNode = nodes.find(n => n.id === idMap[sourceDfId]);
|
const sourceNode = nodes.find(n => n.id === idMap[sourceDfId]);
|
||||||
if (!sourceNode) continue;
|
if (!sourceNode) continue;
|
||||||
const sourceIo = NODE_IO[sourceNode.type] || { outputs: [] };
|
const sourceIo = NODE_IO[sourceNode.type] || { outputs: [] };
|
||||||
const sourceOutName = (sourceIo.outputs && sourceIo.outputs[sourceOutIdx] != null)
|
|
||||||
? sourceIo.outputs[sourceOutIdx]
|
// 2) Fallback: если индекс не распознан или вне диапазона — проверим dfNodes[source].outputs
|
||||||
: `out${sourceOutIdx}`;
|
if (!(sourceOutIdx >= 0) || !(Array.isArray(sourceIo.outputs) && sourceIo.outputs[sourceOutIdx] != null)) {
|
||||||
|
try {
|
||||||
|
const srcDf = dfNodes[sourceDfId];
|
||||||
|
const outsObj = (srcDf && srcDf.outputs) ? srcDf.outputs : {};
|
||||||
|
let found = -1;
|
||||||
|
// Текущая целевая drawflow-нода — это id (внешняя переменная цикла по dfNodes)
|
||||||
|
const tgtDfId = id;
|
||||||
|
for (const k of Object.keys(outsObj || {})) {
|
||||||
|
const conns = (outsObj[k] && Array.isArray(outsObj[k].connections)) ? outsObj[k].connections : [];
|
||||||
|
if (conns.some(c => String(c && c.node) === String(tgtDfId))) {
|
||||||
|
const m2 = String(k).match(/output_(\d+)/);
|
||||||
|
if (m2) { found = parseInt(m2[1], 10) - 1; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found >= 0) sourceOutIdx = found;
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Ещё один safety: если до сих пор индекс невалидный — зажмём в границы
|
||||||
|
if (!(sourceOutIdx >= 0)) sourceOutIdx = 0;
|
||||||
|
if (Array.isArray(sourceIo.outputs) && sourceIo.outputs.length > 0) {
|
||||||
|
if (sourceOutIdx >= sourceIo.outputs.length) sourceOutIdx = sourceIo.outputs.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Вычислим каноническое имя выхода по NODE_IO
|
||||||
|
let sourceOutName;
|
||||||
|
if (Array.isArray(sourceIo.outputs) && sourceIo.outputs[sourceOutIdx] != null) {
|
||||||
|
sourceOutName = sourceIo.outputs[sourceOutIdx];
|
||||||
|
} else {
|
||||||
|
// Fallback на технические имена (совместимость со старыми out0/out1)
|
||||||
|
sourceOutName = `out${sourceOutIdx}`;
|
||||||
|
}
|
||||||
|
|
||||||
refs.push(`${sourceNode.id}.${sourceOutName}`);
|
refs.push(`${sourceNode.id}.${sourceOutName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Каноничное имя входа: по NODE_IO, иначе in{0-based}
|
// Каноничное имя входа: по NODE_IO, иначе in{0-based}
|
||||||
const targetInName = (io.inputs && io.inputs[i] != null)
|
const targetInName = (io.inputs && io.inputs[i] != null)
|
||||||
? io.inputs[i]
|
? io.inputs[i]
|
||||||
: `in${i}`;
|
: `in${i}`;
|
||||||
|
|
||||||
if (!targetNode.in) targetNode.in = {};
|
if (!targetNode.in) targetNode.in = {};
|
||||||
targetNode.in[targetInName] = (refs.length <= 1 ? refs[0] : refs);
|
targetNode.in[targetInName] = (refs.length <= 1 ? refs[0] : refs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { id: 'pipeline_editor', name: 'Edited Pipeline', 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
|
// pipeline JSON -> Drawflow
|
||||||
@@ -103,6 +287,25 @@
|
|||||||
ensureDeps();
|
ensureDeps();
|
||||||
const editor = w.editor;
|
const editor = w.editor;
|
||||||
const NODE_IO = w.NODE_IO;
|
const NODE_IO = w.NODE_IO;
|
||||||
|
// Сохраняем метаданные пайплайна для 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();
|
editor.clear();
|
||||||
let x = 100; let y = 120; // Fallback
|
let x = 100; let y = 120; // Fallback
|
||||||
@@ -243,5 +446,7 @@
|
|||||||
w.AgentUISer = {
|
w.AgentUISer = {
|
||||||
toPipelineJSON,
|
toPipelineJSON,
|
||||||
fromPipelineJSON,
|
fromPipelineJSON,
|
||||||
|
getPipelineMeta,
|
||||||
|
updatePipelineMeta,
|
||||||
};
|
};
|
||||||
})(window);
|
})(window);
|
||||||
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 charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>НадTavern — Pipeline Editor (JSON)</title>
|
<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>
|
<style>
|
||||||
body { font-family: Arial, sans-serif; margin: 24px; }
|
body { font-family: Arial, sans-serif; margin: 24px; }
|
||||||
textarea { width: 100%; height: 70vh; }
|
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())
|
||||||
297
tests/test_edge_cases.py
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def scenario_if_single_quotes_ok():
|
||||||
|
print("\n=== SCENARIO 1: If with single quotes ===")
|
||||||
|
p = {
|
||||||
|
"id": "p_if_single_quotes",
|
||||||
|
"name": "If Single Quotes",
|
||||||
|
"loop_mode": "iterative",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "n1",
|
||||||
|
"type": "SetVars",
|
||||||
|
"config": {
|
||||||
|
"variables": [
|
||||||
|
{"id": "v1", "name": "MSG", "mode": "string", "value": "Hello"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"in": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nIf",
|
||||||
|
"type": "If",
|
||||||
|
"config": {
|
||||||
|
"expr": "[[MSG]] contains 'Hello'"
|
||||||
|
},
|
||||||
|
"in": {
|
||||||
|
"depends": "n1.done"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nRet",
|
||||||
|
"type": "Return",
|
||||||
|
"config": {
|
||||||
|
"target_format": "openai",
|
||||||
|
"text_template": "ok"
|
||||||
|
},
|
||||||
|
"in": {
|
||||||
|
"depends": "nIf.true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
exe = PipelineExecutor(p)
|
||||||
|
try:
|
||||||
|
out = await exe.run(_base_ctx())
|
||||||
|
print("OK:", _pp(out))
|
||||||
|
except Exception as e:
|
||||||
|
print("ERR:", type(e).__name__, str(e))
|
||||||
|
|
||||||
|
async def scenario_if_error_logging():
|
||||||
|
print("\n=== SCENARIO 2: If with unterminated string (expect error log) ===")
|
||||||
|
p = {
|
||||||
|
"id": "p_if_bad_string",
|
||||||
|
"name": "If Bad String",
|
||||||
|
"loop_mode": "iterative",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "n1",
|
||||||
|
"type": "SetVars",
|
||||||
|
"config": {
|
||||||
|
"variables": [
|
||||||
|
{"id": "v1", "name": "MSG", "mode": "string", "value": "Hello"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"in": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nIf",
|
||||||
|
"type": "If",
|
||||||
|
"config": {
|
||||||
|
# Missing closing quote to force tokenizer error
|
||||||
|
"expr": "[[MSG]] contains 'Hello"
|
||||||
|
},
|
||||||
|
"in": {
|
||||||
|
"depends": "n1.done"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nRet",
|
||||||
|
"type": "Return",
|
||||||
|
"config": {
|
||||||
|
"target_format": "openai",
|
||||||
|
"text_template": "should_not_run"
|
||||||
|
},
|
||||||
|
"in": {
|
||||||
|
"depends": "nIf.true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
exe = PipelineExecutor(p)
|
||||||
|
try:
|
||||||
|
out = await exe.run(_base_ctx())
|
||||||
|
print("UNEXPECTED_OK:", _pp(out))
|
||||||
|
except Exception as e:
|
||||||
|
print("EXPECTED_ERROR:", type(e).__name__, str(e))
|
||||||
|
|
||||||
|
async def scenario_multi_depends_array():
|
||||||
|
print("\n=== SCENARIO 3: multi-depends array ===")
|
||||||
|
# n2 and n3 both depend on n1; n4 depends on [n2.done, n3.done]
|
||||||
|
p = {
|
||||||
|
"id": "p_multi_depends",
|
||||||
|
"name": "Multi Depends",
|
||||||
|
"loop_mode": "iterative",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "n1",
|
||||||
|
"type": "SetVars",
|
||||||
|
"config": {
|
||||||
|
"variables": [
|
||||||
|
{"id": "v1", "name": "A", "mode": "string", "value": "foo"},
|
||||||
|
{"id": "v2", "name": "B", "mode": "string", "value": "bar"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"in": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n2",
|
||||||
|
"type": "Return",
|
||||||
|
"config": {
|
||||||
|
"target_format": "openai",
|
||||||
|
"text_template": "[[A]]"
|
||||||
|
},
|
||||||
|
"in": {
|
||||||
|
"depends": "n1.done"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n3",
|
||||||
|
"type": "Return",
|
||||||
|
"config": {
|
||||||
|
"target_format": "openai",
|
||||||
|
"text_template": "[[B]]"
|
||||||
|
},
|
||||||
|
"in": {
|
||||||
|
"depends": "n1.done"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n4",
|
||||||
|
"type": "Return",
|
||||||
|
"config": {
|
||||||
|
"target_format": "openai",
|
||||||
|
"text_template": "[[OUT2]] + [[OUT3]]"
|
||||||
|
},
|
||||||
|
"in": {
|
||||||
|
"depends": ["n2.done", "n3.done"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
exe = PipelineExecutor(p)
|
||||||
|
try:
|
||||||
|
out = await exe.run(_base_ctx())
|
||||||
|
print("OK:", _pp(out))
|
||||||
|
except Exception as e:
|
||||||
|
print("ERR:", type(e).__name__, str(e))
|
||||||
|
|
||||||
|
async def scenario_gate_only_dependency():
|
||||||
|
print("\n=== SCENARIO 4: gate-only dependency (no real parents) ===")
|
||||||
|
# nThen depends only on nIf.true (should run only when gate becomes true)
|
||||||
|
p = {
|
||||||
|
"id": "p_gate_only",
|
||||||
|
"name": "Gate Only",
|
||||||
|
"loop_mode": "iterative",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "n1",
|
||||||
|
"type": "SetVars",
|
||||||
|
"config": {
|
||||||
|
"variables": [
|
||||||
|
{"id": "v1", "name": "FLAG", "mode": "string", "value": "yes"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"in": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nIf",
|
||||||
|
"type": "If",
|
||||||
|
"config": {
|
||||||
|
"expr": "[[FLAG]] == 'yes'"
|
||||||
|
},
|
||||||
|
"in": {
|
||||||
|
"depends": "n1.done"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nThen",
|
||||||
|
"type": "Return",
|
||||||
|
"config": {
|
||||||
|
"target_format": "openai",
|
||||||
|
"text_template": "then-branch"
|
||||||
|
},
|
||||||
|
"in": {
|
||||||
|
# gate-only
|
||||||
|
"depends": "nIf.true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
exe = PipelineExecutor(p)
|
||||||
|
try:
|
||||||
|
out = await exe.run(_base_ctx())
|
||||||
|
print("OK:", _pp(out))
|
||||||
|
except Exception as e:
|
||||||
|
print("ERR:", type(e).__name__, str(e))
|
||||||
|
|
||||||
|
async def scenario_provider_prompt_empty_json_error():
|
||||||
|
print("\n=== SCENARIO 5: ProviderCall with empty PROMPT causing JSON error (collect logs) ===")
|
||||||
|
# Template has [[PROMPT]] surrounded by commas; blocks are empty => PROMPT = ""
|
||||||
|
# Resulting JSON likely invalid -> ExecutionError expected before any network call.
|
||||||
|
p = {
|
||||||
|
"id": "p_prompt_empty",
|
||||||
|
"name": "Prompt Empty JSON Error",
|
||||||
|
"loop_mode": "dag",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "n1",
|
||||||
|
"type": "ProviderCall",
|
||||||
|
"config": {
|
||||||
|
"provider": "openai",
|
||||||
|
"provider_configs": {
|
||||||
|
"openai": {
|
||||||
|
"base_url": "https://api.openai.com",
|
||||||
|
"endpoint": "/v1/chat/completions",
|
||||||
|
"headers": "{\"Authorization\":\"Bearer TEST\"}",
|
||||||
|
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": 0.1\n}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"blocks": [] # empty -> PROMPT empty
|
||||||
|
},
|
||||||
|
"in": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
exe = PipelineExecutor(p)
|
||||||
|
try:
|
||||||
|
out = await exe.run(_base_ctx())
|
||||||
|
print("UNEXPECTED_OK:", _pp(out))
|
||||||
|
except Exception as e:
|
||||||
|
print("EXPECTED_ERROR:", type(e).__name__, str(e))
|
||||||
|
|
||||||
|
async def scenario_rawforward_vendor_unknown():
|
||||||
|
print("\n=== SCENARIO 6: RawForward vendor unknown (non-JSON body simulated) ===")
|
||||||
|
# We simulate incoming.json as a plain string that doesn't look like any known vendor payload.
|
||||||
|
# RawForward will try vendor detect, fail and raise ExecutionError (collect logs, do not fix).
|
||||||
|
p = {
|
||||||
|
"id": "p_rawforward_unknown",
|
||||||
|
"name": "RawForward Unknown",
|
||||||
|
"loop_mode": "dag",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "n1",
|
||||||
|
"type": "RawForward",
|
||||||
|
"config": {
|
||||||
|
# No base_url -> autodetect vendor from incoming.json (will fail)
|
||||||
|
"passthrough_headers": True,
|
||||||
|
"extra_headers": "{}"
|
||||||
|
},
|
||||||
|
"in": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
exe = PipelineExecutor(p)
|
||||||
|
ctx = _base_ctx()
|
||||||
|
ctx["incoming"] = {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://example.test/whatever",
|
||||||
|
"path": "/whatever",
|
||||||
|
"query": "",
|
||||||
|
"headers": {"Content-Type": "text/plain"},
|
||||||
|
"json": "raw-plain-body-simulated" # NOT JSON object -> detect_vendor -> unknown
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
out = await exe.run(ctx)
|
||||||
|
print("UNEXPECTED_OK:", _pp(out))
|
||||||
|
except Exception as e:
|
||||||
|
print("EXPECTED_ERROR:", type(e).__name__, str(e))
|
||||||
|
|
||||||
|
def run_all():
|
||||||
|
async def main():
|
||||||
|
await scenario_if_single_quotes_ok()
|
||||||
|
await scenario_if_error_logging()
|
||||||
|
await scenario_multi_depends_array()
|
||||||
|
await scenario_gate_only_dependency()
|
||||||
|
await scenario_provider_prompt_empty_json_error()
|
||||||
|
await scenario_rawforward_vendor_unknown()
|
||||||
|
print("\n=== EDGE CASES: DONE ===")
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_all()
|
||||||
167
tests/test_executor_iterative.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import asyncio
|
||||||
|
from agentui.pipeline.executor import PipelineExecutor, ExecutionError, Node, NODE_REGISTRY
|
||||||
|
|
||||||
|
|
||||||
|
def run_checks():
|
||||||
|
async def scenario():
|
||||||
|
# Test 1: linear pipeline in iterative mode (SetVars -> Return)
|
||||||
|
p1 = {
|
||||||
|
"id": "pipeline_test_iter_1",
|
||||||
|
"name": "Iterative Linear",
|
||||||
|
"loop_mode": "iterative",
|
||||||
|
"loop_max_iters": 100,
|
||||||
|
"loop_time_budget_ms": 5000,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "n1",
|
||||||
|
"type": "SetVars",
|
||||||
|
"config": {
|
||||||
|
"variables": [
|
||||||
|
{"id": "v1", "name": "MSG", "mode": "string", "value": "Hello"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"in": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n2",
|
||||||
|
"type": "Return",
|
||||||
|
"config": {
|
||||||
|
"target_format": "openai",
|
||||||
|
"text_template": "[[MSG]]"
|
||||||
|
},
|
||||||
|
"in": {
|
||||||
|
"depends": "n1.done"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
ctx = {
|
||||||
|
"model": "gpt-x",
|
||||||
|
"vendor_format": "openai",
|
||||||
|
"params": {},
|
||||||
|
"chat": {"last_user": "hi"},
|
||||||
|
"OUT": {}
|
||||||
|
}
|
||||||
|
ex1 = PipelineExecutor(p1)
|
||||||
|
out1 = await ex1.run(ctx)
|
||||||
|
assert isinstance(out1, dict) and "result" in out1
|
||||||
|
res1 = out1["result"]
|
||||||
|
# OpenAI-like object from Return formatter
|
||||||
|
assert res1.get("object") == "chat.completion"
|
||||||
|
msg1 = res1.get("choices", [{}])[0].get("message", {}).get("content")
|
||||||
|
assert msg1 == "Hello"
|
||||||
|
|
||||||
|
# Test 2: If gating in iterative mode (SetVars -> If -> Return(true))
|
||||||
|
p2 = {
|
||||||
|
"id": "pipeline_test_iter_2",
|
||||||
|
"name": "Iterative If Gate",
|
||||||
|
"loop_mode": "iterative",
|
||||||
|
"loop_max_iters": 100,
|
||||||
|
"loop_time_budget_ms": 5000,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "n1",
|
||||||
|
"type": "SetVars",
|
||||||
|
"config": {
|
||||||
|
"variables": [
|
||||||
|
{"id": "v1", "name": "MSG", "mode": "string", "value": "Hello world"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"in": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nIf",
|
||||||
|
"type": "If",
|
||||||
|
"config": {
|
||||||
|
"expr": '[[MSG]] contains "Hello"'
|
||||||
|
},
|
||||||
|
"in": {
|
||||||
|
"depends": "n1.done"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nRet",
|
||||||
|
"type": "Return",
|
||||||
|
"config": {
|
||||||
|
"target_format": "openai",
|
||||||
|
"text_template": "[[MSG]] ok"
|
||||||
|
},
|
||||||
|
"in": {
|
||||||
|
"depends": "nIf.true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
ex2 = PipelineExecutor(p2)
|
||||||
|
out2 = await ex2.run(ctx)
|
||||||
|
assert "result" in out2
|
||||||
|
res2 = out2["result"]
|
||||||
|
assert res2.get("object") == "chat.completion"
|
||||||
|
msg2 = res2.get("choices", [{}])[0].get("message", {}).get("content")
|
||||||
|
assert msg2 == "Hello world ok"
|
||||||
|
|
||||||
|
# Test 3: [[OUT:...]] is treated as a real dependency in iterative mode
|
||||||
|
class ProbeNode(Node):
|
||||||
|
type_name = "Probe"
|
||||||
|
async def run(self, inputs, context):
|
||||||
|
x = inputs.get("x")
|
||||||
|
assert isinstance(x, dict) and isinstance(x.get("vars"), dict)
|
||||||
|
v = x["vars"].get("MSG")
|
||||||
|
assert v == "Hello OUT"
|
||||||
|
return {"vars": {"X_MSG": v}}
|
||||||
|
|
||||||
|
# Register probe node
|
||||||
|
NODE_REGISTRY[ProbeNode.type_name] = ProbeNode
|
||||||
|
|
||||||
|
p3 = {
|
||||||
|
"id": "pipeline_test_iter_3",
|
||||||
|
"name": "Iterative OUT dependency",
|
||||||
|
"loop_mode": "iterative",
|
||||||
|
"loop_max_iters": 100,
|
||||||
|
"loop_time_budget_ms": 5000,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "n1",
|
||||||
|
"type": "SetVars",
|
||||||
|
"config": {
|
||||||
|
"variables": [
|
||||||
|
{"id": "v1", "name": "MSG", "mode": "string", "value": "Hello OUT"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"in": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n2",
|
||||||
|
"type": "Probe",
|
||||||
|
"config": {},
|
||||||
|
"in": {
|
||||||
|
"x": "[[OUT:n1]]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n3",
|
||||||
|
"type": "Return",
|
||||||
|
"config": {
|
||||||
|
"target_format": "openai",
|
||||||
|
"text_template": "[[VAR:vars.X_MSG]]"
|
||||||
|
},
|
||||||
|
"in": {
|
||||||
|
"depends": "n2.done"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
ex3 = PipelineExecutor(p3)
|
||||||
|
out3 = await ex3.run(ctx)
|
||||||
|
assert "result" in out3
|
||||||
|
res3 = out3["result"]
|
||||||
|
assert res3.get("object") == "chat.completion"
|
||||||
|
msg3 = res3.get("choices", [{}])[0].get("message", {}).get("content")
|
||||||
|
assert msg3 == "Hello OUT"
|
||||||
|
|
||||||
|
asyncio.run(scenario())
|
||||||
|
print("Iterative executor tests: OK")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_checks()
|
||||||
227
tests/test_macros_vars.py
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
import asyncio
|
||||||
|
from agentui.pipeline.executor import PipelineExecutor
|
||||||
|
from agentui.pipeline.storage import clear_var_store
|
||||||
|
from tests.utils import pp as _pp, ctx as _ctx
|
||||||
|
|
||||||
|
|
||||||
|
async def scenario_bare_vars_and_braces():
|
||||||
|
print("\n=== MACROS 1: Bare [[NAME]] и {{ NAME }} + числа/объекты без кавычек ===")
|
||||||
|
p = {
|
||||||
|
"id": "p_macros_1",
|
||||||
|
"name": "Bare and Braces",
|
||||||
|
"loop_mode": "iterative",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "n1",
|
||||||
|
"type": "SetVars",
|
||||||
|
"config": {
|
||||||
|
"variables": [
|
||||||
|
{"id": "v1", "name": "STR", "mode": "string", "value": "строка"},
|
||||||
|
{"id": "v2", "name": "NUM", "mode": "expr", "value": "42"},
|
||||||
|
{"id": "v3", "name": "OBJ", "mode": "expr", "value": '{"x": 1, "y": [2,3]}'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"in": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n2",
|
||||||
|
"type": "Return",
|
||||||
|
"config": {
|
||||||
|
"target_format": "openai",
|
||||||
|
# Вставляем: строка из [[STR]], число через {{ NUM }}, и словарь через {{ OBJ }}
|
||||||
|
"text_template": "[[STR]] | {{ NUM }} | {{ OBJ }}"
|
||||||
|
},
|
||||||
|
"in": { "depends": "n1.done" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
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 = {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://localhost/test?foo=bar",
|
||||||
|
"path": "/test",
|
||||||
|
"query": "foo=bar",
|
||||||
|
"headers": {"authorization": "Bearer X", "x-api-key": "Y"},
|
||||||
|
"json": {"a": None}
|
||||||
|
}
|
||||||
|
p = {
|
||||||
|
"id": "p_macros_2",
|
||||||
|
"name": "VAR and defaults",
|
||||||
|
"loop_mode": "dag",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "n1",
|
||||||
|
"type": "Return",
|
||||||
|
"config": {
|
||||||
|
"target_format": "openai",
|
||||||
|
"text_template": (
|
||||||
|
"auth=[[VAR:incoming.headers.authorization]] | "
|
||||||
|
"xkey=[[VAR:incoming.headers.x-api-key]] | "
|
||||||
|
# nested default: params.a|default(123) -> если param не задан, 123
|
||||||
|
"num={{ params.a|default(123) }} | "
|
||||||
|
# deeper default chain: incoming.json.a|default(params.a|default(456))
|
||||||
|
"num2={{ incoming.json.a|default(params.a|default(456)) }} | "
|
||||||
|
# JSON literal default list/object
|
||||||
|
"lit_list={{ missing|default([1,2,3]) }} | lit_obj={{ missing2|default({\"k\":10}) }}"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
"in": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
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 = {
|
||||||
|
"id": "p_macros_3",
|
||||||
|
"name": "OUT full and short",
|
||||||
|
"loop_mode": "iterative",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "n1",
|
||||||
|
"type": "SetVars",
|
||||||
|
"config": {
|
||||||
|
"variables": [
|
||||||
|
{"id": "v1", "name": "MSG", "mode": "string", "value": "hello"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"in": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n2",
|
||||||
|
"type": "Return",
|
||||||
|
"config": {
|
||||||
|
"target_format": "openai",
|
||||||
|
"text_template": "[[MSG]]"
|
||||||
|
},
|
||||||
|
"in": { "depends": "n1.done" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n3",
|
||||||
|
"type": "Return",
|
||||||
|
"config": {
|
||||||
|
"target_format": "openai",
|
||||||
|
# Две формы: полная от n1.vars.MSG и короткая от n2 => [[OUT2]]
|
||||||
|
"text_template": "[[OUT:n1.vars.MSG]] + [[OUT2]]"
|
||||||
|
},
|
||||||
|
"in": { "depends": "n2.done" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
# начинаем с чистого стора
|
||||||
|
clear_var_store(pid)
|
||||||
|
p = {
|
||||||
|
"id": pid,
|
||||||
|
"name": "STORE across runs",
|
||||||
|
"loop_mode": "iterative",
|
||||||
|
"clear_var_store": False, # критично: не очищать между запусками
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "n1",
|
||||||
|
"type": "SetVars",
|
||||||
|
"config": {
|
||||||
|
"variables": [
|
||||||
|
{"id": "v1", "name": "KEEP", "mode": "string", "value": "persist-me"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"in": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n2",
|
||||||
|
"type": "Return",
|
||||||
|
"config": {
|
||||||
|
"target_format": "openai",
|
||||||
|
"text_template": "first-run"
|
||||||
|
},
|
||||||
|
"in": { "depends": "n1.done" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
# Первый запуск — кладём KEEP в STORE
|
||||||
|
out1 = await PipelineExecutor(p).run(_ctx())
|
||||||
|
print("RUN1:", _pp(out1))
|
||||||
|
# Второй запуск — читаем из STORE через макросы
|
||||||
|
p2 = {
|
||||||
|
"id": pid,
|
||||||
|
"name": "STORE read",
|
||||||
|
"loop_mode": "dag",
|
||||||
|
"clear_var_store": False,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "n1",
|
||||||
|
"type": "Return",
|
||||||
|
"config": {
|
||||||
|
"target_format": "openai",
|
||||||
|
"text_template": "[[STORE:KEEP]] | {{ STORE.KEEP }}"
|
||||||
|
},
|
||||||
|
"in": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
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":[...]
|
||||||
|
p = {
|
||||||
|
"id": "p_macros_5",
|
||||||
|
"name": "PROMPT OpenAI",
|
||||||
|
"loop_mode": "dag",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "n1",
|
||||||
|
"type": "ProviderCall",
|
||||||
|
"config": {
|
||||||
|
"provider": "openai",
|
||||||
|
"provider_configs": {
|
||||||
|
"openai": {
|
||||||
|
"base_url": "https://api.openai.com",
|
||||||
|
"endpoint": "/v1/chat/completions",
|
||||||
|
"headers": "{\"Authorization\":\"Bearer TEST\"}",
|
||||||
|
"template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ params.temperature|default(0.7) }}\n}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"blocks": [
|
||||||
|
{"id": "b1", "name": "sys", "role": "system", "prompt": "You are test", "enabled": True, "order": 0},
|
||||||
|
{"id": "b2", "name": "user", "role": "user", "prompt": "Say [[VAR:chat.last_user]]", "enabled": True, "order": 1}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"in": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
# Сборка и запрос (ожидаемый 401), главное — валидное тело с messages:[...]
|
||||||
|
out = await PipelineExecutor(p).run(_ctx())
|
||||||
|
print("OUT:", _pp(out))
|
||||||
|
|
||||||
|
|
||||||
|
def run_all():
|
||||||
|
async def main():
|
||||||
|
await scenario_bare_vars_and_braces()
|
||||||
|
await scenario_var_path_and_defaults()
|
||||||
|
await scenario_out_macros_full_and_short()
|
||||||
|
await scenario_store_macros_two_runs()
|
||||||
|
await scenario_pm_prompt_blocks_to_provider_structs()
|
||||||
|
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": {},
|
||||||
|
},
|
||||||
|
}
|
||||||