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
|
||||
agentui.log
|
||||
|
||||
# proxy
|
||||
proxy.txt
|
||||
|
||||
|
||||
# Local config
|
||||
.env
|
||||
.env.*
|
||||
*.env
|
||||
|
||||
# Project-specific runtime files
|
||||
presets/
|
||||
pipeline.json
|
||||
|
||||
#
|
||||
# Node
|
||||
node_modules/
|
||||
|
||||
|
||||
@@ -1,80 +1,200 @@
|
||||
# AgentUI Project Overview
|
||||
# НадTavern (AgentUI): объясняем просто
|
||||
|
||||
## Цель проекта
|
||||
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).
|
||||
- Принять запрос от клиента как есть (OpenAI/Gemini/Claude).
|
||||
- При желании подменить его или дополнить.
|
||||
- Отправить к нужному провайдеру (или прямо «пробросить» как есть).
|
||||
- Склеить финальный ответ в том формате, который ждёт клиент.
|
||||
|
||||
---
|
||||
|
||||
## Текущий прогресс
|
||||
- Исправлены баги сериализации соединений во фронтенде.
|
||||
- Добавлены подсказки по макросам.
|
||||
- Реализована топологическая сортировка исполнения.
|
||||
- Создан универсальный рендер макросов `render_template_simple`.
|
||||
- Интегрирован RawForward с макроподстановкой.
|
||||
- ProviderCall теперь преобразует сообщения под формат конкретного провайдера.
|
||||
Как это работает в 5 шагов
|
||||
|
||||
1) Клиент шлёт HTTP‑запрос на ваш сервер.
|
||||
2) Сервер понимает формат (OpenAI/Gemini/Claude) и делает «унифицированный» вид.
|
||||
3) Загружается ваш пайплайн (схема из узлов) из файла pipeline.json.
|
||||
4) Узлы запускаются по очереди «волнами» и обмениваются результатами.
|
||||
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 + установка зависимостей + старт сервера.
|
||||
|
||||
**Что нужно сделать:**
|
||||
- [ ] Спроектировать структуру prompt‑менеджера: массив блоков `{ name, role, prompt, enabled, order }`.
|
||||
- [ ] Добавить универсальный рендер макросов, который применяется ко всем блокам перед конвертацией.
|
||||
- [ ] Доработать конвертеры форматов под OpenAI, Gemini, Anthropic, чтобы они учитывали эти блоки.
|
||||
- [ ] Интегрировать prompt‑менеджер в `ProviderCallNode`:
|
||||
- Сборка последовательности сообщений.
|
||||
- Подстановка макросов.
|
||||
- Конвертация в провайдерский формат.
|
||||
- [ ] Реализовать UI prompt‑менеджера во фронтенде:
|
||||
- CRUD операций над блоками.
|
||||
- Drag&Drop сортировку.
|
||||
- Возможность включать/выключать блок.
|
||||
- Выбор роли (`user`, `system`, `assistant`, `tool`).
|
||||
Вариант C (ручной запуск, если хотите контролировать шаги):
|
||||
- Установите Python 3.10+.
|
||||
- Создайте и активируйте .venv:
|
||||
- Windows (cmd): py -m venv .venv && .\.venv\Scripts\activate
|
||||
- Linux/macOS (bash): python3 -m venv .venv && source .venv/bin/activate
|
||||
- Установите зависимости и стартуйте сервер:
|
||||
- pip install -r [requirements.txt](requirements.txt)
|
||||
- python -m uvicorn agentui.api.server:app --host 127.0.0.1 --port 7860
|
||||
|
||||
Откройте в браузере:
|
||||
- http://127.0.0.1:7860/ui/editor.html — визуальный редактор узлов
|
||||
- http://127.0.0.1:7860/ui/pipeline.html — редактор «сырых» JSON настроек пайплайна
|
||||
- http://127.0.0.1:7860/ — простая страница с примером запроса
|
||||
|
||||
---
|
||||
|
||||
## Важные файлы
|
||||
- [`static/editor.html`](static/editor.html) — визуальный редактор пайплайнов.
|
||||
- [`agentui/pipeline/executor.py`](agentui/pipeline/executor.py) — логика исполнения пайплайнов, макросов и узлов.
|
||||
- [`agentui/api/server.py`](agentui/api/server.py) — REST API для внешних клиентов.
|
||||
- [`pipeline.json`](pipeline.json) — сохранённый пайплайн по умолчанию.
|
||||
Где лежат важные файлы
|
||||
|
||||
- API сервер: [`agentui/api/server.py`](agentui/api/server.py)
|
||||
- Исполнитель узлов: [`agentui/pipeline/executor.py`](agentui/pipeline/executor.py)
|
||||
- Шаблоны и макросы: [`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 urllib.parse import quote
|
||||
import os
|
||||
|
||||
|
||||
def _parse_proxy_line(line: str) -> Optional[str]:
|
||||
@@ -39,6 +40,9 @@ def _read_proxy_from_file() -> Optional[str]:
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
# поддержим дополнительные ключи вида key=value в этом же файле (разберём ниже)
|
||||
if "=" in line:
|
||||
continue
|
||||
url = _parse_proxy_line(line)
|
||||
if url:
|
||||
return url
|
||||
@@ -59,3 +63,136 @@ def build_httpx_proxies() -> Optional[Dict[str, str]]:
|
||||
}
|
||||
|
||||
|
||||
def _read_kv_from_proxy_file() -> Dict[str, str]:
|
||||
"""
|
||||
Поддержка дополнительных опций в proxy.txt:
|
||||
ca=/полный/путь/к/burp-ca.pem
|
||||
verify=false # отключить проверку сертификатов (для отладки)
|
||||
"""
|
||||
out: Dict[str, str] = {}
|
||||
p = Path("proxy.txt")
|
||||
if not p.exists():
|
||||
return out
|
||||
try:
|
||||
for raw in p.read_text(encoding="utf-8").splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
out[k.strip().lower()] = v.strip()
|
||||
except Exception:
|
||||
return out
|
||||
return out
|
||||
|
||||
def _read_second_bare_flag_from_proxy() -> Optional[bool]:
|
||||
"""
|
||||
Читает «вторую голую строку» после URL в proxy.txt и интерпретирует как флаг verify:
|
||||
true/1/yes/on -> True
|
||||
false/0/no/off -> False
|
||||
Возвращает None, если строка отсутствует или не распознана.
|
||||
"""
|
||||
try:
|
||||
p = Path("proxy.txt")
|
||||
if not p.exists():
|
||||
return None
|
||||
lines = [ln.strip() for ln in p.read_text(encoding="utf-8").splitlines()]
|
||||
# найдём первую «URL» строку (без '=' и не пустую/коммент)
|
||||
idx_url = -1
|
||||
for i, ln in enumerate(lines):
|
||||
if not ln or ln.startswith("#") or "=" in ln:
|
||||
continue
|
||||
idx_url = i
|
||||
break
|
||||
if idx_url >= 0:
|
||||
# ищем следующую «голую» строку
|
||||
for j in range(idx_url + 1, len(lines)):
|
||||
ln = lines[j].strip()
|
||||
if not ln or ln.startswith("#") or "=" in ln:
|
||||
continue
|
||||
low = ln.lower()
|
||||
if low in ("1", "true", "yes", "on"):
|
||||
return True
|
||||
if low in ("0", "false", "no", "off"):
|
||||
return False
|
||||
# если это не похожее на флаг — считаем отсутствующим
|
||||
break
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
def get_tls_verify() -> Union[bool, str]:
|
||||
"""
|
||||
Возвращает значение для параметра httpx.AsyncClient(verify=...):
|
||||
- путь к PEM-бандлу (строка), если нашли ca=... или файл proxy-ca.pem в корне
|
||||
- False, если verify=false/insecure=1/AGENTUI_VERIFY=false
|
||||
- True по умолчанию
|
||||
- Новое: можно задать флаг второй «голой» строкой в proxy.txt (после URL прокси):
|
||||
пример:
|
||||
http:127.0.0.1:8888
|
||||
false
|
||||
или
|
||||
http:127.0.0.1:8888
|
||||
true
|
||||
"""
|
||||
# 1) Переменные окружения имеют приоритет
|
||||
env_verify = os.getenv("AGENTUI_VERIFY")
|
||||
if env_verify is not None and env_verify.strip().lower() in ("0", "false", "no", "off"):
|
||||
return False
|
||||
env_ca = os.getenv("AGENTUI_CA")
|
||||
if env_ca:
|
||||
path = Path(env_ca).expanduser()
|
||||
if path.exists():
|
||||
return str(path)
|
||||
|
||||
# 2) proxy.txt ключи
|
||||
kv = _read_kv_from_proxy_file()
|
||||
if kv.get("verify", "").lower() in ("0", "false", "no", "off"):
|
||||
return False
|
||||
if "ca" in kv:
|
||||
path = Path(kv["ca"]).expanduser()
|
||||
if path.exists():
|
||||
return str(path)
|
||||
# 2.1) Дополнительно: поддержка второй строки без ключа — true/false
|
||||
second = _read_second_bare_flag_from_proxy()
|
||||
if second is True:
|
||||
return True
|
||||
if second is False:
|
||||
return False
|
||||
|
||||
# 3) Файл по умолчанию в корне проекта
|
||||
default_ca = Path("proxy-ca.pem")
|
||||
if default_ca.exists():
|
||||
return str(default_ca)
|
||||
|
||||
# 4) По умолчанию строгая проверка
|
||||
return True
|
||||
|
||||
|
||||
def is_verify_explicit() -> bool:
|
||||
"""
|
||||
Возвращает True, если пользователь ЯВНО задал политику проверки TLS,
|
||||
чтобы клиент не переопределял её значением по умолчанию.
|
||||
Учитываются:
|
||||
- переменные окружения: AGENTUI_VERIFY, AGENTUI_CA
|
||||
- ключи в proxy.txt: verify=..., ca=...
|
||||
- файл proxy-ca.pem в корне проекта
|
||||
- Новое: «вторая голая строка» после URL в proxy.txt со значением true/false
|
||||
"""
|
||||
if os.getenv("AGENTUI_VERIFY") is not None:
|
||||
return True
|
||||
if os.getenv("AGENTUI_CA"):
|
||||
return True
|
||||
|
||||
kv = _read_kv_from_proxy_file()
|
||||
if "verify" in kv or "ca" in kv:
|
||||
return True
|
||||
# Вторая «голая» строка как явный флаг
|
||||
second = _read_second_bare_flag_from_proxy()
|
||||
if second is not None:
|
||||
return True
|
||||
|
||||
if Path("proxy-ca.pem").exists():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -4,10 +4,18 @@ from typing import Any, Dict
|
||||
def default_pipeline() -> Dict[str, Any]:
|
||||
# Минимальный дефолт без устаревших нод.
|
||||
# Если пайплайн пустой, сервер вернёт echo-ответ (см. server.execute_pipeline_echo).
|
||||
# Добавлены поля управления режимом исполнения:
|
||||
# - loop_mode: "dag" | "iterative" (по умолчанию "dag")
|
||||
# - loop_max_iters: максимальное число запусков задач (safety)
|
||||
# - loop_time_budget_ms: ограничение по времени (safety)
|
||||
return {
|
||||
"id": "pipeline_default",
|
||||
"name": "Default Chat Pipeline",
|
||||
"parallel_limit": 8,
|
||||
"loop_mode": "dag",
|
||||
"loop_max_iters": 1000,
|
||||
"loop_time_budget_ms": 10000,
|
||||
"clear_var_store": True,
|
||||
"nodes": []
|
||||
}
|
||||
|
||||
|
||||
@@ -8,19 +8,96 @@ from agentui.pipeline.defaults import default_pipeline
|
||||
|
||||
PIPELINE_FILE = Path("pipeline.json")
|
||||
PRESETS_DIR = Path("presets")
|
||||
VARS_DIR = Path(".agentui") / "vars"
|
||||
|
||||
|
||||
# DRY нормализация meta/пайплайна: единый источник дефолтов и типов
|
||||
def normalize_pipeline(pipeline: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Приводит верхнеуровневые ключи пайплайна к согласованному виду, заполняет дефолты.
|
||||
Безопасно к отсутствующим ключам и неверным типам.
|
||||
"""
|
||||
if not isinstance(pipeline, dict):
|
||||
pipeline = {}
|
||||
out: Dict[str, Any] = dict(pipeline)
|
||||
|
||||
def _to_int(v, d):
|
||||
try:
|
||||
n = int(v)
|
||||
return n if n > 0 else d
|
||||
except Exception:
|
||||
return d
|
||||
|
||||
def _to_float(v, d):
|
||||
try:
|
||||
n = float(v)
|
||||
return n if n > 0 else d
|
||||
except Exception:
|
||||
return d
|
||||
|
||||
# Базовые поля
|
||||
out["id"] = str(out.get("id") or "pipeline_editor")
|
||||
out["name"] = str(out.get("name") or "Edited Pipeline")
|
||||
out["parallel_limit"] = _to_int(out.get("parallel_limit"), 8)
|
||||
out["loop_mode"] = str(out.get("loop_mode") or "dag")
|
||||
out["loop_max_iters"] = _to_int(out.get("loop_max_iters"), 1000)
|
||||
out["loop_time_budget_ms"] = _to_int(out.get("loop_time_budget_ms"), 10000)
|
||||
out["clear_var_store"] = bool(out.get("clear_var_store", True))
|
||||
out["http_timeout_sec"] = _to_float(out.get("http_timeout_sec"), 60)
|
||||
|
||||
# Глобальные опции извлечения текста для [[OUTx]]
|
||||
out["text_extract_strategy"] = str(out.get("text_extract_strategy") or "auto")
|
||||
out["text_extract_json_path"] = str(out.get("text_extract_json_path") or "")
|
||||
# Поддержка разных написаний text_join_sep
|
||||
join_sep = out.get("text_join_sep")
|
||||
if join_sep is None:
|
||||
for k in list(out.keys()):
|
||||
if isinstance(k, str) and k.lower() == "text_join_sep":
|
||||
join_sep = out.get(k)
|
||||
break
|
||||
out["text_join_sep"] = str(join_sep or "\n")
|
||||
|
||||
# Пресеты парсинга
|
||||
presets = out.get("text_extract_presets")
|
||||
norm_presets: List[Dict[str, Any]] = []
|
||||
if isinstance(presets, list):
|
||||
for i, it in enumerate(presets):
|
||||
if not isinstance(it, dict):
|
||||
continue
|
||||
norm_presets.append({
|
||||
"id": str(it.get("id") or f"p{i}"),
|
||||
"name": str(it.get("name") or it.get("json_path") or "Preset"),
|
||||
"strategy": str(it.get("strategy") or "auto"),
|
||||
"json_path": str(it.get("json_path") or ""),
|
||||
"join_sep": str(it.get("join_sep") or "\n"),
|
||||
})
|
||||
out["text_extract_presets"] = norm_presets
|
||||
|
||||
# Узлы — список
|
||||
try:
|
||||
nodes = out.get("nodes") or []
|
||||
if not isinstance(nodes, list):
|
||||
nodes = []
|
||||
out["nodes"] = nodes
|
||||
except Exception:
|
||||
out["nodes"] = []
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def load_pipeline() -> Dict[str, Any]:
|
||||
if PIPELINE_FILE.exists():
|
||||
try:
|
||||
return json.loads(PIPELINE_FILE.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
pass
|
||||
return default_pipeline()
|
||||
if PIPELINE_FILE.exists():
|
||||
try:
|
||||
data = json.loads(PIPELINE_FILE.read_text(encoding="utf-8"))
|
||||
return normalize_pipeline(data)
|
||||
except Exception:
|
||||
pass
|
||||
return normalize_pipeline(default_pipeline())
|
||||
|
||||
|
||||
def save_pipeline(pipeline: Dict[str, Any]) -> None:
|
||||
PIPELINE_FILE.write_text(json.dumps(pipeline, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
norm = normalize_pipeline(pipeline or {})
|
||||
PIPELINE_FILE.write_text(json.dumps(norm, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def list_presets() -> List[str]:
|
||||
@@ -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")
|
||||
|
||||
|
||||
# ---------------- 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",
|
||||
"_best_text_from_outputs",
|
||||
"render_template_simple",
|
||||
"eval_condition_expr",
|
||||
]
|
||||
|
||||
# Regex-макросы (общие для бэка)
|
||||
_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)
|
||||
# STORE: постоянное хранилище переменных (пер-пайплайн)
|
||||
_STORE_MACRO_RE = re.compile(r"\[\[\s*STORE\s*[:\s]\s*([^\]]+?)\s*\]\]", re.IGNORECASE)
|
||||
# Единый фрагмент PROMPT (провайдеро-специфичный JSON-фрагмент)
|
||||
_PROMPT_MACRO_RE = re.compile(r"\[\[\s*PROMPT\s*\]\]", re.IGNORECASE)
|
||||
# Короткая форма: [[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, затем в контексте
|
||||
_BARE_MACRO_RE = re.compile(r"\[\[\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[^\]]+?)?)\s*\]\]")
|
||||
# Подстановки {{ ... }} (включая простейший фильтр |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]:
|
||||
@@ -160,12 +169,21 @@ def _best_text_from_outputs(node_out: Any) -> str:
|
||||
# Gemini
|
||||
try:
|
||||
if isinstance(base, dict):
|
||||
cand0 = (base.get("candidates") or [{}])[0]
|
||||
content = cand0.get("content") or {}
|
||||
parts0 = (content.get("parts") or [{}])[0]
|
||||
t = parts0.get("text")
|
||||
if isinstance(t, str):
|
||||
return t
|
||||
cands = base.get("candidates") or []
|
||||
texts: List[str] = []
|
||||
for cand in cands:
|
||||
try:
|
||||
content = cand.get("content") or {}
|
||||
parts = content.get("parts") or []
|
||||
for p in parts:
|
||||
if isinstance(p, dict):
|
||||
t = p.get("text")
|
||||
if isinstance(t, str) and t.strip():
|
||||
texts.append(t.strip())
|
||||
except Exception:
|
||||
continue
|
||||
if texts:
|
||||
return "\n".join(texts)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -192,13 +210,55 @@ def render_template_simple(template: str, context: Dict[str, Any], out_map: Dict
|
||||
value может быть числом, строкой ('..'/".."), массивом/объектом в виде литерала.
|
||||
- [[VAR:path]] — берёт из context
|
||||
- [[OUT:nodeId(.path)*]] — берёт из out_map
|
||||
- [[STORE:path]] — берёт из постоянного хранилища (context.store)
|
||||
Возвращает строку.
|
||||
"""
|
||||
if template is None:
|
||||
return ""
|
||||
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:
|
||||
path = m.group(1).strip()
|
||||
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)
|
||||
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 = _OUT_MACRO_RE.sub(repl_out, s)
|
||||
s = _STORE_MACRO_RE.sub(repl_store, s)
|
||||
|
||||
# [[OUT1]] → текст из ноды n1 (best-effort)
|
||||
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)
|
||||
def repl_braces(m: re.Match) -> str:
|
||||
expr = m.group(1).strip()
|
||||
|
||||
|
||||
def eval_path(p: str) -> Any:
|
||||
p = p.strip()
|
||||
# Приоритет пользовательских переменных для простых идентификаторов {{ 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)
|
||||
return _get_by_path(node_val, rest)
|
||||
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)
|
||||
|
||||
|
||||
default_match = re.match(r"([^|]+)\|\s*default\((.*)\)\s*$", expr)
|
||||
if default_match:
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
import httpx
|
||||
from typing import Optional, Dict
|
||||
from agentui.config import build_httpx_proxies
|
||||
from typing import Optional, Dict, Union
|
||||
import os
|
||||
from agentui.config import build_httpx_proxies, get_tls_verify, is_verify_explicit
|
||||
|
||||
|
||||
def _mask_proxy(url: str) -> str:
|
||||
"""Маскируем часть с логином/паролем в URL прокси, чтобы не утекла в логи."""
|
||||
try:
|
||||
if "://" in url and "@" in url:
|
||||
prefix, rest = url.split("://", 1)
|
||||
auth, host = rest.split("@", 1)
|
||||
return f"{prefix}://***@{host}"
|
||||
return url
|
||||
except Exception:
|
||||
return "<masked>"
|
||||
|
||||
|
||||
def build_client(timeout: float = 60.0) -> httpx.AsyncClient:
|
||||
proxies: Optional[Dict[str, str]] = build_httpx_proxies()
|
||||
verify: Union[bool, str] = get_tls_verify()
|
||||
|
||||
explicit = is_verify_explicit()
|
||||
# По умолчанию при наличии прокси отключаем проверку сертификатов,
|
||||
# но не трогаем, если пользователь явно задал verify или CA.
|
||||
if proxies and (verify is True) and (not explicit):
|
||||
verify = False
|
||||
|
||||
if os.getenv("AGENTUI_DEBUG", "").lower() in ("1", "true", "on", "yes"):
|
||||
masked = {k: _mask_proxy(v) for k, v in (proxies or {}).items()}
|
||||
print("[agentui.http_client] proxies=", masked, " verify=", verify)
|
||||
|
||||
# httpx сам понимает схемы socks://, socks5:// при установленном extras [socks]
|
||||
client = httpx.AsyncClient(timeout=timeout, proxies=proxies, follow_redirects=True)
|
||||
try:
|
||||
client = httpx.AsyncClient(
|
||||
timeout=timeout,
|
||||
proxies=proxies,
|
||||
follow_redirects=True,
|
||||
verify=verify,
|
||||
)
|
||||
except TypeError:
|
||||
if proxies:
|
||||
try:
|
||||
masked = {k: _mask_proxy(v) for k, v in proxies.items()}
|
||||
except Exception:
|
||||
masked = proxies
|
||||
print(f"[agentui.http_client] WARNING: proxies not supported in httpx.AsyncClient, skipping proxies={masked}")
|
||||
client = httpx.AsyncClient(
|
||||
timeout=timeout,
|
||||
follow_redirects=True,
|
||||
verify=verify,
|
||||
)
|
||||
return client
|
||||
|
||||
|
||||
|
||||
@@ -1,328 +1,665 @@
|
||||
# Переменные и макросы НадTavern
|
||||
|
||||
Краткая, человеко‑понятная шпаргалка по тому, какие переменные и макросы доступны в шаблонах (в том числе в Prompt Blocks), как они устроены и как их правильно использовать. Док ниже соответствует текущему коду.
|
||||
НАДTAVERN VARIABLES — ГАЙД ДЛЯ ТЕХ, КТО СЕГОДНЯ «НА МИНИМАЛКАХ» И ВСЁ РАВНО ХОЧЕТ, ЧТОБЫ РАБОТАЛО
|
||||
|
||||
Реализация формирует единый «контекст» переменных для всех нод пайплайна, дополняет его выходами уже выполненных нод, а узел ProviderCall добавляет свои служебные структуры для удобной сборки промпта.
|
||||
Смотри сюда, слабак. Я — твой наидобрейший цун-энциклопедист, и сейчас я очень терпеливо (фрр) объясню так, чтобы даже ты не накосячил. Прочитаешь до конца — и у тебя получится. Может быть. Если постараешься. М-м… не думай, что я делаю это ради тебя!
|
||||
|
||||
Ссылки на код:
|
||||
- Формирование контекста запроса: [build_macro_context()](agentui/api/server.py:142)
|
||||
- Исполнитель пайплайна и снапшот OUT: [PipelineExecutor.run()](agentui/pipeline/executor.py:136)
|
||||
- Узел провайдера (Prompt Blocks → provider payload): [ProviderCallNode.run()](agentui/pipeline/executor.py:650)
|
||||
- Шаблоны/макросы ([[...]] и {{ ... }}): [render_template_simple()](agentui/pipeline/templating.py:187)
|
||||
- Короткая форма [[OUTx]] (извлечение текста): [_best_text_from_outputs()](agentui/pipeline/templating.py:124)
|
||||
- Прямой форвард запросов: [RawForwardNode.run()](agentui/pipeline/executor.py:833)
|
||||
- Источники истины (это значит «код, который реально решает», а не чаты):
|
||||
- Исполнение пайплайна: [PipelineExecutor.run()](agentui/pipeline/executor.py:402)
|
||||
- Нода SetVars — выражения и функции: [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:1290)
|
||||
- Нода ProviderCall — вызов провайдера и PROMPT: [ProviderCallNode.run()](agentui/pipeline/executor.py:2084)
|
||||
- Нода RawForward — прямой прокси: [RawForwardNode.run()](agentui/pipeline/executor.py:3547)
|
||||
- Нода Return — формат финального ответа: [ReturnNode.run()](agentui/pipeline/executor.py:3930)
|
||||
- Нода If — парсер условий: [IfNode.run()](agentui/pipeline/executor.py:4024)
|
||||
- While-обёртка для ProviderCall: [_providercall_run_with_while()](agentui/pipeline/executor.py:4075)
|
||||
- While-обёртка для RawForward: [_rawforward_run_with_while()](agentui/pipeline/executor.py:4243)
|
||||
- Шаблоны: [[...]] и {{ ... }} здесь: [render_template_simple()](agentui/pipeline/templating.py:205)
|
||||
- Условия if/while (&&, ||, contains, скобочки): [eval_condition_expr()](agentui/pipeline/templating.py:391)
|
||||
- JSONPath (упрощённый, но хватит): [_json_path_extract()](agentui/pipeline/executor.py:1569)
|
||||
- Склейка текста при JSONPath: [_stringify_join()](agentui/pipeline/executor.py:1610)
|
||||
- UI инспектор Prompt Blocks: [PM.setupProviderCallPMUI()](static/js/pm-ui.js:9)
|
||||
- Экспорт/импорт пайплайна в редакторе: [AgentUISer.toPipelineJSON()](static/js/serialization.js:104), [AgentUISer.fromPipelineJSON()](static/js/serialization.js:286)
|
||||
- Веб-редактор: [editor.html](static/editor.html)
|
||||
|
||||
---
|
||||
Перед началом (термины в скобках — это определение, не морщи нос):
|
||||
- «Пайплайн» (pipeline — схема исполнения из «узлов»).
|
||||
- «Узел» (node — прямоугольный блок на канвасе).
|
||||
- «Порт» (port — кружок входа/выхода узла).
|
||||
- «Гейт» (gate — ветка выхода If.true/If.false; влияет на порядок выполнения ребёнка).
|
||||
- «STORE» (перманентное хранилище переменных на диск, одна запись на каждый pipeline.id).
|
||||
- «PROMPT» (специальный JSON-фрагмент сообщений, который подставляется в шаблон запроса провайдера).
|
||||
- «OUTx» (короткая ссылка на текст из выхода ноды nX).
|
||||
- «incoming» (снимок входящего HTTP-запроса: метод, URL, заголовки, JSON и т.д.).
|
||||
|
||||
## 1) Общие переменные контекста (для всех нод)
|
||||
|
||||
Эти переменные доступны в шаблонах любой ноды. Они добавляются на стороне сервера при обработке входящего HTTP‑запроса.
|
||||
РАЗДЕЛ 1 — НОДЫ: КТО ЕСТЬ КТО (КРАТКО, ШУТКИ В СТОРОНУ)
|
||||
|
||||
- model — строка с именем модели.
|
||||
Пример: "gpt-4o-mini"
|
||||
- vendor_format — вендор/протокол запроса: "openai" | "gemini" | "claude" | "unknown"
|
||||
- system — «системный» текст, если он был во входящем запросе; иначе пустая строка.
|
||||
1) SetVars (заводит твои переменные)
|
||||
- Входы: нет (только depends).
|
||||
- Выходы: vars — словарь новых переменных.
|
||||
- Поведение: для каждой переменной задаёшь name и mode (string или expr). В режиме string значение обрабатывается шаблоном ([[...]] и {{ ... }}), в режиме expr — безопасным мини-диалектом выражений.
|
||||
- Где смотреть реализацию: [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:1197).
|
||||
|
||||
- params — стандартные параметры генерации (можно использовать как дефолты)
|
||||
- params.temperature — число с плавающей точкой (по умолчанию 0.7)
|
||||
- params.max_tokens — целое или null
|
||||
- params.top_p — число (по умолчанию 1.0)
|
||||
- params.stop — массив строк или null
|
||||
2) If (ветвление по условию)
|
||||
- Входы: depends.
|
||||
- Выходы: true, false (гейты для «детей» по условию).
|
||||
- Поведение: expr парсится как булево выражение (&&, ||, !, ==, !=, <, <=, >, >=, contains, скобочки). Внутри можно использовать [[...]] и {{ ... }}.
|
||||
- Реализация парсера: [eval_condition_expr()](agentui/pipeline/templating.py:391), обёртка ноды: [IfNode.run()](agentui/pipeline/executor.py:3538).
|
||||
|
||||
- chat — сведения о чате во входящем запросе
|
||||
- chat.last_user — последнее сообщение пользователя (строка)
|
||||
- chat.messages — массив сообщений в унифицированной форме:
|
||||
- role — "system" | "user" | "assistant" | "tool"
|
||||
- content — содержимое (обычно строка)
|
||||
- name — опционально, строка
|
||||
- tool_call_id — опционально
|
||||
3) ProviderCall (отправка к провайдеру OpenAI/Gemini/Claude)
|
||||
- Входы: depends.
|
||||
- Выходы: result (сырой JSON ответа), response_text (извлечённый «текст»).
|
||||
- Ключи: provider, provider_configs (base_url, endpoint, headers, template), blocks (Prompt Blocks), prompt_combine (DSL &), while_expr/while_max_iters/ignore_errors, text_extract_*.
|
||||
- Реализация: [ProviderCallNode.run()](agentui/pipeline/executor.py:1991).
|
||||
|
||||
- incoming — детали ВХОДЯЩЕГО HTTP‑запроса
|
||||
- incoming.method — метод ("POST" и т.п.)
|
||||
- incoming.url — полный URL (в query ключи маскируются для логов)
|
||||
- incoming.path — путь (например, /v1/chat/completions)
|
||||
- incoming.query — строка query без вопросительного знака
|
||||
- incoming.query_params — объект со всеми query‑параметрами
|
||||
- incoming.headers — объект всех заголовков запроса
|
||||
- incoming.json — сырой JSON тела запроса, как прислал клиент
|
||||
- incoming.api_keys — удобные «срезы» ключей
|
||||
- incoming.api_keys.authorization — значение из заголовка Authorization (если есть)
|
||||
- incoming.api_keys.key — значение из query (?key=...) — удобно для Gemini
|
||||
4) RawForward (прямой прокси)
|
||||
- Входы: depends.
|
||||
- Выходы: result, response_text.
|
||||
- Ключи: base_url (может автоопределяться по входящему JSON-вендору), override_path, passthrough_headers, extra_headers, while_expr.
|
||||
- Реализация: [RawForwardNode.run()](agentui/pipeline/executor.py:3105).
|
||||
|
||||
Пример использования в шаблоне:
|
||||
- [[VAR:incoming.api_keys.key]] — возьмёт ключ из строки запроса (?key=...).
|
||||
- [[VAR:incoming.headers.x-api-key]] — возьмёт ключ из заголовка x-api-key (типично для Anthropic).
|
||||
- {{ params.temperature|default(0.7) }} — безопасно подставит число, если не задано во входящих данных.
|
||||
5) Return (оформление финального ответа для клиента)
|
||||
- Входы: depends.
|
||||
- Выходы: result (в формате openai/gemini/claude/auto), response_text (то, что вставили).
|
||||
- Ключи: target_format (auto/openai/gemini/claude), text_template (по умолчанию [[OUT1]]).
|
||||
- Реализация: [ReturnNode.run()](agentui/pipeline/executor.py:3444).
|
||||
|
||||
---
|
||||
Под капотом все узлы гоняет исполнитель «волнами» или итеративно:
|
||||
- Главная точка входа: [PipelineExecutor.run()](agentui/pipeline/executor.py:316).
|
||||
- И есть режим retry/циклов в узлах ProviderCall/RawForward — см. while в Разделе 5.
|
||||
|
||||
## 2) Выходы нод (OUT) и ссылки на них
|
||||
|
||||
Во время исполнения пайплайна результаты предыдущих нод собираются в снапшот OUT и доступны при рендере шаблонов следующих нод:
|
||||
РАЗДЕЛ 2 — ПЕРЕМЕННЫЕ И МАКРОСЫ ([[...]] ПРОТИВ {{ ... }}) С 12 ПРИМЕРАМИ
|
||||
|
||||
- OUT — словарь выходов нод, ключи — id нод в пайплайне (например, "n1", "n2").
|
||||
- OUT.n1, OUT.n2, ... — объект результата соответствующей ноды.
|
||||
Смысл (запомни, ладно?):
|
||||
- [[...]] (квадратные макросы) — текстовая подстановка со строкификацией (всегда превращается в строку, объекты — в JSON-строку).
|
||||
- {{ ... }} (фигурные вставки) — типобезопасная подстановка «как есть» (числа остаются числами, объекты — объектами), а ещё есть фильтр |default(...).
|
||||
|
||||
Формы доступа:
|
||||
- Полная форма: [[OUT:n1.result.choices.0.message.content]]
|
||||
(или фигурными скобками: {{ OUT.n1.result.choices.0.message.content }})
|
||||
- Короткая форма «просто текст»: [[OUT1]], [[OUT2]], ...
|
||||
Это эвристика: берётся самое вероятное «текстовое» поле из результата (см. [_best_text_from_outputs()](agentui/pipeline/templating.py:121)).
|
||||
Доступные макросы (см. [render_template_simple()](agentui/pipeline/templating.py:205)):
|
||||
- [[VAR:путь]] — берёт значение по пути из контекста (context/incoming/params/...).
|
||||
- [[OUT:nodeId(.path)*]] — берёт из выходов ноды (сырой JSON).
|
||||
- [[OUTx]] — короткая форма текста из ноды nX (best-effort).
|
||||
- [[STORE:путь]] — читает из стойкого хранилища (store.*).
|
||||
- [[NAME]] — «голая» переменная: сперва ищется в пользовательских переменных (SetVars), иначе в контексте по пути.
|
||||
- [[PROMPT]] — провайдерный JSON-фрагмент сообщений (см. Раздел 6).
|
||||
- Доп. сахар: img(mime)[[...]] → «data:mime;base64,ЗНАЧЕНИЕ» (см. [templating._IMG_WRAPPER_RE](agentui/pipeline/templating.py:41)).
|
||||
|
||||
Что возвращают встроенные ноды:
|
||||
- ProviderCall:
|
||||
- OUT.nX.result — сырой JSON ответа провайдера
|
||||
- OUT.nX.response_text — уже извлечённый «лучший текст» (строка)
|
||||
- RawForward:
|
||||
- OUT.nX.result — JSON, как пришёл от апстрима (или {"error": "...", "text": "..."} при не‑JSON ответе)
|
||||
Фигурные {{ ... }}:
|
||||
- {{ OUT.n2.result.choices.0.message.content }} — доступ к JSON как к полям.
|
||||
- {{ путь|default(значение) }} — цепочки дефолтов, поддерживает вложенность и JSON-литералы в default(...).
|
||||
|
||||
Подсказка по короткой форме [[OUTx]]:
|
||||
- OpenAI: вернёт choices[0].message.content
|
||||
- Gemini: вернёт candidates[0].content.parts[0].text
|
||||
- Claude: склеит content[].text
|
||||
- Если явных полей нет — выполнит «глубокий поиск» по ключам "text"/"content"
|
||||
12 примеров (пониже пояса — для тех, кто любит копипасту):
|
||||
1) Заголовок авторизации в JSON-строке:
|
||||
{"Authorization":"Bearer [[VAR:incoming.headers.authorization]]"}
|
||||
Объяснение: [[VAR:...]] берёт заголовок из входа (incoming.headers.authorization).
|
||||
|
||||
---
|
||||
2) Провайдерная модель «как пришла» (фигурные):
|
||||
"model": "{{ model }}"
|
||||
Объяснение: {{ ... }} вставляет строку без кавычек лишний раз.
|
||||
|
||||
## 3) Макросы подстановки и синтаксис
|
||||
3) Число по умолчанию:
|
||||
"temperature": {{ incoming.json.temperature|default(0.7) }}
|
||||
Объяснение: default(0.7) сработает, если температуры нет.
|
||||
|
||||
В шаблонах доступны обе формы подстановки:
|
||||
4) Лист по умолчанию:
|
||||
"stop": {{ incoming.json.stop|default([]) }}
|
||||
Объяснение: вставляет [] как настоящий массив.
|
||||
|
||||
1) Квадратные скобки [[ ... ]] — простая подстановка
|
||||
- [[VAR:путь]] — взять значение из контекста по точечному пути
|
||||
Пример: [[VAR:incoming.json.max_tokens]]
|
||||
- [[OUT:путь]] — взять значение из OUT (см. раздел выше)
|
||||
Пример: [[OUT:n1.result.choices.0.message.content]]
|
||||
- [[OUT1]] / [[OUT2]] — короткая форма «просто текст»
|
||||
- [[PROMPT]] — специальный JSON‑фрагмент из Prompt Blocks (см. ниже)
|
||||
5) Короткая вытяжка текста из ноды n2:
|
||||
"note": "[[OUT2]]"
|
||||
Объяснение: [[OUT2]] — best-effort текст из ответа.
|
||||
|
||||
2) Фигурные скобки {{ ... }} — «джинджа‑лайт»
|
||||
- {{ путь }} — взять значение по пути из контекста (или из OUT.* если начать с OUT.)
|
||||
Пример: {{ OUT.n1.result }}
|
||||
- Фильтр по умолчанию: {{ что-то|default(значение) }}
|
||||
Примеры:
|
||||
- {{ params.temperature|default(0.7) }}
|
||||
- {{ incoming.json.stop|default([]) }}
|
||||
- {{ anthropic_version|default('2023-06-01') }} — см. «Опциональные поля» ниже
|
||||
- Фигурные скобки удобны там, где нужно вставить внутрь JSON не строку, а ЧИСЛО/ОБЪЕКТ/МАССИВ без кавычек и/или задать дефолт.
|
||||
6) Точное поле из результата:
|
||||
"[[OUT:n2.result.choices.0.message.content]]"
|
||||
Объяснение: берёт конкретную ветку JSON из OUT ноды n2.
|
||||
|
||||
---
|
||||
7) «Голая» переменная SetVars:
|
||||
"key": "[[MyOpenAiKey]]"
|
||||
Объяснение: имя без VAR/OUT — сперва ищется среди переменных.
|
||||
|
||||
## 4) ProviderCall: Prompt Blocks, pm.* и [[PROMPT]]
|
||||
8) STORE (между прогонами):
|
||||
"{{ STORE.KEEP|default('miss') }}"
|
||||
Объяснение: из стойкого хранилища (если clear_var_store=False).
|
||||
|
||||
Узел ProviderCall собирает ваши Prompt Blocks (блоки вида: роль/текст/вкл‑выкл/порядок) в стандартные «сообщения» и превращает их в структуру для конкретного провайдера.
|
||||
9) Прокинуть запрос как есть в Gemini:
|
||||
[[VAR:incoming.json.contents]]
|
||||
Объяснение: квадратные дадут строку (для template это ок: JSON-строка без лишних кавычек — см. PROMPT).
|
||||
|
||||
Внутри шаблонов этого узла доступны:
|
||||
- pm — «сырьевые» структуры из Prompt Blocks
|
||||
- Для OpenAI:
|
||||
- pm.messages — массив { role, content, name? }
|
||||
- pm.system_text — один большой текст из всех system‑блоков
|
||||
- Для Gemini:
|
||||
- pm.contents — массив { role: "user"|"model", parts: [{text}] }
|
||||
- pm.systemInstruction — объект вида { parts: [{text}] } или пустой {}
|
||||
- pm.system_text — строка
|
||||
- Для Claude:
|
||||
- pm.system_text — строка
|
||||
- pm.system — то же самое (удобно подставлять в поле "system")
|
||||
- pm.messages — массив { role: "user"|"assistant", content: [{type:"text", text:"..."}] }
|
||||
10) JSON-путь с фигурными:
|
||||
{{ OUT.n1.result.obj.value|default(0) }}
|
||||
Объяснение: берёт число или 0.
|
||||
|
||||
- [[PROMPT]] — готовый JSON‑фрагмент на основе pm, безопасный для вставки внутрь шаблона:
|
||||
- OpenAI → подставит: "messages": [...]
|
||||
- Gemini → подставит: "contents": [...], "systemInstruction": {...}
|
||||
- Claude → подставит: "system": "...", "messages": [...]
|
||||
11) Картинка из base64 переменной (img()):
|
||||
"image": "[[IMG_B64]])"
|
||||
Объяснение: заменится на data:image/jpeg;base64,....
|
||||
|
||||
Зачем это нужно?
|
||||
- Чтобы 1) удобно собирать промпт из визуальных блоков, 2) не «сломать» JSON руками.
|
||||
Вы можете вручную использовать {{ pm.* }}, но [[PROMPT]] — рекомендуемый и самый безопасный вариант.
|
||||
12) Сложная строка с несколькими макросами:
|
||||
"msg": "User=[[VAR:chat.last_user]] | Echo=[[OUT1]]"
|
||||
Объяснение: комбинируй сколько хочешь, лишь бы JSON остался валидным.
|
||||
|
||||
---
|
||||
|
||||
## 5) Частые сценарии и примеры
|
||||
РАЗДЕЛ 3 — SETVARS: ВЫРАЖЕНИЯ, РАЗРЕШЁННЫЕ ФУНКЦИИ, ОПАСНО НЕ БУДЕМ (10+ ПРИМЕРОВ)
|
||||
|
||||
Примеры ниже можно вклеивать в поле «template» ноды ProviderCall. Они уже используют [[PROMPT]] и аккуратные дефолты.
|
||||
Где код: [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:1197). Он парсит мини-язык через AST, ничего небезопасного не позволит.
|
||||
|
||||
OpenAI (POST /v1/chat/completions):
|
||||
```
|
||||
{
|
||||
"model": "{{ model }}",
|
||||
[[PROMPT]],
|
||||
"temperature": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},
|
||||
"top_p": {{ incoming.json.top_p|default(params.top_p|default(1)) }},
|
||||
"max_tokens": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},
|
||||
"stop": {{ incoming.json.stop|default(params.stop|default([])) }}
|
||||
}
|
||||
```
|
||||
Разрешено:
|
||||
- Литералы: числа, строки, true/false/null (JSON-стиль), списки [...], объекты {...}.
|
||||
- Операции: + - * / // % и унарные + -, сравнения == != < <= > >=, логика and/or.
|
||||
- Вызовы ТОЛЬКО упомянутых функций (без kwargs, без *args):
|
||||
- rand() → float [0,1)
|
||||
- randint(a,b) → int в [a,b]
|
||||
- choice(list) → элемент списка/кортежа
|
||||
- from_json(x) → распарсить строку JSON
|
||||
- jp(value, path, join_sep="\n") → извлечь по JSONPath (см. Раздел 7)
|
||||
- jp_text(value, path, join_sep="\n") → JSONPath + склейка строк
|
||||
- file_b64(path) → прочитать файл и вернуть base64-строку
|
||||
- data_url(b64, mime) → "data:mime;base64,b64"
|
||||
- file_data_url(path, mime?) → прочитать файл и собрать data URL
|
||||
Подсказка: аргументы функций прогоняются через шаблон рендера, так что внутрь jp/… можно передавать строки с [[...]]/{{...}} — они сначала развернутся.
|
||||
|
||||
Gemini (POST /v1beta/models/{model}:generateContent):
|
||||
```
|
||||
{
|
||||
"model": "{{ model }}",
|
||||
[[PROMPT]],
|
||||
"safetySettings": {{ incoming.json.safetySettings|default([]) }},
|
||||
"generationConfig": {
|
||||
"temperature": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},
|
||||
"topP": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},
|
||||
"maxOutputTokens": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},
|
||||
"stopSequences": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }}
|
||||
}
|
||||
}
|
||||
```
|
||||
Подсказка: ключ Gemini удобно брать из строки запроса:
|
||||
в endpoint используйте …?key=[[VAR:incoming.api_keys.key]]
|
||||
Нельзя:
|
||||
- Любые имена/доступы к атрибутам/индексации вне списка/словаря литералом.
|
||||
- Любые другие функции, чем перечисленные.
|
||||
- kwargs/starargs.
|
||||
|
||||
Claude (POST /v1/messages):
|
||||
```
|
||||
{
|
||||
"model": "{{ model }}",
|
||||
[[PROMPT]],
|
||||
"temperature": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},
|
||||
"top_p": {{ incoming.json.top_p|default(params.top_p|default(1)) }},
|
||||
"max_tokens": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},
|
||||
"system": {{ pm.system|default("") }}
|
||||
}
|
||||
```
|
||||
Подсказка: ключ Anthropic обычно передают в заголовке x-api-key.
|
||||
В UI‑пресете это поле уже есть в headers.
|
||||
10+ примеров SetVars (mode=expr):
|
||||
1) Чистая математика:
|
||||
128 + 64
|
||||
|
||||
RawForward (прямой форвард входящего запроса):
|
||||
- Поля конфигурации base_url, override_path, extra_headers проходят через те же макросы, поэтому можно подставлять динамику:
|
||||
- base_url: https://generativelanguage.googleapis.com
|
||||
- override_path: [[VAR:incoming.path]] (или задать свой)
|
||||
- extra_headers (JSON): `{"X-Trace":"req-{{ incoming.query_params.session|default('no-session') }}"}`
|
||||
2) Случайное число:
|
||||
rand()
|
||||
|
||||
---
|
||||
3) Случайное из списка:
|
||||
choice(["red","green","blue"])
|
||||
|
||||
## 6) Опциональные/редкие поля, о которых стоит знать
|
||||
4) Безопасный int-диапазон:
|
||||
randint(10, 20)
|
||||
|
||||
- anthropic_version — используется как HTTP‑заголовок для Claude ("anthropic-version"). В тело запроса не вставляется.
|
||||
Если нужен дефолт, задавайте его в headers (например, в конфиге ноды/шаблоне заголовков). В шаблонах тела используйте [[PROMPT]]/pm.* без anthropic_version.
|
||||
5) from_json + доступ через jp:
|
||||
jp(from_json("{\"a\":{\"b\":[{\"x\":1},{\"x\":2}]}}"), "a.b.1.x") → 2
|
||||
|
||||
- stream — в MVP стриминг отключён, сервер принудительно не стримит ответ.
|
||||
В шаблонах можно встретить поля stream, но по умолчанию они не включены.
|
||||
6) jp_text (склейка строк через « | »):
|
||||
jp_text(from_json("{\"items\":[{\"t\":\"A\"},{\"t\":\"B\"},{\"t\":\"C\"}]}"), "items.*.t", " | ") → "A | B | C"
|
||||
|
||||
---
|
||||
7) Вытянуть из OUT (с шаблонной подстановкой):
|
||||
jp({{ OUT.n2.result }}, "choices.0.message.content") → текст первого ответа
|
||||
|
||||
## 7) Когда использовать [[...]] и когда {{ ... }}
|
||||
8) Собрать data URL из файла:
|
||||
file_data_url("./img/cat.png", "image/png")
|
||||
|
||||
- Внутрь JSON как ОБЪЕКТ/МАССИВ/ЧИСЛО: используйте {{ ... }}
|
||||
(фигурные скобки вставляют «как есть», без кавычек, и умеют |default(...))
|
||||
- Для строк/URL/заголовков/простых значений: можно использовать [[...]]
|
||||
(квадратные скобки удобны и короче писать)
|
||||
9) Ручная сборка data URL из base64:
|
||||
data_url([[IMG_B64]], "image/jpeg")
|
||||
|
||||
10) Преобразование строки JSON:
|
||||
from_json("[1,2,3]") → список [1,2,3]
|
||||
|
||||
11) Комбо с логикой:
|
||||
(rand() > 0.5) and "HEADS" or "TAILS"
|
||||
|
||||
12) Вложенные вызовы:
|
||||
choice(jp(from_json("[{\"v\":10},{\"v\":20}]"), "*.v")) → 10 или 20
|
||||
|
||||
Результат SetVars попадает в:
|
||||
- Текущие «user vars» (сразу доступны как [[NAME]] и {{ NAME }}).
|
||||
- STORE (персистентно) — см. Раздел 8, если clear_var_store=False.
|
||||
|
||||
|
||||
РАЗДЕЛ 4 — IF: ВЫРАЖЕНИЯ, ОПЕРАТОРЫ, 12 ГРОМКИХ ПРИМЕРОВ
|
||||
|
||||
Парсер условий: [eval_condition_expr()](agentui/pipeline/templating.py:391). Он превращает видимые тобой токены в безопасное AST и вычисляет.
|
||||
|
||||
Операторы:
|
||||
- Логика: && (and), || (or), ! (not)
|
||||
- Сравнение: ==, !=, <, <=, >, >=
|
||||
- Специальный contains (как функция contains(a,b)): для строк — подстрока; для списков — membership.
|
||||
- Скобки ( ... )
|
||||
- Литералы: числа, "строки" или 'строки' (без экранирования внутри), true/false/null (через макросы из контекста).
|
||||
- Макросы: [[...]] и {{ ... }} допустимы прямо внутри выражения (они сначала раскрываются в значения).
|
||||
|
||||
12 примеров (да-да, трижды проверено, хватит ныть):
|
||||
1) Проверить, что [[OUT1]] содержит «ok»:
|
||||
[[OUT1]] contains "ok"
|
||||
|
||||
2) Проверка статуса:
|
||||
{{ OUT.n2.result.status|default(0) }} >= 200 && {{ OUT.n2.result.status|default(0) }} < 300
|
||||
|
||||
3) Инверсия:
|
||||
!([[OUT3]] contains "error")
|
||||
|
||||
4) Сравнить переменную:
|
||||
[[LANG]] == "ru"
|
||||
|
||||
5) Двойная логика:
|
||||
([[MSG]] contains "Hello") || ([[MSG]] contains "Привет")
|
||||
|
||||
6) Цепочка со скобками:
|
||||
( [[CITY]] == "Moscow" && {{ params.max_tokens|default(0) }} > 0 ) || [[FALLBACK]] == "yes"
|
||||
|
||||
7) Списки и contains:
|
||||
contains(["a","b","c"], "b")
|
||||
|
||||
8) Числа и сравнения:
|
||||
{{ OUT.n1.result.value|default(0) }} >= 10
|
||||
|
||||
9) Пустые значения:
|
||||
{{ missing|default("") }} == ""
|
||||
|
||||
10) Комбо macOS:
|
||||
contains([[VAR:incoming.url]], "/v1/") && ([[VAR:incoming.method]] == "POST")
|
||||
|
||||
11) Несколько слоёв default():
|
||||
{{ incoming.json.limit|default(params.limit|default(100)) }} > 50
|
||||
|
||||
12) Сложное условие с OUT пути:
|
||||
[[OUT:n2.result.choices.0.message.content]] contains "done"
|
||||
|
||||
Помни: If только выставляет флаги true/false на выходах. «Дети» с входом depends="nIf.true" запустятся только если условие истинно.
|
||||
|
||||
РАЗДЕЛ 4.1 — СПРАВОЧНИК ОПЕРАТОРОВ IF/WHILE (ПРОСТЫМИ СЛОВАМИ)
|
||||
|
||||
- !A — «не A» (инверсия). Пример: !( [[OUT3]] contains "err" ) → «строка из [[OUT3]] НЕ содержит "err"».
|
||||
- A != B — «A не равно B». Пример: [[MODEL]] != "gemini-2.5-pro".
|
||||
- A && B — «A и B одновременно».
|
||||
- A || B — «A или B» (достаточно одного истинного).
|
||||
- contains(A, B) — специальный оператор:
|
||||
- если A — список/множество, это membership: contains(["a","b"], "a") → true
|
||||
- иначе — проверка подстроки: contains("abc", "b") → true
|
||||
- Запись "X contains Y" эквивалентна contains(X, Y).
|
||||
- Скобки управляют приоритетами: !(A || B) отличается от (!A || B).
|
||||
|
||||
Где какой «язык» используется:
|
||||
- Строковые поля (template, headers/extra_headers, base_url/override_path, Return.text_template, строки prompt_preprocess, сегменты prompt_combine) — это шаблоны с подстановками [[...]] и {{ ... }}.
|
||||
- If.expr и while_expr — булевы выражения (&&, ||, !, ==, !=, <, <=, >, >=, contains, скобки) и допускают макросы [[...]] / {{ ... }} внутри.
|
||||
- SetVars (mode=expr) — отдельный безопасный мини-язык (арифметика + - * / // %, and/or, сравнения) и whitelisted-функции: rand, randint, choice, from_json, jp, jp_text, file_b64, data_url, file_data_url.
|
||||
|
||||
Диагностика:
|
||||
- В логах If/While печатается expanded — строковое раскрытие макросов — и result (true/false).
|
||||
- Ошибка парсера (например, несбалансированные скобки) выводится как if_error/while_error и приводит к result=false.
|
||||
|
||||
|
||||
РАЗДЕЛ 5 — WHILE В НОДАХ PROVIDERCALL/RAWFORWARD (РЕТРАЙ, ЦИКЛЫ). МОЖНО ЛОМАЕТЬ IF В БОЛЬШИНСТВЕ СЛУЧАЕВ (12 ПАТТЕРНОВ)
|
||||
|
||||
Где логика:
|
||||
- Для ProviderCall: [_providercall_run_with_while()](agentui/pipeline/executor.py:3589)
|
||||
- Для RawForward: [_rawforward_run_with_while()](agentui/pipeline/executor.py:3741)
|
||||
- Обёртка делает «do-while»: первая итерация выполняется всегда, потом условие проверяется перед следующей.
|
||||
|
||||
Ключи конфигурации у ноды:
|
||||
- while_expr (строка условие как в If)
|
||||
- while_max_iters (safety, по умолчанию 50)
|
||||
- ignore_errors (True — не падать на исключениях, а возвращать result={"error":"..."} и продолжать цикл)
|
||||
|
||||
Добавочные локальные переменные и семантика внутри цикла:
|
||||
- [[cycleindex]] (int, 0..N) — индекс текущей итерации.
|
||||
- [[WAS_ERROR]] (bool) — при проверке while_expr на i>0 равен «была ли ошибка на предыдущей итерации». Внутри самой итерации на старте содержит то же значение и обновляется для следующей проверки по факту результата.
|
||||
- Подсказка: для ретраев по ошибкам используйте «WAS_ERROR» (а не «!WAS_ERROR»); включайте ignore_errors:true, чтобы исключения не прерывали цикл.
|
||||
|
||||
Глобальные переменные, которые нода выставит после цикла для «детей»:
|
||||
- [[WAS_ERROR__nX]] — была ли ошибка на последней итерации
|
||||
- [[CYCLEINDEX__nX]] — последний индекс итерации (например 2 если были 0,1,2)
|
||||
|
||||
12 паттернов:
|
||||
1) Повтори до 3 раз:
|
||||
while_expr: "cycleindex < 3"
|
||||
|
||||
2) Повтори, пока OUT3 не содержит «ok»:
|
||||
while_expr: "!([[OUT3]] contains \"ok\") && cycleindex < 10"
|
||||
|
||||
3) Ретраи на ошибках сети:
|
||||
ignore_errors: true
|
||||
while_expr: "WAS_ERROR || ({{ OUT.n4.result.status|default(0) }} >= 500)"
|
||||
|
||||
4) Комбо с внешним If — заменяем If:
|
||||
Вместо If.true/false делай while_expr, который набивает нужный результат (например, пока не получишь 2xx от апстрима).
|
||||
|
||||
5) Изменение запроса по итерации:
|
||||
Используй [[cycleindex]] внутри template (например, «page»: {{ vars.page_start|default(1) }} + cycleindex).
|
||||
|
||||
6) Дожидаться готовности ресурса:
|
||||
while_expr: "!([[OUT4]] contains \"READY\") && cycleindex < 30"
|
||||
|
||||
7) Прерывание на плохих данных:
|
||||
while_expr: "!([[OUT7]] contains \"fatal\") && cycleindex < 5"
|
||||
|
||||
8) Backoff вручную (временную задержку делай sleep_ms):
|
||||
sleep_ms: {{ cycleindex }} * 500
|
||||
|
||||
9) Прокси-ретрай RawForward по тексту ответа:
|
||||
ignore_errors: true
|
||||
while_expr: "([[OUT:n1.result.text]] contains \"try again\") && cycleindex < 4"
|
||||
|
||||
10) Gemini «Stream failed to …» из коробки:
|
||||
while_expr: "([[OUT3]] contains "Stream failed to") || ([[OUT3]] contains "gemini-2.5-pro")"
|
||||
(ровно как в твоих пресетах)
|
||||
Добавь " || WAS_ERROR" если хочешь ретраить также на исключениях (при ignore_errors: true).
|
||||
|
||||
11) Проверка флага из STORE:
|
||||
while_expr: "{{ STORE.SHALL_CONTINUE|default(false) }} && cycleindex < 10"
|
||||
|
||||
12) Сложный сценарий: first success wins
|
||||
while_expr: "!([[OUT7]] contains \"success\") && cycleindex < 5"
|
||||
Пояснение: крути пока не словишь success, но не более 5.
|
||||
|
||||
Эти while позволяют чаще не городить отдельный If-гейт — ты просто делаешь один узел, который сам повторяет себя, пока условие не «устаканится». Ну и не забудь выставить ignore_errors там, где ретраи — оправдано.
|
||||
|
||||
|
||||
РАЗДЕЛ 5.1 — WAS_ERROR В WHILE ПОСЛЕ ОБНОВЛЕНИЯ (ПОВЕДЕНИЕ И РЕЦЕПТЫ)
|
||||
|
||||
- Семантика do-while:
|
||||
- Итерация i=0 выполняется всегда.
|
||||
- Начиная с i>0, перед проверкой while_expr двигатель подставляет в [[WAS_ERROR]] значение «была ли ошибка (исключение) на предыдущей итерации».
|
||||
- Как учитываются исключения:
|
||||
- При ignore_errors: true исключения внутри итерации не прерывают ноду; результат оформляется как result={"error":"..."}.
|
||||
- Такое событие считается ошибкой и установит [[WAS_ERROR]]=true для следующей проверки условия.
|
||||
- Рецепты:
|
||||
- Ретраить только при ошибке (до 5 раз): while_expr: "WAS_ERROR && (cycleindex < 5)"
|
||||
- Ретраить при ошибке ИЛИ по признаку в ответе: while_expr: "WAS_ERROR || ([[OUT3]] contains "Stream failed to") || ({{ OUT.n3.result.status|default(0) }} >= 500)"
|
||||
- NB: "!WAS_ERROR" означает «продолжать, если ошибки НЕ было» — это обратное «ретраю при ошибке».
|
||||
- Диагностика:
|
||||
- В логах видны строки вида TRACE while: ... expr='...' expanded='...' index=i result=true/false.
|
||||
- Ошибка парсера (например, несбалансированные скобки) логируется как while_error и приводит к result=false.
|
||||
|
||||
РАЗДЕЛ 6 — PROMPT_COMBINE (DSL «&»): ВЫ ТАМ ЛЮБИТЕ МАГИЮ? ВОТ ОНА, ЧТОБЫ НЕ ЛЕПИТЬ РУКАМИ (12 ПРИМЕРОВ)
|
||||
|
||||
Где реализовано: в [ProviderCallNode.run()](agentui/pipeline/executor.py:1991) — см. кусок обработки combine_raw.
|
||||
|
||||
Идея:
|
||||
- Поле prompt_combine — строка вида «СЕГМЕНТ1 & СЕГМЕНТ2 & ...».
|
||||
- СЕГМЕНТ — это либо [[PROMPT]] (спец сегмент текущих Prompt Blocks), либо любая строка/JSON/список сообщений, либо [[VAR:incoming.*]] и т.п.
|
||||
- Для каждой цели (provider) всё приводится к нужным структурам:
|
||||
- openai → messages: [...]
|
||||
- gemini → contents: [...] (+ systemInstruction)
|
||||
- claude → messages: [...] (+ system)
|
||||
- Системный текст (из openai.system / claude.system / gemini.systemInstruction) автоматически извлекается и объединяется.
|
||||
|
||||
Позиционирование:
|
||||
- Можно добавить директиву @pos=prepend | append | N | -1
|
||||
- Она управляет тем, куда вставить элементы из сегмента внутри собираемого массива сообщений/контента. -1 — вставить перед последним.
|
||||
|
||||
Фильтрация:
|
||||
- Пустые сообщения выкидываются (без пустых текстов).
|
||||
- Изображения (inlineData и т.п.) сохраняются.
|
||||
|
||||
12 примеров (разные таргеты и трюки):
|
||||
1) Классика: входящие Gemini contents + твой PROMPT (OpenAI target)
|
||||
"[[VAR:incoming.json.contents]] & [[PROMPT]]"
|
||||
Результат: messages содержит и конвертированные входящие (model→assistant), и твои blocks.
|
||||
|
||||
2) PROMPT первым (OpenAI):
|
||||
"[[PROMPT]]@pos=prepend & [[VAR:incoming.json.contents]]"
|
||||
Результат: system из PROMPT — в самом начале messages.
|
||||
|
||||
3) Вставка в конкретный индекс (OpenAI):
|
||||
"[[VAR:incoming.json.messages]] & [[PROMPT]]@pos=1"
|
||||
Результат: вторым элементом окажутся твои блоки.
|
||||
|
||||
4) Негативный индекс (OpenAI):
|
||||
"[[VAR:incoming.json.messages]] & [[PROMPT]]@pos=-1"
|
||||
Результат: перед самым последним.
|
||||
|
||||
5) Для Gemini: openai.messages + PROMPT
|
||||
"[[VAR:incoming.json.messages]] & [[PROMPT]]"
|
||||
Результат: contents и systemInstruction соберутся; system из incoming и PROMPT сольются.
|
||||
|
||||
6) Для Claude: openai.messages + PROMPT
|
||||
"[[VAR:incoming.json.messages]] & [[PROMPT]]"
|
||||
Результат: messages + top-level system (как строка/блоки).
|
||||
|
||||
7) Сырый JSON-строковый сегмент:
|
||||
"{\"messages\": [{\"role\":\"user\",\"content\":\"Hi\"}] } & [[PROMPT]]"
|
||||
Результат: корректно распарсится и слепится.
|
||||
|
||||
8) Списковая форма сегмента:
|
||||
"[{\"role\":\"user\",\"content\":\"A\"},{\"role\":\"assistant\",\"content\":\"B\"}] & [[PROMPT]]"
|
||||
Результат: нормализуется под целевой провайдер.
|
||||
|
||||
9) Системные тексты из разных форматов — сольются:
|
||||
"[{\"messages\":[{\"role\":\"system\",\"content\":\"SYS IN\"}]}] & [[PROMPT]]"
|
||||
Результат: system_text включает обе части.
|
||||
|
||||
10) Подмешать внешнюю систему в Claude без top-level system (claude_no_system):
|
||||
В конфиге ноды поставь claude_no_system=true — тогда system из PROMPT положим первым user-сообщением.
|
||||
|
||||
11) Очистка пустых:
|
||||
Если твой сегмент даёт пустые тексты — они выкинутся и JSON не сломается. Не плачь.
|
||||
|
||||
12) Микс строк + JSON:
|
||||
"Просто строка & [[PROMPT]]"
|
||||
Результат: «Просто строка» упакуется корректно (как user/text) в нужную структуру провайдера.
|
||||
|
||||
И да, это позволяет не писать руками половину «склейки» в template — ты описываешь, откуда что привнести, а движок доведёт до провайдерного формата.
|
||||
|
||||
|
||||
РАЗДЕЛ 6.1 — PROMPT_PREPROCESS (pre‑merge DSL: фильтрация/позиционирование сегментов ДО prompt_combine)
|
||||
|
||||
Где реализовано: в [ProviderCallNode.run()](agentui/pipeline/executor.py:2083). Это выполняется перед сборкой [[PROMPT]]/prompt_combine. Поле конфигурации ноды: prompt_preprocess (многострочное).
|
||||
|
||||
Идея:
|
||||
- Каждая строка prompt_preprocess задаёт «пред‑сегмент», который будет вставлен в будущий массив сообщений/контента до обработки [prompt_combine (DSL &)](agentui/pipeline/executor.py:2230).
|
||||
- Эти пред‑сегменты конвертируются под целевого провайдера (openai/gemini/claude) так же, как и сегменты prompt_combine, и «вплетаются» первыми.
|
||||
- Если prompt_combine пуст — используются только пред‑сегменты (и при отсутствии пред‑сегментов — исходные Prompt Blocks как раньше).
|
||||
|
||||
Синтаксис строки:
|
||||
SEGMENT [delKeyContains "needle"] [delpos=prepend|append|N|-1] [case=ci|cs] [pruneEmpty]
|
||||
|
||||
- SEGMENT — строка/JSON/список, допускаются макросы [[...]] и {{ ... }}.
|
||||
- delKeyContains "needle" — удалить ключи в любом месте объекта, если строковое представление их значения содержит needle (поддерживаются несколько delKeyContains).
|
||||
- case=ci|cs — управление регистром для contains; по умолчанию case=ci (без учёта регистра).
|
||||
- pruneEmpty — удалять опустевшие {} / [] после чистки (кроме корня); по умолчанию выключено.
|
||||
- delpos=... — позиция вставки элементов пред‑сегмента в массив (как @pos у prompt_combine): prepend | append | N | -1; по умолчанию append.
|
||||
|
||||
Поведение:
|
||||
- Для каждого SEGMENT рендерятся макросы, затем выполняется попытка json.loads() (в т.ч. для двойной JSON‑строки).
|
||||
- После этого применяется фильтрация delKeyContains (если задана), с учётом case и pruneEmpty.
|
||||
- Итог вставляется в текущий собираемый массив сообщений/контента в позицию delpos (prepend/append/индекс/отрицательный индекс).
|
||||
- Системный текст, присутствующий внутри сегмента (Gemini systemInstruction / OpenAI role=system / Claude system), автоматически извлекается и сольётся, как в prompt_combine.
|
||||
|
||||
Примеры:
|
||||
- {{ pm.contents }} — вставит массив как настоящий массив (без кавычек)
|
||||
- {{ params.temperature|default(0.7) }} — безопасный дефолт для числа
|
||||
- [[VAR:incoming.api_keys.authorization]] — быстро подставить строку Authorization
|
||||
1) Удалить поля, где значение содержит «Текст», и вставить перед последним:
|
||||
[[VAR:incoming.json.contents]] delKeyContains "Текст" delpos=-1
|
||||
|
||||
---
|
||||
2) Удалить «debug» с учётом регистра и подчистить пустые контейнеры:
|
||||
[[VAR:incoming.json.messages]] delKeyContains "debug" case=cs pruneEmpty
|
||||
|
||||
## 8) Отладка и рекомендации
|
||||
3) Несколько подстрок + вставка в начало:
|
||||
[[VAR:incoming.json]] delKeyContains "кеш" delKeyContains "cache" delpos=prepend
|
||||
|
||||
- ProviderCall печатает в консоль DEBUG сведения: выбранный провайдер, конечный URL, первые символы тела запроса — удобно для проверки корректности шаблона.
|
||||
- Если «ничего не подставилось»:
|
||||
1) Проверьте, что вы НЕ передаёте сырое входное тело напрямую в ProviderCall (узел строит тело из шаблона и Prompt Blocks).
|
||||
2) Убедитесь, что итоговый JSON валиден (закрывающие скобки, запятые).
|
||||
3) Проверьте точность путей в макросах (OUT vs OUTx, правильные id нод n1/n2/...).
|
||||
- Для ссылок на выходы предыдущих нод используйте [[OUT1]] как «просто текст», либо полные пути [[OUT:n1...]] для точного фрагмента.
|
||||
4) Смешанный пайплайн: сначала пред‑сегменты, затем:
|
||||
prompt_combine: "[[VAR:incoming.json.messages]] & [[PROMPT]]@pos=1"
|
||||
|
||||
---
|
||||
Диагностика:
|
||||
- В логи (SSE) слать событие "prompt_preprocess" с полями lines/used/removed_keys. Смотри [ProviderCallNode.run()](agentui/pipeline/executor.py:2211).
|
||||
|
||||
## 9) Быстрая памятка по ключам доступа
|
||||
Ограничения и заметки:
|
||||
- Это локальная предобработка именно сегментов для промпта (не глобальная фильтрация всего тела запроса).
|
||||
- Если пред‑сегменты и prompt_combine пусты — результат совпадает с классическим [[PROMPT]] (Prompt Blocks).
|
||||
РАЗДЕЛ 7 — JSON PATH (НАШ ПРОСТОЙ ДИАЛЕКТ) + 12 ПРИМЕРОВ
|
||||
|
||||
- Gemini: [[VAR:incoming.api_keys.key]] — рекомендовано; ключ приходит в query (?key=...).
|
||||
- OpenAI: [[VAR:incoming.headers.authorization]] (или [[VAR:incoming.api_keys.authorization]]) — стандартный Bearer‑токен.
|
||||
- Anthropic: [[VAR:incoming.headers.x-api-key]] — ключ в заголовке.
|
||||
Где реализовано: [_json_path_extract()](agentui/pipeline/executor.py:1475).
|
||||
|
||||
---
|
||||
Синтаксис (очень простой):
|
||||
- Путь вида a.b.c — точки для полей объектов.
|
||||
- Числовой индекс для массивов: items.0.title
|
||||
- Шаг «*» разворачивает все значения словаря или все элементы списка: items.*.title
|
||||
- Если на каком-то шаге ничего не найдено — вернёт None.
|
||||
- jp(...) → отдаёт найденное значение или список значений, jp_text(...) → склеит строки через join_sep (см. [_stringify_join()](agentui/pipeline/executor.py:1517)).
|
||||
|
||||
## 10) Ссылки на реализацию (для интересующихся деталями)
|
||||
12 примеров:
|
||||
1) Обычный путь:
|
||||
"a.b.c" на {"a":{"b":{"c":10}}} → 10
|
||||
|
||||
- Контекст (переменные): [build_macro_context()](agentui/api/server.py:142)
|
||||
- Исполнение пайплайна, зависимости, снапшоты OUT: [PipelineExecutor.run()](agentui/pipeline/executor.py:136)
|
||||
- Узел провайдера (Prompt Blocks → провайдер): [ProviderCallNode.run()](agentui/pipeline/executor.py:650)
|
||||
- PM‑структуры для шаблонов: [ProviderCallNode._blocks_struct_for_template()](agentui/pipeline/executor.py:592)
|
||||
- Подстановка [[PROMPT]], макросы, дефолты: [render_template_simple()](agentui/pipeline/templating.py:187)
|
||||
- Короткая форма [[OUTx]] и поиск «лучшего текста»: [_best_text_from_outputs()](agentui/pipeline/templating.py:124)
|
||||
- Прямой форвард входящего запроса: [RawForwardNode.run()](agentui/pipeline/executor.py:833)
|
||||
- Детекция вендора по входному payload: [detect_vendor()](agentui/common/vendors.py:8)
|
||||
2) Индекс массива:
|
||||
"items.1" на {"items":[10,20,30]} → 20
|
||||
|
||||
Удачного редактирования!
|
||||
---
|
||||
## Пользовательские переменные (SetVars) — «для людей»
|
||||
3) Вложено:
|
||||
"items.1.title" на items=[{title:"A"},{title:"B"}] → "B"
|
||||
|
||||
Задача: в начале пайплайна положить свои значения и потом использовать их в шаблонах одной строкой — например [[MY_KEY]] или {{ MAX_TOKENS }}.
|
||||
4) Звёздочка по массиву:
|
||||
"items.*.title" на items=[{title:"A"},{title:"B"}] → ["A","B"]
|
||||
|
||||
Где это в UI
|
||||
- В левой панели добавьте ноду SetVars и откройте её в инспекторе.
|
||||
- Жмите «Добавить переменную», у каждой переменной есть три поля:
|
||||
- name — имя переменной (латинские буквы/цифры/подчёркивание, не с цифры): MY_KEY, REGION, MAX_TOKENS
|
||||
- mode — режим обработки значения:
|
||||
- string — строка, в которой работают макросы ([[...]] и {{ ... }})
|
||||
- expr — «мини‑формула» без макросов (подробнее ниже)
|
||||
- value — собственно значение
|
||||
5) Звёздочка по объекту:
|
||||
"*.*.name" на {"x":{"name":"X"}, "y":{"name":"Y"}} → ["X","Y"]
|
||||
|
||||
Как потом вставлять переменные
|
||||
- Для строк (URL/заголовки/текст) — квадратные скобки: [[MY_KEY]]
|
||||
- Для чисел/массивов/объектов — фигурные скобки: {{ MAX_TOKENS }}, {{ GEN_CFG }}
|
||||
6) Смешанный:
|
||||
"candidates.0.content.parts.*.text" (Gemini) → все тексты
|
||||
|
||||
Примеры «как надо»
|
||||
- Переменная-строка (mode=string):
|
||||
- name: AUTH
|
||||
- value: "Bearer [[VAR:incoming.headers.authorization]]"
|
||||
- Использование в заголовке: "Authorization": "[[AUTH]]"
|
||||
- Переменная-число (mode=expr):
|
||||
- name: MAX_TOKENS
|
||||
- value: 128 + 64
|
||||
- Использование в JSON: "max_tokens": {{ MAX_TOKENS }}
|
||||
- Переменная-объект (mode=expr):
|
||||
- name: GEN_CFG
|
||||
- value: {"temperature": 0.3, "topP": 0.9, "safe": true}
|
||||
- Использование: "generationConfig": {{ GEN_CFG }}
|
||||
7) Несуществующее поле:
|
||||
"obj.miss" → None
|
||||
|
||||
Важно про два режима
|
||||
- string — это «шаблон». Внутри работают все макросы ([[VAR:...]], [[OUT:...]], [[PROMPT]], {{ ... }}). Значение прогоняется через рендер [render_template_simple()](agentui/pipeline/templating.py:184).
|
||||
- expr — это «мини‑формула». Внутри НЕТ макросов и НЕТ доступа к контексту; только литералы и операции (см. ниже). Вычисляет значение безопасно — без eval, на белом списке AST (реализация: [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:291)).
|
||||
8) Склейка текстов (jp_text):
|
||||
jp_text(value, "items.*.desc", " | ") → "a | b | c"
|
||||
|
||||
Что умеет expr (мини‑формулы)
|
||||
- Числа и арифметика: 128 + 64, (5 * 60) + 30, 42 % 2, -5, 23 // 10
|
||||
- Строки: "eu" + "-central" → "eu-central" (строки склеиваем знаком +)
|
||||
- Булева логика: (2 < 3) and (10 % 2 == 0), 1 < 2 < 5
|
||||
- Коллекции: ["fast", "safe"], {"temperature": 0.3, "topP": 0.9, "safe": true}
|
||||
- JSON‑литералы: true/false/null, объекты и массивы — если выражение является чистым JSON, оно разбирается напрямую (без макросов), т.е. true→True, null→None и т.п.
|
||||
- Запрещено: функции (кроме специально разрешённых ниже), доступ к переменным/контексту, атрибуты/индексация/условные выражения.
|
||||
9) Взять base64 из inlineData:
|
||||
"candidates.0.content.parts.1.inlineData.data"
|
||||
|
||||
Рандом в expr
|
||||
- В expr доступны три простые функции случайности:
|
||||
- rand() → число с плавающей точкой в диапазоне [0, 1)
|
||||
- randint(a, b) → целое число от a до b включительно
|
||||
- choice(list) → случайный элемент из списка/кортежа
|
||||
- Примеры:
|
||||
- name: RAND_F, mode: expr, value: rand()
|
||||
- "temperature": {{ RAND_F }}
|
||||
- name: DICE, mode: expr, value: randint(1, 6)
|
||||
- "dice_roll": {{ DICE }}
|
||||
- name: PICK_MODEL, mode: expr, value: choice(["gpt-4o-mini", "gpt-4o", "o3-mini"])
|
||||
- "model": "[[PICK_MODEL]]"
|
||||
- Зерна/seed нет — каждый запуск выдаёт новое значение.
|
||||
10) Несколько уровней массивов:
|
||||
"a.*.b.*.c"
|
||||
|
||||
«Почему в expr нельзя подставлять переменные/макросы?»
|
||||
- Для безопасности и предсказуемости: expr — это закрытый мини‑язык без окружения.
|
||||
- Если нужно использовать другие переменные/макросы — делайте это в режиме string (там всё рендерится шаблонизатором).
|
||||
- Технические детали: защита реализована в [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:291), а вставка string‑значений — через [render_template_simple()](agentui/pipeline/templating.py:184).
|
||||
11) Индекс вне границ:
|
||||
"items.99" → None
|
||||
|
||||
Как это работает внутри (если интересно)
|
||||
- SetVars исполняется как обычная нода пайплайна и отдаёт {"vars": {...}}.
|
||||
- Исполнитель добавляет эти значения в контекст для последующих нод как context.vars (см. [PipelineExecutor.run()](agentui/pipeline/executor.py:131)).
|
||||
- При рендере шаблонов:
|
||||
- [[NAME]] и {{ NAME }} подставляются с приоритетом из пользовательских переменных (см. обработку в [render_template_simple()](agentui/pipeline/templating.py:184)).
|
||||
- Сам SetVars считает переменные в порядке списка и возвращает их одним пакетом (внутри одной ноды значения не зависят друг от друга).
|
||||
12) Список/объект → строка (jp_text сам постарается найти текст глубже):
|
||||
jp_text(value, "response", "\n")
|
||||
|
||||
Частые вопросы
|
||||
- «Хочу собрать строку с частями из внешнего запроса»: делайте mode=string и пишите: "Bearer [[VAR:incoming.headers.authorization]]".
|
||||
- «Хочу массив случайных чисел»: mode=expr → [rand(), rand(), rand()], а в JSON: "numbers": {{ MY_LIST }}
|
||||
- «Почему мои значения не сохраняются?» — нажмите «Сохранить параметры» в инспекторе ноды, затем «Сохранить пайплайн» в шапке. UI синхронизирует данные в node.data и сохраняет в pipeline.json (см. [static/editor.html](static/editor.html)).
|
||||
|
||||
Ссылки на реализацию (для любопытных)
|
||||
- Нода переменных: [SetVarsNode](agentui/pipeline/executor.py:264), [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:291), [SetVarsNode.run()](agentui/pipeline/executor.py:354)
|
||||
- Исполнитель/контекст vars: [PipelineExecutor.run()](agentui/pipeline/executor.py:131)
|
||||
- Шаблоны и макросы (включая «голые» [[NAME]]/{{ NAME }}): [render_template_simple()](agentui/pipeline/templating.py:184)
|
||||
РАЗДЕЛ 8 — OUTx, ИЗВЛЕЧЕНИЕ ТЕКСТА, ПРЕСЕТЫ, ГЛОБАЛЬНЫЕ ОПЦИИ
|
||||
|
||||
Откуда [[OUTx]] берёт текст:
|
||||
- Универсальный алгоритм (см. [templating._best_text_from_outputs()](agentui/pipeline/templating.py:133)) ищет:
|
||||
- OpenAI: choices[0].message.content
|
||||
- Gemini: candidates[].content.parts[].text
|
||||
- Claude: content[].text
|
||||
- Иначе — глубокий поиск текстовых полей.
|
||||
- Для ProviderCall/RawForward нода сама пишет response_text и отдаёт в OUT.
|
||||
|
||||
Настройка, если тебе нужно «не обобщённо, а вот отсюда»:
|
||||
- Глобальная meta (в «Запуск»): text_extract_strategy (auto|deep|jsonpath|openai|gemini|claude), text_extract_json_path, text_join_sep.
|
||||
- Пресеты в «Запуск → Пресеты парсинга OUTx»: создаёшь набор id/json_path, затем в ноде выбираешь preset по id (text_extract_preset_id).
|
||||
- На уровне ноды можно переопределить: text_extract_strategy, text_extract_json_path, text_join_sep.
|
||||
|
||||
Пример (пер-нодовый пресет):
|
||||
- В «Запуск» добавь JSONPath: candidates.0.content.parts.*.text
|
||||
- В ноде ProviderCall выбери этот preset — и [[OUTn]] станет строго вытягивать по нему.
|
||||
|
||||
Пример (жёсткий путь в ноде):
|
||||
- text_extract_strategy: "jsonpath"
|
||||
- text_extract_json_path: "result.echo.payload.parts.*.text"
|
||||
- text_join_sep: " | "
|
||||
|
||||
|
||||
РАЗДЕЛ 9 — ПАНЕЛЬ «ПЕРЕМЕННЫЕ» (STORE), КОПИРОВАНИЕ МАКРОСОВ
|
||||
|
||||
Где посмотреть в UI: [editor.html](static/editor.html) — кнопка «ПЕРЕМЕННЫЕ».
|
||||
- Там список ключей:
|
||||
- vars (текущие пользовательские переменные из SetVars)
|
||||
- snapshot (снимок последнего запуска: incoming, params, model, vendor_format, system, OUT, OUT_TEXT, LAST_NODE, алиасы OUT1/OUT2/…)
|
||||
- По клику копируется готовый макрос:
|
||||
- Для vars → [[NAME]] или {{ NAME }}
|
||||
- Для snapshot.OUT_TEXT.nX → [[OUTx]] или {{ OUT.nX.response_text }}
|
||||
- Для snapshot.OUT.nX.something → [[OUT:nX.something]] или {{ OUT.nX.something }}
|
||||
- Для прочего контекста → [[VAR:path]] или {{ path }}
|
||||
- Переключатель «фигурные» — управляет, в какой форме скопируется (квадратные или фигурные).
|
||||
|
||||
STORE (персистентность между прогонами):
|
||||
- Если pipeline.clear_var_store=false, содержимое не очищается между запуском.
|
||||
- Примеры макросов:
|
||||
- [[STORE:KEEP]]
|
||||
- {{ STORE.KEEP|default('none') }}
|
||||
|
||||
|
||||
РАЗДЕЛ 10 — ЦВЕТА КОННЕКТОВ И КТО О ЧЁМ ШЕПОЧЕТ
|
||||
|
||||
Выглядит мило, да. Это не просто так, это сигнализация (см. [editor.css](static/editor.css)):
|
||||
|
||||
- If.true (зелёный, пунктир): ветка истинности — класс .conn-if-true
|
||||
- If.false (сланцево-серый, пунктир): ветка ложности — .conn-if-false
|
||||
- ProviderCall (приглушённый синий): .conn-provider
|
||||
- RawForward (мягкий фиолетовый): .conn-raw
|
||||
- SetVars (мятный): .conn-setvars
|
||||
- Return (холодный серо-синий): .conn-return
|
||||
- Входящие к узлу с ошибкой подсвечиваются красным: .conn-upstream-err
|
||||
|
||||
А ещё стрелочки направления рисуются поверх линий, и лейблы «true/false» к If-веткам, так что перестань путаться, пожалуйста.
|
||||
|
||||
|
||||
РАЗДЕЛ 11 — ЧАСТЫЕ ПАТТЕРНЫ (РЕЦЕПТЫ НА 1 МИНУТУ)
|
||||
|
||||
1) «Прокинуть, но если 502 — подёргать ещё»
|
||||
- RawForward:
|
||||
- ignore_errors: true
|
||||
- while_expr: "{{ OUT.n3.result.status|default(0) }} == 502 && cycleindex < 3"
|
||||
|
||||
2) «Gemini: взять входные contents, добавить свой system и отправить в OpenAI»
|
||||
- ProviderCall (openai):
|
||||
- prompt_combine: "[[VAR:incoming.json.contents]] & [[PROMPT]]@pos=prepend"
|
||||
|
||||
3) «Сделать язык вывода в зависимости от заголовка X-Lang»
|
||||
- SetVars:
|
||||
- LANG (string): "[[VAR:incoming.headers.X-Lang|default('en')]]"
|
||||
- If:
|
||||
- expr: "[[LANG]] == 'ru'"
|
||||
- Return:
|
||||
- text_template: "[[OUT2]]" (где n2 — твоя ветка для RU)
|
||||
|
||||
4) «Доставать base64 из ответа и вставлять картинкой куда нужно»
|
||||
- jp_text(OUT, "candidates.*.content.parts.*.inlineData.data", "")
|
||||
- Либо сразу data URL через img(png)[[...]] на канвасе.
|
||||
|
||||
5) «Стабильно вытягивать текст из Claude»
|
||||
- Настраиваешь пресет: json_path="content.*.text", join="\n"
|
||||
- В ноде ProviderCall выбираешь этот preset.
|
||||
|
||||
|
||||
РАЗДЕЛ 12 — БЕЗОПАСНОСТЬ И НЕ ПАЛИ КЛЮЧИ
|
||||
|
||||
- Никогда не вписывай реальные ключи в presets/pipeline.json. Никогда — слышишь?
|
||||
- Передавай ключи из клиента заголовками:
|
||||
- OpenAI: Authorization: Bearer X
|
||||
- Anthropic: x-api-key: X
|
||||
- Gemini: ?key=... в URL
|
||||
- В шаблонах юзай [[VAR:incoming.headers.authorization]], [[VAR:incoming.headers.x-api-key]], [[VAR:incoming.api_keys.key]]
|
||||
- Убедись, что логирование не льёт секреты в проде (маскируй, см. сервер).
|
||||
|
||||
|
||||
РАЗДЕЛ 13 — ТРОБЛШУТИНГ (КАК НЕ ПЛАКАТЬ)
|
||||
|
||||
- JSON «не валидный» в ProviderCall:
|
||||
- В template лишняя запятая вокруг [[PROMPT]] или ты вставил строкой не-JSON. Проверь печать «rendered_template» в консоли (см. [ProviderCallNode.run()](agentui/pipeline/executor.py:2055)).
|
||||
|
||||
- Линии «исчезают» в редакторе:
|
||||
- Жми «Загрузить пайплайн» ещё раз — отложенные порты и наблюдатели синхронизируются.
|
||||
|
||||
- [[OUTx]] пустой:
|
||||
- Настрой пресет извлечения OUTx (Раздел 8) либо задействуй явный json_path.
|
||||
|
||||
- While завис навечно:
|
||||
- Проверь while_max_iters и само условие. Помни: это do-while, первая итерация — всегда.
|
||||
|
||||
- Claude system вдруг не где надо:
|
||||
- Смотри флаг claude_no_system (нода ProviderCall) — он переносит system в user.
|
||||
|
||||
|
||||
ПРИЛОЖЕНИЕ — ПОЛНЫЙ ЧЕК-ЛИСТ ПРИ СБОРКЕ НОДЫ PROVIDERCALL
|
||||
|
||||
1) Выбери provider (openai/gemini/claude) в инспекторе.
|
||||
2) Заполни provider_configs.{provider}.(base_url, endpoint, headers, template).
|
||||
- Подстановки в headers/template — через [[...]] / {{ ... }} (см. [render_template_simple()](agentui/pipeline/templating.py:205))
|
||||
3) Заполни Prompt Blocks (system/user/assistant/tool) — они в [[PROMPT]].
|
||||
4) Если нужно смешать с входящим payload — используй prompt_combine (Раздел 6).
|
||||
5) Если нужно ретраить — поставь while_expr/ignore_errors/sleep_ms.
|
||||
6) Если нужно извлекать текст особым образом — выбери preset или text_extract_*.
|
||||
7) Соедини depends по порядку и посмотри цвета проводов (Раздел 10).
|
||||
8) Готово. Без косяков? Правда? Ну, посмотрим.
|
||||
|
||||
СЛИШКОМ ДЛИННО, НЕ ЧИТАЛ:
|
||||
- [[...]] — текстовая подстановка.
|
||||
- {{ ... }} — типобезопасная подстановка (числа/объекты).
|
||||
- SetVars expr — только whitelist функций (rand, randint, choice, from_json, jp, jp_text, file_b64, data_url, file_data_url) и операции + - * / // % and/or/сравнения.
|
||||
- If — && || !, contains, скобки, макросы внутри.
|
||||
- While — do-while в ProviderCall/RawForward, есть cycleindex и WAS_ERROR; можно заменить If в ретраях.
|
||||
- prompt_combine — склейка сообщений из разных форматов с @pos=… и автоконвертацией под провайдера.
|
||||
- JSONPath — a.b.0.*.x, звёздочка и индексы; jp/jp_text.
|
||||
- Цвета линий — true/false — пунктир, по типу ноды — разные цвета; ошибка — красные upstream.
|
||||
- Не пались: ключи только через incoming.headers/URL.
|
||||
|
||||
Если ты дошёл досюда — ну… я не впечатлена. Просто запомни и не ломи моя нервная система, ладно? Хмф.
|
||||
|
||||
|
||||
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
|
||||
pydantic==2.8.2
|
||||
httpx==0.27.0
|
||||
starlette==0.38.2
|
||||
httpx[socks]==0.27.0
|
||||
|
||||
starlette==0.40.0
|
||||
|
||||
brotlicffi
|
||||
brotli
|
||||
@@ -1,27 +1,51 @@
|
||||
@echo off
|
||||
setlocal
|
||||
chcp 65001 >NUL
|
||||
set PORT=7860
|
||||
echo Installing dependencies...
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
REM -------- Config --------
|
||||
if "%PORT%"=="" set PORT=7860
|
||||
if "%HOST%"=="" set HOST=127.0.0.1
|
||||
REM ------------------------
|
||||
|
||||
echo [НадTavern] Preparing virtual environment...
|
||||
|
||||
REM Pick Python launcher
|
||||
where py >NUL 2>&1
|
||||
if %ERRORLEVEL%==0 (
|
||||
set PY=py
|
||||
) else (
|
||||
set PY=python
|
||||
)
|
||||
|
||||
REM Create venv if missing
|
||||
if not exist ".venv\Scripts\python.exe" (
|
||||
%PY% -m venv .venv
|
||||
if errorlevel 1 goto :fail
|
||||
)
|
||||
|
||||
set "VENV_PY=.venv\Scripts\python.exe"
|
||||
|
||||
echo [НадTavern] Upgrading pip...
|
||||
"%VENV_PY%" -m pip install --upgrade pip
|
||||
if errorlevel 1 goto :fail
|
||||
pip install -r requirements.txt
|
||||
|
||||
echo [НадTavern] Installing dependencies from requirements.txt...
|
||||
"%VENV_PY%" -m pip install -r requirements.txt
|
||||
if errorlevel 1 goto :fail
|
||||
echo Starting НадTavern on http://127.0.0.1:%PORT%/
|
||||
|
||||
echo [НадTavern] Starting on http://%HOST%:%PORT%/
|
||||
timeout /t 1 /nobreak >NUL
|
||||
start "" "http://127.0.0.1:%PORT%/ui/editor.html"
|
||||
python -m uvicorn agentui.api.server:app --host 127.0.0.1 --port %PORT% --log-level info
|
||||
start "" "http://%HOST%:%PORT%/ui/editor.html"
|
||||
|
||||
"%VENV_PY%" -m uvicorn agentui.api.server:app --host %HOST% --port %PORT% --log-level info
|
||||
if errorlevel 1 goto :fail
|
||||
goto :end
|
||||
|
||||
:fail
|
||||
echo.
|
||||
echo Server failed with errorlevel %errorlevel%.
|
||||
echo [НадTavern] Server failed with errorlevel %errorlevel%.
|
||||
echo Check the console output above and the file agentui.log for details.
|
||||
pause
|
||||
|
||||
:end
|
||||
pause
|
||||
endlocal
|
||||
|
||||
|
||||
|
||||
46
run_agentui.sh
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# НадTavern Linux/macOS launcher with local .venv bootstrap
|
||||
# Usage:
|
||||
# chmod +x ./run_agentui.sh
|
||||
# ./run_agentui.sh
|
||||
# Optional env: HOST=0.0.0.0 PORT=7860
|
||||
|
||||
# Go to repo root (script location)
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
PORT="${PORT:-7860}"
|
||||
HOST="${HOST:-127.0.0.1}"
|
||||
|
||||
# Pick python
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
PY=python3
|
||||
else
|
||||
PY=python
|
||||
fi
|
||||
|
||||
# Create venv if missing
|
||||
if [ ! -f ".venv/bin/python" ]; then
|
||||
echo "[НадTavern] Creating .venv ..."
|
||||
"$PY" -m venv .venv
|
||||
fi
|
||||
|
||||
VENV_PY=".venv/bin/python"
|
||||
|
||||
echo "[НадTavern] Upgrading pip ..."
|
||||
"$VENV_PY" -m pip install --upgrade pip
|
||||
|
||||
echo "[НадTavern] Installing deps from requirements.txt ..."
|
||||
"$VENV_PY" -m pip install -r requirements.txt
|
||||
|
||||
echo "[НадTavern] Starting on http://$HOST:$PORT/"
|
||||
|
||||
# Try to open UI editor in default browser (non-fatal if fails)
|
||||
if command -v xdg-open >/dev/null 2>&1; then
|
||||
xdg-open "http://$HOST:$PORT/ui/editor.html" >/dev/null 2>&1 || true
|
||||
elif command -v open >/dev/null 2>&1; then
|
||||
open "http://$HOST:$PORT/ui/editor.html" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
exec "$VENV_PY" -m uvicorn agentui.api.server:app --host "$HOST" --port "$PORT" --log-level info
|
||||
1315
static/editor.css
4319
static/editor.html
@@ -4,6 +4,12 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>НадTavern</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 24px; }
|
||||
textarea { width: 100%; height: 200px; }
|
||||
|
||||
@@ -21,10 +21,15 @@
|
||||
// Готовим новые данные с глубокой копией blocks
|
||||
const newData = { ...(n.data || {}), blocks: Array.isArray(d2.blocks) ? d2.blocks.map(b => ({ ...b })) : [] };
|
||||
// 1) Обновляем внутреннее состояние Drawflow, чтобы export() возвращал актуальные данные
|
||||
try { editor.updateNodeDataFromId(id, newData); } catch (e) {}
|
||||
// 2) Обновляем DOM-отражение (источник правды для toPipelineJSON)
|
||||
const el2 = document.querySelector(`#node-${id}`);
|
||||
if (el2) el2.__data = JSON.parse(JSON.stringify(newData));
|
||||
try {
|
||||
if (w.AU && typeof w.AU.updateNodeDataAndDom === 'function') {
|
||||
w.AU.updateNodeDataAndDom(editor, id, newData);
|
||||
} else {
|
||||
editor.updateNodeDataFromId(id, newData);
|
||||
const el2 = document.querySelector(`#node-${id}`);
|
||||
if (el2) el2.__data = JSON.parse(JSON.stringify(newData));
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) {}
|
||||
}
|
||||
// Initial sync to attach blocks into __data for toPipelineJSON
|
||||
@@ -40,6 +45,17 @@
|
||||
const cancelBtn = document.getElementById('pm-cancel');
|
||||
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 (если доступен)
|
||||
@@ -77,11 +93,13 @@
|
||||
li.style.alignItems = 'center';
|
||||
li.style.gap = '6px';
|
||||
li.style.padding = '4px 0';
|
||||
const nameDisp = pmEscapeHtml(b.name || ('Block ' + (i + 1)));
|
||||
const roleDisp = pmEscapeHtml(b.role || 'user');
|
||||
li.innerHTML = `
|
||||
<span class="pm-handle" style="cursor:grab;">☰</span>
|
||||
<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-role" style="opacity:.8">${b.role || 'user'}</span>
|
||||
<span class="pm-name" style="flex:1">${nameDisp}</span>
|
||||
<span class="pm-role" style="opacity:.8">${roleDisp}</span>
|
||||
<button class="pm-edit" 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');
|
||||
}
|
||||
|
||||
// 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
|
||||
function toPipelineJSON() {
|
||||
ensureDeps();
|
||||
@@ -22,20 +112,77 @@
|
||||
|
||||
const dfNodes = (data && data.drawflow && data.drawflow.Home && data.drawflow.Home.data) ? data.drawflow.Home.data : {};
|
||||
|
||||
// 1) Собираем ноды
|
||||
let idx = 1;
|
||||
// 1) Собираем ноды с устойчивыми id на основе _origId (если валиден/уникален)
|
||||
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) {
|
||||
const df = dfNodes[id];
|
||||
const genId = `n${idx++}`;
|
||||
idMap[id] = genId;
|
||||
const el = document.querySelector(`#node-${id}`);
|
||||
// Берём источник правды из DOM.__data (куда жмём «Сохранить параметры») или из drawflow.data
|
||||
const datacopySrc = el && el.__data ? el.__data : (df.data || {});
|
||||
const datacopySrc = mergedNodeData(df, el, id);
|
||||
const tmp = typeof w.applyNodeDefaults === 'function'
|
||||
? w.applyNodeDefaults(df.name, JSON.parse(JSON.stringify(datacopySrc)))
|
||||
: (JSON.parse(JSON.stringify(datacopySrc)));
|
||||
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'
|
||||
? w.applyNodeDefaults(df.name, JSON.parse(JSON.stringify(datacopySrc)))
|
||||
: (JSON.parse(JSON.stringify(datacopySrc)));
|
||||
try { datacopy._origId = idMap[id]; } catch (e) {}
|
||||
|
||||
// Спец-обработка SetVars: гарантированно берём свежие variables из живых данных редактора
|
||||
try {
|
||||
if (String(df.name) === 'SetVars') {
|
||||
const nid = parseInt(id, 10);
|
||||
const nLive = (w.editor && typeof w.editor.getNodeFromId === 'function') ? w.editor.getNodeFromId(nid) : null;
|
||||
const v = nLive && nLive.data && Array.isArray(nLive.data.variables) ? nLive.data.variables : (Array.isArray(datacopy.variables) ? datacopy.variables : []);
|
||||
datacopy.variables = v.map(x => ({ ...(x || {}) })); // глубокая копия
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
nodes.push({
|
||||
id: genId,
|
||||
id: idMap[id],
|
||||
type: df.name,
|
||||
pos_x: df.pos_x,
|
||||
pos_y: df.pos_y,
|
||||
@@ -43,6 +190,7 @@
|
||||
in: {}
|
||||
});
|
||||
}
|
||||
try { console.debug('[AgentUISer.toPipelineJSON] idMap drawflowId->nX', idMap); } catch (e) {}
|
||||
|
||||
// 2) Восстанавливаем связи по входам (inputs)
|
||||
// В Drawflow v0.0.55 inputs/outputs — это объекты вида input_1/output_1
|
||||
@@ -56,15 +204,15 @@
|
||||
const inputKey = `input_${i + 1}`;
|
||||
const input = df.inputs && df.inputs[inputKey];
|
||||
if (!input || !Array.isArray(input.connections) || input.connections.length === 0) continue;
|
||||
|
||||
|
||||
// Собираем все связи этого входа и сохраняем строку либо массив строк (для depends поддерживаем мульти-коннекты)
|
||||
const refs = [];
|
||||
for (const conn of (input.connections || [])) {
|
||||
if (!conn) continue;
|
||||
const sourceDfId = String(conn.node);
|
||||
const outKey = String(conn.output ?? '');
|
||||
|
||||
// conn.output может быть "output_1", "1" (строкой), либо числом 1
|
||||
|
||||
// 1) Попробуем определить индекс выхода из conn.output
|
||||
let sourceOutIdx = -1;
|
||||
let m = outKey.match(/output_(\d+)/);
|
||||
if (m) {
|
||||
@@ -74,28 +222,64 @@
|
||||
} else if (typeof conn.output === 'number') {
|
||||
sourceOutIdx = conn.output - 1;
|
||||
}
|
||||
if (!(sourceOutIdx >= 0)) sourceOutIdx = 0; // safety
|
||||
|
||||
|
||||
const sourceNode = nodes.find(n => n.id === idMap[sourceDfId]);
|
||||
if (!sourceNode) continue;
|
||||
const sourceIo = NODE_IO[sourceNode.type] || { outputs: [] };
|
||||
const sourceOutName = (sourceIo.outputs && sourceIo.outputs[sourceOutIdx] != null)
|
||||
? sourceIo.outputs[sourceOutIdx]
|
||||
: `out${sourceOutIdx}`;
|
||||
|
||||
// 2) Fallback: если индекс не распознан или вне диапазона — проверим dfNodes[source].outputs
|
||||
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}`);
|
||||
}
|
||||
|
||||
|
||||
// Каноничное имя входа: по NODE_IO, иначе in{0-based}
|
||||
const targetInName = (io.inputs && io.inputs[i] != null)
|
||||
? io.inputs[i]
|
||||
: `in${i}`;
|
||||
|
||||
|
||||
if (!targetNode.in) targetNode.in = {};
|
||||
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
|
||||
@@ -103,6 +287,25 @@
|
||||
ensureDeps();
|
||||
const editor = w.editor;
|
||||
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();
|
||||
let x = 100; let y = 120; // Fallback
|
||||
@@ -243,5 +446,7 @@
|
||||
w.AgentUISer = {
|
||||
toPipelineJSON,
|
||||
fromPipelineJSON,
|
||||
getPipelineMeta,
|
||||
updatePipelineMeta,
|
||||
};
|
||||
})(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 name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>НадTavern — Pipeline Editor (JSON)</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 24px; }
|
||||
textarea { width: 100%; height: 70vh; }
|
||||
|
||||
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Make tests a package so imports like "from tests.utils import ..." work.
|
||||
199
tests/test_cancel_modes.py
Normal file
@@ -0,0 +1,199 @@
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any, Dict
|
||||
|
||||
from agentui.pipeline.executor import PipelineExecutor, ExecutionError
|
||||
from agentui.common.cancel import request_cancel, clear_cancel
|
||||
import agentui.providers.http_client as hc
|
||||
import agentui.pipeline.executor as ex
|
||||
from tests.utils import ctx as _ctx
|
||||
|
||||
|
||||
class DummyResponse:
|
||||
def __init__(self, status: int, json_obj: Dict[str, Any]) -> None:
|
||||
self.status_code = status
|
||||
self._json = json_obj
|
||||
self.headers = {}
|
||||
try:
|
||||
self.content = json.dumps(json_obj, ensure_ascii=False).encode("utf-8")
|
||||
except Exception:
|
||||
self.content = b"{}"
|
||||
try:
|
||||
self.text = json.dumps(json_obj, ensure_ascii=False)
|
||||
except Exception:
|
||||
self.text = "{}"
|
||||
|
||||
def json(self) -> Dict[str, Any]:
|
||||
return self._json
|
||||
|
||||
|
||||
class DummyClient:
|
||||
"""
|
||||
Async client with artificial delay to simulate in-flight HTTP that can be cancelled.
|
||||
Provides .post() and .request() compatible with executor usage.
|
||||
"""
|
||||
def __init__(self, delay: float = 0.3, status_code: int = 200) -> None:
|
||||
self._delay = delay
|
||||
self._status = status_code
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
async def post(self, url: str, content: bytes, headers: Dict[str, str]):
|
||||
# Artificial delay to allow cancel/abort to happen while awaiting
|
||||
await asyncio.sleep(self._delay)
|
||||
try:
|
||||
payload = json.loads(content.decode("utf-8"))
|
||||
except Exception:
|
||||
payload = {"_raw": content.decode("utf-8", errors="ignore")}
|
||||
return DummyResponse(self._status, {"echo": payload})
|
||||
|
||||
async def request(self, method: str, url: str, headers: Dict[str, str], content: bytes | None):
|
||||
return await self.post(url, content or b"{}", headers)
|
||||
|
||||
|
||||
def _patch_http_client(delay: float = 0.3):
|
||||
"""
|
||||
Patch both providers.http_client.build_client and executor.build_client
|
||||
to return our DummyClient with a given delay.
|
||||
"""
|
||||
orig_hc = hc.build_client
|
||||
orig_ex = ex.build_client
|
||||
hc.build_client = lambda timeout=60.0: DummyClient(delay=delay) # type: ignore[assignment]
|
||||
ex.build_client = lambda timeout=60.0: DummyClient(delay=delay) # type: ignore[assignment]
|
||||
return orig_hc, orig_ex
|
||||
|
||||
|
||||
def _restore_http_client(orig_hc, orig_ex) -> None:
|
||||
hc.build_client = orig_hc
|
||||
ex.build_client = orig_ex
|
||||
|
||||
|
||||
def test_graceful_cancel_while_providercall():
|
||||
"""
|
||||
Expectation:
|
||||
- Cancel(mode=graceful) during in-flight HTTP should NOT interrupt the current request.
|
||||
- While-wrapper should stop before starting next iteration.
|
||||
- Final CYCLEINDEX__n2 == 0 (only first iteration finished), WAS_ERROR__n2 is False/absent.
|
||||
"""
|
||||
async def main():
|
||||
p = {
|
||||
"id": "p_cancel_soft",
|
||||
"name": "ProviderCall graceful cancel",
|
||||
"loop_mode": "dag",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n2",
|
||||
"type": "ProviderCall",
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"while_expr": "cycleindex < 5",
|
||||
"while_max_iters": 10,
|
||||
# ignore_errors not needed for graceful (no interruption of in-flight)
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "http://dummy.local",
|
||||
"headers": "{}",
|
||||
"template": "{}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"in": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
pid = p["id"]
|
||||
orig_hc, orig_ex = _patch_http_client(delay=0.3)
|
||||
try:
|
||||
ctx = _ctx()
|
||||
exr = PipelineExecutor(p)
|
||||
task = asyncio.create_task(exr.run(ctx))
|
||||
# Give the node time to start HTTP, then request graceful cancel
|
||||
await asyncio.sleep(0.05)
|
||||
request_cancel(pid, mode="graceful")
|
||||
out = await task
|
||||
finally:
|
||||
_restore_http_client(orig_hc, orig_ex)
|
||||
try:
|
||||
clear_cancel(pid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
assert isinstance(out, dict)
|
||||
vars_map = out.get("vars") or {}
|
||||
assert isinstance(vars_map, dict)
|
||||
# Only first iteration should have finished; last index = 0
|
||||
assert vars_map.get("CYCLEINDEX__n2") == 0
|
||||
# No error expected on graceful (we didn't interrupt the in-flight HTTP)
|
||||
assert vars_map.get("WAS_ERROR__n2") in (False, None)
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
def test_abort_cancel_inflight_providercall():
|
||||
"""
|
||||
Expectation:
|
||||
- Cancel(mode=abort) during in-flight HTTP cancels the await with ExecutionError.
|
||||
- While-wrapper with ignore_errors=True converts it into {"result":{"error":...}}.
|
||||
- Final CYCLEINDEX__n2 == 0 and WAS_ERROR__n2 == True; error mentions 'Cancelled by user (abort)'.
|
||||
"""
|
||||
async def main():
|
||||
p = {
|
||||
"id": "p_cancel_abort",
|
||||
"name": "ProviderCall abort cancel",
|
||||
"loop_mode": "dag",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n2",
|
||||
"type": "ProviderCall",
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"while_expr": "cycleindex < 5",
|
||||
"while_max_iters": 10,
|
||||
"ignore_errors": True, # convert cancellation exception into error payload
|
||||
"provider_configs": {
|
||||
"openai": {
|
||||
"base_url": "http://dummy.local",
|
||||
"headers": "{}",
|
||||
"template": "{}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"in": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
pid = p["id"]
|
||||
orig_hc, orig_ex = _patch_http_client(delay=0.3)
|
||||
try:
|
||||
ctx = _ctx()
|
||||
exr = PipelineExecutor(p)
|
||||
task = asyncio.create_task(exr.run(ctx))
|
||||
# Let HTTP start, then trigger hard abort
|
||||
await asyncio.sleep(0.05)
|
||||
request_cancel(pid, mode="abort")
|
||||
out = await task
|
||||
finally:
|
||||
_restore_http_client(orig_hc, orig_ex)
|
||||
try:
|
||||
clear_cancel(pid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
assert isinstance(out, dict)
|
||||
vars_map = out.get("vars") or {}
|
||||
assert isinstance(vars_map, dict)
|
||||
# First iteration was started; after abort it is considered errored and loop stops
|
||||
assert vars_map.get("CYCLEINDEX__n2") == 0
|
||||
assert vars_map.get("WAS_ERROR__n2") is True
|
||||
|
||||
# Error propagated into node's result (ignore_errors=True path)
|
||||
res = out.get("result") or {}
|
||||
assert isinstance(res, dict)
|
||||
err = res.get("error")
|
||||
assert isinstance(err, str) and "Cancelled by user (abort)" in err
|
||||
|
||||
asyncio.run(main())
|
||||
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": {},
|
||||
},
|
||||
}
|
||||