Compare commits

...
96 Commits
Author SHA1 Message Date
pancake f06faba9d6 Release 1.2.6 2025-10-29 00:02:52 +01:00
pancake 8b52c2f6e2 Fix ninja build 2025-10-28 23:55:39 +01:00
pancake b37436fb8d Fix more warnings 2025-10-28 23:41:45 +01:00
pancake 2524e66825 Fix warning unused variable 2025-10-28 23:35:41 +01:00
pancake 519017a75e Fix editing r2js scripts in auto mode 2025-10-28 23:34:05 +01:00
pancake 6f30d6ca37 Handle comments in commands in all the auto calls 2025-10-28 20:49:24 +01:00
pancake 08ea4d11b2 Better last call 2025-10-28 19:51:22 +01:00
pancake 0281308434 Fix the build 2025-10-28 19:42:11 +01:00
pancake 17a59d2057 Add r2ai.debug to see the llm requests and responses 2025-10-28 19:37:01 +01:00
pancake eac0b8c6ef Prompt before leaving the interrupted auto mode 2025-10-28 19:30:11 +01:00
pancake 24aee426b4 Comments in commands are now used as annotations for the conversation 2025-10-28 19:28:05 +01:00
pancake b31bc5c116 Suggested simpler auto prompt 2025-10-28 19:24:40 +01:00
pancake 01738210f5 Increase timeout to 4 minutes 2025-10-28 19:13:05 +01:00
pancake 7b19c4ff99 Resizing the terminal is no longer interrupting the network calls 2025-10-28 19:10:50 +01:00
pancake 1986dd88bd r2ai needs an argument to work 2025-10-28 12:57:20 +01:00
pancake 1ed40401dd Implement r2ai -C to compact the message log 2025-10-28 12:55:46 +01:00
pancake 6ac0ff6352 Add compact prompt 2025-10-28 12:17:49 +01:00
pancake 4158e5f157 Add an option to disable the js_execute tool 2025-10-28 00:05:30 +01:00
pancake 225ff58228 Add r2ai.auto.think to disable reasoning 2025-10-28 00:03:34 +01:00
pancake a51be44c68 Improve native tool calling prompt 2025-10-28 00:00:04 +01:00
pancake 911e455f72 Only capture ONE tool call from the auto response 2025-10-27 23:40:03 +01:00
pancake 70eb975b7f Fix the set_prompt for auto mode 2025-10-27 23:21:58 +01:00
pancake a0d36cc909 Implement the -LR command to create a log report 2025-10-27 23:08:51 +01:00
pancake 030551816b Fix windows build 2025-10-27 23:08:51 +01:00
pancake 88c44c871b Fix tool calling, at least for OpenAI 2025-10-27 23:08:51 +01:00
pancake 9218e3ea27 Capture the thinking 2025-10-27 23:08:51 +01:00
pancake 6f7ffcae1a Oh great now we have a bug in the json serializer of r2 2025-10-27 23:08:51 +01:00
pancake fe12d2f8ee Add r2ai -b for the shell 2025-10-27 23:08:51 +01:00
pancake ae55b2f204 Proper use of RList -20LOC 2025-10-27 23:08:51 +01:00
pancake 7d4e949240 Use RList instead of hardcoded NEWS vectors 2025-10-27 23:08:51 +01:00
pancake 9b97d39c7a Add r2ai -b to change the baseurl 2025-10-27 19:04:16 +01:00
pancake 33f72a997e Refactorings to avoid hardcoded ansi colors. Using Color macros instead 2025-10-27 18:47:49 +01:00
pancake 36a8f13131 Use R2_CURL on Windows 2025-10-27 18:29:01 +01:00
pancake 5c2ded0171 Fix r2ai -E and loading script 2025-10-27 17:46:18 +01:00
pancake d5a39816a8 Use R2__WINDOWS__ instead of defined(_WIN32) 2025-10-27 13:02:36 +01:00
pancake 5aab38ab76 Proper command escapes for curl and pwsh http backends 2025-10-27 13:00:35 +01:00
pancake 9f15ec6c73 Release 1.2.4 2025-10-26 15:04:03 +01:00
pancake 68baac9911 Transform all promptfiles into the new md format 2025-10-26 15:02:48 +01:00
pancake 54a280952e Load API keys from ~/.config/r2ai/apikeys.txt too 2025-10-26 14:46:20 +01:00
pancake efc2368af1 Move the examples into the attic 2025-10-26 14:31:46 +01:00
pancake 28ddcbf638 Refactor and cleanup the http.backend implementations 2025-10-26 14:04:58 +01:00
pancake 85e9ed5c65 Do not accept invalid values in r2ai.http.backend 2025-10-25 19:56:10 +02:00
pancake 8886365925 Release 1.2.2 2025-10-25 11:03:12 +02:00
pancake 01d8e29420 Remove the r2-5.x compat macros 2025-10-25 11:02:39 +02:00
pancake e8b556ee5d Use r_sys_sleep and simplify the overengineered retry_sleep logic 2025-10-25 10:50:05 +02:00
pancake 6c336f48f7 Fix the meson build 2025-10-24 02:35:34 +02:00
pancake 53d61831b2 Initial support for vscode stype prompt files 2025-10-24 02:31:03 +02:00
pancake cbcff6b9dd Add r2ai -E in the cli 2025-10-24 02:29:34 +02:00
pancake f04bf8162e Add the manpage for r2ai.1 2025-10-24 02:04:36 +02:00
pancake 0206ceb390 Add support for r2ai -E and ~/.config/r2ai/rc 2025-10-24 02:00:15 +02:00
pancake ff1f788c48 Minor fix 2025-10-24 01:36:58 +02:00
pancake df135b2c70 Use jan-nano by default and fix the undefined in decai 2025-10-24 01:27:24 +02:00
pancake c152777bbe Dont do rawtools by default yet 2025-10-23 19:06:09 +02:00
pancake 3e084ce797 Implement r2ai.auto.raw to use tool calling with non-tools models 2025-10-23 02:12:36 +02:00
pancake 7fce8788b5 Release 1.2.0 2025-10-21 20:49:18 +02:00
pancake 585e30470e Aim to fix the execute_js tool for r2ai -a 2025-10-21 20:48:28 +02:00
pancake 5594e64ef3 r2ai-q must be public on windows 2025-10-21 20:44:07 +02:00
pancake 904a356290 Update READMEs 2025-10-21 20:40:17 +02:00
pancake d1e2bcf911 Use the powershell http fallback when needed 2025-10-21 20:38:10 +02:00
pancake 21528f7ea8 Use ollama/gpt-oss:20b as default provider/model pair 2025-10-21 20:34:58 +02:00
pancake 2883d134aa Add a WIP decompiler prompt 2025-10-21 20:18:58 +02:00
pancake ec96ba3329 Improve the behaviour of 'r2ai -m' to show, list and select models 2025-10-21 20:12:43 +02:00
pancake f92ea17b01 Move r2ai -n into r2ai -q autoname 2025-10-21 20:07:03 +02:00
pancake 2dc92031e7 Move r2ai -v into r2ai -q varnames 2025-10-21 20:03:16 +02:00
pancake 55d54b4953 Move r2ai -s into r2ai -q signature 2025-10-21 19:58:16 +02:00
pancake 35460e639c Move r2ai -x into r2ai -q explain 2025-10-21 19:49:47 +02:00
pancake a5382c17c4 Move r2ai -V to 'r2ai -q vulns' 2025-10-21 19:41:10 +02:00
pancake f4bdb847db Fix null deref when ^C in r2ai 2025-10-21 19:40:40 +02:00
pancake a7c3d3767c Use r2 -NNH instead of r2 -H to fix build when r2 have broken plugins 2025-10-21 19:29:09 +02:00
pancake 97c47ab28a Add -m, -p, -q to choose model, provider and query from the cli
* Fix the ollama provider
2025-10-21 19:24:11 +02:00
pancake c36bc1500c Dont pass R2AIState if we can pass the RCorePluginSession 2025-10-21 18:58:16 +02:00
pancake 179cb6ca58 Remove statistics global variable 2025-10-21 18:45:49 +02:00
pancake 402c31cc96 Handle -v and -h in the r2ai executable 2025-10-21 18:32:47 +02:00
pancake be14729424 Stop using the deprecated r_cons_singleton 2025-10-21 18:28:09 +02:00
pancake aa9b8e0fba Refactoring with the aim to reduce LOCs in r2ai_http.c 2025-10-21 18:24:09 +02:00
pancake 96225cd555 Update AGENTS.md 2025-10-21 17:45:39 +02:00
pancake 321ca1bb41 Move the LLM related functions from r2ai.c into llm.c 2025-10-21 17:29:14 +02:00
pancake d94ed5a088 Fix r2ai's main and use cps everywhere 2025-10-21 16:40:57 +02:00
pancake 3537d006de Added support to query custom user prompts in r2ai 2025-10-21 00:10:28 +02:00
pancake bdcee9e736 Remove global states, requires r2-6.0 at least 2025-10-20 21:54:48 +02:00
pancake 074b7a6c9f Add AGENTS.md for r2ai.c 2025-10-20 21:25:24 +02:00
pancake 6697df8437 Use RList instead of vector for messages. Fix message count in r2ai -L 2025-10-20 21:04:30 +02:00
pancake ff0fb4c802 Add version information in decai help message and use rcons 2025-10-20 19:49:40 +02:00
pancake ca7d02e03c Refactor the api/providers logic for easier maintainance 2025-10-20 17:35:43 +02:00
pancake bd105b2b0e Fix #176 - Do not accept invalid values in "r2ai -e api=" 2025-10-20 16:57:53 +02:00
pancake 5e18525403 Use clang-format-radare2 instead of the hacky indent.py 2025-10-20 16:48:42 +02:00
pancake c9d0756e74 Fallback to powershell when curl is not found on Windows 2025-10-20 11:05:20 +02:00
Abhi d3fc79f9d1 Fix tmp dir on android(termux) 2025-10-14 09:27:50 +02:00
pancake 8f4c972beb Remove pkg-config hard dependency for r2 >= 6.0.5 2025-10-08 13:51:34 +02:00
pancake f95cda42f7 Release 1.1.2 2025-10-06 23:25:27 +02:00
pancake b241f30432 Fix leak in openai_url 2025-10-06 23:25:01 +02:00
pancake 8eb50e466b Include the r2ai plugin in the meson ci builds 2025-10-06 23:22:01 +02:00
pancake 14b5dcbfb9 Fix make install and warnings 2025-10-06 22:59:22 +02:00
cryptax e264f8f0c8 More references In the README.md 2025-10-06 15:45:10 +02:00
pancake 46c2ce6c62 Fix r2ai install when old symlinks are around 2025-10-03 22:34:36 +02:00
pancake 693ad9270c Handle ollama errors 2025-10-03 18:21:35 +02:00
85 changed files with 3971 additions and 2903 deletions

No files matched your search

