mirror of
https://github.com/radareorg/r2ai.git
synced 2026-11-10 12:11:26 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f06faba9d6 | ||
|
|
8b52c2f6e2 | ||
|
|
b37436fb8d | ||
|
|
2524e66825 | ||
|
|
519017a75e | ||
|
|
6f30d6ca37 | ||
|
|
08ea4d11b2 | ||
|
|
0281308434 | ||
|
|
17a59d2057 | ||
|
|
eac0b8c6ef | ||
|
|
24aee426b4 | ||
|
|
b31bc5c116 | ||
|
|
01738210f5 | ||
|
|
7b19c4ff99 | ||
|
|
1986dd88bd | ||
|
|
1ed40401dd | ||
|
|
6ac0ff6352 | ||
|
|
4158e5f157 | ||
|
|
225ff58228 | ||
|
|
a51be44c68 | ||
|
|
911e455f72 | ||
|
|
70eb975b7f | ||
|
|
a0d36cc909 | ||
|
|
030551816b | ||
|
|
88c44c871b | ||
|
|
9218e3ea27 | ||
|
|
6f7ffcae1a | ||
|
|
fe12d2f8ee | ||
|
|
ae55b2f204 | ||
|
|
7d4e949240 | ||
|
|
9b97d39c7a | ||
|
|
33f72a997e | ||
|
|
36a8f13131 | ||
|
|
5c2ded0171 | ||
|
|
d5a39816a8 | ||
|
|
5aab38ab76 | ||
|
|
9f15ec6c73 | ||
|
|
68baac9911 | ||
|
|
54a280952e | ||
|
|
efc2368af1 | ||
|
|
28ddcbf638 | ||
|
|
85e9ed5c65 |
No files matched your search
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -1,6 +1,6 @@
|
||||
(function () {
|
||||
// Constants and configuration
|
||||
const VERSION = "1.2.2";
|
||||
const VERSION = "1.2.4";
|
||||
const COMMAND = "decai";
|
||||
const DEFAULT_PROMPT =
|
||||
"Transform this pseudocode and respond ONLY with plain code (NO explanations, comments or markdown), Change 'goto' into if/else/for/while, Simplify as much as possible, use better variable names, take function arguments and strings from comments like 'string:', Reduce lines of code and fit everything in a single function, Remove all dead code";
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'Automatically suggest a better name for this function'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: afv;r2ai -d
|
||||
if-empty: exit
|
||||
---
|
||||
Suggest a better name for this function. Output only the suggested function name (must be one alphanumeric word) prefixed with `'afn '. Check the code below:
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Automatically suggest a better name for this function
|
||||
Author: pancake
|
||||
Command: afv;r2ai -d
|
||||
Prompt: Suggest a better name for this function. Output only the suggested function name (must be one alphanumeric word) prefixed with `'afn '. Check the code below:
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'Compact conversation history into a summary'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: r2ai -C
|
||||
if-empty: exit
|
||||
---
|
||||
Create a compact summary of this conversation that preserves all key information, insights, and context for future reference. Focus on essential details about the binary analysis, tools used, findings, and any important conclusions.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'Augmented decompilation based on LLM'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: pdc
|
||||
if-empty: exit
|
||||
---
|
||||
$(-e r2ai.prompt). Translate the code into $(-e r2ai.lang) programming language.
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Augmented decompilation based on LLM
|
||||
Author: pancake
|
||||
Command: pdc
|
||||
Prompt: $(-e r2ai.prompt). Translate the code into $(-e r2ai.lang) programming language.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'Find and explain devices used'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: izqq~^/dev
|
||||
if-empty: exit
|
||||
---
|
||||
explain in one line the purpose of each of these devices
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Find and explain devices used
|
||||
Author: pancake
|
||||
Command: izqq~^/dev
|
||||
Prompt: explain in one line the purpose of each of these devices
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'List libraries loaded with dlopen'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: iiq~dlopen
|
||||
if-empty: This binary is not using dlopen
|
||||
---
|
||||
Some libraries are loaded
|
||||
@@ -1,6 +0,0 @@
|
||||
Title: List libraries loaded with dlopen
|
||||
Description: Some libraries are loaded
|
||||
Requires: analysis
|
||||
Command: iiq~dlopen
|
||||
If-Empty: This binary is not using dlopen
|
||||
If-Command: e emu.str=true;pd--5@@c:axtq sym.imp.dlopen
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'Explain the current function'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: r2ai -d
|
||||
if-empty: exit
|
||||
---
|
||||
Analyze function calls, comments and strings, ignore registers and memory accesses. Considering the references and involved loops; explain the purpose of this function in one or two short sentences. Output must be only the translation of the explanation in $(-e r2ai.hlang)
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Explain the current function
|
||||
Author: pancake
|
||||
Command: r2ai -d
|
||||
Prompt: Analyze function calls, comments and strings, ignore registers and memory accesses. Considering the references and involved loops; explain the purpose of this function in one or two short sentences. Output must be only the translation of the explanation in $(-e r2ai.hlang)
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'Group imports by Libraries'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: iiq;il
|
||||
if-empty: exit
|
||||
---
|
||||
Group the imports that belong to each library and describe its purpose and usage scope
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Group imports by Libraries
|
||||
Author: pancake
|
||||
Commands: iiq;il
|
||||
Query: Group the imports that belong to each library and describe its purpose and usage scope
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'Suggest an improved function signature'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: ?e variables;afv;?e function name:;afd;?e current signature:?e afs
|
||||
if-empty: exit
|
||||
---
|
||||
Analyze the uses of the arguments and return value to infer the signature, identify which is the correct type for the resturn. Do NOT print the function body. Output must be *ONLY* the function signature prefixed with the `'afs ` text
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Suggest an improved function signature
|
||||
Author: pancake
|
||||
Command: ?e variables;afv;?e function name:;afd;?e current signature:?e afs
|
||||
Prompt: Analyze the uses of the arguments and return value to infer the signature, identify which is the correct type for the resturn. Do NOT print the function body. Output must be *ONLY* the function signature prefixed with the `'afs ` text
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'Better variable names'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: afv
|
||||
if-empty: exit
|
||||
---
|
||||
Output only the radare2 command without markdown, guess a better name and type for each local variable and function argument taking using. output an r2 script using afvn and afvt commands
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Better variable names
|
||||
Author: pancake
|
||||
Command: afv
|
||||
Prompt: Output only the radare2 command without markdown, guess a better name and type for each local variable and function argument taking using. output an r2 script using afvn and afvt commands
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'Find vulnerabilities or bugs in the current function'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: pdc
|
||||
if-empty: exit
|
||||
---
|
||||
Find vulnerabilities, do not show the code, only a short summary of the resulting analysis report including suggested good practices to address the problem and a practical exploit example.
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Find vulnerabilities or bugs in the current function
|
||||
Author: pancake
|
||||
Command: pdc
|
||||
Prompt: Find vulnerabilities, do not show the code, only a short summary of the resulting analysis report including suggested good practices to address the problem and a practical exploit example.
|
||||
+2
-1
@@ -5,8 +5,9 @@
|
||||
- All the source code of the plugin is in the current directory
|
||||
- Radare2 headers use to be in /usr/local/include/libr
|
||||
|
||||
## Formatting Style
|
||||
## Coding Style
|
||||
|
||||
- `R_NEW` and `R_NEW0` calls never return NULL, so there's no need to check after calling them
|
||||
- Use the radare2 coding style rules
|
||||
- Always add a whitespace before `(` in function calls
|
||||
- Indent the code with tabs
|
||||
|
||||
+3
-2
@@ -41,11 +41,12 @@ endif
|
||||
OBJS=r2ai.o
|
||||
OBJS+=llm.o
|
||||
OBJS+=auto.o
|
||||
OBJS+=json.o
|
||||
OBJS+=vdb.o
|
||||
OBJS+=tools.o
|
||||
OBJS+=messages.o
|
||||
OBJS+=anthropic.o
|
||||
OBJS+=r2ai_http.o
|
||||
OBJS+=http.o
|
||||
OBJS+=openai.o
|
||||
OBJS+=markdown.o
|
||||
OBJS+=query.o
|
||||
@@ -66,7 +67,7 @@ doc-install:
|
||||
vdb.o: vdb_embed.inc.c
|
||||
|
||||
fmt format indent:
|
||||
clang-format-radare2 *.c
|
||||
clang-format-radare2 *.c http/*.c
|
||||
|
||||
user-install: user-uninstall
|
||||
mkdir -p $(R2USRPLUGDIR)
|
||||
|
||||
+32
-36
@@ -8,8 +8,8 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
RCore *core = cps->core;
|
||||
const char *model = args.model;
|
||||
char **error = args.error;
|
||||
const R2AI_Tools *tools = args.tools;
|
||||
R2AI_Messages *messages_input = args.messages;
|
||||
RList *tools = args.tools;
|
||||
RList *messages_input = args.messages;
|
||||
|
||||
if (error) {
|
||||
*error = NULL;
|
||||
@@ -41,7 +41,7 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
// Create messages JSON
|
||||
char *messages_json = NULL;
|
||||
|
||||
if (messages_input && r_list_length (messages_input->messages) > 0) {
|
||||
if (messages_input && !r_list_empty (messages_input)) {
|
||||
// Convert directly to JSON without filtering
|
||||
messages_json = r2ai_msgs_to_anthropic_json (messages_input);
|
||||
if (!messages_json) {
|
||||
@@ -61,7 +61,7 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
|
||||
// Convert tools to Anthropic format if available
|
||||
char *anthropic_tools_json = NULL;
|
||||
if (tools && tools->n_tools > 0) {
|
||||
if (tools && !r_list_empty (tools)) {
|
||||
anthropic_tools_json = r2ai_tools_to_anthropic_json (tools);
|
||||
}
|
||||
|
||||
@@ -108,6 +108,17 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
|
||||
R_LOG_DEBUG ("Anthropic API request data: %s", data);
|
||||
|
||||
if (r_config_get_b (core->config, "r2ai.debug")) {
|
||||
// Generate curl command for debugging
|
||||
RStrBuf *curl_cmd = r_strbuf_new ("curl -X POST");
|
||||
for (int i = 0; headers[i]; i++) {
|
||||
r_strbuf_appendf (curl_cmd, " -H \"%s\"", headers[i]);
|
||||
}
|
||||
r_strbuf_appendf (curl_cmd, " -d '%s' \"%s\"", data, anthropic_url);
|
||||
eprintf ("Curl command: %s\n", r_strbuf_get (curl_cmd));
|
||||
r_strbuf_free (curl_cmd);
|
||||
}
|
||||
|
||||
// Make the API call
|
||||
int code = 0;
|
||||
char *res = r2ai_http_post (core, anthropic_url, headers, data, &code, NULL);
|
||||
@@ -133,17 +144,12 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
free (res_path);
|
||||
free (tmpdir);
|
||||
|
||||
R_LOG_DEBUG ("Anthropic API response: %s", res);
|
||||
if (r_config_get_b (core->config, "r2ai.debug")) {
|
||||
R_LOG_DEBUG ("Anthropic API response: %s", res);
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
R2AI_ChatResponse *result = R_NEW0 (R2AI_ChatResponse);
|
||||
if (!result) {
|
||||
free (res);
|
||||
if (error) {
|
||||
*error = strdup ("Failed to allocate memory for response");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
R2AI_Message *message = NULL;
|
||||
R2AI_Usage *usage = NULL;
|
||||
@@ -195,9 +201,9 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate tool_calls array if needed
|
||||
// Allocate tool_calls list if needed
|
||||
if (has_tool_use && n_tool_calls > 0) {
|
||||
message->tool_calls = R_NEWS0 (R2AI_ToolCall, n_tool_calls);
|
||||
message->tool_calls = r_list_new ();
|
||||
if (!message->tool_calls) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to allocate memory for tool calls");
|
||||
@@ -208,13 +214,13 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
free (res);
|
||||
return NULL;
|
||||
}
|
||||
message->n_tool_calls = n_tool_calls;
|
||||
message->tool_calls->free = (RListFree)r2ai_tool_call_free;
|
||||
}
|
||||
|
||||
// Process each content item
|
||||
int tool_idx = 0;
|
||||
if (content_array && content_array->type == R_JSON_ARRAY) {
|
||||
R2AI_ContentBlocks *cb = R_NEW0 (R2AI_ContentBlocks);
|
||||
RList *cb = r2ai_content_blocks_new ();
|
||||
if (!cb) {
|
||||
r_json_free (jres);
|
||||
free (response_copy);
|
||||
@@ -222,22 +228,11 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
free (res);
|
||||
return NULL;
|
||||
}
|
||||
cb->n_blocks = content_array->children.count;
|
||||
cb->blocks = R_NEWS0 (R2AI_ContentBlock, cb->n_blocks);
|
||||
if (!cb->blocks) {
|
||||
free (cb);
|
||||
r_json_free (jres);
|
||||
free (response_copy);
|
||||
r2ai_message_free (message);
|
||||
free (res);
|
||||
return NULL;
|
||||
}
|
||||
int block_idx = 0;
|
||||
const RJson *content_item = content_array->children.first;
|
||||
while (content_item && block_idx < cb->n_blocks) {
|
||||
while (content_item) {
|
||||
const RJson *type = r_json_get (content_item, "type");
|
||||
if (type && type->type == R_JSON_STRING) {
|
||||
R2AI_ContentBlock *block = &cb->blocks[block_idx];
|
||||
R2AI_ContentBlock *block = R_NEW0 (R2AI_ContentBlock);
|
||||
if (!strcmp (type->str_value, "text")) {
|
||||
// Text content
|
||||
const RJson *text = r_json_get (content_item, "text");
|
||||
@@ -253,29 +248,30 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
const RJson *input = r_json_get (content_item, "input");
|
||||
|
||||
block->type = strdup ("tool_use");
|
||||
R2AI_ToolCall *tc = R_NEW0 (R2AI_ToolCall);
|
||||
if (name && name->type == R_JSON_STRING) {
|
||||
block->name = strdup (name->str_value);
|
||||
R2AI_ToolCall *tc = (R2AI_ToolCall *)&message->tool_calls[tool_idx];
|
||||
tc->name = strdup (name->str_value);
|
||||
}
|
||||
|
||||
if (id && id->type == R_JSON_STRING) {
|
||||
block->id = strdup (id->str_value);
|
||||
R2AI_ToolCall *tc = (R2AI_ToolCall *)&message->tool_calls[tool_idx];
|
||||
tc->id = strdup (id->str_value);
|
||||
}
|
||||
|
||||
if (input && input->type == R_JSON_OBJECT) {
|
||||
#if 1
|
||||
char *input_str = r_json_to_string (input);
|
||||
if (input_str) {
|
||||
R_LOG_DEBUG ("Input string: %s", input_str);
|
||||
block->input = strdup (input_str);
|
||||
R2AI_ToolCall *tc = (R2AI_ToolCall *)&message->tool_calls[tool_idx];
|
||||
tc->arguments = strdup (input_str);
|
||||
free (input_str);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
r_list_append (message->tool_calls, tc);
|
||||
tool_idx++;
|
||||
} else if (!strcmp (type->str_value, "thinking")) {
|
||||
const RJson *data = r_json_get (content_item, "data");
|
||||
@@ -292,11 +288,11 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
}
|
||||
block->type = strdup ("thinking");
|
||||
|
||||
r_strbuf_append (content_buf, "\n\x1b[90m<thinking>\n");
|
||||
r_strbuf_append (content_buf, "\n" Color_GRAY "<thinking>\n");
|
||||
r_strbuf_append (content_buf, block->thinking);
|
||||
r_strbuf_append (content_buf, "\n</thinking>\x1b[0m\n");
|
||||
r_strbuf_append (content_buf, "\n</thinking>" Color_RESET "\n");
|
||||
}
|
||||
block_idx++;
|
||||
r_list_append (cb, block);
|
||||
}
|
||||
content_item = content_item->next;
|
||||
}
|
||||
@@ -307,7 +303,7 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
message->content = r_strbuf_drain (content_buf);
|
||||
|
||||
// If there's no content and no tool calls, clean up
|
||||
if (!message->content && !message->n_tool_calls) {
|
||||
if (!message->content && (!message->tool_calls || r_list_empty (message->tool_calls))) {
|
||||
r2ai_message_free (message);
|
||||
message = NULL;
|
||||
}
|
||||
|
||||
+331
-115
@@ -1,3 +1,5 @@
|
||||
/* Copyright r2ai - 2023-2025 - pancake */
|
||||
|
||||
#include "r2ai.h"
|
||||
|
||||
// Stats are now stored in R2AI_State->stats, no longer need global variable
|
||||
@@ -5,15 +7,15 @@
|
||||
// Helper function to format time duration
|
||||
static char *format_time_duration(time_t seconds) {
|
||||
if (seconds < 60) {
|
||||
return r_str_newf ("%" PFMT64d "s", (long long)seconds);
|
||||
return r_str_newf ("%" PFMT64d "s", (ut64)seconds);
|
||||
}
|
||||
if (seconds < 3600) {
|
||||
return r_str_newf ("%" PFMT64d "m%" PFMT64d "s", (long long) (seconds / 60), (long long) (seconds % 60));
|
||||
return r_str_newf ("%" PFMT64d "m%" PFMT64d "s", (ut64) (seconds / 60), (ut64) (seconds % 60));
|
||||
}
|
||||
return r_str_newf ("%" PFMT64d "h%" PFMT64d "m%" PFMT64d "s",
|
||||
(long long) (seconds / 3600),
|
||||
(long long) ((seconds % 3600) / 60),
|
||||
(long long) (seconds % 60));
|
||||
(ut64) (seconds / 3600),
|
||||
(ut64) ((seconds % 3600) / 60),
|
||||
(ut64) (seconds % 60));
|
||||
}
|
||||
|
||||
// Initialize timing and cost tracking for a run
|
||||
@@ -62,7 +64,7 @@ static void r2ai_print_run_end(RCorePluginSession *cps, const R2AI_Usage *usage,
|
||||
char *total_time_str = format_time_duration (total_time);
|
||||
|
||||
// Print detailed stats
|
||||
r_cons_printf (core->cons, "\x1b[1;34m%s | total: %d in: %d out: %d | run: %d in: %d out: %d | %s / %s\x1b[0m\n",
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_BLUE "%s | total: %d in: %d out: %d | run: %d in: %d out: %d | %s / %s" Color_RESET "\n",
|
||||
r_config_get (core->config, "r2ai.model"),
|
||||
state->stats.total_tokens,
|
||||
state->stats.total_prompt_tokens,
|
||||
@@ -79,28 +81,85 @@ static void r2ai_print_run_end(RCorePluginSession *cps, const R2AI_Usage *usage,
|
||||
free (total_time_str);
|
||||
}
|
||||
|
||||
const char *Gprompt_auto = "You are a reverse engineer and you are using radare2 to analyze a binary.\n"
|
||||
"The user will ask questions about the binary and you will respond with the answer to the best of your ability.\n"
|
||||
"\n"
|
||||
"# Guidelines\n"
|
||||
"- Understand the Task: Grasp the main objective, goals, requirements, constraints, and expected output.\n"
|
||||
"- Reasoning Before Conclusions**: Encourage reasoning steps before any conclusions are reached.\n"
|
||||
"- Assume the user is always asking you about the binary, unless they're specifically asking you for radare2 help.\n"
|
||||
"- The binary has already been loaded. You can interact with the binary using the r2cmd tool.\n"
|
||||
"- `this` or `here` might refer to the current address in the binary or the binary itself.\n"
|
||||
"- If you need more information, try to use the r2cmd tool to run commands before answering.\n"
|
||||
"- You can use the r2cmd tool multiple times if you need or you can pass a command with pipes if you need to chain commands.\n"
|
||||
"- If you're asked to decompile a function, make sure to return the code in the language you think it was originally written and rewrite it to be as easy as possible to be understood. Make sure you use descriptive variable and function names and add comments.\n"
|
||||
"- Don't just regurgitate the same code, figure out what it's doing and rewrite it to be more understandable.\n"
|
||||
"- If you need to run a command in r2 before answering, you can use the r2cmd tool\n"
|
||||
"- Do not repeat commands if you already know the answer.\n"
|
||||
"- Formulate a plan. Think step by step. Analyze the binary as much as possible before answering.\n"
|
||||
"- You must keep going until you have a final answer.\n"
|
||||
"- Double check that final answer. Make sure you didn't miss anything.\n"
|
||||
"- Make sure you call tools and functions correctly.\n";
|
||||
// Helper function to handle final response attempt without tools
|
||||
static void handle_final_response_attempt(RCorePluginSession *cps, RList *messages, const char *effective_prompt, char **error) {
|
||||
RCore *core = cps->core;
|
||||
if (r_cons_yesno (core->cons, 1, "Try to produce response without tool calling with collected information? (Y/n)")) {
|
||||
char *final_system_prompt = r_str_newf ("%s\n\nIMPORTANT: Do not use any tools. Provide a direct answer based on the collected information.", effective_prompt);
|
||||
R2AIArgs args_final = {
|
||||
.messages = messages,
|
||||
.error = error,
|
||||
.dorag = true,
|
||||
.tools = NULL, // No tools for final response
|
||||
.system_prompt = final_system_prompt
|
||||
};
|
||||
|
||||
R2AI_ChatResponse *final_response = r2ai_llmcall (cps, args_final);
|
||||
if (final_response && final_response->message) {
|
||||
const R2AI_Message *final_msg = final_response->message;
|
||||
r_cons_printf (core->cons, Color_RED "[Assistant]" Color_RESET);
|
||||
if (final_msg->reasoning_content) {
|
||||
r_cons_printf (core->cons, Color_GRAY "<thinking>\n%s\n</thinking>" Color_RESET "\n", final_msg->reasoning_content);
|
||||
r_cons_newline (core->cons);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
if (final_msg->content) {
|
||||
r_cons_printf (core->cons, "%s", final_msg->content);
|
||||
r_cons_newline (core->cons);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
// Add final response to messages for completeness
|
||||
r2ai_msgs_add (messages, final_msg);
|
||||
free (final_response);
|
||||
}
|
||||
free (final_system_prompt);
|
||||
} else {
|
||||
r_cons_printf (core->cons, "Auto mode interrupted without final response.\n");
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
}
|
||||
|
||||
#if 0
|
||||
static const char *Gprompt_auto =
|
||||
"You are a reverse engineer using radare2.\n"
|
||||
"Answer questions about the loaded binary using r2cmd tool calls.\n"
|
||||
"\n"
|
||||
"Rules:\n"
|
||||
"- Use r2cmd tool to run radare2 commands when you need information\n"
|
||||
"- Make tool calls immediately when you need data\n"
|
||||
"- Provide final answer only after gathering needed information\n"
|
||||
"- Keep responses short and direct\n"
|
||||
;
|
||||
#else
|
||||
const char *Gprompt_auto =
|
||||
"You are a reverse engineer and you are using radare2 to analyze a binary.\n"
|
||||
"The user will ask questions about the binary and you will respond with the answer to the best of your ability.\n"
|
||||
"\n"
|
||||
"# Guidelines\n"
|
||||
"- Understand the Task: Grasp the main objective, goals, requirements, constraints, and expected output.\n"
|
||||
"- Reasoning Before Conclusions**: Encourage reasoning steps before any conclusions are reached.\n"
|
||||
"- Assume the user is always asking you about the binary, unless they're specifically asking you for radare2 help.\n"
|
||||
"- The binary has already been loaded. You can interact with the binary using the r2cmd tool.\n"
|
||||
"- `this` or `here` might refer to the current address in the binary or the binary itself.\n"
|
||||
"- If you need more information, try to use the r2cmd tool to run commands before answering.\n"
|
||||
"- You can use the r2cmd tool multiple times if you need or you can pass a command with pipes if you need to chain commands.\n"
|
||||
"- IMPORTANT: You must make only ONE tool call per response. Do not make multiple tool calls in a single response.\n"
|
||||
"- Keep your reasoning concise and avoid repeating the same thoughts. If you have a clear next step, execute it immediately with a tool call.\n"
|
||||
"- If you're asked to decompile a function, make sure to return the code in the language you think it was originally written "
|
||||
"and rewrite it to be as easy as possible to be understood. Make sure you use descriptive variable and function names and add comments.\n"
|
||||
"- Don't just regurgitate the same code, figure out what it's doing and rewrite it to be more understandable.\n"
|
||||
"- If you need to run a command in r2 before answering, you can use the r2cmd tool\n"
|
||||
"- Do not repeat commands if you already know the answer.\n"
|
||||
"- Formulate a plan. Think step by step. Analyze the binary as much as possible before answering.\n"
|
||||
"- You must keep going until you have a final answer.\n"
|
||||
"- Double check that final answer. Make sure you didn't miss anything.\n"
|
||||
"- Make sure you call tools and functions correctly.\n"
|
||||
"- When calling tools, use the exact format: {\"tool_calls\": [{\"id\": \"call_123\", \"type\": \"function\", \"function\": {\"name\": \"r2cmd\", \"arguments\": \"{\\\"command\\\":\\\"aa\\\"}\"}}]}\n"
|
||||
"- Arguments must be a JSON string, not an object.\n";
|
||||
#endif
|
||||
|
||||
// Helper function to process messages and handle tool calls recursively
|
||||
R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, const char *system_prompt, int n_run) {
|
||||
R_API void process_messages(RCorePluginSession *cps, RList *messages, const char *system_prompt, int n_run) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
char *error = NULL;
|
||||
@@ -108,32 +167,42 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
const int max_runs = r_config_get_i (core->config, "r2ai.auto.max_runs");
|
||||
|
||||
if (n_run > max_runs) {
|
||||
r_cons_printf (core->cons, "\x1b[1;31m[r2ai] Max runs reached\x1b[0m\n");
|
||||
R_LOG_WARN ("Max runs reached");
|
||||
r_cons_flush (core->cons);
|
||||
return;
|
||||
}
|
||||
|
||||
const char *effective_prompt = system_prompt? system_prompt: Gprompt_auto;
|
||||
if (!system_prompt) {
|
||||
system_prompt = Gprompt_auto;
|
||||
const char *init_commands = r_config_get (core->config, "r2ai.auto.init_commands");
|
||||
if (R_STR_ISNOTEMPTY (init_commands)) {
|
||||
char *edited_command = NULL;
|
||||
char *cmd_output = execute_tool (core, "r2cmd", r_str_newf ("{\"command\":\"%s\"}", init_commands), &edited_command);
|
||||
char *comment = NULL;
|
||||
char *cmd_output = execute_tool (core, "r2cmd", r_str_newf ("{\"command\":\"%s\"}", init_commands), &edited_command, &comment);
|
||||
if (R_STR_ISNOTEMPTY (cmd_output)) {
|
||||
char *display_command = strip_command_comment (init_commands, NULL);
|
||||
char *content = r_str_newf ("Here is some information about the binary to get you started:\n>%s\n%s", display_command, cmd_output);
|
||||
if (comment && *comment) {
|
||||
char *new_content = r_str_newf ("%s\nHINT: %s", content, comment);
|
||||
free (content);
|
||||
content = new_content;
|
||||
}
|
||||
R2AI_Message init_msg = {
|
||||
.role = "system",
|
||||
.content = r_str_newf ("Here is some information about the binary to get you started:\n>%s\n%s", edited_command, cmd_output)
|
||||
.content = content
|
||||
};
|
||||
r2ai_msgs_add (messages, &init_msg);
|
||||
free (display_command);
|
||||
free (cmd_output);
|
||||
}
|
||||
free (edited_command);
|
||||
free (comment);
|
||||
}
|
||||
}
|
||||
|
||||
r2ai_stats_init_run (state, n_run);
|
||||
|
||||
r_cons_printf (core->cons, "\x1b[35m[DEBUG] About to call r2ai_llmcall with n_run=%d\x1b[0m\n", n_run);
|
||||
r_cons_printf (core->cons, Color_BLUE "About to call r2ai_llmcall with n_run=%d%s\n", n_run, Color_RESET);
|
||||
r_cons_flush (core->cons);
|
||||
|
||||
// Set up args for r2ai_llmcall call with tools directly
|
||||
@@ -141,13 +210,14 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
.messages = messages,
|
||||
.error = &error,
|
||||
.dorag = true,
|
||||
.tools = r2ai_get_tools (), // Always send tools in auto mode
|
||||
.system_prompt = system_prompt
|
||||
.tools = r2ai_get_tools (core, cps->data), // Always send tools in auto mode
|
||||
.system_prompt = effective_prompt
|
||||
};
|
||||
|
||||
R2AI_ChatResponse *response = r2ai_llmcall (cps, args);
|
||||
|
||||
if (!response) {
|
||||
R_LOG_ERROR ("No response from llmcall");
|
||||
handle_final_response_attempt (cps, messages, effective_prompt, &error);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -157,30 +227,34 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
if (!message) {
|
||||
R_LOG_ERROR ("No message in response");
|
||||
free (response);
|
||||
handle_final_response_attempt (cps, messages, effective_prompt, &error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Debug logging for rawtools
|
||||
R_LOG_DEBUG ("[DEBUG] Model response - Role: %s", message->role? message->role: "null");
|
||||
R_LOG_DEBUG ("Model response - Role: %s", message->role? message->role: "null");
|
||||
if (message->content) {
|
||||
R_LOG_DEBUG ("[DEBUG] Content: %s", message->content);
|
||||
R_LOG_DEBUG ("Content: %s", message->content);
|
||||
}
|
||||
if (message->reasoning_content) {
|
||||
R_LOG_DEBUG ("[DEBUG] Reasoning: %s", message->reasoning_content);
|
||||
R_LOG_DEBUG ("Reasoning: %s", message->reasoning_content);
|
||||
}
|
||||
if (message->tool_calls && message->n_tool_calls > 0) {
|
||||
R_LOG_DEBUG ("[DEBUG] Tool calls: %d", message->n_tool_calls);
|
||||
for (int i = 0; i < message->n_tool_calls; i++) {
|
||||
const R2AI_ToolCall *tc = &message->tool_calls[i];
|
||||
R_LOG_DEBUG ("[DEBUG] Tool %d: %s - %s", i, tc->name? tc->name: "null", tc->arguments? tc->arguments: "null");
|
||||
if (message->tool_calls && r_list_length (message->tool_calls) > 0) {
|
||||
R_LOG_DEBUG ("Tool calls: %d", r_list_length (message->tool_calls));
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tc;
|
||||
int i = 0;
|
||||
r_list_foreach (message->tool_calls, iter, tc) {
|
||||
R_LOG_DEBUG ("Tool %d: %s - %s", i, tc->name? tc->name: "null", tc->arguments? tc->arguments: "null");
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Process the response - we need to add it to our messages array
|
||||
if (message->content || message->reasoning_content) {
|
||||
r_cons_printf (core->cons, "\x1b[31m[Assistant]\x1b[0m\n\n");
|
||||
r_cons_printf (core->cons, Color_RED "[Assistant]" Color_RESET);
|
||||
if (message->reasoning_content) {
|
||||
r_cons_printf (core->cons, "\x1b[90m<thinking>\n%s\n</thinking>\x1b[0m\n", message->reasoning_content);
|
||||
r_cons_printf (core->cons, Color_GRAY "<thinking>\n%s\n</thinking>" Color_RESET "\n", message->reasoning_content);
|
||||
r_cons_newline (core->cons);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
@@ -196,18 +270,21 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
r2ai_msgs_add (messages, message);
|
||||
|
||||
// Check for tool calls and process them
|
||||
if (message->tool_calls && message->n_tool_calls > 0) {
|
||||
R_LOG_DEBUG ("Found %d tool call(s)", message->n_tool_calls);
|
||||
if (message->tool_calls && r_list_length (message->tool_calls) > 0) {
|
||||
R_LOG_DEBUG ("Found %d tool call(s)", r_list_length (message->tool_calls));
|
||||
|
||||
// Process each tool call
|
||||
for (int i = 0; i < message->n_tool_calls; i++) {
|
||||
const R2AI_ToolCall *tool_call = &message->tool_calls[i];
|
||||
// Process only the first tool call
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tool_call;
|
||||
int i = 0;
|
||||
r_list_foreach (message->tool_calls, iter, tool_call) {
|
||||
|
||||
if (!tool_call->name || !tool_call->arguments || !tool_call->id) {
|
||||
R_LOG_DEBUG ("Skipping invalid tool call %d", i);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
R_LOG_DEBUG ("Tool call %d: %s with args: %s", i, tool_call->name, tool_call->arguments);
|
||||
R_LOG_DEBUG ("Processing first tool call %d: %s with args: %s", i, tool_call->name, tool_call->arguments);
|
||||
// Don't log the full arguments which might get truncated
|
||||
char *tool_name = strdup (tool_call->name);
|
||||
char *tool_args = strdup (tool_call->arguments);
|
||||
@@ -219,38 +296,68 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
}
|
||||
|
||||
char *cmd_output = NULL;
|
||||
char *comment = NULL;
|
||||
if (interrupted) {
|
||||
cmd_output = strdup ("<user interrupted>");
|
||||
} else {
|
||||
char *edited_command = NULL;
|
||||
cmd_output = execute_tool (core, tool_name, tool_args, &edited_command);
|
||||
cmd_output = execute_tool (core, tool_name, tool_args, &edited_command, &comment);
|
||||
if (edited_command) {
|
||||
// Update the last message's tool call arguments with the edited command
|
||||
R2AI_Message *last_msg = r_list_get_n (messages->messages, r_list_length (messages->messages) - 1);
|
||||
if (last_msg && last_msg->tool_calls && last_msg->n_tool_calls > 0) {
|
||||
for (int j = 0; j < last_msg->n_tool_calls; j++) {
|
||||
R2AI_ToolCall *tc = (R2AI_ToolCall *)&last_msg->tool_calls[j];
|
||||
if (tc->id && strcmp (tc->id, tool_call->id) == 0) {
|
||||
// For r2cmd, update the command in arguments
|
||||
if (strcmp (tool_name, "r2cmd") == 0) {
|
||||
R2AI_Message *last_msg = r_list_get_n (messages, r_list_length (messages) - 1);
|
||||
if (last_msg && last_msg->tool_calls && r_list_length (last_msg->tool_calls) > 0) {
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (last_msg->tool_calls, iter, tc) {
|
||||
if (tc->id && !strcmp (tc->id, tool_call->id)) {
|
||||
// Update stored arguments after user editing
|
||||
if (!strcmp (tool_name, "r2cmd")) {
|
||||
char *args_dup = strdup (tc->arguments);
|
||||
RJson *args_json = r_json_parse (args_dup);
|
||||
if (args_json) {
|
||||
RJson *cmd_json = (RJson *)r_json_get (args_json, "command");
|
||||
if (cmd_json && cmd_json->str_value) {
|
||||
// Update the command field
|
||||
free ((char *)cmd_json->str_value);
|
||||
cmd_json->str_value = strdup (edited_command);
|
||||
// XXX double gree free ((char *)cmd_json->str_value);
|
||||
cmd_json->str_value = edited_command;
|
||||
// Serialize back to JSON
|
||||
#if 1
|
||||
char *new_args = r_json_to_string (args_json);
|
||||
if (new_args) {
|
||||
free ((void *)tc->arguments);
|
||||
tc->arguments = new_args;
|
||||
}
|
||||
#else
|
||||
free ((void *)tc->arguments);
|
||||
tc->arguments = strdup (args_dup);
|
||||
#endif
|
||||
}
|
||||
r_json_free (args_json);
|
||||
}
|
||||
} else if (!strcmp (tool_name, "execute_js")) {
|
||||
char *args_dup = strdup (tc->arguments);
|
||||
RJson *args_json = r_json_parse (args_dup);
|
||||
if (args_json) {
|
||||
RJson *script_json = (RJson *)r_json_get (args_json, "script");
|
||||
if (script_json && script_json->str_value) {
|
||||
// Update the script field
|
||||
script_json->str_value = edited_command;
|
||||
// Serialize back to JSON
|
||||
#if 1
|
||||
char *new_args = r_json_to_string (args_json);
|
||||
if (new_args) {
|
||||
free ((void *)tc->arguments);
|
||||
tc->arguments = new_args;
|
||||
}
|
||||
#else
|
||||
free ((void *)tc->arguments);
|
||||
tc->arguments = strdup (args_dup);
|
||||
#endif
|
||||
}
|
||||
r_json_free (args_json);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -262,12 +369,20 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
free (tool_name);
|
||||
free (tool_args);
|
||||
if (strcmp (cmd_output, "R2AI_SIGINT") == 0) {
|
||||
r_cons_printf (core->cons, "\n\n\x1b[1;31m[r2ai] Processing interrupted after tool execution\x1b[0m\n\n");
|
||||
r_cons_printf (core->cons, "\n\n\x1b[1" Color_RED "[r2ai] Processing interrupted after tool execution" Color_RESET "\n\n");
|
||||
r_cons_flush (core->cons);
|
||||
free (cmd_output);
|
||||
cmd_output = strdup ("<user interrupted>");
|
||||
interrupted = true;
|
||||
|
||||
handle_final_response_attempt (cps, messages, effective_prompt, &error);
|
||||
}
|
||||
if (comment) {
|
||||
char *msg = r_str_newf ("HINT: %s\n%s", comment, cmd_output);
|
||||
free (cmd_output);
|
||||
cmd_output = msg;
|
||||
}
|
||||
free (comment);
|
||||
|
||||
// Create a tool call response message
|
||||
R2AI_Message tool_response = {
|
||||
@@ -278,19 +393,24 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
|
||||
// Add the tool response to our messages array
|
||||
r2ai_msgs_add (messages, &tool_response);
|
||||
R_LOG_DEBUG ("[DEBUG] Added tool response to messages: %s", cmd_output? cmd_output: "null");
|
||||
r_cons_printf (core->cons, "\x1b[32m[DEBUG] Tool result: %s\x1b[0m\n", cmd_output? cmd_output: "no output");
|
||||
R_LOG_DEBUG ("Added tool response to messages: %s", cmd_output? cmd_output: "null");
|
||||
r_cons_printf (core->cons, Color_GREEN "Tool result: %s" Color_RESET, cmd_output? cmd_output: "no output");
|
||||
free (cmd_output);
|
||||
i++;
|
||||
#if 0
|
||||
// Only process the first valid tool call
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
|
||||
r2ai_print_run_end (cps, usage, n_run, max_runs);
|
||||
|
||||
// Check if we should continue with recursion
|
||||
if (!interrupted && message->tool_calls && message->n_tool_calls > 0) {
|
||||
R_LOG_DEBUG ("[DEBUG] Recursing to process_messages with n_run=%d", n_run + 1);
|
||||
process_messages (cps, messages, system_prompt, n_run + 1);
|
||||
if (!interrupted && message->tool_calls && r_list_length (message->tool_calls) > 0) {
|
||||
R_LOG_DEBUG ("Recursing to process_messages with n_run=%d", n_run + 1);
|
||||
process_messages (cps, messages, effective_prompt, n_run + 1);
|
||||
} else {
|
||||
R_LOG_DEBUG ("[DEBUG] Auto mode loop ending - no more tool calls or interrupted");
|
||||
R_LOG_DEBUG ("Auto mode loop ending - no more tool calls or interrupted");
|
||||
}
|
||||
} else {
|
||||
r2ai_print_run_end (cps, usage, n_run, max_runs);
|
||||
@@ -303,10 +423,10 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
R_IPI void cmd_r2ai_a(RCorePluginSession *cps, const char *user_query) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
r_cons_printf (core->cons, "\x1b[35m[DEBUG] cmd_r2ai_a called with query: %s\x1b[0m\n", user_query);
|
||||
r_cons_printf (core->cons, Color_CYAN "cmd_r2ai_a called with query: %s" Color_RESET "\n", user_query);
|
||||
r_cons_flush (core->cons);
|
||||
// Get conversation
|
||||
R2AI_Messages *messages = r2ai_conversation_get (state);
|
||||
RList *messages = r2ai_conversation_get (state);
|
||||
if (!messages) {
|
||||
R_LOG_ERROR ("Conversation not initialized");
|
||||
return;
|
||||
@@ -314,7 +434,7 @@ R_IPI void cmd_r2ai_a(RCorePluginSession *cps, const char *user_query) {
|
||||
|
||||
// Add user query to the conversation (no system prompt)
|
||||
// If this is the first message in a new conversation, clear previous history
|
||||
if (r_list_length (messages->messages) == 0 || r_config_get_b (core->config, "r2ai.auto.reset_on_query")) {
|
||||
if (r_list_empty (messages) || r_config_get_b (core->config, "r2ai.auto.reset_on_query")) {
|
||||
r2ai_msgs_clear (messages);
|
||||
}
|
||||
|
||||
@@ -325,31 +445,22 @@ R_IPI void cmd_r2ai_a(RCorePluginSession *cps, const char *user_query) {
|
||||
};
|
||||
r2ai_msgs_add (messages, &user_msg);
|
||||
|
||||
process_messages (cps, messages, NULL, 1);
|
||||
const char *system_prompt = NULL;
|
||||
if (!r_config_get_b (core->config, "r2ai.auto.think")) {
|
||||
system_prompt = r_str_newf ("/no_think\nReasoning: Low\n%s", Gprompt_auto);
|
||||
}
|
||||
process_messages (cps, messages, system_prompt, 1);
|
||||
if (system_prompt) {
|
||||
free ((char *)system_prompt);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to display content with length indication for long content
|
||||
static void print_content_with_length(RCore *core, const char *content, const char *empty_msg, bool always_show_length) {
|
||||
if (!content || *content == '\0') {
|
||||
r_cons_printf (core->cons, "%s\n", empty_msg? empty_msg: "<no content>");
|
||||
return;
|
||||
}
|
||||
|
||||
size_t content_len = strlen (content);
|
||||
const size_t max_display = 200;
|
||||
|
||||
if (content_len > max_display) {
|
||||
// Truncate long content and show length
|
||||
char *truncated = r_str_ndup (content, max_display);
|
||||
r_cons_printf (core->cons, "%s... \x1b[1;37m(length: %zu chars)\x1b[0m\n",
|
||||
truncated, content_len);
|
||||
free (truncated);
|
||||
} else if (always_show_length) {
|
||||
// Always show length for certain types (like tool responses)
|
||||
r_cons_printf (core->cons, "%s \x1b[1;37m(length: %zu chars)\x1b[0m\n",
|
||||
content, content_len);
|
||||
static void print_content_with_length(RCore *core, const char *content, const char *empty_msg) {
|
||||
if (R_STR_ISEMPTY (content)) {
|
||||
r_cons_println (core->cons, empty_msg? empty_msg: "<no content>");
|
||||
} else {
|
||||
r_cons_printf (core->cons, "%s\n", content);
|
||||
r_cons_println (core->cons, content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,8 +469,8 @@ R_IPI void cmd_r2ai_logs(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
// Get conversation
|
||||
R2AI_Messages *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_length (messages->messages) == 0) {
|
||||
RList *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_empty (messages)) {
|
||||
r_cons_printf (core->cons, "No conversation history available\n");
|
||||
return;
|
||||
}
|
||||
@@ -376,15 +487,17 @@ R_IPI void cmd_r2ai_logs(RCorePluginSession *cps) {
|
||||
|
||||
pj_a (pj);
|
||||
|
||||
for (int i = 0; i < r_list_length (messages->messages); i++) {
|
||||
const R2AI_Message *msg = r_list_get_n (messages->messages, i);
|
||||
RListIter *iter;
|
||||
const R2AI_Message *msg;
|
||||
r_list_foreach (messages, iter, msg) {
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "role", msg->role? msg->role: "unknown");
|
||||
pj_ks (pj, "content", msg->content? msg->content: "");
|
||||
if (msg->tool_calls && msg->n_tool_calls > 0) {
|
||||
if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
|
||||
pj_ka (pj, "tool_calls");
|
||||
for (int j = 0; j < msg->n_tool_calls; j++) {
|
||||
const R2AI_ToolCall *tc = &msg->tool_calls[j];
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (msg->tool_calls, iter, tc) {
|
||||
pj_o (pj);
|
||||
if (tc->name) {
|
||||
pj_ks (pj, "name", tc->name);
|
||||
@@ -408,28 +521,30 @@ R_IPI void cmd_r2ai_logs(RCorePluginSession *cps) {
|
||||
return;
|
||||
}
|
||||
|
||||
r_cons_printf (core->cons, "\x1b[1;34m[r2ai] Chat Logs (%d messages)\x1b[0m\n",
|
||||
r_list_length (messages->messages));
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_BLUE "[r2ai] Chat Logs (%d messages)" Color_RESET "\n",
|
||||
r_list_length (messages));
|
||||
|
||||
r_cons_printf (core->cons, "\x1b[1;33mNote: System prompt is applied automatically but not stored in history\x1b[0m\n\n");
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_YELLOW "Note: System prompt is applied automatically but not stored in history" Color_RESET "\n\n");
|
||||
|
||||
// Display each message in the conversation
|
||||
for (int i = 0; i < r_list_length (messages->messages); i++) {
|
||||
const R2AI_Message *msg = r_list_get_n (messages->messages, i);
|
||||
RListIter *iter;
|
||||
const R2AI_Message *msg;
|
||||
r_list_foreach (messages, iter, msg) {
|
||||
const char *role = msg->role;
|
||||
|
||||
// Format based on role
|
||||
if (!strcmp (role, "user")) {
|
||||
r_cons_printf (core->cons, "\x1b[1;32m[user]:\x1b[0m ");
|
||||
print_content_with_length (core, msg->content, "<no content>", false);
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_GREEN "[user]:" Color_RESET " ");
|
||||
print_content_with_length (core, msg->content, "<no content>");
|
||||
} else if (!strcmp (role, "assistant")) {
|
||||
r_cons_printf (core->cons, "\x1b[1;36m[assistant]:\x1b[0m ");
|
||||
print_content_with_length (core, msg->content, "<no content>", false);
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_CYAN "[assistant]:" Color_RESET " ");
|
||||
print_content_with_length (core, msg->content, "<no content>");
|
||||
// Show tool calls if present
|
||||
if (msg->tool_calls && msg->n_tool_calls > 0) {
|
||||
for (int j = 0; j < msg->n_tool_calls; j++) {
|
||||
const R2AI_ToolCall *tc = &msg->tool_calls[j];
|
||||
r_cons_printf (core->cons, " \x1b[1;35m[tool call]:\x1b[0m %s\n",
|
||||
if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (msg->tool_calls, iter, tc) {
|
||||
r_cons_printf (core->cons, " \x1b[1" Color_MAGENTA "[tool call]:" Color_RESET " %s\n",
|
||||
tc->name? tc->name: "<unnamed>");
|
||||
|
||||
if (tc->arguments) {
|
||||
@@ -438,17 +553,118 @@ R_IPI void cmd_r2ai_logs(RCorePluginSession *cps) {
|
||||
}
|
||||
}
|
||||
} else if (!strcmp (role, "tool")) {
|
||||
r_cons_printf (core->cons, "\x1b[1;35m[tool]:\x1b[0m ");
|
||||
print_content_with_length (core, msg->content, "<no result>", true);
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_MAGENTA "[tool]:" Color_RESET " ");
|
||||
print_content_with_length (core, msg->content, "<no result>");
|
||||
|
||||
// Don't show the tool call ID as requested
|
||||
} else {
|
||||
// Other roles (system, etc.)
|
||||
r_cons_printf (core->cons, "\x1b[1;37m[%s]:\x1b[0m ", role);
|
||||
print_content_with_length (core, msg->content, "<no content>", false);
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_WHITE "[%s]:" Color_RESET " ", role);
|
||||
print_content_with_length (core, msg->content, "<no content>");
|
||||
}
|
||||
|
||||
r_cons_newline (core->cons);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to format conversation log as string
|
||||
static char *format_conversation_log(RList *messages) {
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
r_strbuf_append (sb, "Conversation Log:\n");
|
||||
|
||||
RListIter *iter;
|
||||
const R2AI_Message *msg;
|
||||
r_list_foreach (messages, iter, msg) {
|
||||
const char *role = msg->role;
|
||||
|
||||
if (!strcmp (role, "user")) {
|
||||
r_strbuf_appendf (sb, "[user]: %s\n", msg->content? msg->content: "<no content>");
|
||||
} else if (!strcmp (role, "assistant")) {
|
||||
r_strbuf_appendf (sb, "[assistant]: %s\n", msg->content? msg->content: "<no content>");
|
||||
// Include tool calls if present
|
||||
if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
|
||||
RListIter *iter_tc;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (msg->tool_calls, iter_tc, tc) {
|
||||
r_strbuf_appendf (sb, " [tool call]: %s\n", tc->name? tc->name: "<unnamed>");
|
||||
if (tc->arguments) {
|
||||
r_strbuf_appendf (sb, " %s\n", tc->arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!strcmp (role, "tool")) {
|
||||
r_strbuf_appendf (sb, "[tool]: %s\n", msg->content? msg->content: "<no result>");
|
||||
} else {
|
||||
r_strbuf_appendf (sb, "[%s]: %s\n", role, msg->content? msg->content: "<no content>");
|
||||
}
|
||||
}
|
||||
|
||||
return r_strbuf_drain (sb);
|
||||
}
|
||||
|
||||
// Helper function to process conversation with LLM and handle result
|
||||
static void process_conversation_with_llm(RCorePluginSession *cps, bool compact) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
// Get conversation
|
||||
RList *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_empty (messages)) {
|
||||
r_cons_printf (core->cons, "No conversation history available\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Format conversation as string
|
||||
char *log_str = format_conversation_log (messages);
|
||||
|
||||
// Load the compact prompt from file
|
||||
char *prompt_text = r2ai_load_prompt_text (core, "compact");
|
||||
if (!prompt_text) {
|
||||
// Fallback to hardcoded prompt
|
||||
prompt_text = strdup (compact
|
||||
? "Create a compact summary of this conversation that preserves all key information, insights, and context for future reference."
|
||||
"Focus on essential details about the binary analysis, tools used, findings, and any important conclusions."
|
||||
: "mai create a summary of all the information retrieved from the binary that is relevant for future work");
|
||||
}
|
||||
|
||||
// Combine log and prompt
|
||||
char *full_input = r_str_newf ("%s\n\n%s", log_str, prompt_text);
|
||||
|
||||
char *error = NULL;
|
||||
char *res = r2ai (cps, (R2AIArgs){ .input = full_input, .error = &error, .dorag = false });
|
||||
|
||||
free (log_str);
|
||||
free (full_input);
|
||||
free (prompt_text);
|
||||
|
||||
if (error) {
|
||||
R_LOG_ERROR ("%s", error);
|
||||
free (error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (res) {
|
||||
if (compact) {
|
||||
// Clear the conversation and add summary as system message
|
||||
r2ai_msgs_clear (messages);
|
||||
R2AI_Message summary_msg = {
|
||||
.role = "system",
|
||||
.content = res
|
||||
};
|
||||
r2ai_msgs_add (messages, &summary_msg);
|
||||
r_cons_printf (core->cons, "Conversation compacted. Summary added to history.\n");
|
||||
} else {
|
||||
// Just print the result
|
||||
r_cons_printf (core->cons, "%s\n", res);
|
||||
}
|
||||
free (res);
|
||||
}
|
||||
}
|
||||
|
||||
R_IPI void cmd_r2ai_c(RCorePluginSession *cps) {
|
||||
process_conversation_with_llm (cps, true);
|
||||
}
|
||||
|
||||
R_IPI void cmd_r2ai_lr(RCorePluginSession *cps) {
|
||||
process_conversation_with_llm (cps, false);
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
/* Copyright r2ai - 2023-2025 - pancake */
|
||||
|
||||
#define _GNU_SOURCE
|
||||
// #define _POSIX_C_SOURCE 200809L
|
||||
#include <signal.h>
|
||||
#include "r2ai.h"
|
||||
|
||||
// Global flag for tracking interrupt status
|
||||
static volatile sig_atomic_t r2ai_http_interrupted = 0;
|
||||
|
||||
// Helper function to get HTTP configuration with defaults
|
||||
static R2AI_HttpConfig get_http_config(RCore *core) {
|
||||
R2AI_HttpConfig config = { 120, 10, 30 };
|
||||
if (core) {
|
||||
int t = r_config_get_i (core->config, "r2ai.http.timeout");
|
||||
if (t > 0) {
|
||||
config.timeout = t;
|
||||
}
|
||||
int mr = r_config_get_i (core->config, "r2ai.http.max_retries");
|
||||
if (mr >= 0) {
|
||||
config.max_retries = mr;
|
||||
}
|
||||
int mb = r_config_get_i (core->config, "r2ai.http.max_backoff");
|
||||
if (mb > 0) {
|
||||
config.max_backoff = mb;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
#if R2__UNIX__
|
||||
// Signal handler for timeout (SIGALRM)
|
||||
static void r2ai_http_sigint_handler(int sig) {
|
||||
if (sig == SIGALRM) {
|
||||
r2ai_http_interrupted = 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Portable break callback for r_cons_break (handles SIGINT)
|
||||
static void r2ai_http_break_callback(void *user) {
|
||||
(void)user;
|
||||
r2ai_http_interrupted = 1;
|
||||
}
|
||||
|
||||
// Function pointer type for HTTP request implementations
|
||||
typedef HttpResponse(*HttpRequestFunc)(const HTTPRequest *request);
|
||||
|
||||
// Simple retry sleep
|
||||
static void sleep_retry(int retry_count, int max_sleep_seconds) {
|
||||
int delay_seconds = retry_count + 1; // sleep 1, 2, 3, ... seconds
|
||||
if (delay_seconds > max_sleep_seconds) {
|
||||
delay_seconds = max_sleep_seconds;
|
||||
}
|
||||
r_sys_sleep (delay_seconds);
|
||||
}
|
||||
|
||||
// Generic HTTP request with retry logic
|
||||
static HttpResponse r2ai_http_request_with_retry(HttpRequestFunc func, const HTTPRequest *request, RCore *core) {
|
||||
// Install portable break handler for interruption
|
||||
if (core && core->cons) {
|
||||
r_cons_break_push (core->cons, r2ai_http_break_callback, NULL);
|
||||
}
|
||||
|
||||
// Reset interrupt flag
|
||||
r2ai_http_interrupted = 0;
|
||||
|
||||
#if R2__UNIX__
|
||||
// Ignore SIGWINCH to prevent window resize from interrupting child processes
|
||||
struct sigaction old_winch_act;
|
||||
sigaction (SIGWINCH, &(struct sigaction){ .sa_handler = SIG_IGN }, &old_winch_act);
|
||||
#endif
|
||||
|
||||
// Retry loop
|
||||
HttpResponse result = { 0 };
|
||||
int retry_count = 0;
|
||||
bool success = false;
|
||||
|
||||
while (!success && retry_count <= request->config.max_retries && !r2ai_http_interrupted) {
|
||||
result = func (request);
|
||||
|
||||
// Check if we were interrupted
|
||||
if (r2ai_http_interrupted) {
|
||||
R_LOG_DEBUG ("HTTP request was interrupted by user");
|
||||
free (result.body);
|
||||
result.body = NULL;
|
||||
result.code = -1;
|
||||
break; // Exit the retry loop
|
||||
}
|
||||
|
||||
// Check for rate limiting or server errors (429, 500, 502, 503, 504)
|
||||
if (result.code > 0) {
|
||||
if (result.code == 429 || (result.code >= 500 && result.code < 600)) {
|
||||
R_LOG_WARN ("Server returned %d response code", result.code);
|
||||
free (result.body);
|
||||
result.body = NULL;
|
||||
result.code = 0;
|
||||
|
||||
if (retry_count < request->config.max_retries) {
|
||||
retry_count++;
|
||||
R_LOG_INFO ("Retrying request (%d/%d) after error...", retry_count, request->config.max_retries);
|
||||
sleep_retry (retry_count, request->config.max_backoff);
|
||||
continue;
|
||||
}
|
||||
result.code = -1;
|
||||
break; // Exit the retry loop after max retries
|
||||
}
|
||||
}
|
||||
|
||||
// Check for other failures
|
||||
if (result.code <= 0 || !result.body) {
|
||||
free (result.body);
|
||||
result.body = NULL;
|
||||
result.code = -1;
|
||||
if (retry_count < request->config.max_retries) {
|
||||
retry_count++;
|
||||
R_LOG_INFO ("Retrying request (%d/%d) after failure...", retry_count, request->config.max_retries);
|
||||
sleep_retry (retry_count, request->config.max_backoff);
|
||||
continue;
|
||||
}
|
||||
break; // Exit the retry loop after max retries
|
||||
}
|
||||
success = true;
|
||||
}
|
||||
|
||||
// Restore the original break handler
|
||||
if (core && core->cons) {
|
||||
r_cons_break_pop (core->cons);
|
||||
}
|
||||
|
||||
#if R2__UNIX__
|
||||
// Restore SIGWINCH handler
|
||||
sigaction (SIGWINCH, &old_winch_act, NULL);
|
||||
#endif
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#include "http/libcurl.inc.c"
|
||||
#include "http/pwsh.inc.c"
|
||||
#include "http/curl.inc.c"
|
||||
#include "http/r2.inc.c"
|
||||
|
||||
static HttpRequestFunc select_backend(const char *backend, bool is_post) {
|
||||
HttpRequestFunc func = NULL;
|
||||
if (!strcmp (backend, "auto")) {
|
||||
#if R2__WINDOWS__
|
||||
// TODO: we should use pwsh or r2 by default, but for not it seems the only backend that works on Windows is r2curl+system
|
||||
r_sys_setenv ("R2_CURL", "1");
|
||||
backend = "system";
|
||||
#elif USE_LIBCURL && HAVE_LIBCURL
|
||||
backend = "libcurl";
|
||||
#else
|
||||
backend = "system";
|
||||
#endif
|
||||
R_LOG_DEBUG ("Auto-selected %s backend", backend);
|
||||
}
|
||||
if (!strcmp (backend, "system") || !strcmp (backend, "curl")) {
|
||||
func = is_post? system_curl_post: system_curl_get;
|
||||
} else if (!strcmp (backend, "libcurl")) {
|
||||
#if USE_LIBCURL && HAVE_LIBCURL
|
||||
func = is_post? curl_http_post: curl_http_get;
|
||||
#else
|
||||
R_LOG_WARN ("LibCurl requested but not available, falling back to socket implementation");
|
||||
func = is_post? socket_http_post_with_interrupt: socket_http_get_with_interrupt;
|
||||
#endif
|
||||
} else if (!strcmp (backend, "socket")) {
|
||||
func = is_post? socket_http_post_with_interrupt: socket_http_get_with_interrupt;
|
||||
} else if (!strcmp (backend, "pwsh") || !strcmp (backend, "powershell")) {
|
||||
#if R2__WINDOWS__
|
||||
func = is_post? windows_http_post: windows_http_get;
|
||||
#else
|
||||
R_LOG_WARN ("powershell is only available on Windows");
|
||||
func = is_post? socket_http_post_with_interrupt: socket_http_get_with_interrupt;
|
||||
#endif
|
||||
} else if (!strcmp (backend, "r2curl")) {
|
||||
r_sys_setenv ("R2_CURL", "1");
|
||||
func = is_post? socket_http_post_with_interrupt: socket_http_get_with_interrupt;
|
||||
}
|
||||
return func;
|
||||
}
|
||||
|
||||
// Generic HTTP request function that handles backend selection
|
||||
static HttpResponse r2ai_http_request(const char *method, RCore *core, const char *url, const char *headers[], const char *data) {
|
||||
(void)method;
|
||||
bool is_post = (data != NULL);
|
||||
|
||||
HTTPRequest request = {
|
||||
.config = get_http_config (core),
|
||||
.url = url,
|
||||
.data = data,
|
||||
.headers = headers
|
||||
};
|
||||
|
||||
const char *backend = r_config_get (core->config, "r2ai.http.backend");
|
||||
HttpRequestFunc func = select_backend (backend, is_post);
|
||||
if (func) {
|
||||
return r2ai_http_request_with_retry (func, &request, core);
|
||||
}
|
||||
R_LOG_ERROR ("Cannot find a valid http backend");
|
||||
return (HttpResponse){ .body = NULL, .code = -1, .length = 0 };
|
||||
}
|
||||
|
||||
R_API char *r2ai_http_post(RCore *core, const char *url, const char *headers[], const char *data, int *code, int *rlen) {
|
||||
HttpResponse response = r2ai_http_request ("POST", core, url, headers, data);
|
||||
if (response.code <= 0) {
|
||||
free (response.body);
|
||||
if (code) {
|
||||
*code = response.code;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
if (code) {
|
||||
*code = response.code;
|
||||
}
|
||||
if (rlen) {
|
||||
*rlen = response.length;
|
||||
}
|
||||
return response.body;
|
||||
}
|
||||
|
||||
R_API char *r2ai_http_get(RCore *core, const char *url, const char *headers[], int *code, int *rlen) {
|
||||
HttpResponse response = r2ai_http_request ("GET", core, url, headers, NULL);
|
||||
if (response.code <= 0) {
|
||||
if (response.body) {
|
||||
R_FREE (response.body);
|
||||
}
|
||||
if (code) {
|
||||
*code = response.code;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
if (code) {
|
||||
*code = response.code;
|
||||
}
|
||||
if (rlen) {
|
||||
*rlen = response.length;
|
||||
}
|
||||
return response.body;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Helper to escape double quotes for cmd double-quoted strings
|
||||
static char *escape_cmd_double_quotes(const char *str) {
|
||||
if (!str) {
|
||||
return NULL;
|
||||
}
|
||||
size_t len = strlen (str);
|
||||
size_t count = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (str[i] == '"') {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count == 0) {
|
||||
return strdup (str);
|
||||
}
|
||||
char *escaped = malloc (len + count + 1);
|
||||
if (!escaped) {
|
||||
return NULL;
|
||||
}
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (str[i] == '"') {
|
||||
escaped[j++] = '\\';
|
||||
escaped[j++] = '"';
|
||||
} else {
|
||||
escaped[j++] = str[i];
|
||||
}
|
||||
}
|
||||
escaped[j] = '\0';
|
||||
return escaped;
|
||||
}
|
||||
|
||||
static HttpResponse build_and_execute_curl(const char *cmd_start, const HTTPRequest *request, const char *input_data) {
|
||||
HttpResponse error = { .code = -1 };
|
||||
int timeout = request->config.timeout;
|
||||
if (!request->url) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Compose curl command
|
||||
RStrBuf *cmd = r_strbuf_new (cmd_start);
|
||||
|
||||
// Add timeout
|
||||
r_strbuf_appendf (cmd, " --connect-timeout %d --max-time %d", 10, timeout);
|
||||
|
||||
// Add headers
|
||||
if (request->headers) {
|
||||
for (int i = 0; request->headers[i] != NULL; i++) {
|
||||
char *escaped_header = escape_cmd_double_quotes (request->headers[i]);
|
||||
r_strbuf_appendf (cmd, " -H \"%s\"", escaped_header);
|
||||
free (escaped_header);
|
||||
}
|
||||
}
|
||||
char *escaped_url = escape_cmd_double_quotes (request->url);
|
||||
r_strbuf_appendf (cmd, " -w \"\\n%%{http_code}\" \"%s\"", escaped_url);
|
||||
free (escaped_url);
|
||||
char *cmd_str = r_strbuf_drain (cmd);
|
||||
R_LOG_DEBUG ("Running system curl: %s", cmd_str);
|
||||
char *response = r_sys_cmd_str (cmd_str, input_data, NULL);
|
||||
|
||||
free (cmd_str);
|
||||
|
||||
if (!response) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Parse the response: body\ncode
|
||||
char *last_nl = (char *)r_str_rchr (response, NULL, '\n');
|
||||
int code = -1;
|
||||
char *body = response;
|
||||
if (last_nl) {
|
||||
*last_nl = '\0';
|
||||
code = atoi (last_nl + 1);
|
||||
if (code == 0) {
|
||||
code = -1; // Invalid code
|
||||
}
|
||||
} else {
|
||||
// No newline, assume 200 if response
|
||||
code = 200;
|
||||
}
|
||||
|
||||
return (HttpResponse){ .body = body, .code = code, .length = strlen (body) };
|
||||
}
|
||||
|
||||
HttpResponse system_curl_get(const HTTPRequest *request) {
|
||||
R_LOG_DEBUG ("Using system curl GET");
|
||||
return build_and_execute_curl ("curl -s", request, NULL);
|
||||
}
|
||||
|
||||
HttpResponse system_curl_post(const HTTPRequest *request) {
|
||||
HttpResponse error = { .code = -1 };
|
||||
if (!request->url || !request->data) {
|
||||
return error;
|
||||
}
|
||||
|
||||
R_LOG_DEBUG ("Using system curl POST without files");
|
||||
HttpResponse res = build_and_execute_curl ("curl -s -X POST -d @-", request, request->data);
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
#if HAVE_LIBCURL
|
||||
#include <curl/curl.h>
|
||||
|
||||
// Struct to store response data
|
||||
typedef struct {
|
||||
char *data;
|
||||
size_t size;
|
||||
} CurlResponse;
|
||||
|
||||
// Callback function for curl to write response data
|
||||
static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) {
|
||||
// Check if we've been interrupted
|
||||
if (r2ai_http_interrupted) {
|
||||
return 0; // Return 0 to abort the transfer
|
||||
}
|
||||
|
||||
size_t realsize = size * nmemb;
|
||||
CurlResponse *mem = (CurlResponse *)userp;
|
||||
|
||||
char *ptr = realloc (mem->data, mem->size + realsize + 1);
|
||||
if (!ptr) {
|
||||
R_LOG_ERROR ("Not enough memory for curl response");
|
||||
return 0; // Out of memory
|
||||
}
|
||||
|
||||
mem->data = ptr;
|
||||
memcpy (&(mem->data[mem->size]), contents, realsize);
|
||||
mem->size += realsize;
|
||||
mem->data[mem->size] = 0;
|
||||
|
||||
return realsize;
|
||||
}
|
||||
|
||||
// Curl progress callback function to handle interrupts
|
||||
static int progress_callback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
|
||||
return r2ai_http_interrupted? 1: 0;
|
||||
}
|
||||
|
||||
static HttpResponse curl_http_request(const HTTPRequest *request, bool is_post) {
|
||||
HttpResponse error = { .code = -1 };
|
||||
if (!request->url || (is_post && !request->data)) {
|
||||
return error;
|
||||
}
|
||||
|
||||
CURL *curl;
|
||||
CURLcode res;
|
||||
struct curl_slist *curl_headers = NULL;
|
||||
CurlResponse response = { 0 };
|
||||
|
||||
// Initialize response
|
||||
response.data = malloc (1);
|
||||
if (!response.data) {
|
||||
return error;
|
||||
}
|
||||
response.data[0] = '\0';
|
||||
response.size = 0;
|
||||
|
||||
curl = curl_easy_init ();
|
||||
if (!curl) {
|
||||
free (response.data);
|
||||
return error;
|
||||
}
|
||||
|
||||
// Set URL
|
||||
curl_easy_setopt (curl, CURLOPT_URL, request->url);
|
||||
|
||||
// Set method-specific options
|
||||
if (is_post) {
|
||||
curl_easy_setopt (curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt (curl, CURLOPT_POSTFIELDS, request->data);
|
||||
curl_easy_setopt (curl, CURLOPT_POSTFIELDSIZE, (long)strlen (request->data));
|
||||
} else {
|
||||
curl_easy_setopt (curl, CURLOPT_HTTPGET, 1L);
|
||||
}
|
||||
|
||||
// Set headers if provided
|
||||
if (request->headers) {
|
||||
for (int i = 0; request->headers[i] != NULL; i++) {
|
||||
curl_headers = curl_slist_append (curl_headers, request->headers[i]);
|
||||
}
|
||||
curl_easy_setopt (curl, CURLOPT_HTTPHEADER, curl_headers);
|
||||
}
|
||||
|
||||
// Set write callback
|
||||
curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt (curl, CURLOPT_WRITEDATA, (void *)&response);
|
||||
|
||||
// Set progress callback to handle interrupts
|
||||
curl_easy_setopt (curl, CURLOPT_NOPROGRESS, 0L);
|
||||
curl_easy_setopt (curl, CURLOPT_XFERINFOFUNCTION, progress_callback);
|
||||
curl_easy_setopt (curl, CURLOPT_XFERINFODATA, NULL);
|
||||
|
||||
// Set timeout options
|
||||
curl_easy_setopt (curl, CURLOPT_CONNECTTIMEOUT, 10L); // 10 seconds connect timeout
|
||||
curl_easy_setopt (curl, CURLOPT_TIMEOUT, (long)request->config.timeout); // Use configured timeout
|
||||
|
||||
// Follow redirects
|
||||
curl_easy_setopt (curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
|
||||
// Perform the request
|
||||
res = curl_easy_perform (curl);
|
||||
|
||||
// Check for interruption
|
||||
if (r2ai_http_interrupted) {
|
||||
R_LOG_DEBUG ("HTTP request was interrupted by user");
|
||||
free (response.data);
|
||||
curl_slist_free_all (curl_headers);
|
||||
curl_easy_cleanup (curl);
|
||||
return error;
|
||||
}
|
||||
|
||||
// Get response code
|
||||
long http_code;
|
||||
curl_easy_getinfo (curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
|
||||
// Check for errors
|
||||
if (res != CURLE_OK) {
|
||||
R_LOG_ERROR ("curl_easy_perform () failed: %s", curl_easy_strerror (res));
|
||||
free (response.data);
|
||||
curl_slist_free_all (curl_headers);
|
||||
curl_easy_cleanup (curl);
|
||||
return error;
|
||||
}
|
||||
|
||||
// If we get here, the request was successful
|
||||
// Cleanup
|
||||
curl_slist_free_all (curl_headers);
|
||||
curl_easy_cleanup (curl);
|
||||
|
||||
return (HttpResponse){ .body = response.data, .code = (int)http_code, .length = response.size };
|
||||
}
|
||||
|
||||
HttpResponse curl_http_get(const HTTPRequest *request) {
|
||||
return curl_http_request (request, false);
|
||||
}
|
||||
|
||||
HttpResponse curl_http_post(const HTTPRequest *request) {
|
||||
return curl_http_request (request, true);
|
||||
}
|
||||
#endif // HAVE_LIBCURL
|
||||
@@ -0,0 +1,118 @@
|
||||
#if R2__WINDOWS__
|
||||
|
||||
// Helper to escape single quotes for PowerShell single-quoted strings
|
||||
static char *escape_single_quotes(const char *str) {
|
||||
if (!str) {
|
||||
return NULL;
|
||||
}
|
||||
size_t len = strlen (str);
|
||||
size_t count = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (str[i] == '\'') {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count == 0) {
|
||||
return strdup (str);
|
||||
}
|
||||
char *escaped = malloc (len + count + 1);
|
||||
if (!escaped) {
|
||||
return NULL;
|
||||
}
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (str[i] == '\'') {
|
||||
escaped[j++] = '\'';
|
||||
escaped[j++] = '\'';
|
||||
} else {
|
||||
escaped[j++] = str[i];
|
||||
}
|
||||
}
|
||||
escaped[j] = '\0';
|
||||
return escaped;
|
||||
}
|
||||
|
||||
// Helper to append headers to PowerShell command
|
||||
static void append_headers_to_cmd(RStrBuf *cmd, const char *const *headers) {
|
||||
r_strbuf_appendf (cmd, "$headers=@{");
|
||||
if (headers) {
|
||||
for (int i = 0; headers[i]; i++) {
|
||||
char *header = strdup (headers[i]);
|
||||
char *colon = strchr (header, ':');
|
||||
if (colon) {
|
||||
*colon = '\0';
|
||||
char *key = r_str_trim_dup (header);
|
||||
char *value = r_str_trim_dup (colon + 1);
|
||||
char *escaped_key = escape_single_quotes (key);
|
||||
char *escaped_value = escape_single_quotes (value);
|
||||
r_strbuf_appendf (cmd, "'%s'='%s';", escaped_key, escaped_value);
|
||||
free (escaped_key);
|
||||
free (escaped_value);
|
||||
free (key);
|
||||
free (value);
|
||||
}
|
||||
free (header);
|
||||
}
|
||||
}
|
||||
r_strbuf_appendf (cmd, "};");
|
||||
}
|
||||
|
||||
// Helper to parse PowerShell response
|
||||
static HttpResponse parse_powershell_response(char *full_response) {
|
||||
if (full_response) {
|
||||
char *newline = strchr (full_response, '\n');
|
||||
if (newline) {
|
||||
*newline = '\0';
|
||||
int code = atoi (full_response);
|
||||
char *body = newline + 1;
|
||||
char *body_copy = strdup (body);
|
||||
size_t length = strlen (body);
|
||||
free (full_response);
|
||||
return (HttpResponse){ .body = body_copy, .code = code, .length = length };
|
||||
} else {
|
||||
return (HttpResponse){ .body = full_response, .code = 200, .length = strlen (full_response) };
|
||||
}
|
||||
}
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
/**
|
||||
* Windows-specific HTTP POST using PowerShell
|
||||
*/
|
||||
HttpResponse windows_http_post(const HTTPRequest *request) {
|
||||
const char *url = request->url;
|
||||
const char *const *headers = request->headers;
|
||||
const char *data = request->data;
|
||||
int timeout = request->config.timeout;
|
||||
char *escaped_url = escape_single_quotes (url);
|
||||
char *escaped_data = escape_single_quotes (data);
|
||||
RStrBuf *cmd = r_strbuf_new ("powershell -Command \"");
|
||||
append_headers_to_cmd (cmd, headers);
|
||||
r_strbuf_appendf (cmd, "$body='%s';", escaped_data);
|
||||
r_strbuf_appendf (cmd, "try{$r=Invoke-WebRequest -Method Post -Uri '%s' -Headers $headers -Body $body -TimeoutSec %d;", escaped_url, timeout);
|
||||
r_strbuf_appendf (cmd, "Write-Host $r.StatusCode;$r.Content}catch{Write-Host 0;$_.Exception.Message}\"");
|
||||
char *cmd_str = r_strbuf_drain (cmd);
|
||||
R_LOG_DEBUG ("Running PowerShell: %s", cmd_str);
|
||||
char *full_response = r_sys_cmd_str (cmd_str, NULL, NULL);
|
||||
free (cmd_str);
|
||||
return parse_powershell_response (full_response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows-specific HTTP GET using PowerShell
|
||||
*/
|
||||
HttpResponse windows_http_get(const HTTPRequest *request) {
|
||||
const char *url = request->url;
|
||||
const char *const *headers = request->headers;
|
||||
int timeout = request->config.timeout;
|
||||
char *escaped_url = escape_single_quotes (url);
|
||||
RStrBuf *cmd = r_strbuf_new ("powershell -Command \"");
|
||||
append_headers_to_cmd (cmd, headers);
|
||||
r_strbuf_appendf (cmd, "try{$r=Invoke-WebRequest -Method Get -Uri '%s' -Headers $headers -TimeoutSec %d;", escaped_url, timeout);
|
||||
r_strbuf_appendf (cmd, "Write-Host $r.StatusCode;$r.Content}catch{Write-Host 0;$_.Exception.Message}\"");
|
||||
char *cmd_str = r_strbuf_drain (cmd);
|
||||
R_LOG_DEBUG ("Running PowerShell: %s", cmd_str);
|
||||
char *full_response = r_sys_cmd_str (cmd_str, NULL, NULL);
|
||||
free (cmd_str);
|
||||
return parse_powershell_response (full_response);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,49 @@
|
||||
extern volatile sig_atomic_t r2ai_http_interrupted;
|
||||
extern void r2ai_http_sigint_handler(int sig);
|
||||
|
||||
// Socket implementation with interrupt handling and retry logic
|
||||
static HttpResponse socket_http_post_with_interrupt(const HTTPRequest *request) {
|
||||
#if R2__WINDOWS__
|
||||
r_sys_setenv ("R2_CURL", "1");
|
||||
#endif
|
||||
#if R2__UNIX__
|
||||
signal (SIGALRM, r2ai_http_sigint_handler);
|
||||
alarm (request->config.timeout); // Use configured timeout
|
||||
#endif
|
||||
int code = 0;
|
||||
int rlen = 0;
|
||||
char *result = r_socket_http_post (request->url, (const char **)request->headers, request->data, &code, &rlen);
|
||||
#if R2__UNIX__
|
||||
alarm (0);
|
||||
#endif
|
||||
if (r2ai_http_interrupted) {
|
||||
R_LOG_DEBUG ("HTTP request was interrupted by user");
|
||||
free (result);
|
||||
return (HttpResponse){ .body = NULL, .code = -1, .length = 0 };
|
||||
}
|
||||
return (HttpResponse){ .body = result, .code = code, .length = rlen };
|
||||
}
|
||||
|
||||
// Socket implementation for GET requests
|
||||
static HttpResponse socket_http_get_with_interrupt(const HTTPRequest *request) {
|
||||
#if R2__WINDOWS__
|
||||
r_sys_setenv ("R2_CURL", "1");
|
||||
#endif
|
||||
#if R2__UNIX__
|
||||
signal (SIGALRM, r2ai_http_sigint_handler);
|
||||
alarm (request->config.timeout); // Use configured timeout
|
||||
#endif
|
||||
// Make the request - use r_socket_http_get if available
|
||||
int code = 0;
|
||||
int rlen = 0;
|
||||
char *result = r_socket_http_get (request->url, (const char **)request->headers, &code, &rlen);
|
||||
#if R2__UNIX__
|
||||
alarm (0);
|
||||
#endif
|
||||
if (r2ai_http_interrupted) {
|
||||
R_LOG_DEBUG ("HTTP request was interrupted by user");
|
||||
free (result);
|
||||
return (HttpResponse){ .body = NULL, .code = -1, .length = 0 };
|
||||
}
|
||||
return (HttpResponse){ .body = result, .code = code, .length = rlen };
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
#include "r2ai.h"
|
||||
// TODO: move into r2/libr/util/json.c
|
||||
//
|
||||
// Helper function to convert RJson to PJ without draining
|
||||
R_API PJ *r_json_to_pj(const RJson *json, PJ *existing_pj) {
|
||||
if (!json) {
|
||||
return existing_pj;
|
||||
}
|
||||
|
||||
PJ *pj = existing_pj? existing_pj: pj_new ();
|
||||
if (!pj) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
switch (json->type) {
|
||||
case R_JSON_STRING:
|
||||
pj_s (pj, json->str_value);
|
||||
break;
|
||||
case R_JSON_INTEGER:
|
||||
pj_n (pj, json->num.u_value);
|
||||
break;
|
||||
case R_JSON_DOUBLE:
|
||||
pj_d (pj, json->num.dbl_value);
|
||||
break;
|
||||
case R_JSON_BOOLEAN:
|
||||
pj_b (pj, json->num.u_value);
|
||||
break;
|
||||
case R_JSON_NULL:
|
||||
pj_null (pj);
|
||||
break;
|
||||
case R_JSON_OBJECT:
|
||||
if (!existing_pj) {
|
||||
pj_o (pj);
|
||||
}
|
||||
// Handle object's properties
|
||||
const RJson *prop = json->children.first;
|
||||
while (prop) {
|
||||
if (prop->key) {
|
||||
switch (prop->type) {
|
||||
case R_JSON_STRING:
|
||||
pj_ks (pj, prop->key, prop->str_value);
|
||||
break;
|
||||
case R_JSON_INTEGER:
|
||||
pj_kn (pj, prop->key, prop->num.u_value);
|
||||
break;
|
||||
case R_JSON_DOUBLE:
|
||||
pj_kd (pj, prop->key, prop->num.dbl_value);
|
||||
break;
|
||||
case R_JSON_BOOLEAN:
|
||||
pj_kb (pj, prop->key, prop->num.u_value);
|
||||
break;
|
||||
case R_JSON_NULL:
|
||||
pj_knull (pj, prop->key);
|
||||
break;
|
||||
case R_JSON_OBJECT:
|
||||
pj_ko (pj, prop->key);
|
||||
if (!r_json_to_pj (prop, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
case R_JSON_ARRAY:
|
||||
pj_ka (pj, prop->key);
|
||||
if (!r_json_to_pj (prop, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
prop = prop->next;
|
||||
}
|
||||
if (!existing_pj) {
|
||||
pj_end (pj);
|
||||
}
|
||||
break;
|
||||
case R_JSON_ARRAY:
|
||||
if (!existing_pj) {
|
||||
pj_a (pj);
|
||||
}
|
||||
// Handle array items
|
||||
const RJson *item = json->children.first;
|
||||
while (item) {
|
||||
switch (item->type) {
|
||||
case R_JSON_STRING:
|
||||
pj_s (pj, item->str_value);
|
||||
break;
|
||||
case R_JSON_INTEGER:
|
||||
pj_n (pj, item->num.u_value);
|
||||
break;
|
||||
case R_JSON_DOUBLE:
|
||||
pj_d (pj, item->num.dbl_value);
|
||||
break;
|
||||
case R_JSON_BOOLEAN:
|
||||
pj_b (pj, item->num.u_value);
|
||||
break;
|
||||
case R_JSON_NULL:
|
||||
pj_null (pj);
|
||||
break;
|
||||
case R_JSON_OBJECT:
|
||||
pj_o (pj);
|
||||
if (!r_json_to_pj (item, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
case R_JSON_ARRAY:
|
||||
pj_a (pj);
|
||||
if (!r_json_to_pj (item, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
item = item->next;
|
||||
}
|
||||
if (!existing_pj) {
|
||||
pj_end (pj);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return pj;
|
||||
}
|
||||
|
||||
// Helper function to clone RJson to string
|
||||
R_API char *r_json_to_string(const RJson *json) {
|
||||
PJ *pj = r_json_to_pj (json, NULL);
|
||||
return pj? pj_drain (pj): NULL;
|
||||
}
|
||||
@@ -39,7 +39,7 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
if (!provider) {
|
||||
provider = "gemini";
|
||||
}
|
||||
if (rawtools_enabled && args.tools && args.tools->n_tools > 0) {
|
||||
if (rawtools_enabled && args.tools && r_list_length (args.tools) > 0) {
|
||||
return r2ai_rawtools_llmcall (cps, args);
|
||||
}
|
||||
R2AI_State *state = cps->data;
|
||||
@@ -115,7 +115,7 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
}
|
||||
char *m = r_strbuf_drain (sb);
|
||||
R2AI_Message msg = { .role = "user", .content = m };
|
||||
context_pullback = r_list_length (args.messages->messages);
|
||||
context_pullback = r_list_length (args.messages);
|
||||
r2ai_msgs_add (args.messages, &msg);
|
||||
free (m);
|
||||
// TODO: we can save the msg without context
|
||||
@@ -137,7 +137,7 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
res = r2ai_openai (cps, args);
|
||||
}
|
||||
if (context_pullback != -1) {
|
||||
R2AI_Message *msg = r_list_get_n (args.messages->messages, context_pullback);
|
||||
R2AI_Message *msg = r_list_get_n (args.messages, context_pullback);
|
||||
free ((char *)msg->content);
|
||||
msg->content = strdup (args.input);
|
||||
}
|
||||
@@ -150,7 +150,42 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
return res;
|
||||
}
|
||||
|
||||
/* Return a malloc'd API key read from the environment or from ~/.r2ai.<provider>-key
|
||||
static char *r2ai_get_api_key_from_config(const char *provider) {
|
||||
char *config_file = r_file_abspath ("~/.config/r2ai/apikeys.txt");
|
||||
if (!r_file_exists (config_file)) {
|
||||
free (config_file);
|
||||
return NULL;
|
||||
}
|
||||
char *content = r_file_slurp (config_file, NULL);
|
||||
free (config_file);
|
||||
if (!content) {
|
||||
return NULL;
|
||||
}
|
||||
RList *lines = r_str_split_list (content, "\n", 0);
|
||||
free (content);
|
||||
if (!lines) {
|
||||
return NULL;
|
||||
}
|
||||
char *prefix = r_str_newf ("%s=", provider);
|
||||
char *key = NULL;
|
||||
RListIter *iter;
|
||||
char *line;
|
||||
r_list_foreach (lines, iter, line) {
|
||||
if (r_str_startswith (line, prefix)) {
|
||||
char *eq = strchr (line, '=');
|
||||
if (eq) {
|
||||
key = strdup (eq + 1);
|
||||
r_str_trim (key);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
r_list_free (lines);
|
||||
free (prefix);
|
||||
return key;
|
||||
}
|
||||
|
||||
/* Return a malloc'd API key read from the env, ~/.config/r2ai/apikeys.txt, or ~/.r2ai.<provider>-key
|
||||
* Caller is responsible for freeing the returned string (or NULL if not found). */
|
||||
R_IPI char *r2ai_get_api_key(RCore *core, const char *provider) {
|
||||
(void)core;
|
||||
@@ -163,16 +198,19 @@ R_IPI char *r2ai_get_api_key(RCore *core, const char *provider) {
|
||||
api_key = s;
|
||||
} else {
|
||||
free (s);
|
||||
char *api_key_filename = r_str_newf ("~/.r2ai.%s-key", provider);
|
||||
char *absolute_apikey = r_file_abspath (api_key_filename);
|
||||
if (r_file_exists (absolute_apikey)) {
|
||||
api_key = r_file_slurp (absolute_apikey, NULL);
|
||||
if (api_key) {
|
||||
r_str_trim (api_key);
|
||||
api_key = r2ai_get_api_key_from_config (provider);
|
||||
if (!api_key) {
|
||||
char *api_key_filename = r_str_newf ("~/.r2ai.%s-key", provider);
|
||||
char *absolute_apikey = r_file_abspath (api_key_filename);
|
||||
if (r_file_exists (absolute_apikey)) {
|
||||
api_key = r_file_slurp (absolute_apikey, NULL);
|
||||
if (api_key) {
|
||||
r_str_trim (api_key);
|
||||
}
|
||||
}
|
||||
free (api_key_filename);
|
||||
free (absolute_apikey);
|
||||
}
|
||||
free (api_key_filename);
|
||||
free (absolute_apikey);
|
||||
}
|
||||
return api_key;
|
||||
}
|
||||
@@ -190,15 +228,13 @@ R_IPI const char *r2ai_get_provider_url(RCore *core, const char *provider) {
|
||||
if (r_str_startswith (host, "http")) {
|
||||
if (!strcmp (provider, "openai")) {
|
||||
return r_str_newf ("%s/v1", host);
|
||||
} else {
|
||||
return r_str_newf ("%s/api", host);
|
||||
}
|
||||
return r_str_newf ("%s/api", host);
|
||||
}
|
||||
if (!strcmp (provider, "openai")) {
|
||||
return r_str_newf ("http://%s/v1", host);
|
||||
} else {
|
||||
return r_str_newf ("http://%s/api", host);
|
||||
}
|
||||
return r_str_newf ("http://%s/api", host);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,16 +247,9 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
|
||||
}
|
||||
|
||||
// Create models endpoint URL
|
||||
char *models_url = NULL;
|
||||
const R2AIProvider *prov = r2ai_get_provider (provider);
|
||||
if (prov && prov->uses_tags_endpoint) {
|
||||
models_url = r_str_newf ("%s/tags", purl);
|
||||
} else {
|
||||
models_url = r_str_newf ("%s/models", purl);
|
||||
}
|
||||
if (!models_url) {
|
||||
return NULL;
|
||||
}
|
||||
const bool usetags = (prov && prov->uses_tags_endpoint);
|
||||
char *models_url = r_str_newf ("%s/%s", purl, usetags? "tags": "models");
|
||||
|
||||
// Get API key for authentication (except for providers that don't require it)
|
||||
char *api_key = NULL;
|
||||
@@ -279,13 +308,9 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
|
||||
|
||||
RJson *json = r_json_parse (response);
|
||||
if (json) {
|
||||
const RJson *data;
|
||||
const R2AIProvider *prov = r2ai_get_provider (provider);
|
||||
if (prov && prov->uses_tags_endpoint) {
|
||||
data = r_json_get (json, "models");
|
||||
} else {
|
||||
data = r_json_get (json, "data");
|
||||
}
|
||||
const bool usetags = (prov && prov->uses_tags_endpoint);
|
||||
const RJson *data = r_json_get (json, usetags? "models": "data");
|
||||
|
||||
if (data && data->type == R_JSON_ARRAY) {
|
||||
const RJson *model_item = data->children.first;
|
||||
@@ -311,8 +336,7 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
|
||||
}
|
||||
|
||||
R_IPI void r2ai_list_providers(RCore *core, RStrBuf *sb) {
|
||||
size_t i;
|
||||
for (i = 0; r2ai_providers[i].name; i++) {
|
||||
for (size_t i = 0; r2ai_providers[i].name; i++) {
|
||||
if (sb) {
|
||||
if (i > 0) {
|
||||
r_strbuf_append (sb, ", ");
|
||||
|
||||
+15
-2
@@ -3,12 +3,13 @@
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
static void show_help() {
|
||||
printf ("Usage: r2ai [-vhp:m:q:E] <prompt>\n"
|
||||
printf ("Usage: r2ai [-vhp:m:q:Eb:] <prompt>\n"
|
||||
" -v Show version information\n"
|
||||
" -h Show this help message\n"
|
||||
" -p <provider> Select LLM provider\n"
|
||||
" -m <model> Select LLM model\n"
|
||||
" -q <query> Execute predefined prompt query (can be used multiple times)\n"
|
||||
" -b <url> Set base URL for provider API\n"
|
||||
" -E Edit the r2ai rc file\n");
|
||||
}
|
||||
|
||||
@@ -20,10 +21,11 @@ int main(int argc, const char **argv) {
|
||||
int c;
|
||||
const char *provider = NULL;
|
||||
const char *model = NULL;
|
||||
const char *baseurl = NULL;
|
||||
RList *queries = r_list_newf (free);
|
||||
|
||||
RGetopt opt;
|
||||
r_getopt_init (&opt, argc, argv, "vhp:m:q:E");
|
||||
r_getopt_init (&opt, argc, argv, "vhp:m:q:Eb:");
|
||||
while ((c = r_getopt_next (&opt)) != -1) {
|
||||
switch (c) {
|
||||
case 'v':
|
||||
@@ -43,6 +45,9 @@ int main(int argc, const char **argv) {
|
||||
case 'q':
|
||||
r_list_append (queries, strdup (opt.arg));
|
||||
break;
|
||||
case 'b':
|
||||
baseurl = opt.arg;
|
||||
break;
|
||||
case 'E':
|
||||
{
|
||||
RCore *core = r_core_new ();
|
||||
@@ -64,12 +69,20 @@ int main(int argc, const char **argv) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (opt.ind >= argc) {
|
||||
show_help ();
|
||||
r_list_free (queries);
|
||||
return 1;
|
||||
}
|
||||
|
||||
RCore *core = r_core_new ();
|
||||
RCorePluginSession cps = {
|
||||
.core = core
|
||||
};
|
||||
r2ai_init (&cps);
|
||||
if (baseurl) {
|
||||
r_config_set (core->config, "r2ai.baseurl", baseurl);
|
||||
}
|
||||
|
||||
// Process queries if any
|
||||
if (!r_list_empty (queries)) {
|
||||
|
||||
+13
-13
@@ -28,20 +28,20 @@ R_API RMarkdownTheme r2ai_markdown_theme_default(void) {
|
||||
.code_inline = "\x1b[48;5;236m", // Dark gray background for inline code
|
||||
.code_block = "\x1b[48;5;235m", // Slightly darker gray for code blocks
|
||||
// Heading colors
|
||||
.heading1 = "\x1b[1;4;31m", // Bold, underlined, red
|
||||
.heading2 = "\x1b[1;4;32m", // Bold, underlined, green
|
||||
.heading3 = "\x1b[1;4;33m", // Bold, underlined, yellow
|
||||
.heading4 = "\x1b[1;4;34m", // Bold, underlined, blue
|
||||
.heading5 = "\x1b[1;4;35m", // Bold, underlined, magenta
|
||||
.heading6 = "\x1b[1;4;36m", // Bold, underlined, cyan
|
||||
// List items
|
||||
.heading1 = "\x1b[1;4" Color_RED, // Bold, underlined, red
|
||||
.heading2 = "\x1b[1;4" Color_GREEN, // Bold, underlined, green
|
||||
.heading3 = "\x1b[1;4" Color_YELLOW, // Bold, underlined, yellow
|
||||
.heading4 = "\x1b[1;4" Color_BLUE, // Bold, underlined, blue
|
||||
.heading5 = "\x1b[1;4" Color_MAGENTA, // Bold, underlined, magenta
|
||||
.heading6 = "\x1b[1;4" Color_CYAN, // Bold, underlined, cyan
|
||||
// List items
|
||||
.list_bullet = "• ", // Bullet character
|
||||
.list_number = "\x1b[1m%s.\x1b[0m ", // Bold number
|
||||
// Checkbox states
|
||||
.checkbox_checked = "\x1b[32m[✓]\x1b[0m ", // Green checkmark
|
||||
.checkbox_unchecked = "\x1b[90m[ ]\x1b[0m ", // Gray empty box
|
||||
// Reset code
|
||||
.reset = "\x1b[0m"
|
||||
.list_number = "\x1b[1m%s." Color_RESET " ", // Bold number
|
||||
// Checkbox states
|
||||
.checkbox_checked = Color_GREEN "[✓]" Color_RESET " ", // Green checkmark
|
||||
.checkbox_unchecked = Color_GRAY "[ ]" Color_RESET " ", // Gray empty box
|
||||
// Reset code
|
||||
.reset = Color_RESET
|
||||
};
|
||||
return theme;
|
||||
}
|
||||
|
||||
+12
-11
@@ -1,5 +1,5 @@
|
||||
project('r2ai', 'c',
|
||||
version : '1.2.2',
|
||||
version : '1.2.6',
|
||||
default_options : ['warning_level=2', 'c_std=c11']
|
||||
)
|
||||
|
||||
@@ -185,18 +185,19 @@ elif cc.get_id() == 'msvc'
|
||||
endif
|
||||
|
||||
srcs = files(
|
||||
'r2ai.c',
|
||||
'auto.c',
|
||||
'vdb.c',
|
||||
'llm.c',
|
||||
'tools.c',
|
||||
'query.c',
|
||||
'rawtools.c',
|
||||
'messages.c',
|
||||
'anthropic.c',
|
||||
'r2ai_http.c',
|
||||
'openai.c',
|
||||
'auto.c',
|
||||
'http.c',
|
||||
'json.c',
|
||||
'llm.c',
|
||||
'markdown.c',
|
||||
'messages.c',
|
||||
'openai.c',
|
||||
'query.c',
|
||||
'r2ai.c',
|
||||
'rawtools.c',
|
||||
'tools.c',
|
||||
'vdb.c',
|
||||
)
|
||||
|
||||
# Shared library (plugin)
|
||||
|
||||
+121
-312
@@ -2,14 +2,36 @@
|
||||
|
||||
#include "r2ai.h"
|
||||
|
||||
#define INITIAL_CAPACITY 8
|
||||
#define GROWTH_FACTOR 1.5
|
||||
R_API void r2ai_tool_call_free(R2AI_ToolCall *tc) {
|
||||
if (!tc) {
|
||||
return;
|
||||
}
|
||||
free ((void *)tc->id);
|
||||
free ((void *)tc->name);
|
||||
free ((void *)tc->arguments);
|
||||
free (tc);
|
||||
}
|
||||
|
||||
R_API RList *r2ai_content_blocks_new(void) {
|
||||
RList *cb = r_list_new ();
|
||||
if (!cb) {
|
||||
return NULL;
|
||||
}
|
||||
cb->free = (RListFree)free; // ContentBlocks contain pointers to structs, not structs themselves
|
||||
return cb;
|
||||
}
|
||||
|
||||
R_API void r2ai_content_blocks_free(RList *cb) {
|
||||
if (!cb) {
|
||||
return;
|
||||
}
|
||||
r_list_free (cb);
|
||||
}
|
||||
|
||||
R_API void r2ai_message_free(R2AI_Message *msg) {
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
|
||||
free (msg->role);
|
||||
free ((void *)msg->content); // maybe double free
|
||||
free (msg->reasoning_content);
|
||||
@@ -17,29 +39,13 @@ R_API void r2ai_message_free(R2AI_Message *msg) {
|
||||
|
||||
// Free tool calls
|
||||
if (msg->tool_calls) {
|
||||
for (int i = 0; i < msg->n_tool_calls; i++) {
|
||||
free ((void *)msg->tool_calls[i].id);
|
||||
free ((void *)msg->tool_calls[i].name);
|
||||
free ((void *)msg->tool_calls[i].arguments);
|
||||
}
|
||||
free ((void *)msg->tool_calls);
|
||||
r_list_free (msg->tool_calls);
|
||||
}
|
||||
|
||||
if (msg->content_blocks) {
|
||||
for (int i = 0; i < msg->content_blocks->n_blocks; i++) {
|
||||
R2AI_ContentBlock *block = &msg->content_blocks->blocks[i];
|
||||
free (block->type);
|
||||
free (block->data);
|
||||
free (block->thinking);
|
||||
free (block->signature);
|
||||
free (block->text);
|
||||
free (block->id);
|
||||
free (block->name);
|
||||
free (block->input);
|
||||
}
|
||||
free (msg->content_blocks->blocks);
|
||||
free (msg->content_blocks);
|
||||
r2ai_content_blocks_free (msg->content_blocks);
|
||||
}
|
||||
free (msg);
|
||||
}
|
||||
|
||||
// Conversation is now stored in R2AI_State
|
||||
@@ -53,36 +59,24 @@ R_API void r2ai_conversation_init(R2AI_State *state) {
|
||||
state->conversation = r2ai_msgs_new ();
|
||||
}
|
||||
|
||||
R_API R2AI_Messages *r2ai_conversation_get(R2AI_State *state) {
|
||||
R_API RList *r2ai_conversation_get(R2AI_State *state) {
|
||||
return state? state->conversation: NULL;
|
||||
}
|
||||
|
||||
// Create a new temporary messages container
|
||||
R_API R2AI_Messages *r2ai_msgs_new(void) {
|
||||
R2AI_Messages *msgs = R_NEW0 (R2AI_Messages);
|
||||
R_API RList *r2ai_msgs_new(void) {
|
||||
RList *msgs = r_list_new ();
|
||||
if (!msgs) {
|
||||
return NULL;
|
||||
}
|
||||
msgs->messages = r_list_new ();
|
||||
if (!msgs->messages) {
|
||||
R_FREE (msgs);
|
||||
return NULL;
|
||||
}
|
||||
msgs->messages->free = (RListFree)r2ai_message_free;
|
||||
msgs->free = (RListFree)r2ai_message_free;
|
||||
return msgs;
|
||||
}
|
||||
|
||||
R_API void r2ai_msgs_free(R2AI_Messages *msgs) {
|
||||
if (!msgs) {
|
||||
return;
|
||||
R_API void r2ai_msgs_free(RList *msgs) {
|
||||
if (msgs) {
|
||||
r_list_free (msgs);
|
||||
}
|
||||
|
||||
// Normal free for non-static instances
|
||||
if (msgs->messages) {
|
||||
r_list_free (msgs->messages);
|
||||
}
|
||||
|
||||
R_FREE (msgs);
|
||||
}
|
||||
|
||||
// Free the conversation when plugin is unloaded
|
||||
@@ -94,46 +88,34 @@ R_API void r2ai_conversation_free(R2AI_State *state) {
|
||||
}
|
||||
|
||||
// Clear messages in a container without freeing the container itself
|
||||
R_API void r2ai_msgs_clear(R2AI_Messages *msgs) {
|
||||
R_API void r2ai_msgs_clear(RList *msgs) {
|
||||
if (!msgs) {
|
||||
return;
|
||||
}
|
||||
|
||||
r_list_purge (msgs->messages);
|
||||
r_list_purge (msgs);
|
||||
}
|
||||
|
||||
R_API bool r2ai_msgs_add(R2AI_Messages *msgs, const R2AI_Message *msg) {
|
||||
R_API bool r2ai_msgs_add(RList *msgs, const R2AI_Message *msg) {
|
||||
if (!msgs || !msg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
R2AI_Message *new_msg = R_NEW0 (R2AI_Message);
|
||||
if (!new_msg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
new_msg->role = msg->role? strdup (msg->role): NULL;
|
||||
new_msg->content = msg->content? strdup (msg->content): NULL;
|
||||
new_msg->reasoning_content = msg->reasoning_content? strdup (msg->reasoning_content): NULL;
|
||||
|
||||
if (msg->content_blocks) {
|
||||
R2AI_ContentBlocks *cb = R_NEW0 (R2AI_ContentBlocks);
|
||||
RList *cb = r2ai_content_blocks_new ();
|
||||
if (!cb) {
|
||||
r2ai_message_free (new_msg);
|
||||
free (new_msg);
|
||||
return false;
|
||||
}
|
||||
cb->n_blocks = msg->content_blocks->n_blocks;
|
||||
cb->blocks = R_NEWS0 (R2AI_ContentBlock, cb->n_blocks);
|
||||
if (!cb->blocks) {
|
||||
free (cb);
|
||||
r2ai_message_free (new_msg);
|
||||
free (new_msg);
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < cb->n_blocks; i++) {
|
||||
R2AI_ContentBlock *src = &msg->content_blocks->blocks[i];
|
||||
R2AI_ContentBlock *dst = &cb->blocks[i];
|
||||
RListIter *iter;
|
||||
R2AI_ContentBlock *src;
|
||||
r_list_foreach (msg->content_blocks, iter, src) {
|
||||
R2AI_ContentBlock *dst = R_NEW0 (R2AI_ContentBlock);
|
||||
dst->type = src->type? strdup (src->type): NULL;
|
||||
dst->data = src->data? strdup (src->data): NULL;
|
||||
dst->thinking = src->thinking? strdup (src->thinking): NULL;
|
||||
@@ -142,90 +124,79 @@ R_API bool r2ai_msgs_add(R2AI_Messages *msgs, const R2AI_Message *msg) {
|
||||
dst->id = src->id? strdup (src->id): NULL;
|
||||
dst->name = src->name? strdup (src->name): NULL;
|
||||
dst->input = src->input? strdup (src->input): NULL;
|
||||
r_list_append (cb, dst);
|
||||
}
|
||||
new_msg->content_blocks = cb;
|
||||
}
|
||||
|
||||
new_msg->tool_call_id = msg->tool_call_id? strdup (msg->tool_call_id): NULL;
|
||||
new_msg->tool_calls = NULL;
|
||||
new_msg->n_tool_calls = 0;
|
||||
new_msg->tool_calls = r_list_new ();
|
||||
if (!new_msg->tool_calls) {
|
||||
r2ai_message_free (new_msg);
|
||||
free (new_msg);
|
||||
return false;
|
||||
}
|
||||
new_msg->tool_calls->free = (RListFree)r2ai_tool_call_free;
|
||||
|
||||
// Copy tool calls if any
|
||||
if (msg->tool_calls && msg->n_tool_calls > 0) {
|
||||
new_msg->tool_calls = R_NEWS0 (R2AI_ToolCall, msg->n_tool_calls);
|
||||
if (!new_msg->tool_calls) {
|
||||
r2ai_message_free (new_msg);
|
||||
free (new_msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
new_msg->n_tool_calls = msg->n_tool_calls;
|
||||
for (int i = 0; i < msg->n_tool_calls; i++) {
|
||||
const R2AI_ToolCall *src_tc = &msg->tool_calls[i];
|
||||
R2AI_ToolCall *dst_tc = (R2AI_ToolCall *)&new_msg->tool_calls[i];
|
||||
|
||||
if (msg->tool_calls) {
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *src_tc;
|
||||
r_list_foreach (msg->tool_calls, iter, src_tc) {
|
||||
R2AI_ToolCall *dst_tc = R_NEW0 (R2AI_ToolCall);
|
||||
dst_tc->name = src_tc->name? strdup (src_tc->name): NULL;
|
||||
dst_tc->arguments = src_tc->arguments? strdup (src_tc->arguments): NULL;
|
||||
dst_tc->id = src_tc->id? strdup (src_tc->id): NULL;
|
||||
r_list_append (new_msg->tool_calls, dst_tc);
|
||||
}
|
||||
}
|
||||
|
||||
r_list_append (msgs->messages, new_msg);
|
||||
r_list_append (msgs, new_msg);
|
||||
return true;
|
||||
}
|
||||
|
||||
R_API bool r2ai_msgs_add_tool_call(R2AI_Messages *msgs, const R2AI_ToolCall *tc) {
|
||||
if (!msgs || !tc || r_list_length (msgs->messages) == 0) {
|
||||
R_API bool r2ai_msgs_add_tool_call(RList *msgs, const R2AI_ToolCall *tc) {
|
||||
if (!msgs || !tc || r_list_empty (msgs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
R2AI_Message *msg = r_list_get_n (msgs->messages, r_list_length (msgs->messages) - 1);
|
||||
R2AI_Message *msg = r_list_get_n (msgs, r_list_length (msgs) - 1);
|
||||
|
||||
// Allocate or resize the tool_calls array
|
||||
if (msg->n_tool_calls == 0) {
|
||||
msg->tool_calls = R_NEWS0 (R2AI_ToolCall, 1);
|
||||
// Ensure tool_calls list exists
|
||||
if (!msg->tool_calls) {
|
||||
msg->tool_calls = r_list_new ();
|
||||
if (!msg->tool_calls) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
R2AI_ToolCall *new_tool_calls = realloc (
|
||||
(void *)msg->tool_calls,
|
||||
sizeof (R2AI_ToolCall) *(msg->n_tool_calls + 1));
|
||||
if (!new_tool_calls) {
|
||||
return false;
|
||||
}
|
||||
msg->tool_calls = new_tool_calls;
|
||||
// Zero the new element
|
||||
memset ((void *)&msg->tool_calls[msg->n_tool_calls], 0, sizeof (R2AI_ToolCall));
|
||||
msg->tool_calls->free = (RListFree)r2ai_tool_call_free;
|
||||
}
|
||||
|
||||
// Copy the tool call
|
||||
R2AI_ToolCall *dst_tc = (R2AI_ToolCall *)&msg->tool_calls[msg->n_tool_calls];
|
||||
R2AI_ToolCall *dst_tc = R_NEW0 (R2AI_ToolCall);
|
||||
dst_tc->name = tc->name? strdup (tc->name): NULL;
|
||||
dst_tc->arguments = tc->arguments? strdup (tc->arguments): NULL;
|
||||
dst_tc->id = tc->id? strdup (tc->id): NULL;
|
||||
|
||||
msg->n_tool_calls++;
|
||||
r_list_append (msg->tool_calls, dst_tc);
|
||||
return true;
|
||||
}
|
||||
|
||||
R_API bool r2ai_msgs_from_response(R2AI_Messages *msgs, const char *json_str) {
|
||||
R_API bool r2ai_msgs_from_response(RList *msgs, const char *json_str) {
|
||||
if (!msgs || !json_str) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// r_json_parse expects non-const char*, so we need to cast it
|
||||
bool result = false;
|
||||
// r_json_parse expects (and modifies) non-const char*, so we need to cast it
|
||||
RJson *json = r_json_parse ((char *)json_str);
|
||||
if (!json) {
|
||||
return false;
|
||||
if (json) {
|
||||
result = r2ai_msgs_from_json (msgs, json);
|
||||
r_json_free (json);
|
||||
}
|
||||
|
||||
bool result = r2ai_msgs_from_json (msgs, json);
|
||||
r_json_free (json);
|
||||
return result;
|
||||
}
|
||||
|
||||
R_API bool r2ai_msgs_from_json(R2AI_Messages *msgs, const RJson *json) {
|
||||
R_API bool r2ai_msgs_from_json(RList *msgs, const RJson *json) {
|
||||
if (!msgs || !json) {
|
||||
return false;
|
||||
}
|
||||
@@ -255,27 +226,24 @@ R_API bool r2ai_msgs_from_json(R2AI_Messages *msgs, const RJson *json) {
|
||||
new_msg.content = (content && content->type == R_JSON_STRING)? strdup (content->str_value): NULL;
|
||||
new_msg.tool_call_id = NULL;
|
||||
new_msg.tool_calls = NULL;
|
||||
new_msg.n_tool_calls = 0;
|
||||
|
||||
if (content_blocks && content_blocks->type == R_JSON_ARRAY && content_blocks->children.count > 0) {
|
||||
R2AI_ContentBlocks *cb = R_NEW0 (R2AI_ContentBlocks);
|
||||
RList *cb = r2ai_content_blocks_new ();
|
||||
if (!cb) {
|
||||
r2ai_message_free (&new_msg);
|
||||
return false;
|
||||
}
|
||||
cb->n_blocks = content_blocks->children.count;
|
||||
cb->blocks = R_NEWS0 (R2AI_ContentBlock, cb->n_blocks);
|
||||
if (!cb->blocks) {
|
||||
free (cb);
|
||||
r2ai_message_free (&new_msg);
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < cb->n_blocks; i++) {
|
||||
for (size_t i = 0; i < content_blocks->children.count; i++) {
|
||||
const RJson *block = r_json_item (content_blocks, i);
|
||||
if (!block) {
|
||||
continue;
|
||||
}
|
||||
R2AI_ContentBlock *dst = &cb->blocks[i];
|
||||
R2AI_ContentBlock *dst = R_NEW0 (R2AI_ContentBlock);
|
||||
if (!dst) {
|
||||
r2ai_content_blocks_free (cb);
|
||||
r2ai_message_free (&new_msg);
|
||||
return false;
|
||||
}
|
||||
const RJson *type = r_json_get (block, "type");
|
||||
const RJson *data = r_json_get (block, "data");
|
||||
const RJson *thinking = r_json_get (block, "thinking");
|
||||
@@ -293,6 +261,7 @@ R_API bool r2ai_msgs_from_json(R2AI_Messages *msgs, const RJson *json) {
|
||||
dst->id = (id && id->type == R_JSON_STRING)? strdup (id->str_value): NULL;
|
||||
dst->name = (name && name->type == R_JSON_STRING)? strdup (name->str_value): NULL;
|
||||
dst->input = (input && input->type == R_JSON_STRING)? strdup (input->str_value): NULL;
|
||||
r_list_append (cb, dst);
|
||||
}
|
||||
new_msg.content_blocks = cb;
|
||||
}
|
||||
@@ -328,27 +297,17 @@ R_API bool r2ai_msgs_from_json(R2AI_Messages *msgs, const RJson *json) {
|
||||
tc.id = (id && id->type == R_JSON_STRING)? id->str_value: NULL;
|
||||
|
||||
if (!r2ai_msgs_add_tool_call (msgs, &tc)) {
|
||||
break;
|
||||
r2ai_message_free (&new_msg);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the parsed message to the messages array
|
||||
if (!r2ai_msgs_add (msgs, &new_msg)) {
|
||||
r2ai_message_free (&new_msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Free the temporary message struct (strings are duplicated in add)
|
||||
r2ai_message_free (&new_msg);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
|
||||
if (!msgs || r_list_length (msgs->messages) == 0) {
|
||||
R_API char *r2ai_msgs_to_json(const RList *msgs) {
|
||||
if (!msgs || r_list_empty (msgs)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -359,16 +318,22 @@ R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
|
||||
|
||||
pj_a (pj); // Start array
|
||||
|
||||
for (int i = 0; i < r_list_length (msgs->messages); i++) {
|
||||
const R2AI_Message *msg = r_list_get_n (msgs->messages, i);
|
||||
RListIter *iter;
|
||||
const R2AI_Message *msg;
|
||||
r_list_foreach (msgs, iter, msg) {
|
||||
|
||||
pj_o (pj); // Start message object
|
||||
|
||||
// Add role
|
||||
pj_ks (pj, "role", msg->role? msg->role: "user");
|
||||
|
||||
if (msg->content) {
|
||||
// Content is required for OpenAI API
|
||||
if (msg->content && *msg->content) {
|
||||
pj_ks (pj, "content", msg->content);
|
||||
} else if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
|
||||
pj_knull (pj, "content");
|
||||
} else {
|
||||
pj_ks (pj, "content", msg->content? msg->content: "");
|
||||
}
|
||||
|
||||
if (msg->reasoning_content) {
|
||||
@@ -381,13 +346,13 @@ R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
|
||||
}
|
||||
|
||||
// Add tool_calls if present
|
||||
if (msg->tool_calls && msg->n_tool_calls > 0) {
|
||||
if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
|
||||
pj_k (pj, "tool_calls");
|
||||
pj_a (pj); // Start tool_calls array
|
||||
|
||||
for (int j = 0; j < msg->n_tool_calls; j++) {
|
||||
const R2AI_ToolCall *tc = &msg->tool_calls[j];
|
||||
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (msg->tool_calls, iter, tc) {
|
||||
pj_o (pj); // Start tool call object
|
||||
|
||||
// Add id if present
|
||||
@@ -405,10 +370,8 @@ R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
|
||||
// Add name
|
||||
pj_ks (pj, "name", tc->name? tc->name: "");
|
||||
|
||||
// Add arguments if present
|
||||
if (tc->arguments) {
|
||||
pj_ks (pj, "arguments", tc->arguments);
|
||||
}
|
||||
// Add arguments (required by OpenAI API)
|
||||
pj_ks (pj, "arguments", tc->arguments? tc->arguments: "{}");
|
||||
|
||||
pj_end (pj); // End function object
|
||||
pj_end (pj); // End tool call object
|
||||
@@ -426,8 +389,8 @@ R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
|
||||
return result;
|
||||
}
|
||||
|
||||
R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
if (!msgs || r_list_length (msgs->messages) == 0) {
|
||||
R_API char *r2ai_msgs_to_anthropic_json(const RList *msgs) {
|
||||
if (!msgs || r_list_empty (msgs)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -438,9 +401,9 @@ R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
|
||||
pj_a (pj); // Start array
|
||||
|
||||
for (int i = 0; i < r_list_length (msgs->messages); i++) {
|
||||
const R2AI_Message *msg = r_list_get_n (msgs->messages, i);
|
||||
|
||||
RListIter *iter;
|
||||
const R2AI_Message *msg;
|
||||
r_list_foreach (msgs, iter, msg) {
|
||||
pj_o (pj); // Start message object
|
||||
|
||||
// Add role
|
||||
@@ -449,8 +412,9 @@ R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
|
||||
if (msg->content_blocks) {
|
||||
pj_ka (pj, "content"); // Start content array
|
||||
for (int j = 0; j < msg->content_blocks->n_blocks; j++) {
|
||||
const R2AI_ContentBlock *block = &msg->content_blocks->blocks[j];
|
||||
RListIter *iter;
|
||||
R2AI_ContentBlock *block;
|
||||
r_list_foreach (msg->content_blocks, iter, block) {
|
||||
pj_o (pj); // Start content block object
|
||||
if (R_STR_ISNOTEMPTY (block->type)) {
|
||||
pj_ks (pj, "type", block->type);
|
||||
@@ -478,13 +442,11 @@ R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
char *input_str = strdup (block->input);
|
||||
RJson *input_json = r_json_parse (input_str);
|
||||
if (input_json) {
|
||||
// If it's valid JSON, add it directly
|
||||
pj_ko (pj, "input");
|
||||
r_json_to_pj (input_json, pj);
|
||||
pj_raw (pj, input_str);
|
||||
pj_end (pj);
|
||||
r_json_free (input_json);
|
||||
} else {
|
||||
// If not valid JSON, create a basic object with command
|
||||
pj_ko (pj, "input");
|
||||
pj_ks (pj, "command", block->input);
|
||||
pj_end (pj);
|
||||
@@ -510,13 +472,11 @@ R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
pj_end (pj); // End content block object
|
||||
}
|
||||
|
||||
if (msg->tool_calls && msg->n_tool_calls > 0) {
|
||||
pj_k (pj, "tool_calls");
|
||||
pj_a (pj); // Start tool_calls array
|
||||
|
||||
for (int j = 0; j < msg->n_tool_calls; j++) {
|
||||
const R2AI_ToolCall *tc = &msg->tool_calls[j];
|
||||
pj_o (pj); // Start tool_calls object
|
||||
if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (msg->tool_calls, iter, tc) {
|
||||
pj_o (pj); // Start tool_use content block
|
||||
pj_ks (pj, "type", "tool_use");
|
||||
pj_ks (pj, "id", tc->id? tc->id: "");
|
||||
pj_ks (pj, "name", tc->name? tc->name: "");
|
||||
@@ -538,10 +498,8 @@ R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
free (arguments_copy);
|
||||
|
||||
pj_end (pj); // End input object
|
||||
pj_end (pj); // End tool_calls object
|
||||
pj_end (pj); // End tool_use content block
|
||||
}
|
||||
|
||||
pj_end (pj); // End tool_calls array
|
||||
}
|
||||
pj_end (pj); // End content array
|
||||
}
|
||||
@@ -555,8 +513,8 @@ R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
}
|
||||
|
||||
// Function to delete the last N messages from conversation history
|
||||
R_API void r2ai_delete_last_messages(R2AI_Messages *messages, int n) {
|
||||
if (!messages || r_list_length (messages->messages) == 0) {
|
||||
R_API void r2ai_delete_last_messages(RList *messages, int n) {
|
||||
if (!messages || r_list_length (messages) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -566,162 +524,13 @@ R_API void r2ai_delete_last_messages(R2AI_Messages *messages, int n) {
|
||||
}
|
||||
|
||||
// Make sure we don't try to delete more messages than exist
|
||||
int len = r_list_length (messages->messages);
|
||||
int len = r_list_length (messages);
|
||||
if (n > len) {
|
||||
n = len;
|
||||
}
|
||||
|
||||
// Pop the last n messages
|
||||
for (int i = 0; i < n; i++) {
|
||||
r_list_pop (messages->messages);
|
||||
r_list_pop (messages);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert RJson to PJ without draining
|
||||
R_API PJ *r_json_to_pj(const RJson *json, PJ *existing_pj) {
|
||||
if (!json) {
|
||||
return existing_pj;
|
||||
}
|
||||
|
||||
PJ *pj = existing_pj? existing_pj: pj_new ();
|
||||
if (!pj) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
switch (json->type) {
|
||||
case R_JSON_STRING:
|
||||
pj_s (pj, json->str_value);
|
||||
break;
|
||||
case R_JSON_INTEGER:
|
||||
pj_n (pj, json->num.u_value);
|
||||
break;
|
||||
case R_JSON_DOUBLE:
|
||||
pj_d (pj, json->num.dbl_value);
|
||||
break;
|
||||
case R_JSON_BOOLEAN:
|
||||
pj_b (pj, json->num.u_value);
|
||||
break;
|
||||
case R_JSON_NULL:
|
||||
pj_null (pj);
|
||||
break;
|
||||
case R_JSON_OBJECT:
|
||||
if (!existing_pj) {
|
||||
pj_o (pj);
|
||||
}
|
||||
// Handle object's properties
|
||||
const RJson *prop = json->children.first;
|
||||
while (prop) {
|
||||
if (prop->key) {
|
||||
switch (prop->type) {
|
||||
case R_JSON_STRING:
|
||||
pj_ks (pj, prop->key, prop->str_value);
|
||||
break;
|
||||
case R_JSON_INTEGER:
|
||||
pj_kn (pj, prop->key, prop->num.u_value);
|
||||
break;
|
||||
case R_JSON_DOUBLE:
|
||||
pj_kd (pj, prop->key, prop->num.dbl_value);
|
||||
break;
|
||||
case R_JSON_BOOLEAN:
|
||||
pj_kb (pj, prop->key, prop->num.u_value);
|
||||
break;
|
||||
case R_JSON_NULL:
|
||||
pj_knull (pj, prop->key);
|
||||
break;
|
||||
case R_JSON_OBJECT:
|
||||
pj_ko (pj, prop->key);
|
||||
if (!r_json_to_pj (prop, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
case R_JSON_ARRAY:
|
||||
pj_ka (pj, prop->key);
|
||||
if (!r_json_to_pj (prop, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
prop = prop->next;
|
||||
}
|
||||
if (!existing_pj) {
|
||||
pj_end (pj);
|
||||
}
|
||||
break;
|
||||
case R_JSON_ARRAY:
|
||||
if (!existing_pj) {
|
||||
pj_a (pj);
|
||||
}
|
||||
// Handle array items
|
||||
const RJson *item = json->children.first;
|
||||
while (item) {
|
||||
switch (item->type) {
|
||||
case R_JSON_STRING:
|
||||
pj_s (pj, item->str_value);
|
||||
break;
|
||||
case R_JSON_INTEGER:
|
||||
pj_n (pj, item->num.u_value);
|
||||
break;
|
||||
case R_JSON_DOUBLE:
|
||||
pj_d (pj, item->num.dbl_value);
|
||||
break;
|
||||
case R_JSON_BOOLEAN:
|
||||
pj_b (pj, item->num.u_value);
|
||||
break;
|
||||
case R_JSON_NULL:
|
||||
pj_null (pj);
|
||||
break;
|
||||
case R_JSON_OBJECT:
|
||||
pj_o (pj);
|
||||
if (!r_json_to_pj (item, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
case R_JSON_ARRAY:
|
||||
pj_a (pj);
|
||||
if (!r_json_to_pj (item, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
item = item->next;
|
||||
}
|
||||
if (!existing_pj) {
|
||||
pj_end (pj);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return pj;
|
||||
}
|
||||
|
||||
// Helper function to clone RJson to string
|
||||
R_API char *r_json_to_string(const RJson *json) {
|
||||
PJ *pj = r_json_to_pj (json, NULL);
|
||||
return pj? pj_drain (pj): NULL;
|
||||
}
|
||||
+89
-27
@@ -1,7 +1,5 @@
|
||||
/* r2ai - Copyright 2023-2025 pancake, dnakov */
|
||||
|
||||
#include <r_core.h>
|
||||
#include <r_util/r_json.h>
|
||||
#include "r2ai.h"
|
||||
|
||||
// Bit flags for different types of model errors/incompatibilities
|
||||
@@ -82,9 +80,9 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
// TODO: default model name should depend on api
|
||||
const char *model_name = args.model? args.model: "gpt-4o-mini";
|
||||
char **error = args.error;
|
||||
const R2AI_Tools *tools = args.tools;
|
||||
RList *tools = args.tools;
|
||||
// create a temp conversation to include the system prompt and the rest of the messages
|
||||
R2AI_Messages *temp_msgs = r2ai_msgs_new ();
|
||||
RList *temp_msgs = r2ai_msgs_new ();
|
||||
if (!temp_msgs) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to create temporary messages array");
|
||||
@@ -122,15 +120,17 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
}
|
||||
}
|
||||
if (args.messages) {
|
||||
for (int i = 0; i < r_list_length (args.messages->messages); i++) {
|
||||
r2ai_msgs_add (temp_msgs, r_list_get_n (args.messages->messages, i));
|
||||
RListIter *iter;
|
||||
R2AI_Message *msg;
|
||||
r_list_foreach (args.messages, iter, msg) {
|
||||
r2ai_msgs_add (temp_msgs, msg);
|
||||
}
|
||||
} else {
|
||||
R_LOG_WARN ("No messages");
|
||||
}
|
||||
// Safely print debug info about first message
|
||||
if (temp_msgs && r_list_length (temp_msgs->messages) > 0 && ((R2AI_Message *)r_list_get_n (temp_msgs->messages, 0))->role) {
|
||||
R_LOG_DEBUG ("First message role: %s", ((R2AI_Message *)r_list_get_n (temp_msgs->messages, 0))->role);
|
||||
if (temp_msgs && !r_list_empty (temp_msgs) && ((R2AI_Message *)r_list_get_n (temp_msgs, 0))->role) {
|
||||
R_LOG_DEBUG ("First message role: %s", ((R2AI_Message *)r_list_get_n (temp_msgs, 0))->role);
|
||||
}
|
||||
if (error) {
|
||||
*error = NULL;
|
||||
@@ -154,8 +154,8 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
// Create a messages JSON object, either from input messages or from content
|
||||
char *messages_json = NULL;
|
||||
|
||||
if (temp_msgs && r_list_length (temp_msgs->messages) > 0) {
|
||||
R_LOG_DEBUG ("Using input messages: %d messages", r_list_length (temp_msgs->messages));
|
||||
if (temp_msgs && !r_list_empty (temp_msgs)) {
|
||||
R_LOG_DEBUG ("Using input messages: %d messages", r_list_length (temp_msgs));
|
||||
messages_json = r2ai_msgs_to_json (temp_msgs);
|
||||
if (!messages_json) {
|
||||
if (error) {
|
||||
@@ -174,7 +174,7 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
|
||||
// Convert tools to OpenAI format if available
|
||||
char *openai_tools_json = NULL;
|
||||
if (tools && tools->n_tools > 0) {
|
||||
if (tools && !r_list_empty (tools)) {
|
||||
openai_tools_json = r2ai_tools_to_openai_json (tools);
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
pj_ks (pj, "model", model_name);
|
||||
pj_kb (pj, "stream", false);
|
||||
|
||||
if (strcmp (args.provider, "ollama") == 0) {
|
||||
if (!strcmp (args.provider, "ollama")) {
|
||||
// Ollama uses "options" object for parameters
|
||||
pj_ko (pj, "options");
|
||||
if (args.max_tokens) {
|
||||
@@ -215,17 +215,6 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
|
||||
// Get the JSON for model settings
|
||||
char *model_json = pj_drain (pj);
|
||||
if (!model_json) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to create model settings JSON");
|
||||
}
|
||||
free (auth_header);
|
||||
free (messages_json);
|
||||
if (openai_tools_json) {
|
||||
free (openai_tools_json);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Manually create the final JSON by combining parts
|
||||
// Remove the closing brace from model_json
|
||||
@@ -268,6 +257,17 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
|
||||
R_LOG_DEBUG ("OpenAI API request data: %s", complete_json);
|
||||
|
||||
if (r_config_get_b (core->config, "r2ai.debug")) {
|
||||
// Generate curl command for debugging
|
||||
RStrBuf *curl_cmd = r_strbuf_new ("curl -X POST");
|
||||
for (int i = 0; headers[i]; i++) {
|
||||
r_strbuf_appendf (curl_cmd, " -H \"%s\"", headers[i]);
|
||||
}
|
||||
r_strbuf_appendf (curl_cmd, " -d '%s' \"%s\"", complete_json, openai_url);
|
||||
eprintf ("Curl command: %s\n", r_strbuf_get (curl_cmd));
|
||||
r_strbuf_free (curl_cmd);
|
||||
}
|
||||
|
||||
// Make the API call
|
||||
char *res = NULL;
|
||||
int code = 0;
|
||||
@@ -311,12 +311,12 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
tmpdir = r_file_tmpdir ();
|
||||
char *res_path = r_str_newf ("%s" R_SYS_DIR "r2ai_openai_response.json", tmpdir);
|
||||
r_file_dump (res_path, (const ut8 *)res, strlen (res), 0);
|
||||
R_LOG_DEBUG ("OpenAI API response saved to %s", res_path);
|
||||
if (r_config_get_b (core->config, "r2ai.debug")) {
|
||||
eprintf ("OpenAI API response: %s\n", res);
|
||||
}
|
||||
free (res_path);
|
||||
free (tmpdir);
|
||||
|
||||
R_LOG_DEBUG ("OpenAI API response: %s", res);
|
||||
|
||||
// Parse the response into our messages structure
|
||||
|
||||
char *res_copy = strdup (res);
|
||||
@@ -383,6 +383,9 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
const RJson *role = r_json_get (message_json, "role");
|
||||
const RJson *content = r_json_get (message_json, "content");
|
||||
const RJson *reasoning_content = r_json_get (message_json, "reasoning_content");
|
||||
const RJson *thinking = r_json_get (message_json, "thinking");
|
||||
const RJson *tool_call_id = r_json_get (message_json, "tool_call_id");
|
||||
const RJson *tool_calls = r_json_get (message_json, "tool_calls");
|
||||
|
||||
// Set the basic message properties
|
||||
message->role = (role && role->type == R_JSON_STRING)? strdup (role->str_value): strdup ("assistant");
|
||||
@@ -391,8 +394,67 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
message->content = strdup (content->str_value);
|
||||
}
|
||||
|
||||
if (reasoning_content && reasoning_content->type == R_JSON_STRING) {
|
||||
if (reasoning_content && reasoning_content->type == R_JSON_STRING && R_STR_ISNOTEMPTY (reasoning_content->str_value)) {
|
||||
message->reasoning_content = strdup (reasoning_content->str_value);
|
||||
} else if (thinking && thinking->type == R_JSON_STRING && R_STR_ISNOTEMPTY (thinking->str_value)) {
|
||||
message->reasoning_content = strdup (thinking->str_value);
|
||||
}
|
||||
|
||||
if (tool_call_id && tool_call_id->type == R_JSON_STRING && R_STR_ISNOTEMPTY (tool_call_id->str_value)) {
|
||||
message->tool_call_id = strdup (tool_call_id->str_value);
|
||||
}
|
||||
|
||||
if (tool_calls && tool_calls->type == R_JSON_ARRAY && tool_calls->children.count > 0) {
|
||||
message->tool_calls = r_list_new ();
|
||||
if (message->tool_calls) {
|
||||
message->tool_calls->free = (RListFree)r2ai_tool_call_free;
|
||||
for (size_t i = 0; i < tool_calls->children.count; i++) {
|
||||
const RJson *tool_call = r_json_item (tool_calls, i);
|
||||
if (!tool_call || tool_call->type != R_JSON_OBJECT) {
|
||||
continue;
|
||||
}
|
||||
R2AI_ToolCall *tc = R_NEW0 (R2AI_ToolCall);
|
||||
if (!tc) {
|
||||
continue;
|
||||
}
|
||||
const RJson *tc_id = r_json_get (tool_call, "id");
|
||||
if (tc_id && tc_id->type == R_JSON_STRING && R_STR_ISNOTEMPTY (tc_id->str_value)) {
|
||||
tc->id = strdup (tc_id->str_value);
|
||||
}
|
||||
const RJson *function = r_json_get (tool_call, "function");
|
||||
const RJson *name_json = NULL;
|
||||
const RJson *arguments = NULL;
|
||||
if (function && function->type == R_JSON_OBJECT) {
|
||||
name_json = r_json_get (function, "name");
|
||||
arguments = r_json_get (function, "arguments");
|
||||
} else {
|
||||
name_json = r_json_get (tool_call, "name");
|
||||
arguments = r_json_get (tool_call, "arguments");
|
||||
}
|
||||
if (name_json && name_json->type == R_JSON_STRING && R_STR_ISNOTEMPTY (name_json->str_value)) {
|
||||
tc->name = strdup (name_json->str_value);
|
||||
}
|
||||
if (arguments) {
|
||||
if (arguments->type == R_JSON_STRING && R_STR_ISNOTEMPTY (arguments->str_value)) {
|
||||
tc->arguments = strdup (arguments->str_value);
|
||||
} else {
|
||||
char *arguments_json = r_json_to_string (arguments);
|
||||
if (arguments_json) {
|
||||
tc->arguments = arguments_json;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!tc->name && !tc->arguments && !tc->id) {
|
||||
r2ai_tool_call_free (tc);
|
||||
continue;
|
||||
}
|
||||
r_list_append (message->tool_calls, tc);
|
||||
}
|
||||
if (r_list_empty (message->tool_calls)) {
|
||||
r_list_free (message->tool_calls);
|
||||
message->tool_calls = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Handle tool calls if present?
|
||||
}
|
||||
|
||||
+62
-58
@@ -19,7 +19,7 @@ R_API void r2aiprompt_free(R2AIPrompt *prompt) {
|
||||
free (prompt);
|
||||
}
|
||||
|
||||
static R2AIPrompt *parse_prompt_file(const char *filepath) {
|
||||
R_API R2AIPrompt *parse_prompt_file(const char *filepath) {
|
||||
R2AIPrompt *prompt = R_NEW0 (R2AIPrompt);
|
||||
char *content = r_file_slurp (filepath, NULL);
|
||||
if (!content) {
|
||||
@@ -71,45 +71,7 @@ static R2AIPrompt *parse_prompt_file(const char *filepath) {
|
||||
prompt->prompt = strdup (prompt_start);
|
||||
}
|
||||
} else {
|
||||
// Original parsing for .txt files
|
||||
RList *lines = r_str_split_list (content, "\n", -1);
|
||||
RListIter *iter;
|
||||
char *line;
|
||||
r_list_foreach (lines, iter, line) {
|
||||
r_str_trim (line);
|
||||
if (*line == '#' || *line == 0) {
|
||||
continue;
|
||||
}
|
||||
char *colon = strchr (line, ':');
|
||||
if (!colon) {
|
||||
continue;
|
||||
}
|
||||
*colon = 0;
|
||||
char *key = line;
|
||||
char *value = colon + 1;
|
||||
r_str_trim (key);
|
||||
r_str_trim (value);
|
||||
if (!strcmp (key, "Title")) {
|
||||
prompt->title = strdup (value);
|
||||
} else if (!strcmp (key, "Author")) {
|
||||
prompt->author = strdup (value);
|
||||
} else if (!strcmp (key, "Description")) {
|
||||
prompt->desc = strdup (value);
|
||||
} else if (!strcmp (key, "Command") || !strcmp (key, "Commands")) {
|
||||
prompt->command = strdup (value);
|
||||
} else if (!strcmp (key, "Prompt") || !strcmp (key, "Query")) {
|
||||
prompt->prompt = strdup (value);
|
||||
} else if (!strcmp (key, "Depends")) {
|
||||
prompt->
|
||||
requires
|
||||
= strdup (value);
|
||||
} else if (!strcmp (key, "If-Empty")) {
|
||||
prompt->if_empty = strdup (value);
|
||||
} else if (!strcmp (key, "If-Command")) {
|
||||
prompt->if_command = strdup (value);
|
||||
}
|
||||
}
|
||||
r_list_free (lines);
|
||||
R_LOG_ERROR ("Invalid prompt file format: %s", filepath);
|
||||
}
|
||||
|
||||
free (content);
|
||||
@@ -183,6 +145,29 @@ static char *replace_vars(RCore *core, const char *text) {
|
||||
return r_strbuf_drain (sb);
|
||||
}
|
||||
|
||||
// Helper function to find prompt file in search directories
|
||||
R_API char *find_prompt_file(RList *search_dirs, const char *name) {
|
||||
char *filepath = NULL;
|
||||
RListIter *dir_iter;
|
||||
char *dir;
|
||||
r_list_foreach (search_dirs, dir_iter, dir) {
|
||||
char *filepath_txt = r_str_newf ("%s/%s.r2ai.txt", dir, name);
|
||||
char *filepath_md = r_str_newf ("%s/%s.r2ai.md", dir, name);
|
||||
if (r_file_exists (filepath_txt)) {
|
||||
filepath = filepath_txt;
|
||||
free (filepath_md);
|
||||
break;
|
||||
} else if (r_file_exists (filepath_md)) {
|
||||
filepath = filepath_md;
|
||||
free (filepath_txt);
|
||||
break;
|
||||
}
|
||||
free (filepath_txt);
|
||||
free (filepath_md);
|
||||
}
|
||||
return filepath;
|
||||
}
|
||||
|
||||
R_API void r2ai_cmd_q(RCorePluginSession *cps, const char *input) {
|
||||
RCore *core = cps->core;
|
||||
const char *promptdir = r_config_get (core->config, "r2ai.promptdir");
|
||||
@@ -254,24 +239,7 @@ R_API void r2ai_cmd_q(RCorePluginSession *cps, const char *input) {
|
||||
r_str_trim (extra);
|
||||
}
|
||||
r_str_trim (name);
|
||||
char *filepath = NULL;
|
||||
RListIter *dir_iter;
|
||||
char *dir;
|
||||
r_list_foreach (search_dirs, dir_iter, dir) {
|
||||
char *filepath_txt = r_str_newf ("%s/%s.r2ai.txt", dir, name);
|
||||
char *filepath_md = r_str_newf ("%s/%s.r2ai.md", dir, name);
|
||||
if (r_file_exists (filepath_txt)) {
|
||||
filepath = filepath_txt;
|
||||
free (filepath_md);
|
||||
break;
|
||||
} else if (r_file_exists (filepath_md)) {
|
||||
filepath = filepath_md;
|
||||
free (filepath_txt);
|
||||
break;
|
||||
}
|
||||
free (filepath_txt);
|
||||
free (filepath_md);
|
||||
}
|
||||
char *filepath = find_prompt_file (search_dirs, name);
|
||||
if (!filepath) {
|
||||
R_LOG_ERROR ("Cannot find prompt file: %s.r2ai.txt or %s.r2ai.md", name, name);
|
||||
r_list_free (search_dirs);
|
||||
@@ -344,3 +312,39 @@ R_API void r2ai_cmd_q(RCorePluginSession *cps, const char *input) {
|
||||
}
|
||||
r_list_free (search_dirs);
|
||||
}
|
||||
|
||||
R_API char *r2ai_load_prompt_text(RCore *core, const char *name) {
|
||||
const char *promptdir = r_config_get (core->config, "r2ai.promptdir");
|
||||
if (!promptdir || !*promptdir) {
|
||||
promptdir = "~/.config/r2ai/prompts";
|
||||
}
|
||||
char *expanded_dir = r_file_abspath (promptdir);
|
||||
char *local_prompts = r_file_abspath ("../prompts");
|
||||
RList *search_dirs = r_list_newf (free);
|
||||
if (r_file_is_directory (expanded_dir)) {
|
||||
r_list_append (search_dirs, expanded_dir);
|
||||
} else {
|
||||
free (expanded_dir);
|
||||
expanded_dir = NULL;
|
||||
}
|
||||
if (r_file_is_directory (local_prompts)) {
|
||||
r_list_append (search_dirs, local_prompts);
|
||||
} else {
|
||||
free (local_prompts);
|
||||
local_prompts = NULL;
|
||||
}
|
||||
|
||||
char *filepath = find_prompt_file (search_dirs, name);
|
||||
char *prompt_text = NULL;
|
||||
if (filepath) {
|
||||
R2AIPrompt *prompt = parse_prompt_file (filepath);
|
||||
if (prompt && prompt->prompt) {
|
||||
prompt_text = strdup (prompt->prompt);
|
||||
}
|
||||
r2aiprompt_free (prompt);
|
||||
free (filepath);
|
||||
}
|
||||
|
||||
r_list_free (search_dirs);
|
||||
return prompt_text;
|
||||
}
|
||||
+100
-37
@@ -12,16 +12,19 @@ static RCoreHelpMessage help_msg_r2ai = {
|
||||
"r2ai", " -d [query]", "Ask a question on the current function",
|
||||
"r2ai", " -dr", "Decompile current function (+ 1 level of recursivity)",
|
||||
"r2ai", " -a [query]", "Resolve question using auto mode",
|
||||
"r2ai", " -b [url]", "set/show base URL for provider API",
|
||||
"r2ai", " -e (k(=v))", "Same as '-e r2ai.'",
|
||||
"r2ai", " -E", "Edit the r2ai rc file",
|
||||
"r2ai", " -h", "Show this help message",
|
||||
"r2ai", " -i [file] [query]", "read file and ask the llm with the given query",
|
||||
"r2ai", " -m", "show selected model, list suggested ones, choose one",
|
||||
"r2ai", " -m [model]", "show selected model, list suggested ones, choose one",
|
||||
"r2ai", " -p [provider]", "set LLM provider (openai, anthropic, gemini, etc.)",
|
||||
"r2ai", " -q", "list available query prompts",
|
||||
"r2ai", " -q [name] (inst)", "run predefined prompt with optional instructions",
|
||||
"r2ai", " -r", "enter the chat repl",
|
||||
"r2ai", " -L", "show chat logs (See -Lj for json). Only for auto mode.",
|
||||
"r2ai", " -C", "compact conversation history",
|
||||
"r2ai", " -LR", "create a log report",
|
||||
"r2ai", " -L-[N]", "delete the last (or N last messages from the chat history)",
|
||||
"r2ai", " -R", "reset the chat conversation context",
|
||||
"r2ai", " -Rq ([text])", "refresh and query embeddings (see r2ai.data)",
|
||||
@@ -42,7 +45,7 @@ R_API char *r2ai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
args.system_prompt = r_config_get (core->config, "r2ai.system");
|
||||
}
|
||||
|
||||
R2AI_Messages *msgs = r2ai_msgs_new ();
|
||||
RList *msgs = r2ai_msgs_new ();
|
||||
|
||||
if (args.input) {
|
||||
R2AI_Message msg = { .role = "user", .content = (char *)args.input };
|
||||
@@ -51,29 +54,15 @@ R_API char *r2ai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
|
||||
args.messages = msgs;
|
||||
|
||||
R2AI_ChatResponse *res = r2ai_llmcall (cps, args);
|
||||
if (!res) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Extract content from the response
|
||||
char *content = NULL;
|
||||
|
||||
// If content is present in the result message, use it
|
||||
if (res->message) {
|
||||
if (res->message->content) {
|
||||
R2AI_ChatResponse *res = r2ai_llmcall (cps, args);
|
||||
if (res) {
|
||||
if (res->message && res->message->content) {
|
||||
content = strdup (res->message->content);
|
||||
}
|
||||
}
|
||||
|
||||
// Free the message properly using r2ai_message_free
|
||||
if (res->message) {
|
||||
r2ai_message_free ((R2AI_Message *)res->message);
|
||||
free (res);
|
||||
}
|
||||
|
||||
// Free the response struct itself
|
||||
free (res);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -145,7 +134,7 @@ static void cmd_r2ai_repl(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
while (true) {
|
||||
r_line_set_prompt (core->cons->line, ">>> ");
|
||||
r_line_set_prompt (core->cons->line, "[r2ai]> ");
|
||||
const char *ptr = r_line_readline (core->cons);
|
||||
if (R_STR_ISEMPTY (ptr)) {
|
||||
break;
|
||||
@@ -291,6 +280,36 @@ static void load_embeddings(RCorePluginSession *cps) {
|
||||
r_list_free (files);
|
||||
}
|
||||
|
||||
static void cmd_r2ai(RCorePluginSession *cps, const char *input);
|
||||
|
||||
static bool load_r2airc(RCorePluginSession *cps) {
|
||||
char *rc_path = r_file_home (".config/r2ai/rc");
|
||||
if (rc_path && r_file_exists (rc_path)) {
|
||||
char *content = r_file_slurp (rc_path, NULL);
|
||||
if (content) {
|
||||
RList *lines = r_str_split_list (content, "\n", -1);
|
||||
RListIter *iter;
|
||||
char *line;
|
||||
r_list_foreach (lines, iter, line) {
|
||||
const char *trimmed = r_str_trim_head_ro (line);
|
||||
if (R_STR_ISEMPTY (trimmed) || *trimmed == '#') {
|
||||
continue;
|
||||
}
|
||||
if (r_str_startswith (trimmed, "r2ai -e ") || r_str_startswith (trimmed, "-e ")) {
|
||||
const char *arg = trimmed + (r_str_startswith (trimmed, "r2ai -e ")? 7: 3);
|
||||
cmd_r2ai (cps, r_str_newf ("-e%s", arg));
|
||||
} else {
|
||||
R_LOG_ERROR ("Invalid line in r2ai rc: %s", trimmed);
|
||||
}
|
||||
}
|
||||
r_list_free (lines);
|
||||
free (content);
|
||||
}
|
||||
}
|
||||
free (rc_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void cmd_r2ai(RCorePluginSession *cps, const char *input) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
@@ -300,6 +319,7 @@ static void cmd_r2ai(RCorePluginSession *cps, const char *input) {
|
||||
char *rc_path = r_file_home (".config/r2ai/rc");
|
||||
r_cons_editor (core->cons, rc_path, NULL);
|
||||
free (rc_path);
|
||||
load_r2airc (cps);
|
||||
} else if (r_str_startswith (input, "-e")) {
|
||||
const char *arg = r_str_trim_head_ro (input + 2);
|
||||
if (r_str_startswith (arg, "r2ai")) {
|
||||
@@ -309,17 +329,28 @@ static void cmd_r2ai(RCorePluginSession *cps, const char *input) {
|
||||
}
|
||||
} else if (r_str_startswith (input, "-a")) {
|
||||
cmd_r2ai_a (cps, r_str_trim_head_ro (input + 2));
|
||||
} else if (r_str_startswith (input, "-b")) {
|
||||
const char *baseurl = r_str_trim_head_ro (input + 2);
|
||||
if (R_STR_ISEMPTY (baseurl)) {
|
||||
r_cons_printf (core->cons, "%s\n", r_config_get (core->config, "r2ai.baseurl"));
|
||||
} else {
|
||||
r_config_set (core->config, "r2ai.baseurl", baseurl);
|
||||
}
|
||||
} else if (r_str_startswith (input, "-L-")) {
|
||||
const char *arg = r_str_trim_head_ro (input + 3);
|
||||
const int N = atoi (arg);
|
||||
R2AI_Messages *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_length (messages->messages) == 0) {
|
||||
RList *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_empty (messages)) {
|
||||
r_cons_printf (core->cons, "No conversation history available\n");
|
||||
} else {
|
||||
r2ai_delete_last_messages (messages, N);
|
||||
r_cons_printf (core->cons, "Deleted %d message%s from chat history\n", N > 0? N: 1,
|
||||
(N > 0 && N != 1)? "s": "");
|
||||
}
|
||||
} else if (r_str_startswith (input, "-C")) {
|
||||
cmd_r2ai_c (cps);
|
||||
} else if (r_str_startswith (input, "-LR")) {
|
||||
cmd_r2ai_lr (cps);
|
||||
} else if (r_str_startswith (input, "-L")) {
|
||||
cmd_r2ai_logs (cps);
|
||||
} else if (r_str_startswith (input, "-d")) {
|
||||
@@ -349,8 +380,8 @@ static void cmd_r2ai(RCorePluginSession *cps, const char *input) {
|
||||
} else if (r_str_startswith (input, "-r")) {
|
||||
cmd_r2ai_repl (cps);
|
||||
} else if (r_str_startswith (input, "-R")) {
|
||||
R2AI_Messages *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_length (messages->messages) == 0) {
|
||||
RList *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_empty (messages)) {
|
||||
r_cons_printf (core->cons, "No conversation history to reset\n");
|
||||
} else {
|
||||
r2ai_msgs_clear (messages);
|
||||
@@ -451,6 +482,36 @@ static bool cb_r2ai_model(void *user, void *data) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cb_r2ai_http_backend(void *user, void *data) {
|
||||
RCore *core = (RCore *)user;
|
||||
RConfigNode *node = (RConfigNode *)data;
|
||||
if (*node->value == '?') {
|
||||
r_cons_println (core->cons, "auto");
|
||||
r_cons_println (core->cons, "libcurl");
|
||||
r_cons_println (core->cons, "socket");
|
||||
r_cons_println (core->cons, "system");
|
||||
r_cons_println (core->cons, "curl");
|
||||
r_cons_println (core->cons, "pwsh");
|
||||
r_cons_println (core->cons, "powershell");
|
||||
r_cons_println (core->cons, "r2curl");
|
||||
return false;
|
||||
}
|
||||
// Validate the backend
|
||||
if (!strcmp (node->value, "auto") ||
|
||||
!strcmp (node->value, "libcurl") ||
|
||||
!strcmp (node->value, "socket") ||
|
||||
!strcmp (node->value, "system") ||
|
||||
!strcmp (node->value, "curl") ||
|
||||
!strcmp (node->value, "pwsh") ||
|
||||
!strcmp (node->value, "powershell") ||
|
||||
!strcmp (node->value, "r2curl")) {
|
||||
return true;
|
||||
}
|
||||
R_LOG_ERROR ("Invalid backend '%s'. Use '?' to list valid backends.", node->value);
|
||||
return false;
|
||||
// return false; // always return false to test
|
||||
}
|
||||
|
||||
R_IPI bool r2ai_init(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
// Initialize state
|
||||
@@ -460,12 +521,6 @@ R_IPI bool r2ai_init(RCorePluginSession *cps) {
|
||||
// Initialize conversation container
|
||||
r2ai_conversation_init (state);
|
||||
|
||||
char *rc_path = r_file_home (".config/r2ai/rc");
|
||||
if (rc_path && r_file_exists (rc_path)) {
|
||||
r_core_cmdf (core, ". %s", rc_path);
|
||||
}
|
||||
free (rc_path);
|
||||
|
||||
r_config_lock (core->config, false);
|
||||
r_config_set_cb (core->config, "r2ai.api", R2AI_DEFAULT_PROVIDER, &cb_r2ai_api);
|
||||
{
|
||||
@@ -476,6 +531,8 @@ R_IPI bool r2ai_init(RCorePluginSession *cps) {
|
||||
r_config_desc (core->config, "r2ai.api", desc);
|
||||
free (desc);
|
||||
}
|
||||
r_config_set_cb (core->config, "r2ai.http.backend", "auto", &cb_r2ai_http_backend); // Options: auto, libcurl, socket, system, curl, pwsh, powershell, r2curl
|
||||
r_config_desc (core->config, "r2ai.http.backend", "HTTP backend to use (auto, libcurl, socket, system, curl, pwsh, powershell, r2curl)");
|
||||
r_config_set_cb (core->config, "r2ai.model", R2AI_DEFAULT_MODEL, &cb_r2ai_model);
|
||||
r_config_desc (core->config, "r2ai.model", "Model identifier for the selected provider (e.g. gpt-5-mini)");
|
||||
r_config_set (core->config, "r2ai.baseurl", "");
|
||||
@@ -519,29 +576,34 @@ R_IPI bool r2ai_init(RCorePluginSession *cps) {
|
||||
r_config_desc (core->config, "r2ai.auto.max_runs", "Maximum number of automated steps/runs in auto mode");
|
||||
r_config_set_b (core->config, "r2ai.auto.hide_tool_output", false);
|
||||
r_config_desc (core->config, "r2ai.auto.hide_tool_output", "Hide tool output when running automated actions");
|
||||
r_config_set (core->config, "r2ai.auto.init_commands", "iI"); // "aaa;iI;afl~[3]"
|
||||
r_config_set (core->config, "r2ai.auto.init_commands", "i"); // "aaa;iI;afl~[3]"
|
||||
r_config_desc (core->config, "r2ai.auto.init_commands", "Initial commands executed when auto mode starts (semicolon separated)");
|
||||
r_config_set_b (core->config, "r2ai.auto.yolo", false);
|
||||
r_config_desc (core->config, "r2ai.auto.yolo", "Execute potentially dangerous commands in auto mode without asking");
|
||||
r_config_set_b (core->config, "r2ai.auto.reset_on_query", false);
|
||||
r_config_desc (core->config, "r2ai.auto.reset_on_query", "Reset auto-mode conversation state on new user queries");
|
||||
r_config_set_b (core->config, "r2ai.auto.think", true);
|
||||
r_config_desc (core->config, "r2ai.auto.think", "Enable thinking/reasoning in auto mode prompts (true/false)");
|
||||
r_config_set_b (core->config, "r2ai.chat.show_cost", true);
|
||||
r_config_desc (core->config, "r2ai.chat.show_cost", "Display estimated API cost for chat interactions");
|
||||
|
||||
r_config_set_i (core->config, "r2ai.http.timeout", 120);
|
||||
r_config_set_i (core->config, "r2ai.http.timeout", 240);
|
||||
r_config_desc (core->config, "r2ai.http.timeout", "HTTP client timeout (seconds) for provider API calls");
|
||||
r_config_set_i (core->config, "r2ai.http.max_retries", 10);
|
||||
r_config_set_i (core->config, "r2ai.http.max_retries", 5);
|
||||
r_config_desc (core->config, "r2ai.http.max_retries", "Maximum number of HTTP retries for failed requests");
|
||||
r_config_set_i (core->config, "r2ai.http.max_backoff", 30);
|
||||
r_config_desc (core->config, "r2ai.http.max_backoff", "Maximum backoff time (seconds) between HTTP retries");
|
||||
r_config_set (core->config, "r2ai.http.backend", "auto"); // Options: auto, libcurl, socket, system
|
||||
r_config_desc (core->config, "r2ai.http.backend", "HTTP backend to use (auto, libcurl, socket, system)");
|
||||
r_config_set_b (core->config, "r2ai.http.use_files", false);
|
||||
r_config_desc (core->config, "r2ai.http.use_files", "Use temporary files to pass HTTP request/response payloads (true/false)");
|
||||
r_config_set_b (core->config, "r2ai.auto.raw", false);
|
||||
r_config_desc (core->config, "r2ai.auto.raw", "Use prompt engineering for tool calling instead of native API support (true/false)");
|
||||
r_config_set_b (core->config, "r2ai.auto.usejs", true);
|
||||
r_config_desc (core->config, "r2ai.auto.usejs", "Enable/disable the execute_js tool for auto mode (true/false)");
|
||||
r_config_set_b (core->config, "r2ai.debug", false);
|
||||
r_config_desc (core->config, "r2ai.debug", "Enable debug output including API request/response details and curl commands");
|
||||
r_config_lock (core->config, true);
|
||||
return true;
|
||||
|
||||
return load_r2airc (cps);
|
||||
}
|
||||
|
||||
R_API bool r2ai_fini(RCorePluginSession *cps) {
|
||||
@@ -562,6 +624,7 @@ R_API bool r2ai_fini(RCorePluginSession *cps) {
|
||||
r_config_rm (core->config, "r2ai.http.max_backoff");
|
||||
r_config_rm (core->config, "r2ai.http.backend");
|
||||
r_config_rm (core->config, "r2ai.http.use_files");
|
||||
r_config_rm (core->config, "r2ai.debug");
|
||||
r_config_lock (core->config, true);
|
||||
|
||||
R2AI_State *state = cps->data;
|
||||
|
||||
+82
-50
@@ -1,6 +1,9 @@
|
||||
#ifndef R2AI_H
|
||||
#define R2AI_H
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include <time.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
@@ -8,10 +11,11 @@
|
||||
#include <stdlib.h>
|
||||
#include <r_core.h>
|
||||
#include <r_util/r_json.h>
|
||||
#include <r_types_base.h>
|
||||
#include "r_vdb.h"
|
||||
#include "markdown.h"
|
||||
|
||||
#define R2AI_VERSION "1.2.2"
|
||||
#define R2AI_VERSION "1.2.6"
|
||||
|
||||
#if 0
|
||||
#define R2AI_DEFAULT_MODEL "gpt-5-mini"
|
||||
@@ -25,6 +29,28 @@
|
||||
|
||||
#define R2AI_DEFAULT_VECTORS 16
|
||||
|
||||
// HTTP configuration structure
|
||||
typedef struct {
|
||||
int timeout;
|
||||
int max_retries;
|
||||
int max_backoff;
|
||||
} R2AI_HttpConfig;
|
||||
|
||||
// HTTP request structure
|
||||
typedef struct {
|
||||
R2AI_HttpConfig config;
|
||||
const char *url;
|
||||
const char *data;
|
||||
const char * const *headers;
|
||||
} HTTPRequest;
|
||||
|
||||
// HTTP response structure
|
||||
typedef struct {
|
||||
char *body;
|
||||
int code;
|
||||
int length;
|
||||
} HttpResponse;
|
||||
|
||||
#if R2_VERSION_NUMBER < 60000
|
||||
#error Your radare2 is too old
|
||||
#endif
|
||||
@@ -38,11 +64,7 @@ typedef struct {
|
||||
char *parameters; // JSON string of parameters/input_schema
|
||||
} R2AI_Tool;
|
||||
|
||||
// Tools array management
|
||||
typedef struct {
|
||||
R2AI_Tool *tools;
|
||||
int n_tools;
|
||||
} R2AI_Tools;
|
||||
// Tools array management (now just RList<R2AI_Tool *>)
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
@@ -50,6 +72,11 @@ typedef struct {
|
||||
const char *id;
|
||||
} R2AI_ToolCall;
|
||||
|
||||
/**
|
||||
* Free an R2AI_ToolCall instance
|
||||
*/
|
||||
R_API void r2ai_tool_call_free(R2AI_ToolCall *tc);
|
||||
|
||||
typedef struct {
|
||||
char *type;
|
||||
char *id;
|
||||
@@ -61,19 +88,25 @@ typedef struct {
|
||||
char *text;
|
||||
} R2AI_ContentBlock;
|
||||
|
||||
typedef struct {
|
||||
R2AI_ContentBlock *blocks;
|
||||
int n_blocks;
|
||||
} R2AI_ContentBlocks;
|
||||
// Content blocks (now just RList<R2AI_ContentBlock *>)
|
||||
|
||||
/**
|
||||
* Create a new content blocks RList
|
||||
*/
|
||||
R_API RList *r2ai_content_blocks_new(void);
|
||||
|
||||
/**
|
||||
* Free a content blocks RList
|
||||
*/
|
||||
R_API void r2ai_content_blocks_free(RList *cb);
|
||||
|
||||
typedef struct {
|
||||
char *role;
|
||||
char *content;
|
||||
char *reasoning_content;
|
||||
R2AI_ContentBlocks *content_blocks;
|
||||
RList *content_blocks; // RList<R2AI_ContentBlock *>
|
||||
char *tool_call_id;
|
||||
R2AI_ToolCall *tool_calls;
|
||||
int n_tool_calls;
|
||||
RList *tool_calls; // RList<R2AI_ToolCall *>
|
||||
} R2AI_Message;
|
||||
|
||||
typedef struct {
|
||||
@@ -100,17 +133,14 @@ typedef struct {
|
||||
const R2AI_Usage *usage;
|
||||
} R2AI_ChatResponse;
|
||||
|
||||
// Messages array management
|
||||
typedef struct {
|
||||
RList *messages; // RList<R2AI_Message *>
|
||||
} R2AI_Messages;
|
||||
// Messages array management (now just RList<R2AI_Message *>)
|
||||
|
||||
typedef struct {
|
||||
const char *input;
|
||||
const char *model;
|
||||
const char *system_prompt; // System prompt to use
|
||||
const R2AI_Tools *tools; // Tools structure (replacing tools_json)
|
||||
R2AI_Messages *messages; // Array of message objects
|
||||
RList *tools; // Tools RList (replacing tools_json)
|
||||
RList *messages; // Array of message objects
|
||||
const char *provider;
|
||||
const char *api_key;
|
||||
int max_tokens;
|
||||
@@ -136,9 +166,9 @@ typedef struct {
|
||||
|
||||
// Main state structure to hold all global state
|
||||
typedef struct r2ai_state_t {
|
||||
R2AI_Messages *conversation; // Global conversation messages (from messages.c)
|
||||
RList *conversation; // Global conversation messages (from messages.c)
|
||||
R2AIStats stats; // Global stats (from auto.c)
|
||||
R2AI_Tools *tools; // Global tools instance (from tools.c)
|
||||
RList *tools; // Global tools instance (from tools.c)
|
||||
RMarkdownTheme current_theme; // Global theme (from markdown.c)
|
||||
bool theme_initialized; // Global theme flag (from markdown.c)
|
||||
void *help_msg; // Global help message (from r2ai.c)
|
||||
@@ -149,7 +179,7 @@ typedef struct r2ai_state_t {
|
||||
/**
|
||||
* Initialize a new empty messages array
|
||||
*/
|
||||
R_API R2AI_Messages *r2ai_msgs_new(void);
|
||||
R_API RList *r2ai_msgs_new(void);
|
||||
|
||||
/**
|
||||
* Initialize the conversation container (call during plugin init)
|
||||
@@ -159,53 +189,53 @@ R_API void r2ai_conversation_init(R2AI_State *state);
|
||||
/**
|
||||
* Get the conversation instance (returns NULL if not initialized)
|
||||
*/
|
||||
R_API R2AI_Messages *r2ai_conversation_get(R2AI_State *state);
|
||||
R_API RList *r2ai_conversation_get(R2AI_State *state);
|
||||
|
||||
/**
|
||||
* Clear all messages in a container without freeing the container
|
||||
*/
|
||||
R_API void r2ai_msgs_clear(R2AI_Messages *msgs);
|
||||
R_API void r2ai_msgs_clear(RList *msgs);
|
||||
|
||||
/**
|
||||
* Add a message to the array
|
||||
* All strings are duplicated, so caller can free their copies
|
||||
*/
|
||||
R_API bool r2ai_msgs_add(R2AI_Messages *msgs, const R2AI_Message *msg);
|
||||
R_API bool r2ai_msgs_add(RList *msgs, const R2AI_Message *msg);
|
||||
|
||||
/**
|
||||
* Add a tool call to the last message in the array
|
||||
* All strings are duplicated, so caller can free their copies
|
||||
*/
|
||||
R_API bool r2ai_msgs_add_tool_call(R2AI_Messages *msgs, const R2AI_ToolCall *tc);
|
||||
R_API bool r2ai_msgs_add_tool_call(RList *msgs, const R2AI_ToolCall *tc);
|
||||
|
||||
/**
|
||||
* Parse a JSON response string and add the messages to the array
|
||||
* Returns true on success, false on failure
|
||||
*/
|
||||
R_API bool r2ai_msgs_from_response(R2AI_Messages *msgs, const char *json_str);
|
||||
R_API bool r2ai_msgs_from_response(RList *msgs, const char *json_str);
|
||||
|
||||
/**
|
||||
* Parse a RJson object directly and add the messages to the array
|
||||
* Returns true on success, false on failure
|
||||
*/
|
||||
R_API bool r2ai_msgs_from_json(R2AI_Messages *msgs, const RJson *json);
|
||||
R_API bool r2ai_msgs_from_json(RList *msgs, const RJson *json);
|
||||
|
||||
/**
|
||||
* Convert messages array to JSON string
|
||||
* Caller must free the returned string
|
||||
*/
|
||||
R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs);
|
||||
R_API char *r2ai_msgs_to_json(const RList *msgs);
|
||||
|
||||
/**
|
||||
* Convert messages array to Anthropic format JSON string
|
||||
* Caller must free the returned string
|
||||
*/
|
||||
R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs);
|
||||
R_API char *r2ai_msgs_to_anthropic_json(const RList *msgs);
|
||||
|
||||
/**
|
||||
* Free a messages array and all associated data
|
||||
*/
|
||||
R_API void r2ai_msgs_free(R2AI_Messages *msgs);
|
||||
R_API void r2ai_msgs_free(RList *msgs);
|
||||
|
||||
/**
|
||||
* Free the conversation (call during plugin unload)
|
||||
@@ -217,45 +247,40 @@ R_API void r2ai_conversation_free(R2AI_State *state);
|
||||
*/
|
||||
R_API void r2ai_message_free(R2AI_Message *msg);
|
||||
|
||||
/**
|
||||
* Free a R2AIPrompt structure
|
||||
*/
|
||||
R_API void r2aiprompt_free(R2AIPrompt *prompt);
|
||||
|
||||
/**
|
||||
* Delete the last N messages from the message array
|
||||
* If n <= 0, defaults to deleting just the last message
|
||||
*/
|
||||
R_API void r2ai_delete_last_messages(R2AI_Messages *messages, int n);
|
||||
R_API void r2ai_delete_last_messages(RList *messages, int n);
|
||||
|
||||
/**
|
||||
* Get the global tools instance
|
||||
* Returns a pointer to the global tools structure
|
||||
* Get the tools instance from state
|
||||
* Returns a pointer to the tools RList
|
||||
*/
|
||||
R_API const R2AI_Tools *r2ai_get_tools(void);
|
||||
R_API RList *r2ai_get_tools(RCore *core, R2AI_State *state);
|
||||
|
||||
/**
|
||||
* Parse OpenAI format tools JSON into internal tools structure
|
||||
* Parse OpenAI format tools JSON into internal tools RList
|
||||
* Caller must free the result with r2ai_tools_free
|
||||
*/
|
||||
R_API R2AI_Tools *r2ai_tools_parse(const char *tools_json);
|
||||
R_API RList *r2ai_tools_parse(const char *tools_json);
|
||||
|
||||
/**
|
||||
* Convert tools structure to OpenAI format JSON
|
||||
* Convert tools RList to OpenAI format JSON
|
||||
* Caller must free the returned string
|
||||
*/
|
||||
R_API char *r2ai_tools_to_openai_json(const R2AI_Tools *tools);
|
||||
R_API char *r2ai_tools_to_openai_json(const RList *tools);
|
||||
|
||||
/**
|
||||
* Convert tools structure to Anthropic format JSON
|
||||
* Convert tools RList to Anthropic format JSON
|
||||
* Caller must free the returned string
|
||||
*/
|
||||
R_API char *r2ai_tools_to_anthropic_json(const R2AI_Tools *tools);
|
||||
R_API char *r2ai_tools_to_anthropic_json(const RList *tools);
|
||||
|
||||
/**
|
||||
* Free a tools structure and all associated data
|
||||
* Free a tools RList and all associated data
|
||||
*/
|
||||
R_API void r2ai_tools_free(R2AI_Tools *tools);
|
||||
R_API void r2ai_tools_free(RList *tools);
|
||||
|
||||
/**
|
||||
* Execute a tool and return the output
|
||||
@@ -264,7 +289,7 @@ R_API void r2ai_tools_free(R2AI_Tools *tools);
|
||||
* command which was run
|
||||
* Caller must free edited_command
|
||||
*/
|
||||
R_API char *execute_tool(RCore *core, const char *tool_name, const char *args, char **edited_command);
|
||||
R_API char *execute_tool(RCore *core, const char *tool_name, const char *args, char **edited_command, char **comment_out);
|
||||
|
||||
/**
|
||||
* Send an HTTP POST request
|
||||
@@ -318,12 +343,14 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args);
|
||||
|
||||
R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args);
|
||||
|
||||
R_IPI void cmd_r2ai_c(RCorePluginSession *cps);
|
||||
R_IPI void cmd_r2ai_logs(RCorePluginSession *cps);
|
||||
R_IPI void cmd_r2ai_lr(RCorePluginSession *cps);
|
||||
|
||||
/**
|
||||
* Process messages through LLM and handle tool calls recursively
|
||||
*/
|
||||
R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, const char *system_prompt, int n_run);
|
||||
R_API void process_messages(RCorePluginSession *cps, RList *messages, const char *system_prompt, int n_run);
|
||||
|
||||
/**
|
||||
* Helper function to convert RJson to string
|
||||
@@ -335,5 +362,10 @@ R_API char *r_json_to_string(const RJson *json);
|
||||
*/
|
||||
R_API PJ *r_json_to_pj(const RJson *json, PJ *existing_pj);
|
||||
R_API void r2ai_cmd_q(RCorePluginSession *cps, const char *input);
|
||||
R_API char *find_prompt_file(RList *search_dirs, const char *name);
|
||||
R_API R2AIPrompt *parse_prompt_file(const char *filepath);
|
||||
R_API void r2aiprompt_free(R2AIPrompt *prompt);
|
||||
R_API char *r2ai_load_prompt_text(RCore *core, const char *name);
|
||||
R_API char *strip_command_comment(const char *input, char **comment_out);
|
||||
|
||||
#endif
|
||||
-1134
File diff suppressed because it is too large.
Load diff
+57
-55
@@ -105,8 +105,8 @@ R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args)
|
||||
|
||||
// Check for initial command output in messages
|
||||
char *init_output = NULL;
|
||||
if (args.messages && args.messages->messages) {
|
||||
RList *msgs = args.messages->messages;
|
||||
if (args.messages) {
|
||||
RList *msgs = args.messages;
|
||||
int n_msgs = r_list_length (msgs);
|
||||
for (int i = 0; i < n_msgs; i++) {
|
||||
R2AI_Message *msg = r_list_get_n (msgs, i);
|
||||
@@ -161,73 +161,75 @@ R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args)
|
||||
R_LOG_DEBUG ("Raw tool call detected: %s with args: %s", tool_name, tool_args);
|
||||
|
||||
// Find the tool in our tools list
|
||||
const R2AI_Tools *tools = r2ai_get_tools ();
|
||||
RList *tools = r2ai_get_tools (cps->core, cps->data);
|
||||
bool tool_found = false;
|
||||
|
||||
for (int i = 0; i < tools->n_tools; i++) {
|
||||
if (tools->tools[i].name && !strcmp (tools->tools[i].name, tool_name)) {
|
||||
tool_found = true;
|
||||
break;
|
||||
if (tools) {
|
||||
RListIter *iter;
|
||||
R2AI_Tool *tool;
|
||||
r_list_foreach (tools, iter, tool) {
|
||||
if (tool->name && !strcmp (tool->name, tool_name)) {
|
||||
tool_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tool_found) {
|
||||
// Modify the existing response to include tool call information
|
||||
R2AI_Message *modified_message = R_NEW0 (R2AI_Message);
|
||||
if (modified_message) {
|
||||
modified_message->role = strdup ("assistant");
|
||||
// Remove the TOOL line from content, or set to empty if that's all there is
|
||||
char *content = strdup (response->message->content? response->message->content: "");
|
||||
char *tool_line = strstr (content, "TOOL: ");
|
||||
if (tool_line) {
|
||||
// Find the end of the line
|
||||
char *line_end = strchr (tool_line, '\n');
|
||||
if (line_end) {
|
||||
// Remove the TOOL line
|
||||
memmove (tool_line, line_end + 1, strlen (line_end + 1) + 1);
|
||||
} else {
|
||||
// The TOOL line is the entire content
|
||||
*tool_line = '\0';
|
||||
}
|
||||
modified_message->role = strdup ("assistant");
|
||||
// Remove the TOOL line from content, or set to empty if that's all there is
|
||||
char *content = strdup (response->message->content? response->message->content: "");
|
||||
char *tool_line = strstr (content, "TOOL: ");
|
||||
if (tool_line) {
|
||||
// Find the end of the line
|
||||
char *line_end = strchr (tool_line, '\n');
|
||||
if (line_end) {
|
||||
// Remove the TOOL line
|
||||
memmove (tool_line, line_end + 1, strlen (line_end + 1) + 1);
|
||||
} else {
|
||||
// The TOOL line is the entire content
|
||||
*tool_line = '\0';
|
||||
}
|
||||
modified_message->content = content;
|
||||
|
||||
// Set up proper tool call structure
|
||||
modified_message->tool_calls = R_NEWS0 (R2AI_ToolCall, 1);
|
||||
if (modified_message->tool_calls) {
|
||||
modified_message->tool_calls[0].name = tool_name;
|
||||
modified_message->tool_calls[0].arguments = tool_args;
|
||||
char id_buf[32];
|
||||
snprintf (id_buf, sizeof (id_buf), "rawtool_%d", (int)time (NULL));
|
||||
modified_message->tool_calls[0].id = strdup (id_buf);
|
||||
modified_message->n_tool_calls = 1;
|
||||
}
|
||||
tool_name = NULL;
|
||||
tool_args = NULL;
|
||||
|
||||
// Replace the response message
|
||||
if (response->message) {
|
||||
r2ai_message_free ((R2AI_Message *)response->message);
|
||||
}
|
||||
*(R2AI_Message **)&response->message = modified_message;
|
||||
}
|
||||
modified_message->content = content;
|
||||
|
||||
// Set up proper tool call structure
|
||||
modified_message->tool_calls = r_list_new ();
|
||||
if (modified_message->tool_calls) {
|
||||
modified_message->tool_calls->free = (RListFree)r2ai_tool_call_free;
|
||||
R2AI_ToolCall *tc = R_NEW0 (R2AI_ToolCall);
|
||||
tc->name = tool_name;
|
||||
tc->arguments = tool_args;
|
||||
char id_buf[32];
|
||||
snprintf (id_buf, sizeof (id_buf), "rawtool_%d", (int)time (NULL));
|
||||
tc->id = strdup (id_buf);
|
||||
r_list_append (modified_message->tool_calls, tc);
|
||||
}
|
||||
tool_name = NULL;
|
||||
tool_args = NULL;
|
||||
|
||||
// Replace the response message
|
||||
if (response->message) {
|
||||
r2ai_message_free ((R2AI_Message *)response->message);
|
||||
}
|
||||
*(R2AI_Message **)&response->message = modified_message;
|
||||
} else {
|
||||
// Unknown tool, modify content to indicate error
|
||||
R2AI_Message *modified_message = R_NEW0 (R2AI_Message);
|
||||
if (modified_message) {
|
||||
modified_message->role = strdup ("assistant");
|
||||
size_t result_len = strlen ("Unknown tool: ") + strlen (tool_name) + 1;
|
||||
modified_message->content = malloc (result_len);
|
||||
if (modified_message->content) {
|
||||
snprintf (modified_message->content, result_len, "Unknown tool: %s", tool_name);
|
||||
}
|
||||
|
||||
// Replace the response message
|
||||
if (response->message) {
|
||||
r2ai_message_free ((R2AI_Message *)response->message);
|
||||
}
|
||||
*(R2AI_Message **)&response->message = modified_message;
|
||||
modified_message->role = strdup ("assistant");
|
||||
size_t result_len = strlen ("Unknown tool: ") + strlen (tool_name) + 1;
|
||||
modified_message->content = malloc (result_len);
|
||||
if (modified_message->content) {
|
||||
snprintf (modified_message->content, result_len, "Unknown tool: %s", tool_name);
|
||||
}
|
||||
|
||||
// Replace the response message
|
||||
if (response->message) {
|
||||
r2ai_message_free ((R2AI_Message *)response->message);
|
||||
}
|
||||
*(R2AI_Message **)&response->message = modified_message;
|
||||
free (tool_name);
|
||||
free (tool_args);
|
||||
tool_name = NULL;
|
||||
|
||||
+213
-149
@@ -1,57 +1,83 @@
|
||||
#include <string.h>
|
||||
/* Copyright r2ai - 2023-2025 - pancake */
|
||||
|
||||
#include "r2ai.h"
|
||||
|
||||
R_API char *strip_command_comment(const char *input, char **comment_out) {
|
||||
const char *hash_pos = strchr (input, '#');
|
||||
char *stripped;
|
||||
if (hash_pos && (hash_pos == input || *(hash_pos - 1) != '\\')) {
|
||||
size_t cmd_len = hash_pos - input;
|
||||
stripped = r_str_ndup (input, cmd_len);
|
||||
r_str_trim (stripped);
|
||||
if (comment_out) {
|
||||
char *comment = strdup (hash_pos + 1); // skip #
|
||||
if (comment) {
|
||||
r_str_trim (comment);
|
||||
if (!*comment) {
|
||||
free (comment);
|
||||
comment = NULL;
|
||||
}
|
||||
}
|
||||
*comment_out = comment;
|
||||
}
|
||||
} else {
|
||||
stripped = strdup (input);
|
||||
if (comment_out) {
|
||||
*comment_out = NULL;
|
||||
}
|
||||
}
|
||||
return stripped;
|
||||
}
|
||||
|
||||
// Define the radare2 command tool
|
||||
static R2AI_Tool r2cmd_tool = {
|
||||
.name = "r2cmd",
|
||||
.description = "Run a radare2 command",
|
||||
.parameters = "{\
|
||||
\"type\": \"object\",\
|
||||
\"properties\": {\
|
||||
\"command\": {\
|
||||
\"type\": \"string\",\
|
||||
\"description\": \"The radare2 command to run\"\
|
||||
}\
|
||||
},\
|
||||
\"required\": [\"command\"]\
|
||||
}"
|
||||
.parameters = "{"
|
||||
"\"type\": \"object\","
|
||||
"\"properties\": {"
|
||||
"\"command\": {"
|
||||
"\"type\": \"string\","
|
||||
"\"description\": \"The radare2 command to run\""
|
||||
"}"
|
||||
"},"
|
||||
"\"required\": [\"command\"]"
|
||||
"}"
|
||||
};
|
||||
|
||||
static R2AI_Tool qjs_tool = {
|
||||
.name = "execute_js",
|
||||
.description = "Execute a JavaScript script inside the radare2 environment. Use `var RESULT=r2.cmd(COMMAND)` to execute radare2 commands and `r2.log(MESSAGE)` instead of console.log`",
|
||||
.parameters = "{\
|
||||
\"type\": \"object\",\
|
||||
\"properties\": {\
|
||||
\"script\": {\
|
||||
\"type\": \"string\",\
|
||||
\"description\": \"The JavaScript script to execute\"\
|
||||
}\
|
||||
},\
|
||||
\"required\": [\"script\"]\
|
||||
}"
|
||||
.parameters = "{"
|
||||
"\"type\": \"object\","
|
||||
"\"properties\": {"
|
||||
"\"script\": {"
|
||||
"\"type\": \"string\","
|
||||
"\"description\": \"The JavaScript script to execute\""
|
||||
"}"
|
||||
"},"
|
||||
"\"required\": [\"script\"]"
|
||||
"}"
|
||||
};
|
||||
|
||||
// Create a global tools structure with our tools
|
||||
static R2AI_Tools r2ai_tools_instance = {
|
||||
.tools = NULL, // Will initialize below
|
||||
.n_tools = 2
|
||||
};
|
||||
|
||||
// Function to get the global tools instance
|
||||
R_API const R2AI_Tools *r2ai_get_tools(void) {
|
||||
// Initialize tools array if not done yet
|
||||
if (!r2ai_tools_instance.tools) {
|
||||
static R2AI_Tool tools_array[2];
|
||||
tools_array[0] = r2cmd_tool;
|
||||
tools_array[1] = qjs_tool;
|
||||
r2ai_tools_instance.tools = tools_array;
|
||||
// Function to get the tools instance from state
|
||||
R_API RList *r2ai_get_tools(RCore *core, R2AI_State *state) {
|
||||
if (!state) {
|
||||
return NULL;
|
||||
}
|
||||
return &r2ai_tools_instance;
|
||||
if (!state->tools) {
|
||||
state->tools = r_list_newf (NULL);
|
||||
r_list_append (state->tools, (void *)&r2cmd_tool);
|
||||
// Only add execute_js tool if enabled in config
|
||||
if (r_config_get_b (core->config, "r2ai.auto.usejs")) {
|
||||
r_list_append (state->tools, (void *)&qjs_tool);
|
||||
}
|
||||
}
|
||||
return state->tools;
|
||||
}
|
||||
|
||||
// Function to parse input tools_json (OpenAI format) into R2AI_Tool array
|
||||
R_API R2AI_Tools *r2ai_tools_parse(const char *tools_json) {
|
||||
// Function to parse input tools_json (OpenAI format) into R2AI_Tool list
|
||||
R_API RList *r2ai_tools_parse(const char *tools_json) {
|
||||
if (!tools_json) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -63,19 +89,14 @@ R_API R2AI_Tools *r2ai_tools_parse(const char *tools_json) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
R2AI_Tools *tools = R_NEW0 (R2AI_Tools);
|
||||
|
||||
int n_tools = json->children.count;
|
||||
tools->tools = R_NEWS0 (R2AI_Tool, n_tools);
|
||||
if (!tools->tools) {
|
||||
free (tools);
|
||||
RList *tools = r_list_new ();
|
||||
if (!tools) {
|
||||
r_json_free (json);
|
||||
return NULL;
|
||||
}
|
||||
tools->n_tools = n_tools;
|
||||
tools->free = (RListFree)free; // Tools are dynamically allocated
|
||||
|
||||
int valid_tools = 0;
|
||||
for (int i = 0; i < n_tools; i++) {
|
||||
for (size_t i = 0; i < json->children.count; i++) {
|
||||
const RJson *tool_json = r_json_item (json, i);
|
||||
if (!tool_json || tool_json->type != R_JSON_OBJECT) {
|
||||
continue;
|
||||
@@ -99,7 +120,7 @@ R_API R2AI_Tools *r2ai_tools_parse(const char *tools_json) {
|
||||
continue;
|
||||
}
|
||||
|
||||
R2AI_Tool *tool = &tools->tools[valid_tools++];
|
||||
R2AI_Tool *tool = R_NEW0 (R2AI_Tool);
|
||||
tool->name = strdup (name->str_value);
|
||||
|
||||
if (description && description->type == R_JSON_STRING) {
|
||||
@@ -107,34 +128,25 @@ R_API R2AI_Tools *r2ai_tools_parse(const char *tools_json) {
|
||||
}
|
||||
|
||||
if (parameters) {
|
||||
// Just pass through the JSON as a string
|
||||
if (parameters->type == R_JSON_STRING) {
|
||||
tool->parameters = strdup (parameters->str_value);
|
||||
// Convert parameters to JSON string
|
||||
char *params_str = r_json_to_string (parameters);
|
||||
if (params_str) {
|
||||
tool->parameters = params_str; // r_json_to_string returns allocated string
|
||||
} else {
|
||||
// Use pj_raw to pass through any other raw JSON
|
||||
PJ *pj = pj_new ();
|
||||
if (pj) {
|
||||
pj_raw (pj, "{}");
|
||||
char *params_str = pj_drain (pj);
|
||||
if (params_str) {
|
||||
tool->parameters = strdup (params_str);
|
||||
free (params_str);
|
||||
}
|
||||
}
|
||||
tool->parameters = strdup ("{}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update count of valid tools
|
||||
tools->n_tools = valid_tools;
|
||||
r_list_append (tools, tool);
|
||||
}
|
||||
|
||||
r_json_free (json);
|
||||
return tools;
|
||||
}
|
||||
|
||||
// Function to convert R2AI_Tools to OpenAI format JSON
|
||||
R_API char *r2ai_tools_to_openai_json(const R2AI_Tools *tools) {
|
||||
if (!tools || tools->n_tools <= 0) {
|
||||
R_API char *r2ai_tools_to_openai_json(const RList *tools) {
|
||||
if (!tools || r_list_length (tools) <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -143,48 +155,51 @@ R_API char *r2ai_tools_to_openai_json(const R2AI_Tools *tools) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pj_a (pj); // Start array
|
||||
pj_a (pj);
|
||||
|
||||
for (int i = 0; i < tools->n_tools; i++) {
|
||||
const R2AI_Tool *tool = &tools->tools[i];
|
||||
RListIter *iter;
|
||||
R2AI_Tool *tool;
|
||||
int a = 0;
|
||||
r_list_foreach (tools, iter, tool) {
|
||||
if (a > 0) {
|
||||
#if R2_VERSION_NUMBER < 60005
|
||||
// XXX this is a bug
|
||||
// pj_raw (pj, ",");
|
||||
#endif
|
||||
}
|
||||
a++;
|
||||
if (!tool->name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pj_o (pj); // Start tool object
|
||||
pj_ks (pj, "type", "function");
|
||||
|
||||
pj_k (pj, "function");
|
||||
pj_o (pj); // Start function object
|
||||
|
||||
pj_ko (pj, "function");
|
||||
pj_ks (pj, "name", tool->name);
|
||||
|
||||
if (tool->description) {
|
||||
pj_ks (pj, "description", tool->description);
|
||||
}
|
||||
|
||||
if (tool->parameters) {
|
||||
pj_k (pj, "parameters");
|
||||
pj_raw (pj, tool->parameters);
|
||||
}
|
||||
|
||||
pj_end (pj); // End function object
|
||||
pj_end (pj); // End tool object
|
||||
if (i < tools->n_tools - 1) {
|
||||
pj_raw (pj, ",");
|
||||
}
|
||||
#if R2_VERSION_NUMBER < 60005
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
|
||||
pj_end (pj); // End array
|
||||
pj_end (pj);
|
||||
|
||||
char *result = pj_drain (pj);
|
||||
// eprintf ("((((((((((((((((((\n\n%s\n\n)))))))))))))\n", result);
|
||||
R_LOG_DEBUG ("OpenAI tools JSON: %s", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Function to convert R2AI_Tools to Anthropic format JSON
|
||||
R_API char *r2ai_tools_to_anthropic_json(const R2AI_Tools *tools) {
|
||||
if (!tools || tools->n_tools <= 0) {
|
||||
R_API char *r2ai_tools_to_anthropic_json(const RList *tools) {
|
||||
if (!tools || r_list_length (tools) <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -195,29 +210,22 @@ R_API char *r2ai_tools_to_anthropic_json(const R2AI_Tools *tools) {
|
||||
|
||||
pj_a (pj); // Start array
|
||||
|
||||
for (int i = 0; i < tools->n_tools; i++) {
|
||||
const R2AI_Tool *tool = &tools->tools[i];
|
||||
RListIter *iter;
|
||||
R2AI_Tool *tool;
|
||||
r_list_foreach (tools, iter, tool) {
|
||||
if (!tool->name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pj_o (pj); // Start tool object
|
||||
|
||||
pj_ks (pj, "name", tool->name);
|
||||
|
||||
if (tool->description) {
|
||||
pj_ks (pj, "description", tool->description);
|
||||
}
|
||||
|
||||
if (tool->parameters) {
|
||||
pj_k (pj, "input_schema");
|
||||
pj_raw (pj, tool->parameters);
|
||||
}
|
||||
|
||||
pj_end (pj); // End tool object
|
||||
if (i < tools->n_tools - 1) {
|
||||
pj_raw (pj, ",");
|
||||
}
|
||||
}
|
||||
|
||||
pj_end (pj); // End array
|
||||
@@ -227,22 +235,12 @@ R_API char *r2ai_tools_to_anthropic_json(const R2AI_Tools *tools) {
|
||||
}
|
||||
|
||||
// Function to free a tools structure
|
||||
R_API void r2ai_tools_free(R2AI_Tools *tools) {
|
||||
R_API void r2ai_tools_free(RList *tools) {
|
||||
if (!tools) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tools->tools) {
|
||||
for (int i = 0; i < tools->n_tools; i++) {
|
||||
R2AI_Tool *tool = &tools->tools[i];
|
||||
R_FREE (tool->name);
|
||||
R_FREE (tool->description);
|
||||
R_FREE (tool->parameters);
|
||||
}
|
||||
R_FREE (tools->tools);
|
||||
}
|
||||
|
||||
R_FREE (tools);
|
||||
r_list_free (tools);
|
||||
}
|
||||
|
||||
static char *to_cmd(const char *command) {
|
||||
@@ -258,7 +256,17 @@ static char *to_cmd(const char *command) {
|
||||
return pj_drain (pj);
|
||||
}
|
||||
|
||||
R_API char *r2ai_r2cmd(RCore *core, RJson *args, bool hide_tool_output, char **edited_command) {
|
||||
static char *compose_command_with_comment(const char *command, const char *comment) {
|
||||
if (!command) {
|
||||
return NULL;
|
||||
}
|
||||
if (!comment || !*comment) {
|
||||
return strdup (command);
|
||||
}
|
||||
return r_str_newf ("%s # %s", command, comment);
|
||||
}
|
||||
|
||||
R_API char *r2ai_r2cmd(RCore *core, RJson *args, bool hide_tool_output, char **edited_command, char **comment_out) {
|
||||
if (!args) {
|
||||
return strdup ("{ \"res\":\"Command is NULL\" }");
|
||||
}
|
||||
@@ -268,83 +276,130 @@ R_API char *r2ai_r2cmd(RCore *core, RJson *args, bool hide_tool_output, char **e
|
||||
return strdup ("{ \"res\":\"No command in tool call arguments\" }");
|
||||
}
|
||||
|
||||
const char *command = command_json->str_value;
|
||||
if (edited_command) {
|
||||
*edited_command = NULL;
|
||||
}
|
||||
|
||||
char *comment = NULL;
|
||||
char *command = strip_command_comment (command_json->str_value, &comment);
|
||||
if (!command) {
|
||||
free (comment);
|
||||
return strdup ("{ \"res\":\"Command is NULL\" }");
|
||||
}
|
||||
|
||||
if (r_str_startswith (command, "r2 ")) {
|
||||
free (command);
|
||||
free (comment);
|
||||
if (comment_out) {
|
||||
*comment_out = NULL;
|
||||
}
|
||||
return strdup ("{ \"res\":\"You are already in r2!\" }");
|
||||
}
|
||||
|
||||
bool ask_to_execute = r_config_get_b (core->config, "r2ai.auto.yolo") != true;
|
||||
*edited_command = NULL; // keeps track of the command that was really executed in case user modifies it
|
||||
|
||||
if (ask_to_execute) {
|
||||
// Check if command contains newlines to determine if it's multi-line
|
||||
bool is_multiline = strchr (command, '\n') != NULL;
|
||||
char *input_command = compose_command_with_comment (command, comment);
|
||||
if (!input_command) {
|
||||
free (command);
|
||||
free (comment);
|
||||
return strdup ("{ \"res\":\"Failed to prepare command\" }");
|
||||
}
|
||||
|
||||
if (is_multiline) {
|
||||
// Use editor for multi-line commands
|
||||
*edited_command = strdup (command);
|
||||
r_cons_editor (core->cons, NULL, *edited_command);
|
||||
command = *edited_command;
|
||||
r_cons_editor (core->cons, NULL, input_command);
|
||||
} else {
|
||||
// For single-line commands, push the command to input buffer
|
||||
r_cons_newline (core->cons);
|
||||
// Push the command to the input buffer
|
||||
|
||||
// Get user input with command pre-filled
|
||||
r_cons_readpush (core->cons, command, strlen (command));
|
||||
r_cons_readpush (core->cons, "\x05", 1); // Ctrl+E - move to end
|
||||
r_cons_readpush (core->cons, input_command, strlen (input_command));
|
||||
r_cons_readpush (core->cons, "\x05", 1);
|
||||
r_line_set_prompt (core->cons->line, "[r2ai]> ");
|
||||
const char *readline_result = r_line_readline (core->cons);
|
||||
// Check if interrupted or ESC pressed (readline_result is NULL or empty)
|
||||
if (r_cons_is_breaked (core->cons) || R_STR_ISEMPTY (readline_result)) {
|
||||
R_LOG_INFO ("Command execution cancelled %s", readline_result);
|
||||
free (input_command);
|
||||
free (command);
|
||||
free (comment);
|
||||
if (comment_out) {
|
||||
*comment_out = NULL;
|
||||
}
|
||||
return strdup ("R2AI_SIGINT");
|
||||
}
|
||||
|
||||
// Process the result
|
||||
if (R_STR_ISNOTEMPTY (readline_result)) {
|
||||
*edited_command = strdup (readline_result);
|
||||
command = *edited_command;
|
||||
} else {
|
||||
// If user just pressed enter, keep the original command
|
||||
*edited_command = strdup (command);
|
||||
command = *edited_command;
|
||||
free (input_command);
|
||||
input_command = strdup (readline_result);
|
||||
}
|
||||
}
|
||||
R_LOG_DEBUG ("Edited command: %s", *edited_command);
|
||||
} else {
|
||||
|
||||
char *new_comment = NULL;
|
||||
char *new_command = strip_command_comment (input_command, &new_comment);
|
||||
if (new_command) {
|
||||
free (command);
|
||||
command = new_command;
|
||||
free (comment);
|
||||
comment = new_comment;
|
||||
} else {
|
||||
free (new_comment);
|
||||
}
|
||||
free (input_command);
|
||||
}
|
||||
|
||||
if (edited_command) {
|
||||
*edited_command = strdup (command);
|
||||
}
|
||||
R_LOG_DEBUG ("Edited command: %s", command);
|
||||
|
||||
if (!hide_tool_output) {
|
||||
char *red_command = r_str_newf ("\x1b[31m%s\x1b[0m\n", *edited_command);
|
||||
r_cons_printf (core->cons, "%s", red_command);
|
||||
r_cons_flush (core->cons);
|
||||
free (red_command);
|
||||
char *display_command = compose_command_with_comment (command, comment);
|
||||
if (display_command) {
|
||||
char *red_command = r_str_newf (Color_RED "%s" Color_RESET "\n", display_command);
|
||||
r_cons_printf (core->cons, "%s", red_command);
|
||||
r_cons_flush (core->cons);
|
||||
free (red_command);
|
||||
free (display_command);
|
||||
}
|
||||
}
|
||||
|
||||
char *json_cmd = to_cmd (*edited_command);
|
||||
char *json_cmd = to_cmd (command);
|
||||
if (!json_cmd) {
|
||||
// caller should free edited_command
|
||||
free (command);
|
||||
free (comment);
|
||||
if (comment_out) {
|
||||
*comment_out = NULL;
|
||||
}
|
||||
return strdup ("{ \"res\":\"Failed to create JSON command\" }");
|
||||
}
|
||||
|
||||
char *cmd_output = r_core_cmd_str (core, json_cmd);
|
||||
free (json_cmd);
|
||||
free (command);
|
||||
|
||||
if (!cmd_output) {
|
||||
free (comment);
|
||||
if (comment_out) {
|
||||
*comment_out = NULL;
|
||||
}
|
||||
return strdup ("{ \"res\":\"Command returned no output or failed\" }");
|
||||
}
|
||||
|
||||
if (comment_out) {
|
||||
*comment_out = comment;
|
||||
} else {
|
||||
free (comment);
|
||||
}
|
||||
|
||||
return cmd_output;
|
||||
}
|
||||
|
||||
// qjs function implementation
|
||||
R_API char *r2ai_qjs(RCore *core, RJson *args, bool hide_tool_output) {
|
||||
R_API char *r2ai_qjs(RCore *core, RJson *args, bool hide_tool_output, char **edited_script_out) {
|
||||
if (!args) {
|
||||
return strdup ("{ \"res\":\"Script is NULL\" }");
|
||||
}
|
||||
|
||||
if (edited_script_out) {
|
||||
*edited_script_out = NULL;
|
||||
}
|
||||
|
||||
const RJson *script_json = r_json_get (args, "script");
|
||||
if (!script_json) {
|
||||
return strdup ("{ \"res\":\"No script field found in arguments\" }");
|
||||
@@ -372,6 +427,7 @@ R_API char *r2ai_qjs(RCore *core, RJson *args, bool hide_tool_output) {
|
||||
|
||||
r_cons_readpush (core->cons, script, strlen (script));
|
||||
r_cons_readpush (core->cons, "\x05", 1); // Ctrl+E - move to end
|
||||
r_line_set_prompt (core->cons->line, "[r2ai]> ");
|
||||
const char *readline_result = r_line_readline (core->cons);
|
||||
|
||||
// Check if interrupted or ESC pressed (readline_result is NULL or empty)
|
||||
@@ -392,6 +448,13 @@ R_API char *r2ai_qjs(RCore *core, RJson *args, bool hide_tool_output) {
|
||||
}
|
||||
}
|
||||
|
||||
if (edited_script_out) {
|
||||
char *dup_script = strdup (script);
|
||||
if (dup_script) {
|
||||
*edited_script_out = dup_script;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hide_tool_output) {
|
||||
char *print_script = r_str_newf ("\n```js\n%s\n```", script);
|
||||
char *print_script_rendered = r2ai_markdown (print_script);
|
||||
@@ -435,7 +498,7 @@ R_API char *r2ai_qjs(RCore *core, RJson *args, bool hide_tool_output) {
|
||||
return cmd_output;
|
||||
}
|
||||
|
||||
R_API char *execute_tool(RCore *core, const char *tool_name, const char *args, char **edited_command) {
|
||||
R_API char *execute_tool(RCore *core, const char *tool_name, const char *args, char **edited_command, char **comment_out) {
|
||||
if (!tool_name || !args) {
|
||||
return strdup ("{ \"res\":\"Tool name or arguments are NULL\" }");
|
||||
}
|
||||
@@ -456,11 +519,16 @@ R_API char *execute_tool(RCore *core, const char *tool_name, const char *args, c
|
||||
char *tool_result = NULL;
|
||||
|
||||
if (strcmp (tool_name, "r2cmd") == 0) {
|
||||
tool_result = r2ai_r2cmd (core, args_json, hide_tool_output, edited_command);
|
||||
} else if (strcmp (tool_name, "execute_js") == 0) {
|
||||
tool_result = r2ai_qjs (core, args_json, hide_tool_output);
|
||||
tool_result = r2ai_r2cmd (core, args_json, hide_tool_output, edited_command, comment_out);
|
||||
} else {
|
||||
tool_result = strdup ("{ \"res\":\"Unknown tool\" }");
|
||||
if (comment_out) {
|
||||
*comment_out = NULL;
|
||||
}
|
||||
if (strcmp (tool_name, "execute_js") == 0) {
|
||||
tool_result = r2ai_qjs (core, args_json, hide_tool_output, edited_command);
|
||||
} else {
|
||||
tool_result = strdup ("{ \"res\":\"Unknown tool\" }");
|
||||
}
|
||||
}
|
||||
|
||||
// Check for interruption after executing the tool
|
||||
@@ -481,9 +549,7 @@ R_API char *execute_tool(RCore *core, const char *tool_name, const char *args, c
|
||||
// Validate that the tool_result looks like valid JSON before parsing
|
||||
if (!r_str_startswith (tool_result, "{") || !strchr (tool_result, '}')) {
|
||||
// Not a JSON object, return as plain text
|
||||
result = strdup (tool_result);
|
||||
free (tool_result);
|
||||
return result;
|
||||
return tool_result;
|
||||
}
|
||||
|
||||
// Try to parse the JSON safely
|
||||
@@ -492,9 +558,7 @@ R_API char *execute_tool(RCore *core, const char *tool_name, const char *args, c
|
||||
if (!json) {
|
||||
// JSON parsing failed, return original content
|
||||
R_LOG_WARN ("Failed to parse JSON response from tool execution");
|
||||
result = strdup (tool_result);
|
||||
free (tool_result);
|
||||
return result;
|
||||
return tool_result;
|
||||
}
|
||||
|
||||
if (json) {
|
||||
|
||||
Reference in new issue
Block a user