+5 -2
View File
@@ -93,8 +93,11 @@ jobs:
shell: bash
run: |
mkdir -p release
cp -rf src/r2ai.dll src/r2ai.exe src/r2ai src/r2ai.dylib src/r2ai.so release/ || true
cp -rf src/build/r2ai src/build/r2ai.dylib src/build/r2ai.so release/ || true
# Collect common binary artefact names produced by Make/Meson
# Include both "r2ai.*" and "libr2ai.*" variants for DLL/dylib/so on all platforms.
find src -type f \( -iname "r2ai.dll" -o -iname "libr2ai.dll" -o -iname "r2ai.dylib" -o -iname "libr2ai.dylib" -o -iname "r2ai.so" -o -iname "libr2ai.so" -o -iname "r2ai.exe" \) -exec cp -f {} release/ \; || true
# Also check top-level build output directories
find . -maxdepth 3 -type f \( -iname "r2ai.dll" -o -iname "libr2ai.dll" -o -iname "r2ai.dylib" -o -iname "libr2ai.dylib" -o -iname "r2ai.so" -o -iname "libr2ai.so" -o -iname "r2ai.exe" \) -exec cp -f {} release/ \; || true
cp -f decai/decai.r2.js release/ || true
if command -v zip >/dev/null 2>&1; then
zip -j "release/r2ai-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.build_system }}.zip" release/*
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
View File
@@ -1 +0,0 @@
recursive-include r2ai/ui *.tcss
+3 -1
View File
@@ -1,4 +1,6 @@
TARGETS=all clean mrproper user-install install user-uninstall uninstall
TARGETS=all clean mrproper user-install install user-uninstall uninstall fmt indent
$(TARGETS):
$(MAKE) -C src $@
.PHONY: $(TARGETS)
+22 -10
View File
@@ -19,8 +19,8 @@
This repository contains two plugins for radare2:
* **r2ai** - native plugin for radare2
* **decai** - r2js plugin with special focus on decompilation
* **r2ai** - native [AI plugin](./src/README.md) for radare2
* **decai** - r2js plugin with special [focus on decompilation](./decai/README.md)
If you are looking to use radare2 with other agents via MCP:
@@ -37,12 +37,21 @@ If you are looking to use radare2 with other agents via MCP:
* RAG markdown, code or textfiles using its native vector database
* Embed the output of an r2 command and resolve questions on the given data
## Practical Examples
## User defined Prompts
* Enhanced decompilation with `r2ai -d`
* Autoname functions with `r2ai -n`
* Explain function with `r2ai -x`
* Find vulnerabilities with `r2ai -V`
```console
[0x00000000]> r2ai -q
explain: Explain the current function -
devices: Find and explain devices used -
libs: Group imports by Libraries -
varnames: Better variable names -
autoname: Automatically suggest a better name for this function -
vulns: Find vulnerabilities or bugs in the current function -
signature: Suggest an improved function signature -
dlopen: List libraries loaded with dlopen - Some libraries are loaded
decompile: Augmented decompilation based on LLM -
[0x00000000]>
```
## Installation
@@ -85,12 +94,15 @@ r2ai -e model=claude-3-7-sonnet-20250219
r2ai -e max_tokens=64000
```
## Documentation
## Further Reading
* There's [a chapter](https://book.rada.re/plugins/r2ai.html) in the official r2book
* Cryptax on [lmstudio+gptoss](https://cryptax.medium.com/r2ai-with-lmstudio-and-gpt-oss-08efa5ea2476) blog post
* Malware analysis [with r2ai](https://github.com/cryptax/talks/blob/master/BSidesKristiansand-2025/r2ai.pdf) by Cryptax
* Malware analysis [with r2ai](https://arxiv.org/pdf/2504.07574) by Cryptax and Daniel Nakov
* Analysis of [Linux/Trigona ransomware](https://cryptax.medium.com/linux-trigona-analysis-with-r2ai-3e2bd1815e52), [Linux/Prometei botnet](https://cryptax.medium.com/reversing-a-prometei-botnet-binary-with-r2-and-ai-part-one-3cdb3dc6ffab) and [W32/SkyAI](https://cryptax.medium.com/w32-skyai-uses-ai-so-do-i-d33f04d63534with) with r2ai
## Videos
- https://infosec.exchange/@radareorg/111946255058894583
- [Solving a crackme](https://infosec.exchange/@radareorg/111946255058894583)
- [De-obfuscation of malware Linux/Ladvix](https://asciinema.org/a/724126)
- [Analysis of the /fast option inside Linux/Trigona ransomware](https://asciinema.org/a/pBPEaJhp6cunWSKFpBUDTgPt4)
+1 -1
View File
@@ -1 +1 @@
1.1.1
1.2.6
+3 -1
View File
@@ -1,3 +1,5 @@
#!/bin/sh
vim src/r2ai.c
vim decai/decai.r2.js
vim src/r2ai.h
vim src/meson.build
vim VERSION
+18 -6
View File
@@ -1,5 +1,6 @@
(function () {
// Constants and configuration
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";
@@ -727,8 +728,9 @@ Use radare2 to resolve user requests.
"x-api-key: " + key[0],
];
let res = {};
try {
const res = http.post(
res = http.post(
provider.defaultBaseurl + "/v1/messages",
headers,
JSON.stringify(payload),
@@ -765,13 +767,20 @@ Use radare2 to resolve user requests.
try {
const res = http.post(url, [], JSON.stringify(payload));
return utils.filterResponse(res.message.content);
} catch (e) {
if (res && res.error) {
throw new Error(res.error);
}
if (res.message && res.message.content) {
return utils.filterResponse(res.message.content);
}
throw new Error(JSON.stringify(res));
if (res.error && res.error.indexOf("try pulling")) {
const modelName = res.error.split(/"/g)[1];
res.error += "\n!ollama run " + modelName;
}
return "ERROR: " + (res.error || e.message);
} catch (e) {
console.error(e.stack);
return "ERROR: " + e.message;
}
},
@@ -866,8 +875,10 @@ Use radare2 to resolve user requests.
// Command handlers
const commands = {
help: () => {
const msg = (m) => console.error(" " + COMMAND + " " + m);
console.error("Usage: " + COMMAND + " (-h) ...");
let helpmsg = "";
const msg = (m) => helpmsg += " " + COMMAND + " " + m + "\n";
helpmsg += "Usage: " + COMMAND + " (-h) ...\n";
helpmsg += "Version: " + VERSION + "\n";
msg("-a [query] - solve query with auto mode");
msg("-d [f1 ..] - decompile given functions");
msg("-dd [..] - same as above, but ignoring cache");
@@ -888,6 +899,7 @@ Use radare2 to resolve user requests.
msg("-v - show local variables");
msg("-V - find vulnerabilities");
msg("-x[*] - eXplain current function (-x* for r2 script)");
r2.log(helpmsg.trim());
},
decompile: (args, extraQuery, useCache, recursiveCalls) => {
+43
View File
@@ -0,0 +1,43 @@
# R2AI Prompts
This directory contains predefined prompt files for the r2ai plugin.
## Format
Each prompt is stored in a `.r2ai` file using a simple Key: Value format.
Lines starting with `#` are comments and ignored.
Supported keys:
- `Title`: The title of the prompt (required)
- `Author`: The author of the prompt
- `Description`: A brief description
- `Command` or `Commands`: Commands to execute before sending the prompt, separated by `;` (required)
- `Prompt` or `Query`: The main prompt text sent to the LLM
- `Requires`: Requirements for the prompt (e.g., "analysis" to ensure binary analysis)
- `If-Empty`: Alternative prompt or message if the command output is empty
- `If-Command`: Additional commands to run if the main command produces output
## Variable Substitution
In the `Prompt` field, you can use:
- `${VAR}`: Replaced with the value of the environment variable `VAR`
- `$(cmd)`: Replaced with the output of the r2 command `cmd`
## Usage
- `r2ai -q`: List all available prompts
- `r2ai -q name`: Run the prompt named `name`
- `r2ai -q name extra text`: Run the prompt with additional text appended
## Conditionals
- `If-Empty`: If the command output is empty, use this as the prompt instead
- `If-Command`: If the command produces output, run these additional commands and append their output
## Future Features
- Support for r2js scripts
- More advanced conditionals
+8
View File
@@ -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:
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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
+8
View File
@@ -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
+8
View File
@@ -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)
+8
View File
@@ -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
+8
View File
@@ -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
+10
View File
@@ -0,0 +1,10 @@
---
description: 'Decompile this code into PHP'
version: 0.1
author: pancake
command: pdg
if-empty: exit
---
Your goal is to $(-e r2ai.prompt).
Translate the code into $(-e r2ai.lang) programming language.
+8
View File
@@ -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
+8
View File
@@ -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.
-40
View File
@@ -1,40 +0,0 @@
BasedOnStyle: LLVM
Language: Cpp
PointerAlignment: Right
AlwaysBreakAfterDefinitionReturnType: None
BinPackParameters: true
MaxEmptyLinesToKeep: 1
SpaceInEmptyParentheses: false
SpacesInContainerLiterals: true
SpaceBeforeParens: Custom
SpaceBeforeParensOptions:
AfterIfMacros: true
AfterFunctionDefinitionName: false
AfterFunctionDeclarationName: false
AfterForeachMacros: true
AfterControlStatements: true
BeforeNonEmptyParentheses: false
SpacesInParentheses: false
InsertBraces: true
ContinuationIndentWidth: 8
IndentCaseLabels: false
IndentFunctionDeclarationAfterType: false
IndentWidth: 8
UseTab: ForContinuationAndIndentation
ColumnLimit: 0
BreakBeforeBraces: Attach
BreakBeforeTernaryOperators: true
SpaceBeforeSquareBrackets: false
SpaceInEmptyBlock: false
AllowShortIfStatementsOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: true
AllowShortFunctionsOnASingleLine: Inline
AllowShortLoopsOnASingleLine: false
AlignAfterOpenBracket: DontAlign
AlignEscapedNewlines: DontAlign
AlignConsecutiveMacros: true
AlignTrailingComments: false
AlignOperands: false
Cpp11BracedListStyle: false
ForEachMacros: ['r_list_foreach', 'ls_foreach', 'fcn_tree_foreach_intersect', 'r_skiplist_foreach', 'graph_foreach_anode', 'r_list_foreach_safe', 'r_pvector_foreach', 'r_rbtree_foreach', 'r_interval_ tree_foreach']
SortIncludes: false
+33
View File
@@ -0,0 +1,33 @@
# R2AI Plugin for radare2
## Locations
- All the source code of the plugin is in the current directory
- Radare2 headers use to be in /usr/local/include/libr
## 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
- Indent comments with spaces
- Do not define variables inside the `for` parenthesis
- Do not check for null before calling free
## Test
Run `r2ai` cli tool or radare2 oneliners running the `r2ai` command.
* `r2pm -r r2ai -h` -> help message for the r2ai tool
* `r2 -qc 'r2ai -h' /bin/ls -> help message of the r2ai plugin
* `r2 -qc 'r2ai -a what arch is used here' -> testing auto mode
* `r2 -e r2ai.model=gemma3:12b -e r2ai.auto.raw=true -qc 'r2ai -a what arch is used here' -> testing auto mode
## Actions
Run the following commands to perform the action described
- FORMAT: `make fmt`
- COMPILE: `make` (with no extra arguments)
- INSTALL: `make user-install`
+37 -12
View File
@@ -1,13 +1,21 @@
DOTEXE=
# DOTEXE=.exe
DOTLIB=.$(shell r2 -H R2_LIBEXT)
DOTLIB=.$(shell r2 -NNH R2_LIBEXT)
CFLAGS+=$(shell pkg-config --cflags r_core)
LDFLAGS+=$(shell pkg-config --libs r_core)
R2SYSPLUGDIR=$(shell r2 -H R2_LIBR_PLUGINS)
R2USRPLUGDIR=$(shell r2 -H R2_USER_PLUGINS)
R2SYSPLUGDIR=$(shell r2 -NNH R2_LIBR_PLUGINS)
R2USRPLUGDIR=$(shell r2 -NNH R2_USER_PLUGINS)
R2BINDIR=$(shell r2pm -H R2PM_BINDIR)
R2MANDIR=$(shell r2pm -H R2PM_MANDIR)
R2_CFLAGS=$(shell pkg-config --cflags r_core)
R2_LDFLAGS=$(shell pkg-config --libs r_core)
ifeq ($(R2_CFLAGS),)
R2_CFLAGS=$(shell r2 -NNH R2_CFLAGS)
R2_LDFLAGS=$(shell r2 -NNH R2_LDFLAGS)
endif
CFLAGS+=$(R2_CFLAGS)
LDFLAGS+=$(R2_LDFLAGS)
CFLAGS+=-I. -fPIC
CFLAGS+=-Wall
CFLAGS+=-g
@@ -31,14 +39,18 @@ ifeq ($(USE_LIBCURL),1)
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
OBJS+=rawtools.o
all: $(OBJS) r2check
$(CC) -fPIC -shared -o r2ai$(DOTLIB) $(OBJS) $(CFLAGS) $(LDFLAGS)
@@ -54,24 +66,37 @@ doc-install:
vdb.o: vdb_embed.inc.c
indent:
for a in *.c ; do python indent.py $$a ; done
fmt format indent:
clang-format-radare2 *.c http/*.c
user-install: user-uninstall
mkdir -p $(R2USRPLUGDIR)
cp -f r2ai$(DOTLIB) $(R2USRPLUGDIR)
cp -f r2ai$(DOTEXE) $(R2BINDIR)
mkdir -p $(R2BINDIR)
mkdir -p $(R2MANDIR)/man1
mkdir -p $(HOME)/.config/r2ai/prompts
$(MAKE) user-uninstall
cp -f r2ai$(DOTLIB) $(R2USRPLUGDIR)/r2ai$(DOTLIB)
cp -f r2ai$(DOTEXE) $(R2BINDIR)/r2ai$(DOTEXE)
cp -f r2ai.1 $(R2MANDIR)/man1/r2ai.1
cp -r ../prompts/* $(HOME)/.config/r2ai/prompts/
user-uninstall:
rm -f $(R2USRPLUGDIR)/r2ai$(DOTLIB)
rm -f $(R2BINDIR)/r2ai$(DOTEXE)
rm -f $(R2MANDIR)/man1/r2ai.1
install:
cp -f r2ai$(DOTLIB) $(R2SYSPLUGDIR)
$(MAKE) uninstall
mkdir -p $(R2USRPLUGDIR)
mkdir -p $(R2BINDIR)
cp -f r2ai$(DOTLIB) $(R2SYSPLUGDIR)/r2ai$(DOTLIB)
cp -f r2ai$(DOTEXE) $(R2BINDIR)/r2ai$(DOTEXE)
uninstall:
rm -f $(R2SYSPLUGDIR)/r2ai$(DOTLIB)
rm -f $(R2BINDIR)/r2ai$(DOTEXE)
r2check:
@r2 -qcq --
@r2 -NN -qcq --
clean:
rm -f *.o *.d
+40 -17
View File
@@ -1,12 +1,10 @@
# C-R2AI
# R2AI
This directory contains the C rewrite of the r2ai plugin for radare2.
The r2ai plugin for radare2.
## Installation
Requires radare2 from git if you want to use the stream HTTP APIs.
Otherwise the only requirement is a C compiler.
Requires a C compiler and modern version of radare2 if you want to use the stream HTTP APIs. It will fallback to curl or powershell if not available and `R2_CURL` environment is set to `1`.
```console
make
@@ -37,24 +35,53 @@ Usage: r2ai [-args] [...]
| 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 -e Same as '-e r2ai.'
| r2ai -e (k(=v)) Same as '-e r2ai.'
| 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 -n suggest a better name for the current function
| 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 -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)
| r2ai -s function signature
| r2ai -x explain current function
| r2ai -v suggest better variables names and types
| r2ai -V[r] find vulnerabilities in the decompiled code (-Vr uses -dr)
| r2ai [arg] send a post request to talk to r2ai and print the output
| r2ai [query] query the selected model+provider with the given query
[0x100003f58]>
```
## Query Prompts
Query prompts are predefined templates that combine radare2 commands with structured prompts to the LLM for common reverse engineering tasks. They are stored in the `prompts/` directory as `.r2ai.txt` files.
### Available Prompts
- `autoname`: Automatically rename functions and variables
- `decompile`: Decompile the current function
- `devices`: Analyze device-related code
- `dlopen`: Analyze dynamic library loading
- `explain`: Explain the purpose of the current function
- `libs`: Analyze library usage
- `signature`: Improve function signatures
- `varnames`: Rename variables based on context
- `vulns`: Find vulnerabilities in the current function
### Usage
- `r2ai -q`: List all available query prompts
- `r2ai -q [name]`: Run the specified prompt on the current function
- `r2ai -q [name] [instructions]`: Run the prompt with additional custom instructions
### Example
```console
[0x00001303]> r2ai -q explain
The main function implements a simple substitution cipher program that reads user input, encrypts it using a hardcoded alphabet, and then decrypts it back to demonstrate the algorithm.
```
Query prompts automate repetitive analysis tasks by executing predefined radare2 commands and sending context-aware prompts to the LLM.
## Configuration
These can be set with `r2ai -e <keyname>=<value>`
@@ -76,7 +103,6 @@ These can be set with `r2ai -e <keyname>=<value>`
| r2ai.http.use_files | If you get CURL argument too long error, set this to true and arguments will be stored in a file.|
## Example
- Get the [substitute example binary](https://github.com/radareorg/r2ai/tree/master/examples)
@@ -215,8 +241,6 @@ Note: System prompt is applied automatically but not stored in history
{"command":"pdf @ main"}
```
## TODO
* add "undo" command to drop the last message
@@ -225,7 +249,6 @@ Note: System prompt is applied automatically but not stored in history
* keep context between direct commands unless explicitly reset
* modify context if tool command is user edited
## Messages API
The r2ai project includes a new API for managing messages and tool calls in conversations with AI models. This API is implemented in `messages.c` and `messages.h`.
@@ -268,4 +291,4 @@ In r2ai, as lm-studio uses an OpenAI-like API, configure:
- `r2ai -e api=openai`
- `r2ai -e baseurl=http://LM-STUDIO-IP:PORT`
- `r2ai -e model=?` to list available models
- `r2ai -e model=?` to list available models
+49 -42
View File
@@ -1,14 +1,15 @@
#include "r2ai.h"
static const char *modelname(const char *model_name) {
return model_name ? model_name : "claude-3-7-sonnet-20250219";
return model_name? model_name: "claude-3-7-sonnet-20250219";
}
R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args) {
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;
@@ -40,7 +41,7 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args) {
// Create messages JSON
char *messages_json = NULL;
if (messages_input && messages_input->n_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) {
@@ -60,7 +61,7 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, 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);
}
@@ -98,10 +99,26 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args) {
free (messages_json);
// Save the full JSON for debugging
r_file_dump ("/tmp/r2ai_anthropic_request.json", (const ut8 *)data, strlen (data), 0);
R_LOG_DEBUG ("Full request saved to /tmp/r2ai_anthropic_request.json");
char *tmpdir = r_file_tmpdir ();
char *req_path = r_str_newf ("%s" R_SYS_DIR "r2ai_anthropic_request.json", tmpdir);
r_file_dump (req_path, (const ut8 *)data, strlen (data), 0);
R_LOG_DEBUG ("Full request saved to %s", req_path);
free (req_path);
free (tmpdir);
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);
@@ -120,19 +137,19 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args) {
}
// Save the response for inspection
r_file_dump ("/tmp/r2ai_anthropic_response.json", (const ut8 *)res, strlen (res), 0);
R_LOG_DEBUG ("Anthropic API response saved to /tmp/r2ai_anthropic_response.json");
R_LOG_DEBUG ("Anthropic API response: %s", res);
tmpdir = r_file_tmpdir ();
char *res_path = r_str_newf ("%s" R_SYS_DIR "r2ai_anthropic_response.json", tmpdir);
r_file_dump (res_path, (const ut8 *)res, strlen (res), 0);
R_LOG_DEBUG ("Anthropic API response saved to %s", res_path);
free (res_path);
free (tmpdir);
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;
@@ -184,9 +201,9 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, 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");
@@ -197,13 +214,13 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, 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);
@@ -211,22 +228,11 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, 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");
@@ -242,29 +248,30 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, 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");
@@ -281,11 +288,11 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, 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;
}
@@ -296,7 +303,7 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, 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;
}
+435 -178
View File
@@ -1,76 +1,62 @@
/* Copyright r2ai - 2023-2025 - pancake */
#include "r2ai.h"
#include <time.h>
// Forward declaration of the r2ai_llmcall function
extern R2AI_ChatResponse *r2ai_llmcall(RCore *core, R2AIArgs args);
// Add a global structure to track timing and costs
typedef struct {
double total_cost;
double run_cost;
int total_tokens;
int run_tokens;
int total_prompt_tokens;
int run_prompt_tokens;
int total_completion_tokens;
int run_completion_tokens;
time_t start_time;
time_t total_start_time;
} R2AIStats;
static R2AIStats stats = { 0 };
// Stats are now stored in R2AI_State->stats, no longer need global variable
// Helper function to format time duration
static char *format_time_duration(time_t seconds) {
if (seconds < 60) {
return r_str_newf ("%llds", (long long)seconds);
return r_str_newf ("%" PFMT64d "s", (ut64)seconds);
}
if (seconds < 3600) {
return r_str_newf ("%lldm%llds", (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 ("%lldh%lldm%llds",
(long long)(seconds / 3600),
(long long)((seconds % 3600) / 60),
(long long)(seconds % 60));
return r_str_newf ("%" PFMT64d "h%" PFMT64d "m%" PFMT64d "s",
(ut64) (seconds / 3600),
(ut64) ((seconds % 3600) / 60),
(ut64) (seconds % 60));
}
// Initialize timing and cost tracking for a run
static void r2ai_stats_init_run(int n_run) {
static void r2ai_stats_init_run(R2AI_State *state, int n_run) {
time_t run_start = time (NULL);
if (n_run == 1) {
// First run, initialize total timing
stats.total_cost = 0.0;
stats.run_cost = 0.0;
stats.total_start_time = run_start;
stats.total_tokens = 0;
stats.run_tokens = 0;
stats.total_prompt_tokens = 0;
stats.run_prompt_tokens = 0;
stats.total_completion_tokens = 0;
stats.run_completion_tokens = 0;
state->stats.total_cost = 0.0;
state->stats.run_cost = 0.0;
state->stats.total_start_time = run_start;
state->stats.total_tokens = 0;
state->stats.run_tokens = 0;
state->stats.total_prompt_tokens = 0;
state->stats.run_prompt_tokens = 0;
state->stats.total_completion_tokens = 0;
state->stats.run_completion_tokens = 0;
}
stats.start_time = run_start;
state->stats.start_time = run_start;
}
// Print a simple run indicator at the start
static void r2ai_print_run_end(RCore *core, const R2AI_Usage *usage, int n_run, int max_runs) {
static void r2ai_print_run_end(RCorePluginSession *cps, const R2AI_Usage *usage, int n_run, int max_runs) {
RCore *core = cps->core;
R2AI_State *state = cps->data;
(void)n_run;
(void)max_runs;
time_t run_time = time (NULL) - stats.start_time;
time_t total_time = time (NULL) - stats.total_start_time;
time_t run_time = time (NULL) - state->stats.start_time;
time_t total_time = time (NULL) - state->stats.total_start_time;
if (usage) {
stats.run_tokens = usage->total_tokens;
stats.run_prompt_tokens = usage->prompt_tokens;
stats.run_completion_tokens = usage->completion_tokens;
stats.total_tokens += usage->total_tokens;
stats.total_prompt_tokens += usage->prompt_tokens;
stats.total_completion_tokens += usage->completion_tokens;
state->stats.run_tokens = usage->total_tokens;
state->stats.run_prompt_tokens = usage->prompt_tokens;
state->stats.run_completion_tokens = usage->completion_tokens;
state->stats.total_tokens += usage->total_tokens;
state->stats.total_prompt_tokens += usage->prompt_tokens;
state->stats.total_completion_tokens += usage->completion_tokens;
}
if (r_config_get_b (core->config, "r2ai.chat.show_cost") == true) {
// TODO: calculate cost
stats.run_cost = 0.0 * run_time;
stats.total_cost += stats.run_cost;
state->stats.run_cost = 0.0 * run_time;
state->stats.total_cost += state->stats.run_cost;
}
// Format times for display
@@ -78,85 +64,160 @@ static void r2ai_print_run_end(RCore *core, const R2AI_Usage *usage, int n_run,
char *total_time_str = format_time_duration (total_time);
// Print detailed stats
R2_PRINTF ("\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"),
stats.total_tokens,
stats.total_prompt_tokens,
stats.total_completion_tokens,
stats.run_tokens,
stats.run_prompt_tokens,
stats.run_completion_tokens,
state->stats.total_tokens,
state->stats.total_prompt_tokens,
state->stats.total_completion_tokens,
state->stats.run_tokens,
state->stats.run_prompt_tokens,
state->stats.run_completion_tokens,
run_time_str,
total_time_str);
R2_NEWLINE ();
R2_FLUSH ();
r_cons_newline (core->cons);
r_cons_flush (core->cons);
free (run_time_str);
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(RCore *core, 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;
bool interrupted = false;
const int max_runs = r_config_get_i (core->config, "r2ai.auto.max_runs");
if (n_run > max_runs) {
R2_PRINTF ("\x1b[1;31m[r2ai] Max runs reached\x1b[0m\n");
R2_FLUSH ();
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) {
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);
if (cmd_output) {
system_prompt = r_str_newf ("%s\n\nHere is some information about the binary to get you started:\n>%s\n%s", Gprompt_auto, edited_command, cmd_output);
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 = content
};
r2ai_msgs_add (messages, &init_msg);
free (display_command);
free (cmd_output);
}
free (edited_command);
} else {
system_prompt = Gprompt_auto;
free (comment);
}
}
r2ai_stats_init_run (n_run);
r2ai_stats_init_run (state, 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
R2AIArgs args = {
.messages = messages,
.error = &error,
.dorag = true,
.tools = r2ai_get_tools (),
.system_prompt = system_prompt
.tools = r2ai_get_tools (core, cps->data), // Always send tools in auto mode
.system_prompt = effective_prompt
};
// Call r2ai_llmcall to get a response
R2AI_ChatResponse *response = r2ai_llmcall (core, args);
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;
}
@@ -166,21 +227,41 @@ R_API void process_messages(RCore *core, R2AI_Messages *messages, const char *sy
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 ("Model response - Role: %s", message->role? message->role: "null");
if (message->content) {
R_LOG_DEBUG ("Content: %s", message->content);
}
if (message->reasoning_content) {
R_LOG_DEBUG ("Reasoning: %s", message->reasoning_content);
}
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) {
R2_PRINTF ("\x1b[31m[Assistant]\x1b[0m\n\n");
r_cons_printf (core->cons, Color_RED "[Assistant]" Color_RESET);
if (message->reasoning_content) {
R2_PRINTF ("\x1b[90m<thinking>\n%s\n</thinking>\x1b[0m\n", message->reasoning_content);
R2_NEWLINE ();
R2_FLUSH ();
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);
}
if (message->content) {
R2_PRINTF ("%s", message->content);
R2_NEWLINE ();
R2_FLUSH ();
r_cons_printf (core->cons, "%s", message->content);
r_cons_newline (core->cons);
r_cons_flush (core->cons);
}
}
@@ -189,17 +270,21 @@ R_API void process_messages(RCore *core, R2AI_Messages *messages, const char *sy
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: %s", tool_call->name);
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);
@@ -211,54 +296,137 @@ R_API void process_messages(RCore *core, R2AI_Messages *messages, const char *sy
}
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);
// TODO: need to edit the R2AI_Messages* and modify the command of the last tool_use
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, 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
// 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;
}
}
}
}
free (edited_command);
}
free (tool_name);
free (tool_args);
if (strcmp (cmd_output, "R2AI_SIGINT") == 0) {
R2_PRINTF ("\n\n\x1b[1;31m[r2ai] Processing interrupted after tool execution\x1b[0m\n\n");
R2_FLUSH ();
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 = {
.role = "tool",
.tool_call_id = tool_call->id,
.tool_call_id = strdup (tool_call->id),
.content = cmd_output
};
// Add the tool response to our messages array
r2ai_msgs_add (messages, &tool_response);
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 (core, usage, n_run, max_runs);
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) {
process_messages (core, 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 ("Auto mode loop ending - no more tool calls or interrupted");
}
} else {
r2ai_print_run_end (core, usage, n_run, max_runs);
r2ai_print_run_end (cps, usage, n_run, max_runs);
}
// Free the response struct itself since r2ai_message_free doesn't do it anymore
free (response);
}
R_IPI void cmd_r2ai_a(RCore *core, const char *user_query) {
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, 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 ();
RList *messages = r2ai_conversation_get (state);
if (!messages) {
R_LOG_ERROR ("Conversation not initialized");
return;
@@ -266,51 +434,44 @@ R_IPI void cmd_r2ai_a(RCore *core, 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 (messages->n_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);
}
// Add user query
R2AI_Message user_msg = {
.role = "user",
.content = user_query
.content = (char *)user_query
};
r2ai_msgs_add (messages, &user_msg);
process_messages (core, 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') {
R2_PRINTF ("%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);
R2_PRINTF ("%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)
R2_PRINTF ("%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 {
R2_PRINTF ("%s\n", content);
r_cons_println (core->cons, content);
}
}
// Add this function right after cmd_r2ai_a
R_IPI void cmd_r2ai_logs(RCore *core) {
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 ();
if (!messages || messages->n_messages == 0) {
R2_PRINTF ("No conversation history available\n");
RList *messages = r2ai_conversation_get (state);
if (!messages || r_list_empty (messages)) {
r_cons_printf (core->cons, "No conversation history available\n");
return;
}
@@ -326,88 +487,184 @@ R_IPI void cmd_r2ai_logs(RCore *core) {
pj_a (pj);
for (int i = 0; i < messages->n_messages; i++) {
const R2AI_Message *msg = &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) {
pj_ks (pj, "role", msg->role? msg->role: "unknown");
pj_ks (pj, "content", msg->content? msg->content: "");
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);
}
if (tc->arguments) {
pj_ks (pj, "arguments", tc->arguments);
}
pj_end (pj);
}
pj_end (pj);
}
pj_end (pj);
}
pj_end (pj);
char *json_str = pj_drain (pj);
R2_PRINTF ("%s\n", json_str);
r_cons_printf (core->cons, "%s\n", json_str);
free (json_str);
return;
}
R2_PRINTF ("\x1b[1;34m[r2ai] Chat Logs (%d messages)\x1b[0m\n",
core->cons, messages->n_messages);
r_cons_printf (core->cons, "\x1b[1" Color_BLUE "[r2ai] Chat Logs (%d messages)" Color_RESET "\n",
r_list_length (messages));
R2_PRINTF ("\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 < messages->n_messages; i++) {
const R2AI_Message *msg = &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")) {
R2_PRINTF ("\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")) {
R2_PRINTF ("\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];
R2_PRINTF (" \x1b[1;35m[tool call]:\x1b[0m %s\n",
tc->name ? tc->name : "<unnamed>");
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) {
R2_PRINTF (" %s\n", tc->arguments);
r_cons_printf (core->cons, " %s\n", tc->arguments);
}
}
}
} else if (!strcmp (role, "tool")) {
R2_PRINTF ("\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.)
R2_PRINTF ("\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>");
}
R2_NEWLINE ();
R2_FLUSH ();
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
View File
@@ -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;
}
+99
View File
@@ -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;
}
+140
View File
@@ -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
+118
View File
@@ -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
+49
View File
@@ -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 };
}
-39
View File
@@ -1,39 +0,0 @@
import re
import sys
import subprocess
arg = sys.argv[1]
# Step 1: Format the file in-place using clang-format
subprocess.run(["clang-format", "-i", arg], check=True)
def is_function(s):
return s and not s[0].isspace()
def is_control_structure(s):
return s in {"if", "for", "while", "switch", "catch", "return"}
def fix_line(line):
# Skip lines that are empty or only whitespace
if not line.strip():
return line
# Match function calls like: foo(bar) => foo (bar)
# Avoid if/for/while/catch/return and function *definitions*
pattern = r'\b([a-zA-Z_]\w*)\('
def replacer(match):
name = match.group(1)
if is_control_structure(name) or is_function(line):
return match.group(0) # No change
return f'{name} ('
return re.sub(pattern, replacer, line)
# Step 2: Read the file, transform it, and write it back
with open(arg, "r", encoding="utf-8") as f:
lines = f.readlines()
with open(arg, "w", encoding="utf-8") as f:
for line in lines:
f.write(fix_line(line))
+151
View File
@@ -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;
}
+388
View File
@@ -0,0 +1,388 @@
#define R_LOG_ORIGIN "llm"
#include "r2ai.h"
#include "r2ai_priv.h"
#include <string.h>
static const R2AIProvider r2ai_providers[] = {
{ "openai", "https://api.openai.com/v1", true, false, false, false },
{ "gemini", "https://generativelanguage.googleapis.com/v1beta/openai", true, false, false, false },
{ "anthropic", "https://api.anthropic.com/v1", true, true, false, false },
{ "ollama", "http://localhost:11434/api", false, false, true, true },
{ "openapi", "http://127.0.0.1:11434", false, false, false, false },
{ "xai", "https://api.x.ai/v1", true, false, false, false },
{ "openrouter", "https://openrouter.ai/api/v1", true, false, false, false },
{ "groq", "https://api.groq.com/openai/v1", true, false, false, false },
{ "mistral", "https://api.mistral.ai/v1", true, false, false, false },
{ NULL, NULL, false, false, false, false } // sentinel
};
R_IPI const R2AIProvider *r2ai_get_provider(const char *name) {
if (R_STR_ISEMPTY (name)) {
return NULL;
}
for (int i = 0; r2ai_providers[i].name; i++) {
if (!strcmp (name, r2ai_providers[i].name)) {
return &r2ai_providers[i];
}
}
return NULL;
}
// Forward declaration for rawtools
R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
RCore *core = cps->core;
// Check if rawtools mode is enabled
bool rawtools_enabled = r_config_get_b (core->config, "r2ai.auto.raw");
const char *provider = args.provider? args.provider: r_config_get (core->config, "r2ai.api");
if (!provider) {
provider = "gemini";
}
if (rawtools_enabled && args.tools && r_list_length (args.tools) > 0) {
return r2ai_rawtools_llmcall (cps, args);
}
R2AI_State *state = cps->data;
R2AI_ChatResponse *res = NULL;
if (!args.model) {
const char *config_model = r_config_get (core->config, "r2ai.model");
args.model = strdup (config_model? config_model: "");
}
if (!args.provider) {
args.provider = strdup (provider);
}
if (!args.max_tokens) {
args.max_tokens = r_config_get_i (core->config, "r2ai.max_tokens");
}
if (!args.temperature) {
const char *configtemp = r_config_get (core->config, "r2ai.temperature");
args.temperature = configtemp? atof (configtemp): 0;
}
const R2AIProvider *prov = r2ai_get_provider (provider);
if (!prov) {
R_LOG_ERROR ("Unknown provider: %s", provider);
return NULL;
}
char *api_key = NULL;
if (prov->requires_api_key) {
api_key = r2ai_get_api_key (core, provider);
if (api_key) {
args.api_key = api_key;
}
}
// Make sure we have an API key before proceeding
if (prov->requires_api_key) {
if (R_STR_ISEMPTY (args.api_key)) {
char *Provider = strdup (provider);
r_str_case (Provider, true);
R_LOG_ERROR ("No API key found for %s provider. Please set one with: r2ai "
"%s_API_KEY=YOUR_KEY",
provider, Provider);
free (Provider);
return NULL;
}
}
// Set system_prompt from config if it's not already set
if (!args.system_prompt) {
args.system_prompt = r_config_get (core->config, "r2ai.system");
}
if (!args.messages) {
args.messages = r2ai_msgs_new ();
}
int context_pullback = -1;
// context and user message
if (args.input && r_config_get_b (core->config, "r2ai.data")) {
const int K = r_config_get_i (core->config, "r2ai.data.nth");
if (!state->db) {
state->db = r_vdb_new (R2AI_DEFAULT_VECTORS);
r2ai_refresh_embeddings (cps);
}
RStrBuf *sb = r_strbuf_new ("");
r_strbuf_appendf (sb, "\n ## Query\n\n%s\n ## Context\n", args.input);
RVdbResultSet *rs = r_vdb_query (state->db, args.input, K);
if (rs) {
int i;
for (i = 0; i < rs->size; i++) {
RVdbResult *r = &rs->results[i];
KDNode *n = r->node;
r_strbuf_appendf (sb, "- %s.\n", n->text);
}
r_vdb_result_free (rs);
}
char *m = r_strbuf_drain (sb);
R2AI_Message msg = { .role = "user", .content = m };
context_pullback = r_list_length (args.messages);
r2ai_msgs_add (args.messages, &msg);
free (m);
// TODO: we can save the msg without context
}
// Add the rest of the messages one by one
if (!args.messages && args.input) {
R2AI_Message msg = { .role = "user", .content = (char *)args.input };
args.messages = r2ai_msgs_new ();
r2ai_msgs_add (args.messages, &msg);
}
args.thinking_tokens = r_config_get_i (core->config, "r2ai.thinking_tokens");
const R2AIProvider *p = r2ai_get_provider (provider);
if (p && p->uses_anthropic_header) {
res = r2ai_anthropic (cps, args);
} else {
res = r2ai_openai (cps, args);
}
if (context_pullback != -1) {
R2AI_Message *msg = r_list_get_n (args.messages, context_pullback);
free ((char *)msg->content);
msg->content = strdup (args.input);
}
if (*args.error) {
R_LOG_ERROR ("%s", *args.error);
free (*args.error);
*args.error = NULL;
}
return res;
}
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;
char *api_key = NULL;
char *api_key_env = r_str_newf ("%s_API_KEY", provider);
r_str_case (api_key_env, true);
char *s = r_sys_getenv (api_key_env);
free (api_key_env);
if (R_STR_ISNOTEMPTY (s)) {
api_key = s;
} else {
free (s);
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);
}
}
return api_key;
}
R_IPI const char *r2ai_get_provider_url(RCore *core, const char *provider) {
const R2AIProvider *p = r2ai_get_provider (provider);
if (!p) {
return NULL;
}
// Handle providers that support custom baseurl
if (!strcmp (provider, "openai") || !strcmp (provider, "ollama")) {
const char *host = r_config_get (core->config, "r2ai.baseurl");
if (R_STR_ISNOTEMPTY (host)) {
if (r_str_startswith (host, "http")) {
if (!strcmp (provider, "openai")) {
return r_str_newf ("%s/v1", host);
}
return r_str_newf ("%s/api", host);
}
if (!strcmp (provider, "openai")) {
return r_str_newf ("http://%s/v1", host);
}
return r_str_newf ("http://%s/api", host);
}
}
return p->url;
}
R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
const char *purl = r2ai_get_provider_url (core, provider);
if (!purl) {
return NULL;
}
// Create models endpoint URL
const R2AIProvider *prov = r2ai_get_provider (provider);
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;
const R2AIProvider *p = r2ai_get_provider (provider);
if (p && p->requires_api_key) {
// Consolidated helper to fetch the API key from env or file
api_key = r2ai_get_api_key (core, provider);
}
int code = 0;
char *response = NULL;
if (api_key) {
const char *headers[4] = { "Content-Type: application/json", NULL, NULL, NULL };
char *auth_header = NULL;
char *version_header = NULL;
const R2AIProvider *prov = r2ai_get_provider (provider);
if (prov && prov->uses_anthropic_header) {
// Anthropic uses different header format
auth_header = r_str_newf ("x-api-key: %s", api_key);
version_header = strdup ("anthropic-version: 2023-06-01");
headers[1] = auth_header;
headers[2] = version_header;
} else {
// Standard OpenAI-compatible format
auth_header = r_str_newf ("Authorization: Bearer %s", api_key);
headers[1] = auth_header;
}
// Make HTTP GET request
R_LOG_DEBUG ("GET %s Headers: %s", models_url, headers);
response = r2ai_http_get (core, models_url, headers, &code, NULL);
free (auth_header);
free (version_header);
free (api_key);
} else {
// We have no headers
R_LOG_DEBUG ("GET %s", models_url);
response = r2ai_http_get (core, models_url, NULL, &code, NULL);
}
free (models_url);
if (!response || code != 200) {
R_LOG_DEBUG ("Failed to fetch models from %s (code: %d)", provider, code);
free (response);
return NULL;
}
// Parse JSON response
RList *models = r_list_newf (free);
if (!models) {
free (response);
return NULL;
}
RJson *json = r_json_parse (response);
if (json) {
const R2AIProvider *prov = r2ai_get_provider (provider);
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;
while (model_item) {
const RJson *id;
if (prov && prov->uses_tags_endpoint) {
id = r_json_get (model_item, "model");
} else {
id = r_json_get (model_item, "id");
}
if (id && id->type == R_JSON_STRING && R_STR_ISNOTEMPTY (id->str_value)) {
R_LOG_DEBUG ("Model: %s", id->str_value);
r_list_append (models, strdup (id->str_value));
}
model_item = model_item->next;
}
}
r_json_free (json);
}
free (response);
return models;
}
R_IPI void r2ai_list_providers(RCore *core, RStrBuf *sb) {
for (size_t i = 0; r2ai_providers[i].name; i++) {
if (sb) {
if (i > 0) {
r_strbuf_append (sb, ", ");
}
r_strbuf_append (sb, r2ai_providers[i].name);
} else {
r_cons_println (core->cons, r2ai_providers[i].name);
}
}
}
R_IPI void r2ai_refresh_embeddings(RCorePluginSession *cps) {
RCore *core = cps->core;
R2AI_State *state = cps->data;
RListIter *iter, *iter2;
char *line;
char *file;
// refresh embeddings database
r_vdb_free (state->db);
state->db = r_vdb_new (R2AI_DEFAULT_VECTORS);
// enumerate .txt files in directory
const char *path = r_config_get (core->config, "r2ai.data.path");
RList *files = r_sys_dir (path);
if (r_list_empty (files)) {
R_LOG_WARN ("Cannot find any file in r2ai.data.path");
}
r_list_foreach (files, iter, file) {
if (!r_str_endswith (file, ".txt")) {
continue;
}
R_LOG_DEBUG ("Index %s", file);
char *filepath = r_file_new (path, file, NULL);
char *text = r_file_slurp (filepath, NULL);
if (text) {
R_LOG_DEBUG ("Index %s", file);
RList *lines = r_str_split_list (text, "\n", -1);
r_list_foreach (lines, iter2, line) {
if (r_str_trim_head_ro (line)[0] == 0) {
continue;
}
r_vdb_insert (state->db, line);
R_LOG_DEBUG ("Insert %s", line);
}
r_list_free (lines);
}
free (filepath);
}
r_list_free (files);
}
+115 -19
View File
@@ -1,31 +1,127 @@
/* Copyright r2ai - 2023-2025 - pancake */
#include <r_core.h>
#include "r2ai.h"
#include "r2ai_priv.h"
int main(int argc, char **argv) {
if (argc < 2) {
eprintf ("Usage: r2ai <prompt>\n");
static void show_help() {
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");
}
static void show_version() {
printf ("r2ai " R2AI_VERSION "\n");
}
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:Eb:");
while ((c = r_getopt_next (&opt)) != -1) {
switch (c) {
case 'v':
show_version ();
r_list_free (queries);
return 0;
case 'h':
show_help ();
r_list_free (queries);
return 0;
case 'p':
provider = opt.arg;
break;
case 'm':
model = opt.arg;
break;
case 'q':
r_list_append (queries, strdup (opt.arg));
break;
case 'b':
baseurl = opt.arg;
break;
case 'E':
{
RCore *core = r_core_new ();
RCorePluginSession cps = {
.core = core
};
r2ai_init (&cps);
char *rc_path = r_file_home (".config/r2ai/rc");
r_cons_editor (core->cons, rc_path, NULL);
free (rc_path);
r_list_free (queries);
r2ai_fini (&cps);
r_core_free (core);
return 0;
}
default:
show_help ();
r_list_free (queries);
return 1;
}
}
if (opt.ind >= argc) {
show_help ();
r_list_free (queries);
return 1;
}
const char *prompt = argv[1];
RCore *core = r_core_new ();
char *err = NULL;
R2AIArgs args = {
.input = prompt,
.error = &err,
.dorag = false,
RCorePluginSession cps = {
.core = core
};
char *res = r2ai (core, args);
if (res) {
R_LOG_INFO (res);
free (res);
r2ai_init (&cps);
if (baseurl) {
r_config_set (core->config, "r2ai.baseurl", baseurl);
}
free (err);
// Process queries if any
if (!r_list_empty (queries)) {
RListIter *iter;
char *query;
r_list_foreach (queries, iter, query) {
r2ai_cmd_q (&cps, query);
}
} else {
if (opt.ind >= argc) {
// List available prompts
r2ai_cmd_q (&cps, "");
r_list_free (queries);
r2ai_fini (&cps);
r_core_free (core);
return 0;
}
const char *prompt = argv[opt.ind];
char *err = NULL;
R2AIArgs args = {
.input = prompt,
.provider = provider,
.model = model,
.error = &err,
.dorag = false,
};
char *res = r2ai (&cps, args);
if (res) {
r_cons_println (core->cons, res);
free (res);
}
free (err);
r_cons_flush (core->cons);
}
r_list_free (queries);
r2ai_fini (&cps);
r_core_free (core);
return 0;
}
+17 -17
View File
@@ -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;
}
@@ -103,7 +103,7 @@ static void append_formatted(RStrBuf *sb, const char *text, int len, MarkdownSta
static int count_indent(const char *str) {
int count = 0;
while (*str == ' ' || *str == '\t') {
count += (*str == '\t') ? 4 : 1;
count += (*str == '\t')? 4: 1;
str++;
}
return count;
@@ -257,7 +257,7 @@ R_API char *r2ai_markdown(const char *markdown) {
append_formatted (sb, start, digit_start - start, state);
int num_len = p - digit_start;
char num_buf[16] = { 0 };
r_str_ncpy (num_buf, digit_start, num_len < 15 ? num_len + 1 : 15);
r_str_ncpy (num_buf, digit_start, num_len < 15? num_len + 1: 15);
// Use the actual number from markdown and format it
char formatted_number[32];
@@ -302,7 +302,7 @@ R_API char *r2ai_markdown(const char *markdown) {
if (p > start) {
append_formatted (sb, start, p - start, state);
}
p += double_marker ? 2 : 1;
p += double_marker? 2: 1;
start = p;
if (double_marker && marker == '*') {
@@ -317,7 +317,7 @@ R_API char *r2ai_markdown(const char *markdown) {
} else if (p > markdown && isalnum (*(p - 1)) && !isalnum (p[1 + double_marker])) {
// Closing marker
append_formatted (sb, start, p - start, state);
p += double_marker ? 2 : 1;
p += double_marker? 2: 1;
start = p;
state = NORMAL;
} else {
+12 -8
View File
@@ -1,5 +1,5 @@
project('r2ai', 'c',
version : '0.1.0',
version : '1.2.6',
default_options : ['warning_level=2', 'c_std=c11']
)
@@ -185,15 +185,19 @@ elif cc.get_id() == 'msvc'
endif
srcs = files(
'r2ai.c',
'auto.c',
'vdb.c',
'tools.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)
+1 -1
View File
@@ -1,6 +1,6 @@
option('r2_prefix',
type: 'string',
value: '',
value: '/usr/local',
description: 'Path prefix where radare2 is installed (include/ and lib/).'
)
+179 -424
View File
@@ -2,273 +2,201 @@
#include "r2ai.h"
// AITODO: remove globals
// Static global message store for session persistence
static R2AI_Messages *conversation = NULL;
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);
}
#define INITIAL_CAPACITY 8
#define GROWTH_FACTOR 1.5
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 (msg->content);
free ((void *)msg->content); // maybe double free
free (msg->reasoning_content);
free (msg->tool_call_id);
// 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);
}
// Initialize conversation container (call this during plugin init)
R_API void r2ai_conversation_init(void) {
if (conversation) {
// Already initialized
// Conversation is now stored in R2AI_State
R_API void r2ai_conversation_init(R2AI_State *state) {
if (!state || state->conversation) {
// Already initialized or invalid state
return;
}
conversation = R_NEW0 (R2AI_Messages);
conversation->cap_messages = INITIAL_CAPACITY;
conversation->messages = R_NEWS0 (R2AI_Message, conversation->cap_messages);
if (!conversation->messages) {
R_FREE (conversation);
}
state->conversation = r2ai_msgs_new ();
}
// Get the conversation instance (returns NULL if not initialized)
R_API R2AI_Messages *r2ai_conversation_get(void) {
return conversation;
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->cap_messages = INITIAL_CAPACITY;
msgs->messages = R_NEWS0 (R2AI_Message, msgs->cap_messages);
if (!msgs->messages) {
R_FREE (msgs);
return NULL;
}
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);
}
// Don't actually free if it's our static instance
if (msgs == conversation) {
// Just clear the messages but keep the allocated structure
for (int i = 0; i < msgs->n_messages; i++) {
r2ai_message_free (&msgs->messages[i]);
}
msgs->n_messages = 0;
return;
}
// Normal free for non-static instances
if (msgs->messages) {
for (int i = 0; i < msgs->n_messages; i++) {
r2ai_message_free (&msgs->messages[i]);
}
R_FREE (msgs->messages);
}
R_FREE (msgs);
}
// Free the conversation when plugin is unloaded
R_API void r2ai_conversation_free(void) {
if (conversation) {
if (conversation->messages) {
for (int i = 0; i < conversation->n_messages; i++) {
r2ai_message_free (&conversation->messages[i]);
}
R_FREE (conversation->messages);
}
R_FREE (conversation);
conversation = NULL;
R_API void r2ai_conversation_free(R2AI_State *state) {
if (state && state->conversation) {
r2ai_msgs_free (state->conversation);
state->conversation = NULL;
}
}
// 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;
}
for (int i = 0; i < msgs->n_messages; i++) {
r2ai_message_free (&msgs->messages[i]);
}
msgs->n_messages = 0;
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;
}
// Check if we need to resize
if (msgs->n_messages >= msgs->cap_messages) {
int new_cap = msgs->cap_messages * GROWTH_FACTOR;
R2AI_Message *new_messages = realloc (msgs->messages, sizeof (R2AI_Message) * new_cap);
if (!new_messages) {
return false;
}
msgs->messages = new_messages;
msgs->cap_messages = new_cap;
// Zero the newly allocated portion
memset (&msgs->messages[msgs->n_messages], 0,
sizeof (R2AI_Message) * (msgs->cap_messages - msgs->n_messages));
}
// Copy the message to the array
R2AI_Message *dest = &msgs->messages[msgs->n_messages++];
dest->role = msg->role ? strdup (msg->role) : NULL;
dest->content = msg->content ? strdup (msg->content) : NULL;
dest->reasoning_content = msg->reasoning_content ? strdup (msg->reasoning_content) : NULL;
R2AI_Message *new_msg = R_NEW0 (R2AI_Message);
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 (dest);
msgs->n_messages--;
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 (dest);
msgs->n_messages--;
return false;
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;
dst->signature = src->signature? strdup (src->signature): NULL;
dst->text = src->text? strdup (src->text): NULL;
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);
}
for (int i = 0; i < cb->n_blocks; i++) {
R2AI_ContentBlock *src = &msg->content_blocks->blocks[i];
R2AI_ContentBlock *dst = &cb->blocks[i];
dst->type = src->type ? strdup (src->type) : NULL;
dst->data = src->data ? strdup (src->data) : NULL;
dst->thinking = src->thinking ? strdup (src->thinking) : NULL;
dst->signature = src->signature ? strdup (src->signature) : NULL;
dst->text = src->text ? strdup (src->text) : NULL;
dst->id = src->id ? strdup (src->id) : NULL;
dst->name = src->name ? strdup (src->name) : NULL;
dst->input = src->input ? strdup (src->input) : NULL;
}
dest->content_blocks = cb;
new_msg->content_blocks = cb;
}
dest->tool_call_id = msg->tool_call_id ? strdup (msg->tool_call_id) : NULL;
dest->tool_calls = NULL;
dest->n_tool_calls = 0;
new_msg->tool_call_id = msg->tool_call_id? strdup (msg->tool_call_id): NULL;
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) {
dest->tool_calls = R_NEWS0 (R2AI_ToolCall, msg->n_tool_calls);
if (!dest->tool_calls) {
// Clean up and return error
r2ai_message_free (dest);
msgs->n_messages--;
return false;
}
dest->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 *)&dest->tool_calls[i];
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;
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, new_msg);
return true;
}
R_API bool r2ai_msgs_add_tool_call(R2AI_Messages *msgs, const R2AI_ToolCall *tc) {
if (!msgs || !tc || msgs->n_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 = &msgs->messages[msgs->n_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];
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;
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;
}
@@ -294,31 +222,28 @@ R_API bool r2ai_msgs_from_json(R2AI_Messages *msgs, const RJson *json) {
// Create a new message to add
R2AI_Message new_msg = { 0 };
new_msg.role = (role && role->type == R_JSON_STRING) ? strdup (role->str_value) : strdup ("assistant");
new_msg.content = (content && content->type == R_JSON_STRING) ? strdup (content->str_value) : NULL;
new_msg.role = (role && role->type == R_JSON_STRING)? strdup (role->str_value): strdup ("assistant");
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");
@@ -328,14 +253,15 @@ R_API bool r2ai_msgs_from_json(R2AI_Messages *msgs, const RJson *json) {
const RJson *name = r_json_get (block, "name");
const RJson *input = r_json_get (block, "input");
dst->type = (type && type->type == R_JSON_STRING) ? strdup (type->str_value) : NULL;
dst->data = (data && data->type == R_JSON_STRING) ? strdup (data->str_value) : NULL;
dst->thinking = (thinking && thinking->type == R_JSON_STRING) ? strdup (thinking->str_value) : NULL;
dst->signature = (signature && signature->type == R_JSON_STRING) ? strdup (signature->str_value) : NULL;
dst->text = (text && text->type == R_JSON_STRING) ? strdup (text->str_value) : NULL;
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;
dst->type = (type && type->type == R_JSON_STRING)? strdup (type->str_value): NULL;
dst->data = (data && data->type == R_JSON_STRING)? strdup (data->str_value): NULL;
dst->thinking = (thinking && thinking->type == R_JSON_STRING)? strdup (thinking->str_value): NULL;
dst->signature = (signature && signature->type == R_JSON_STRING)? strdup (signature->str_value): NULL;
dst->text = (text && text->type == R_JSON_STRING)? strdup (text->str_value): NULL;
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;
}
@@ -366,12 +292,13 @@ R_API bool r2ai_msgs_from_json(R2AI_Messages *msgs, const RJson *json) {
const RJson *arguments = r_json_get (function, "arguments");
R2AI_ToolCall tc = { 0 };
tc.name = (name && name->type == R_JSON_STRING) ? name->str_value : NULL;
tc.arguments = (arguments && arguments->type == R_JSON_STRING) ? arguments->str_value : NULL;
tc.id = (id && id->type == R_JSON_STRING) ? id->str_value : NULL;
tc.name = (name && name->type == R_JSON_STRING)? name->str_value: NULL;
tc.arguments = (arguments && arguments->type == R_JSON_STRING)? arguments->str_value: NULL;
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;
}
}
}
@@ -379,8 +306,8 @@ R_API bool r2ai_msgs_from_json(R2AI_Messages *msgs, const RJson *json) {
return true;
}
R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
if (!msgs || msgs->n_messages == 0) {
R_API char *r2ai_msgs_to_json(const RList *msgs) {
if (!msgs || r_list_empty (msgs)) {
return NULL;
}
@@ -391,16 +318,22 @@ R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
pj_a (pj); // Start array
for (int i = 0; i < msgs->n_messages; i++) {
const R2AI_Message *msg = &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");
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) {
@@ -413,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
@@ -435,12 +368,10 @@ R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
pj_o (pj); // Start function object
// Add name
pj_ks (pj, "name", tc->name ? tc->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
@@ -458,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 || msgs->n_messages == 0) {
R_API char *r2ai_msgs_to_anthropic_json(const RList *msgs) {
if (!msgs || r_list_empty (msgs)) {
return NULL;
}
@@ -470,19 +401,20 @@ R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
pj_a (pj); // Start array
for (int i = 0; i < msgs->n_messages; i++) {
const R2AI_Message *msg = &msgs->messages[i];
RListIter *iter;
const R2AI_Message *msg;
r_list_foreach (msgs, iter, msg) {
pj_o (pj); // Start message object
// Add role
const char *role = msg->role ? msg->role : "user";
pj_ks (pj, "role", strcmp (role, "tool") == 0 ? "user" : role);
const char *role = msg->role? msg->role: "user";
pj_ks (pj, "role", strcmp (role, "tool") == 0? "user": role);
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);
@@ -510,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);
@@ -542,20 +472,18 @@ 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 : "");
pj_ks (pj, "id", tc->id? tc->id: "");
pj_ks (pj, "name", tc->name? tc->name: "");
// Create a non-const copy for r_json_parse
char *arguments_copy = tc->arguments ? strdup (tc->arguments) : NULL;
RJson *arguments = arguments_copy ? r_json_parse (arguments_copy) : NULL;
char *arguments_copy = tc->arguments? strdup (tc->arguments): NULL;
RJson *arguments = arguments_copy? r_json_parse (arguments_copy): NULL;
pj_ko (pj, "input"); // Start input object
if (arguments) {
@@ -570,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
}
@@ -587,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 || messages->n_messages == 0) {
R_API void r2ai_delete_last_messages(RList *messages, int n) {
if (!messages || r_list_length (messages) == 0) {
return;
}
@@ -598,184 +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
if (n > messages->n_messages) {
n = messages->n_messages;
int len = r_list_length (messages);
if (n > len) {
n = len;
}
// Free the last n messages
for (int i = messages->n_messages - n; i < messages->n_messages; i++) {
r2ai_message_free (&messages->messages[i]);
// Pop the last n messages
for (int i = 0; i < n; i++) {
r_list_pop (messages);
}
// Update the message count
messages->n_messages -= n;
}
// 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;
}
// Create a conversation with optional initial user message
R_API R2AI_Messages *create_conversation(const char *user_message) {
// Create a temporary message container (not using static storage)
R2AI_Messages *msgs = r2ai_msgs_new ();
if (!msgs) {
return NULL;
}
// Add user message if provided (no system message - that's added during processing)
if (R_STR_ISNOTEMPTY (user_message)) {
R2AI_Message user_msg = {
.role = "user",
.content = user_message
};
r2ai_msgs_add (msgs, &user_msg);
}
return msgs;
}
+210 -157
View File
@@ -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
@@ -20,37 +18,19 @@ typedef struct {
int error_flags; // Bitfield of ModelErrorFlags
} ModelCompat;
// Hash table to store model compatibility info
static HtPP *model_compat_db = NULL;
#if 0
// Function to check if a model has a specific error flag
static bool model_has_error(const char *provider, const char *model, ModelErrorFlags flag) {
if (!model_compat_db) {
return false;
}
char *key = r_str_newf ("%s:%s", provider, model ? model : "default");
bool found_flag = false;
ModelCompat *compat = ht_pp_find (model_compat_db, key, &found_flag);
free (key);
if (found_flag && compat) {
return (compat->error_flags & flag) != 0;
}
return false;
}
#endif
// Function to add an error flag to a model
static void model_add_error(const char *provider, const char *model, ModelErrorFlags flag) {
if (!model_compat_db) {
model_compat_db = ht_pp_new0 ();
static void model_add_error(R2AI_State *state, const char *provider, const char *model, ModelErrorFlags flag) {
if (!state) {
return;
}
char *key = r_str_newf ("%s:%s", provider, model ? model : "default");
if (!state->model_compat_db) {
state->model_compat_db = ht_pp_new0 ();
}
char *key = r_str_newf ("%s:%s", provider, model? model: "default");
bool found_flag = false;
ModelCompat *compat = ht_pp_find (model_compat_db, key, &found_flag);
ModelCompat *compat = ht_pp_find (state->model_compat_db, key, &found_flag);
if (found_flag && compat) {
// Update existing entry
@@ -60,7 +40,7 @@ static void model_add_error(const char *provider, const char *model, ModelErrorF
compat = R_NEW0 (ModelCompat);
compat->model_id = strdup (key);
compat->error_flags = flag;
ht_pp_insert (model_compat_db, key, compat);
ht_pp_insert (state->model_compat_db, key, compat);
}
free (key);
}
@@ -78,27 +58,31 @@ static bool model_compat_free_cb(void *user, const void *k, const void *v) {
}
// Function to free the model_compat_db hash table
R_IPI void r2ai_openai_fini(void) {
if (model_compat_db) {
ht_pp_foreach (model_compat_db, model_compat_free_cb, NULL);
ht_pp_free (model_compat_db);
model_compat_db = NULL;
R_IPI void r2ai_openai_fini(R2AI_State *state) {
if (state && state->model_compat_db) {
ht_pp_foreach (state->model_compat_db, model_compat_free_cb, NULL);
ht_pp_free (state->model_compat_db);
state->model_compat_db = NULL;
}
}
R_IPI R2AI_ChatResponse *r2ai_openai(RCore *core, R2AIArgs args) {
R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
RCore *core = cps->core;
R2AI_State *state = cps->data;
// Initialize compatibility database if needed
if (!model_compat_db) {
model_compat_db = ht_pp_new0 ();
if (!state->model_compat_db) {
state->model_compat_db = ht_pp_new0 ();
}
args.provider = r_config_get (core->config, "r2ai.api");
args.model = r_config_get (core->config, "r2ai.model");
const char *base_url = r2ai_get_provider_url (core, args.provider);
// TODO: default model name should depend on api
const char *model_name = args.model ? args.model : "gpt-4o-mini";
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");
@@ -107,7 +91,7 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCore *core, R2AIArgs args) {
}
R2AI_Message system_msg = {
.role = "system",
.content = args.system_prompt
.content = (char *)args.system_prompt
};
// Add system message if available from args.system_prompt
if (R_STR_ISNOTEMPTY (args.system_prompt)) {
@@ -115,10 +99,10 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCore *core, R2AIArgs args) {
// if the model name contains "o1" or "o3", it's "developer" role
if (strstr (model_name, "o1") || strstr (model_name, "o3")) {
system_msg.role = "developer";
system_msg.content = args.system_prompt;
system_msg.content = (char *)args.system_prompt;
} else {
system_msg.role = "system";
system_msg.content = args.system_prompt;
system_msg.content = (char *)args.system_prompt;
}
r2ai_msgs_add (temp_msgs, &system_msg);
} else {
@@ -131,20 +115,22 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCore *core, R2AIArgs args) {
} else {
system_msg.role = "system";
}
system_msg.content = sysprompt;
system_msg.content = (char *)sysprompt;
r2ai_msgs_add (temp_msgs, &system_msg);
}
}
if (args.messages) {
for (int i = 0; i < args.messages->n_messages; i++) {
r2ai_msgs_add (temp_msgs, &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 && temp_msgs->n_messages > 0 && temp_msgs->messages && temp_msgs->messages[0].role) {
R_LOG_DEBUG ("First message role: %s", 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;
@@ -160,20 +146,16 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCore *core, R2AIArgs args) {
headers[0] = "Content-Type: application/json";
headers[1] = auth_header;
}
const char *openai_url;
if (strcmp (args.provider, "ollama") == 0) {
openai_url = r_str_newf ("%s/chat", base_url);
} else {
openai_url = r_str_newf ("%s/chat/completions", base_url);
}
R_LOG_DEBUG ("OpenAI URL: %s", openai_url);
const char *urlfmt = strcmp (args.provider, "ollama")
? "%s/chat/completions"
: "%s/chat";
char *openai_url = r_str_newf (urlfmt, base_url);
// Create a messages JSON object, either from input messages or from content
char *messages_json = NULL;
if (temp_msgs && temp_msgs->n_messages > 0) {
R_LOG_DEBUG ("Using input messages: %d messages", temp_msgs->n_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) {
@@ -192,7 +174,7 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCore *core, 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);
}
@@ -202,36 +184,37 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCore *core, R2AIArgs args) {
pj_ks (pj, "model", model_name);
pj_kb (pj, "stream", false);
if (!strcmp (args.provider, "ollama")) {
// Ollama uses "options" object for parameters
pj_ko (pj, "options");
if (args.max_tokens) {
pj_kn (pj, "num_predict", args.max_tokens);
}
if (args.temperature > 0) {
pj_kd (pj, "temperature", args.temperature);
}
pj_end (pj); // end options
} else {
#if 0
// gpt-5-mini-chat is the only gpt-5 model that supports temperature
// gpt-5 gpt-5-mini and gpt-5-nano just throw an error
// Only add temperature if this provider/model doesn't have the temperature error flag
if (!model_has_error (args.provider, model_name, MODEL_ERROR_TEMPERATURE)) {
pj_kd (pj, "temperature", args.temperature ? args.temperature : 0.01);
}
// gpt-5-mini-chat is the only gpt-5 model that supports temperature
// gpt-5 gpt-5-mini and gpt-5-nano just throw an error
// Only add temperature if this provider/model doesn't have the temperature error flag
if (!model_has_error (args.provider, model_name, MODEL_ERROR_TEMPERATURE)) {
pj_kd (pj, "temperature", args.temperature? args.temperature: 0.01);
}
#endif
if (strcmp (args.provider, "mistral") == 0) {
pj_kn (pj, "max_tokens", args.max_tokens ? args.max_tokens : 5128);
} else {
pj_kn (pj, "max_completion_tokens", args.max_tokens ? args.max_tokens : 5128);
if (strcmp (args.provider, "mistral") == 0) {
pj_kn (pj, "max_tokens", args.max_tokens? args.max_tokens: 5128);
} else {
pj_kn (pj, "max_completion_tokens", args.max_tokens? args.max_tokens: 5128);
}
}
pj_end (pj);
// 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
@@ -265,15 +248,32 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCore *core, R2AIArgs args) {
// Save the full JSON to a file for inspection
// XXX: only create request/response files when r2ai.debug is set
r_file_dump ("/tmp/r2ai_openai_request.json", (const ut8 *)complete_json, strlen (complete_json), 0);
R_LOG_DEBUG ("Full request saved to /tmp/r2ai_openai_request.json");
char *tmpdir = r_file_tmpdir ();
char *req_path = r_str_newf ("%s" R_SYS_DIR "r2ai_openai_request.json", tmpdir);
r_file_dump (req_path, (const ut8 *)complete_json, strlen (complete_json), 0);
R_LOG_DEBUG ("Full request saved to %s", req_path);
free (req_path);
free (tmpdir);
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;
res = r2ai_http_post (core, openai_url, headers, complete_json, &code, NULL);
free (complete_json);
free (openai_url);
if (code != 200) {
R_LOG_ERROR ("OpenAI API error %d", code);
@@ -281,30 +281,25 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCore *core, R2AIArgs args) {
R_LOG_ERROR ("OpenAI API error response: %s", res);
// Check for specific error types in the response
ModelErrorFlags error_flag = MODEL_ERROR_NONE;
const char *model_name = args.model ? args.model : "gpt-5-mini";
// Check for temperature errors
if (strstr (res, "temperature")) {
R_LOG_DEBUG ("Detected temperature error for %s model %s", args.provider, model_name);
R_LOG_DEBUG ("Detected temperature error for %s model %s", args.provider, args.model);
error_flag |= MODEL_ERROR_TEMPERATURE;
}
// Add more error type checks as needed
// if (strstr (res, "top_p")) {
// error_flag |= MODEL_ERROR_TOP_P;
// }
if (error_flag != MODEL_ERROR_NONE) {
// Record the error flags for this provider/model
model_add_error (args.provider, model_name, error_flag);
model_add_error (state, args.provider, args.model, error_flag);
// Clean up
free (auth_header);
free (res);
// Retry the call (it will skip problematic parameters this time)
R_LOG_INFO ("Retrying request with adjusted parameters for %s/%s", args.provider, model_name);
return r2ai_openai (core, args);
R_LOG_INFO ("Retrying request with adjusted parameters for %s/%s", args.provider, args.model);
RCorePluginSession retry_cps = { .core = core, .data = state };
return r2ai_openai (&retry_cps, args);
}
}
free (auth_header);
@@ -313,9 +308,14 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCore *core, R2AIArgs args) {
}
// Save the response for inspection
r_file_dump ("/tmp/r2ai_openai_response.json", (const ut8 *)res, strlen (res), 0);
R_LOG_DEBUG ("OpenAI API response saved to /tmp/r2ai_openai_response.json");
R_LOG_DEBUG ("OpenAI API response: %s", res);
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);
if (r_config_get_b (core->config, "r2ai.debug")) {
eprintf ("OpenAI API response: %s\n", res);
}
free (res_path);
free (tmpdir);
// Parse the response into our messages structure
@@ -327,83 +327,136 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCore *core, R2AIArgs args) {
R2AI_Usage *usage = R_NEW0 (R2AI_Usage);
if (message) {
// Process the response using our r2ai_msgs_from_json logic
const RJson *usage_json = r_json_get (jres, "usage");
const RJson *usage_json = NULL;
if (strcmp (args.provider, "ollama") == 0) {
// Ollama has usage info at top level
usage_json = jres;
} else {
usage_json = r_json_get (jres, "usage");
}
if (usage_json && usage_json->type == R_JSON_OBJECT) {
const RJson *prompt_tokens = r_json_get (usage_json, "prompt_tokens");
const RJson *completion_tokens = r_json_get (usage_json, "completion_tokens");
const RJson *total_tokens = r_json_get (usage_json, "total_tokens");
if (prompt_tokens && prompt_tokens->type == R_JSON_INTEGER) {
usage->prompt_tokens = prompt_tokens->num.u_value;
if (strcmp (args.provider, "ollama") == 0) {
// Ollama field names
const RJson *prompt_tokens = r_json_get (usage_json, "prompt_eval_count");
const RJson *completion_tokens = r_json_get (usage_json, "eval_count");
if (prompt_tokens && prompt_tokens->type == R_JSON_INTEGER) {
usage->prompt_tokens = prompt_tokens->num.u_value;
}
if (completion_tokens && completion_tokens->type == R_JSON_INTEGER) {
usage->completion_tokens = completion_tokens->num.u_value;
}
// Calculate total_tokens for ollama
usage->total_tokens = usage->prompt_tokens + usage->completion_tokens;
} else {
// OpenAI field names
const RJson *prompt_tokens = r_json_get (usage_json, "prompt_tokens");
const RJson *completion_tokens = r_json_get (usage_json, "completion_tokens");
const RJson *total_tokens = r_json_get (usage_json, "total_tokens");
if (prompt_tokens && prompt_tokens->type == R_JSON_INTEGER) {
usage->prompt_tokens = prompt_tokens->num.u_value;
}
if (completion_tokens && completion_tokens->type == R_JSON_INTEGER) {
usage->completion_tokens = completion_tokens->num.u_value;
}
if (total_tokens && total_tokens->type == R_JSON_INTEGER) {
usage->total_tokens = total_tokens->num.u_value;
}
}
if (completion_tokens && completion_tokens->type == R_JSON_INTEGER) {
usage->completion_tokens = completion_tokens->num.u_value;
}
if (total_tokens && total_tokens->type == R_JSON_INTEGER) {
usage->total_tokens = total_tokens->num.u_value;
}
const RJson *message_json = NULL;
if (strcmp (args.provider, "ollama") == 0) {
// Ollama returns message directly
message_json = r_json_get (jres, "message");
} else {
// OpenAI-style response with choices array
const RJson *choices = r_json_get (jres, "choices");
if (choices && choices->type == R_JSON_ARRAY) {
const RJson *choice = r_json_item (choices, 0);
if (choice) {
message_json = r_json_get (choice, "message");
}
}
}
const RJson *choices = r_json_get (jres, "choices");
if (choices && choices->type == R_JSON_ARRAY) {
const RJson *choice = r_json_item (choices, 0);
if (choice) {
const RJson *message_json = r_json_get (choice, "message");
if (message_json) {
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");
// Set the basic message properties
message->role = (role && role->type == R_JSON_STRING) ? strdup (role->str_value) : strdup ("assistant");
if (message_json) {
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");
if (content && content->type == R_JSON_STRING) {
message->content = strdup (content->str_value);
}
// Set the basic message properties
message->role = (role && role->type == R_JSON_STRING)? strdup (role->str_value): strdup ("assistant");
if (reasoning_content && reasoning_content->type == R_JSON_STRING) {
message->reasoning_content = strdup (reasoning_content->str_value);
}
if (content && content->type == R_JSON_STRING) {
message->content = strdup (content->str_value);
}
// Handle tool calls if present
const RJson *tool_calls = r_json_get (message_json, "tool_calls");
if (tool_calls && tool_calls->type == R_JSON_ARRAY && tool_calls->children.count > 0) {
int n_tool_calls = tool_calls->children.count;
R2AI_ToolCall *tool_calls_array = R_NEWS0 (R2AI_ToolCall, n_tool_calls);
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_calls_array) {
for (int i = 0; i < n_tool_calls; i++) {
const RJson *tool_call = r_json_item (tool_calls, i);
if (!tool_call) {
continue;
}
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);
}
const RJson *id = r_json_get (tool_call, "id");
const RJson *function = r_json_get (tool_call, "function");
if (!function) {
continue;
}
const RJson *name = r_json_get (function, "name");
const RJson *arguments = r_json_get (function, "arguments");
if (id && id->type == R_JSON_STRING) {
tool_calls_array[i].id = strdup (id->str_value);
}
if (name && name->type == R_JSON_STRING) {
tool_calls_array[i].name = strdup (name->str_value);
}
if (arguments && arguments->type == R_JSON_STRING) {
tool_calls_array[i].arguments = strdup (arguments->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;
}
}
message->tool_calls = tool_calls_array;
message->n_tool_calls = n_tool_calls;
}
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?
}
}
r_json_free (jres);
+350
View File
@@ -0,0 +1,350 @@
/* r2ai - Copyright 2023-2025 pancake */
#include "r2ai.h"
R_API void r2aiprompt_free(R2AIPrompt *prompt) {
if (!prompt) {
return;
}
free (prompt->title);
free (prompt->author);
free (prompt->desc);
free (prompt->command);
free (prompt->prompt);
free (prompt->requires);
free (prompt->if_empty);
free (prompt->if_command);
free (prompt->model);
free (prompt->provider);
free (prompt);
}
R_API R2AIPrompt *parse_prompt_file(const char *filepath) {
R2AIPrompt *prompt = R_NEW0 (R2AIPrompt);
char *content = r_file_slurp (filepath, NULL);
if (!content) {
free (prompt);
return NULL;
}
// Check if it's a markdown file with YAML frontmatter
if (r_str_startswith (content, "---\n")) {
// Parse YAML frontmatter
char *frontmatter_end = strstr (content + 4, "\n---\n");
if (frontmatter_end) {
char *frontmatter = r_str_ndup (content + 4, frontmatter_end - (content + 4));
RList *lines = r_str_split_list (frontmatter, "\n", -1);
RListIter *iter;
char *line;
r_list_foreach (lines, iter, line) {
r_str_trim (line);
if (*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->desc = strdup (value);
} else if (!strcmp (key, "author")) {
prompt->author = strdup (value);
} else if (!strcmp (key, "description")) {
prompt->title = strdup (value);
} else if (!strcmp (key, "command")) {
prompt->command = strdup (value);
} else if (!strcmp (key, "model")) {
prompt->model = strdup (value);
} else if (!strcmp (key, "provider")) {
prompt->provider = strdup (value);
}
}
r_list_free (lines);
free (frontmatter);
// The prompt content starts after the second ---
char *prompt_start = frontmatter_end + 5; // Skip "\n---\n"
prompt->prompt = strdup (prompt_start);
}
} else {
R_LOG_ERROR ("Invalid prompt file format: %s", filepath);
}
free (content);
return prompt;
}
static char *run_commands(RCore *core, const char *commands) {
if (!commands) {
return NULL;
}
RStrBuf *sb = r_strbuf_new ("");
char *cmds_str = strdup (commands);
RList *cmds = r_str_split_list (cmds_str, ";", -1);
RListIter *iter;
char *cmd;
r_list_foreach (cmds, iter, cmd) {
r_str_trim (cmd);
if (*cmd) {
char *out = r_core_cmd_str (core, cmd);
r_strbuf_append (sb, out);
free (out);
}
}
r_list_free (cmds);
free (cmds_str);
return r_strbuf_drain (sb);
}
static char *replace_vars(RCore *core, const char *text) {
if (!text) {
return NULL;
}
RStrBuf *sb = r_strbuf_new ("");
const char *p = text;
while (*p) {
if (*p == '$' && *(p + 1) == '{') {
p += 2;
const char *end = strchr (p, '}');
if (end) {
char *var = r_str_ndup (p, end - p);
char *val = getenv (var);
if (val) {
r_strbuf_append (sb, val);
}
free (var);
p = end + 1;
} else {
r_strbuf_append (sb, "${");
p += 2;
}
} else if (*p == '$' && *(p + 1) == '(') {
p += 2;
const char *end = strchr (p, ')');
if (end) {
char *cmd = r_str_ndup (p, end - p);
char *val = r_core_cmd_str (core, cmd);
r_str_trim (val);
r_strbuf_append (sb, val);
free (val);
free (cmd);
p = end + 1;
} else {
r_strbuf_append (sb, "$(");
p += 2;
}
} else {
r_strbuf_append_n (sb, p, 1);
p++;
}
}
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");
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;
}
if (r_list_empty (search_dirs)) {
R_LOG_ERROR ("No prompt directories found");
r_list_free (search_dirs);
return;
}
if (!input || !*input) {
// list prompts
RListIter *dir_iter;
char *dir;
r_list_foreach (search_dirs, dir_iter, dir) {
RList *files = r_sys_dir (dir);
RListIter *iter;
char *file;
r_list_foreach (files, iter, file) {
if (r_str_endswith (file, ".r2ai.txt") || r_str_endswith (file, ".r2ai.md")) {
char *dot = strchr (file, '.');
char *name = dot? r_str_ndup (file, dot - file): strdup (file);
char *filepath_txt = r_str_newf ("%s/%s.r2ai.txt", dir, name);
char *filepath_md = r_str_newf ("%s/%s.r2ai.md", dir, name);
char *filepath = NULL;
if (r_file_exists (filepath_txt)) {
filepath = filepath_txt;
} else if (r_file_exists (filepath_md)) {
filepath = filepath_md;
}
if (filepath) {
R2AIPrompt *prompt = parse_prompt_file (filepath);
if (prompt) {
r_cons_printf (core->cons, "%s: %s - %s\n", name, prompt->title? prompt->title: "", prompt->desc? prompt->desc: "");
r2aiprompt_free (prompt);
} else {
r_cons_println (core->cons, name);
}
}
free (name);
free (filepath_txt);
free (filepath_md);
}
}
r_list_free (files);
}
} else {
// run prompt
char *name = strdup (input);
char *extra = strchr (name, ' ');
if (extra) {
*extra++ = 0;
r_str_trim (extra);
}
r_str_trim (name);
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);
free (name);
return;
}
R2AIPrompt *prompt = parse_prompt_file (filepath);
if (!prompt) {
R_LOG_ERROR ("Cannot read prompt file: %s", filepath);
free (filepath);
r_list_free (search_dirs);
free (name);
return;
}
if (!prompt->title || !prompt->command) {
R_LOG_WARN ("Prompt %s is missing required Title or Command directive", name);
}
free (filepath);
// run commands
char *cmd_output = run_commands (core, prompt->command);
// run if_command if output not empty
if (cmd_output && *cmd_output && prompt->if_command) {
char *extra_out = run_commands (core, prompt->if_command);
if (extra_out) {
char *new_output = r_str_newf ("%s\n%s", cmd_output, extra_out);
free (cmd_output);
cmd_output = new_output;
free (extra_out);
}
}
// determine the prompt to use
char *use_prompt = prompt->prompt;
if ((!cmd_output || !*cmd_output) && prompt->if_empty) {
if (!strcmp (prompt->if_empty, "exit")) {
R_LOG_ERROR ("Command output is empty, exiting prompt");
r2aiprompt_free (prompt);
free (cmd_output);
free (name);
r_list_free (search_dirs);
return;
} else {
use_prompt = prompt->if_empty;
}
}
// replace vars in prompt
char *replaced_prompt = replace_vars (core, use_prompt);
RStrBuf *sb = r_strbuf_new (replaced_prompt);
free (replaced_prompt);
if (cmd_output) {
r_strbuf_appendf (sb, "\n%s", cmd_output);
free (cmd_output);
}
if (extra) {
r_strbuf_appendf (sb, "\n%s", extra);
}
char *final_prompt = r_strbuf_drain (sb);
// now send to LLM
char *error = NULL;
char *res = r2ai (cps, (R2AIArgs){ .input = final_prompt, .model = prompt->model, .provider = prompt->provider, .error = &error, .dorag = true });
if (error) {
R_LOG_ERROR ("%s", error);
free (error);
} else if (res) {
r_cons_printf (core->cons, "%s\n", res);
free (res);
}
r2aiprompt_free (prompt);
free (final_prompt);
free (name);
}
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;
}
+158
View File
@@ -0,0 +1,158 @@
.TH R2AI 1 "October 2025" "radare2" "User Commands"
.SH NAME
r2ai \- AI assistant for radare2
.SH SYNOPSIS
.B r2ai
[\fIoptions\fR] [\fIquery\fR]
.SH DESCRIPTION
r2ai is an AI-powered plugin and command-line tool for radare2 that provides assistance with reverse engineering tasks, including decompilation, function analysis, vulnerability detection, and more. It integrates with various LLM providers like OpenAI, Anthropic, and Ollama.
.SH OPTIONS
.TP
.B \-d
Decompile the current function.
.TP
.B \-d \fIquery\fR
Ask a question about the current function.
.TP
.B \-dr
Decompile the current function with one level of recursivity.
.TP
.B \-a \fIquery\fR
Resolve a question using auto mode, which allows the AI to use tools.
.TP
.B \-e \fIk(=v)\fR
Set configuration option (same as 'r2ai.' prefix).
.TP
.B \-h
Show help message.
.TP
.B \-i \fIfile\fR \fIquery\fR
Read a file and ask the LLM with the given query.
.TP
.B \-m
Show selected model, list suggested ones, and choose one.
.TP
.B \-p \fIprovider\fR
Set LLM provider (openai, anthropic, gemini, etc.).
.TP
.B \-q
List available query prompts.
.TP
.B \-q \fIname\fR [\fIinstructions\fR]
Run predefined prompt with optional custom instructions.
.TP
.B \-r
Enter the chat REPL.
.TP
.B \-L
Show chat logs (use -Lj for JSON). Only for auto mode.
.TP
.B \-L-\fIN\fR
Delete the last N messages from the chat history.
.TP
.B \-R
Reset the chat conversation context.
.TP
.B \-Rq [\fItext\fR]
Refresh and query embeddings (see r2ai.data).
.TP
.B \fIquery\fR
Query the selected model and provider with the given query.
.SH CONFIGURATION
Configuration options can be set with \fBr2ai -e \fIkeyname\fR=\fIvalue\fR.
.TP
.B r2ai.api
Name of the provider (e.g., openai). List possibilities with \fBr2ai -e r2ai.api=?\fR.
.TP
.B r2ai.model
Model name. List possibilities with \fBr2ai -e r2ai.model=?\fR.
.TP
.B r2ai.baseurl
Remote LLM base URL (e.g., http://127.0.0.1:11434).
.TP
.B r2ai.max_tokens
Maximum output tokens or total tokens.
.TP
.B r2ai.temperature
Creativity level (0=not creative, 1=very creative).
.TP
.B r2ai.cmds
R2 command to issue and send output in context to model.
.TP
.B r2ai.lang
Programming language for decompilation (only for \fBr2ai -d\fR).
.TP
.B r2ai.hlang
Language for AI responses.
.TP
.B r2ai.prompt
User prompt for \fBr2ai -d\fR.
.TP
.B r2ai.auto.max_runs
Maximum loops in auto mode (\fBr2ai -a\fR).
.TP
.B r2ai.auto.hide_tool_output
Hide tool output in auto mode.
.TP
.B r2ai.auto.yolo
Allow AI to run commands without approval (dangerous, default false).
.TP
.B r2ai.http.use_files
Use files for arguments if CURL argument too long.
.SH QUERY PROMPTS
Query prompts are predefined templates for common tasks. Available prompts:
.TP
.B autoname
Automatically rename functions and variables.
.TP
.B decompile
Decompile the current function.
.TP
.B devices
Analyze device-related code.
.TP
.B dlopen
Analyze dynamic library loading.
.TP
.B explain
Explain the purpose of the current function.
.TP
.B libs
Analyze library usage.
.TP
.B signature
Improve function signatures.
.TP
.B varnames
Rename variables based on context.
.TP
.B vulns
Find vulnerabilities in the current function.
.SH EXAMPLES
.TP
Decompile current function:
.B r2ai -d
.TP
Ask about current function:
.B r2ai -d "what is the input buffer size?"
.TP
Use auto mode:
.B r2ai -a "find vulnerabilities in this function"
.TP
List prompts:
.B r2ai -q
.TP
Run a prompt:
.B r2ai -q explain
.TP
Set provider:
.B r2ai -p openai
.TP
Enter REPL:
.B r2ai -r
.SH SEE ALSO
.BR radare2 (1),
.BR r2pm (1),
.BR r2agent (1)
.SH AUTHOR
radare2 community
+246 -626
View File
File diff suppressed because it is too large. Load diff
+167 -81
View File
@@ -1,25 +1,62 @@
#ifndef R2AI_H
#define R2AI_H
#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
#include <time.h>
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#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"
#if R2_VERSION_NUMBER >= 50909
#define R2_PRINTF(...) r_cons_printf (core->cons, __VA_ARGS__)
#define R2_FLUSH() r_cons_flush (core->cons)
#define R2_NEWLINE() r_cons_newline (core->cons)
#define R2_PRINTLN(x) r_cons_println (core->cons, x)
#define R2_INTERRUPTED() r_cons_is_breaked (core->cons)
#define R2AI_VERSION "1.2.6"
#if 0
#define R2AI_DEFAULT_MODEL "gpt-5-mini"
#define R2AI_DEFAULT_PROVIDER "openai"
#else
#define R2_PRINTF(...) r_cons_printf (__VA_ARGS__)
#define R2_FLUSH() r_cons_flush ()
#define R2_NEWLINE() r_cons_newline()
#define R2_PRINTLN(x) r_cons_println(x)
#define R2_INTERRUPTED() r_cons_is_breaked ()
// #define R2AI_DEFAULT_MODEL "gpt-oss:20b"
// #define R2AI_DEFAULT_MODEL "gemma3:12b"
#define R2AI_DEFAULT_MODEL "mannix/jan-nano"
#define R2AI_DEFAULT_PROVIDER "ollama"
#endif
#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
// Tool definition structure
typedef struct {
char *name;
@@ -27,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;
@@ -39,32 +72,56 @@ typedef struct {
const char *id;
} R2AI_ToolCall;
/**
* Free an R2AI_ToolCall instance
*/
R_API void r2ai_tool_call_free(R2AI_ToolCall *tc);
typedef struct {
const char *type;
const char *id;
const char *name;
const char *input;
const char *data;
const char *thinking;
const char *signature;
const char *text;
char *type;
char *id;
char *name;
char *input;
char *data;
char *thinking;
char *signature;
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 {
const char *role;
const char *content;
const char *reasoning_content;
const R2AI_ContentBlocks *content_blocks;
const char *tool_call_id;
const R2AI_ToolCall *tool_calls;
int n_tool_calls;
char *role;
char *content;
char *reasoning_content;
RList *content_blocks; // RList<R2AI_ContentBlock *>
char *tool_call_id;
RList *tool_calls; // RList<R2AI_ToolCall *>
} R2AI_Message;
typedef struct {
char *title;
char *author;
char *desc;
char *command;
char *prompt;
char *requires;
char *if_empty;
char *if_command;
char *model;
char *provider;
} R2AIPrompt;
typedef struct {
uint64_t prompt_tokens;
uint64_t completion_tokens;
@@ -76,19 +133,14 @@ typedef struct {
const R2AI_Usage *usage;
} R2AI_ChatResponse;
// Messages array management
typedef struct {
R2AI_Message *messages;
int n_messages;
int cap_messages;
} 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;
@@ -98,71 +150,97 @@ typedef struct {
char **error;
} R2AIArgs;
// Stats structure from auto.c
typedef struct {
double total_cost;
double run_cost;
int total_tokens;
int run_tokens;
int total_prompt_tokens;
int run_prompt_tokens;
int total_completion_tokens;
int run_completion_tokens;
time_t start_time;
time_t total_start_time;
} R2AIStats;
// Main state structure to hold all global state
typedef struct r2ai_state_t {
RList *conversation; // Global conversation messages (from messages.c)
R2AIStats stats; // Global stats (from auto.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)
RVdb *db; // Vector database for embeddings
HtPP *model_compat_db; // Model compatibility database (from openai.c)
} R2AI_State;
/**
* 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)
*/
R_API void r2ai_conversation_init(void);
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(void);
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)
*/
R_API void r2ai_conversation_free(void);
R_API void r2ai_conversation_free(R2AI_State *state);
/**
* Free a R2AI_Message structure
@@ -173,36 +251,36 @@ R_API void r2ai_message_free(R2AI_Message *msg);
* 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
@@ -211,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
@@ -236,7 +314,7 @@ R_API char *r2ai_http_post(RCore *core, const char *url, const char *headers[],
* @param rlen Pointer to store the response length
* @return Response body as string (must be freed by caller) or NULL on error
*/
R_API char *r2ai_http_get(const char *url, const char *headers[], int *code, int *rlen);
R_API char *r2ai_http_get(RCore *core, const char *url, const char *headers[], int *code, int *rlen);
/**
* Get the base URL for a given provider
@@ -248,29 +326,31 @@ R_API char *r2ai_http_get(const char *url, const char *headers[], int *code, int
R_IPI const char *r2ai_get_provider_url(RCore *core, const char *provider);
// anthropic
R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args);
R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args);
// openai
R_IPI R2AI_ChatResponse *r2ai_openai(RCore *core, R2AIArgs args);
R_IPI void r2ai_openai_fini(void);
R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args);
R_IPI void r2ai_openai_fini(R2AI_State *state);
// auto mode
R_IPI void cmd_r2ai_a(RCore *core, const char *user_query);
R_API char *r2ai(RCore *core, R2AIArgs args);
R_IPI void cmd_r2ai_a(RCorePluginSession *cps, const char *user_query);
// R_API char *r2ai(RCore *core, R2AI_State *state, R2AIArgs args);
R_API char *r2ai(RCorePluginSession *cps, R2AIArgs args);
R_API bool r2ai_init(RCorePluginSession *cps);
R_API bool r2ai_fini(RCorePluginSession *cps);
R_IPI R2AI_ChatResponse *r2ai_llmcall(RCore *core, R2AIArgs args);
R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args);
R_IPI void cmd_r2ai_logs(RCore *core);
R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args);
/**
* Create a conversation with system prompt and optional user message
*/
R_API R2AI_Messages *create_conversation(const char *user_message);
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(RCore *core, 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
@@ -281,5 +361,11 @@ R_API char *r_json_to_string(const RJson *json);
* Helper function to convert RJson to PJ
*/
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
-1018
View File
File diff suppressed because it is too large. Load diff
+22
View File
@@ -0,0 +1,22 @@
#ifndef INCLUDE_R2AI_PRIV__H
#define INCLUDE_R2AI_PRIV__H
#include "r2ai.h"
typedef struct {
const char *name;
const char *url;
bool requires_api_key;
bool uses_anthropic_header;
bool uses_tags_endpoint;
bool uses_system_ls;
} R2AIProvider;
R_IPI char *r2ai_get_api_key(RCore *core, const char *provider);
R_IPI const R2AIProvider *r2ai_get_provider(const char *name);
R_IPI const char *r2ai_get_provider_url(RCore *core, const char *provider);
R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider);
R_IPI void r2ai_list_providers(RCore *core, RStrBuf *sb);
R_IPI void r2ai_refresh_embeddings(RCorePluginSession *cps);
#endif
+299
View File
@@ -0,0 +1,299 @@
/* r2ai - Copyright 2023-2025 pancake */
#define R_LOG_ORIGIN "rawtools"
#include "r2ai.h"
#include "r2ai_priv.h"
// Function to build the rawtools prompt with available tools
static const char *RAWTOOLS_PROMPT =
"Common radare2 commands you can use with r2cmd:\n"
"- i: Show binary information\n"
"- iI: Show detailed binary info\n"
"- iz: Show strings\n"
"- afl: List functions\n"
"- pdf @ function: Disassemble function\n"
"- px @ address: Show hexdump\n"
"- aaa: Analyze all\n"
"- s address: Seek to address\n"
"- ? command: Get help\n\n"
"To run radare2 commands, use the r2cmd tool with JSON arguments.\n\n"
"TOOL: r2cmd\n"
"USAGE: TOOL: r2cmd {\"command\": \"your_command_here\"}\n\n"
"Example: TOOL: r2cmd {\"command\": \"i\"}\n\n"
"If you don't need to run commands, just answer directly.\n\n";
// Function to parse raw tool call from response text
static bool parse_raw_tool_call(const char *response, char **tool_name, char **tool_args) {
if (!response || !tool_name || !tool_args) {
return false;
}
*tool_name = NULL;
*tool_args = NULL;
// First try to parse "TOOL: " format
const char *tool_prefix = "TOOL: ";
const char *line_start = response;
while ((line_start = strstr (line_start, tool_prefix))) {
// Check if it's at the beginning of a line
if (line_start == response || *(line_start - 1) == '\n') {
// Found a tool call
const char *tool_start = line_start + strlen (tool_prefix);
const char *space_pos = strchr (tool_start, ' ');
if (space_pos) {
// Extract tool name
size_t name_len = space_pos - tool_start;
*tool_name = r_str_ndup (tool_start, name_len);
// Extract arguments (rest of the line)
*tool_args = strdup (space_pos + 1);
// Trim whitespace from arguments (simple implementation)
if (*tool_args) {
char *start = *tool_args;
while (*start && (*start == ' ' || *start == '\t')) {
start++;
}
char *end = start + strlen (start) - 1;
while (end > start && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) {
end--;
}
*(end + 1) = '\0';
if (start != *tool_args) {
memmove (*tool_args, start, strlen (start) + 1);
}
}
return true;
}
}
line_start += strlen (tool_prefix);
}
// If no TOOL: format found, try to parse JSON response
// Look for JSON that looks like tool arguments
const char *json_start = strstr (response, "{\"command\"");
if (json_start) {
*tool_name = strdup ("r2cmd");
*tool_args = strdup (json_start);
// Trim trailing whitespace and newlines
char *end = *tool_args + strlen (*tool_args) - 1;
while (end > *tool_args && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) {
*end = '\0';
end--;
}
return true;
}
return false;
}
// Function to handle rawtools mode in LLM call
R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args) {
if (!cps) {
return NULL;
}
RCore *core = cps->core;
// Modify the system prompt to include rawtools instructions only if no tool messages
const char *original_system_prompt = args.system_prompt;
// For rawtools, use a shorter system prompt to avoid token limits
const char *short_system_prompt = "You are a reverse engineer using radare2. The binary is loaded. Use r2cmd tool for analysis.";
// Check for initial command output in messages
char *init_output = NULL;
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);
if (msg && msg->role && !strcmp (msg->role, "system") && msg->content &&
strstr (msg->content, "Here is some information about the binary")) {
init_output = strdup (msg->content);
break;
}
}
}
char *enhanced_system_prompt = NULL;
if (init_output) {
enhanced_system_prompt = r_str_newf ("%s\n\n%s\n\n%s", short_system_prompt, init_output, RAWTOOLS_PROMPT);
} else {
enhanced_system_prompt = r_str_newf ("%s\n\n%s", short_system_prompt, RAWTOOLS_PROMPT);
}
free (init_output);
// Temporarily modify args to use enhanced prompt and no tools (since we're using prompt engineering)
R2AIArgs rawtools_args = args;
rawtools_args.system_prompt = enhanced_system_prompt;
rawtools_args.tools = NULL; // Disable native tool calling
// Make the LLM call directly to provider
const char *provider = rawtools_args.provider? rawtools_args.provider: r_config_get (core->config, "r2ai.api");
if (!provider) {
R_LOG_ERROR ("No provider defined");
return NULL;
}
R2AI_ChatResponse *response = NULL;
const R2AIProvider *p = r2ai_get_provider (provider);
if (p && p->uses_anthropic_header) {
response = r2ai_anthropic (cps, rawtools_args);
} else {
response = r2ai_openai (cps, rawtools_args);
}
free (enhanced_system_prompt);
if (!response || !response->message || !response->message->content) {
return response;
}
R_LOG_DEBUG ("[RAWTOOLS] Raw model response: %s", response->message->content);
// Check if the response contains a raw tool call
char *tool_name = NULL;
char *tool_args = NULL;
if (parse_raw_tool_call (response->message->content, &tool_name, &tool_args)) {
R_LOG_DEBUG ("Raw tool call detected: %s with args: %s", tool_name, tool_args);
// Find the tool in our tools list
RList *tools = r2ai_get_tools (cps->core, cps->data);
bool tool_found = false;
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);
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_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);
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;
tool_args = NULL;
}
return response;
}
// No tool call found
free (tool_name);
free (tool_args);
// If the response has no content, try again with normal mode (without rawtools prompt)
if (!response->message || !response->message->content || !*response->message->content) {
R_LOG_DEBUG ("No tool call found and no content, falling back to normal mode");
// Free the current response
if (response->message) {
r2ai_message_free ((R2AI_Message *)response->message);
}
free (response);
// Modify args to use original system prompt without rawtools enhancement
R2AIArgs fallback_args = args;
fallback_args.system_prompt = original_system_prompt;
fallback_args.tools = args.tools; // Keep tools for fallback
// Call provider directly
const char *provider = fallback_args.provider? fallback_args.provider: r_config_get (core->config, "r2ai.api");
if (!provider) {
R_LOG_ERROR ("No provider defined");
return NULL;
}
R2AI_ChatResponse *fallback_response = NULL;
const R2AIProvider *p = r2ai_get_provider (provider);
if (p && p->uses_anthropic_header) {
fallback_response = r2ai_anthropic (cps, fallback_args);
} else {
fallback_response = r2ai_openai (cps, fallback_args);
}
if (fallback_response && fallback_response->message && fallback_response->message->content && *fallback_response->message->content) {
return fallback_response;
}
// If still no content, create a response with warning
if (fallback_response) {
if (fallback_response->message) {
r2ai_message_free ((R2AI_Message *)fallback_response->message);
}
free (fallback_response);
}
// Create a warning response
R2AI_ChatResponse *warning_response = R_NEW0 (R2AI_ChatResponse);
R2AI_Message *msg = R_NEW0 (R2AI_Message);
msg->role = strdup ("assistant");
msg->content = strdup ("Warning: LLM provided no response content");
*((R2AI_Message **)&warning_response->message) = msg;
R_LOG_WARN ("LLM provided no response content");
return warning_response;
}
return response;
}
+223 -193
View File
@@ -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 in a quickjs environment. Only what you console.log will be returned.",
.parameters = "{\
\"type\": \"object\",\
\"properties\": {\
\"script\": {\
\"type\": \"string\",\
\"description\": \"The JavaScript script to execute\"\
}\
},\
\"required\": [\"script\"]\
}"
.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\"]"
"}"
};
// 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,95 +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);
#if R2_VERSION_NUMBER >= 50909
r_cons_editor (core->cons, NULL, *edited_command);
#else
r_cons_editor (NULL, *edited_command);
#endif
command = *edited_command;
r_cons_editor (core->cons, NULL, input_command);
} else {
// For single-line commands, push the command to input buffer
R2_NEWLINE ();
R2_PRINTF ("\x1b[31m[edit cmd]>\x1b[0m ");
R2_FLUSH ();
// Push the command to the input buffer
// Get user input with command pre-filled
#if R2_VERSION_NUMBER >= 50909
r_cons_readpush (core->cons, command, strlen (command));
r_cons_readpush (core->cons, "\x05", 1); // Ctrl+E - move to end
r_cons_newline (core->cons);
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);
#else
r_cons_readpush (command, strlen (command));
r_cons_readpush ("\x05", 1); // Ctrl+E - move to end
const char *readline_result = r_line_readline ();
#endif
// Check if interrupted or ESC pressed (readline_result is NULL or empty)
if (R2_INTERRUPTED () || R_STR_ISEMPTY (readline_result)) {
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);
R2_PRINTF ("%s", red_command);
R2_FLUSH ();
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\" }");
@@ -377,29 +420,18 @@ R_API char *r2ai_qjs(RCore *core, RJson *args, bool hide_tool_output) {
if (is_multiline) {
// Use editor for multi-line scripts
edited_script = strdup (script);
#if R2_VERSION_NUMBER >= 50909
r_cons_editor (core->cons, NULL, edited_script);
#else
r_cons_editor (NULL, edited_script);
#endif
script = edited_script;
} else {
// For single-line scripts, push the script to input buffer
R2_PRINTF ("\x1b[31m[edit js]>\x1b[0m ");
R2_FLUSH ();
#if R2_VERSION_NUMBER >= 50909
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);
#else
r_cons_readpush (script, strlen (script));
r_cons_readpush ("\x05", 1); // Ctrl+E - move to end
const char *readline_result = r_line_readline ();
#endif
// Check if interrupted or ESC pressed (readline_result is NULL or empty)
if (R2_INTERRUPTED () || R_STR_ISEMPTY (readline_result)) {
if (r_cons_is_breaked (core->cons) || R_STR_ISEMPTY (readline_result)) {
free (edited_script); // Free if already allocated
return strdup ("R2AI_SIGINT");
}
@@ -416,11 +448,18 @@ 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);
R2_PRINTF ("%s\n", print_script_rendered);
R2_FLUSH ();
r_cons_printf (core->cons, "%s\n", print_script_rendered);
r_cons_flush (core->cons);
free (print_script);
free (print_script_rendered);
}
@@ -435,11 +474,7 @@ R_API char *r2ai_qjs(RCore *core, RJson *args, bool hide_tool_output) {
return strdup ("{ \"res\":\"Failed to create script payload\" }");
}
#if R2_VERSION_NUMBER >= 50909
char *payload_base64 = r_base64_encode_dyn ((const ut8 *)payload, strlen (payload));
#else
char *payload_base64 = r_base64_encode_dyn ((const char *)payload, strlen (payload));
#endif
if (!payload_base64) {
free (payload);
return strdup ("{ \"res\":\"Failed to encode script\" }");
@@ -463,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\" }");
}
@@ -481,20 +516,19 @@ R_API char *execute_tool(RCore *core, const char *tool_name, const char *args, c
}
bool hide_tool_output = r_config_get_b (core->config, "r2ai.auto.hide_tool_output");
char *print_name = r_str_newf ("\x1b[1;32m\x1b[4m[%s]>\x1b[0m ", tool_name);
R2_PRINTF ("%s\n", print_name);
R2_FLUSH ();
free (print_name);
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
@@ -515,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
@@ -526,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) {
@@ -627,10 +657,10 @@ R_API char *execute_tool(RCore *core, const char *tool_name, const char *args, c
return r_str_newf ("Error running tool: Unknown error\nCommand: %s", args);
}
if (!hide_tool_output) {
R2_NEWLINE ();
R2_PRINTF ("%s\n", result);
R2_NEWLINE ();
R2_FLUSH ();
r_cons_newline (core->cons);
r_cons_printf (core->cons, "%s\n", result);
r_cons_newline (core->cons);
r_cons_flush (core->cons);
}
r_str_ansi_strip (result);
+7 -8
View File
@@ -1,7 +1,6 @@
/* r2ai - MIT - Copyright 2024-2025 pancake */
#include <r_util.h>
#include "r_vdb.h"
#include "r2ai.h"
static Vector vector_new(int dim) {
Vector v;
@@ -19,7 +18,7 @@ static void vector_free(Vector *v) {
/*-------------------------------
* Distance Function Using Squared Euclidean Distance
* For two unit vectors: dist^2 = 2 - 2 * (dot product)
* For two unit vectors: dist^2 = 2 - 2 *(dot product)
*-------------------------------*/
static float squared_distance(const Vector *a, const Vector *b) {
if (a->dim != b->dim) {
@@ -34,14 +33,14 @@ static float squared_distance(const Vector *a, const Vector *b) {
}
/*-------------------------------
KDNode Utility Functions
-------------------------------*/
KDNode Utility Functions
-------------------------------*/
static KDNode *create_kdnode(const Vector *v, const char *text, int split_dim) {
KDNode *node = (KDNode *)malloc (sizeof (KDNode));
node->point.dim = v->dim;
node->point.data = (float *)malloc (sizeof (float) * v->dim);
memcpy (node->point.data, v->data, sizeof (float) * v->dim);
node->text = text ? strdup (text) : NULL;
node->text = text? strdup (text): NULL;
node->split_dim = split_dim;
node->left = NULL;
node->right = NULL;
@@ -212,8 +211,8 @@ void kd_search_knn_recursive(KDNode *node, const Vector *query, RVdbResultSet *r
int axis = node->split_dim;
float diff = query->data[axis] - node->point.data[axis];
KDNode *first = (diff < 0) ? node->left : node->right;
KDNode *second = (diff < 0) ? node->right : node->left;
KDNode *first = (diff < 0)? node->left: node->right;
KDNode *second = (diff < 0)? node->right: node->left;
kd_search_knn_recursive (first, query, rs, depth + 1, dim);
+1 -1
View File
@@ -136,7 +136,7 @@ static void compute_embedding(RVdb *db, const char *text, float *embedding, unsi
// Compute term frequency: tf = 1 + log (token_count)
float tf = 1.0f + log ((float)dt->count);
RVdbToken *t = gtfidf_find (db->tokens, dt->token);
float df_value = t ? t->df : 1.0f;
float df_value = t? t->df: 1.0f;
// Compute inverse document frequency;
float idf = log (((float)db->total_docs + 1.0f) / ((float)df_value + 1.0f)) + 1.0f;
float weight = tf * idf;