mirror of
https://github.com/radareorg/r2ai.git
synced 2026-11-10 04:01:27 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83b96f7a3e | ||
|
|
e2ee8cdbb6 | ||
|
|
8630e95d83 | ||
|
|
e07293e40f | ||
|
|
415c546f92 |
No files matched your search
@@ -93,11 +93,8 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p release
|
||||
# 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 -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
|
||||
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/*
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
TARGETS=all clean mrproper user-install install user-uninstall uninstall fmt indent
|
||||
|
||||
TARGETS=all clean mrproper user-install install user-uninstall uninstall
|
||||
$(TARGETS):
|
||||
$(MAKE) -C src $@
|
||||
|
||||
.PHONY: $(TARGETS)
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
This repository contains two plugins for radare2:
|
||||
|
||||
* **r2ai** - native [AI plugin](./src/README.md) for radare2
|
||||
* **decai** - r2js plugin with special [focus on decompilation](./decai/README.md)
|
||||
* **r2ai** - native plugin for radare2
|
||||
* **decai** - r2js plugin with special focus on decompilation
|
||||
|
||||
If you are looking to use radare2 with other agents via MCP:
|
||||
|
||||
@@ -37,21 +37,12 @@ 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
|
||||
|
||||
## User defined Prompts
|
||||
## Practical Examples
|
||||
|
||||
```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]>
|
||||
```
|
||||
* Enhanced decompilation with `r2ai -d`
|
||||
* Autoname functions with `r2ai -n`
|
||||
* Explain function with `r2ai -x`
|
||||
* Find vulnerabilities with `r2ai -V`
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -94,15 +85,12 @@ r2ai -e model=claude-3-7-sonnet-20250219
|
||||
r2ai -e max_tokens=64000
|
||||
```
|
||||
|
||||
## Further Reading
|
||||
## Documentation
|
||||
|
||||
* 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://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
|
||||
* Malware analysis [with r2ai](https://github.com/cryptax/talks/blob/master/BSidesKristiansand-2025/r2ai.pdf) by Cryptax
|
||||
|
||||
## Videos
|
||||
|
||||
- [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)
|
||||
- https://infosec.exchange/@radareorg/111946255058894583
|
||||
+1
-3
@@ -1,5 +1,3 @@
|
||||
#!/bin/sh
|
||||
vim decai/decai.r2.js
|
||||
vim src/r2ai.h
|
||||
vim src/meson.build
|
||||
vim src/r2ai.c
|
||||
vim VERSION
|
||||
+5
-16
@@ -1,6 +1,5 @@
|
||||
(function () {
|
||||
// Constants and configuration
|
||||
const VERSION = "1.2.0";
|
||||
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";
|
||||
@@ -766,20 +765,13 @@ Use radare2 to resolve user requests.
|
||||
|
||||
try {
|
||||
const res = http.post(url, [], JSON.stringify(payload));
|
||||
if (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));
|
||||
return utils.filterResponse(res.message.content);
|
||||
} catch (e) {
|
||||
if (res.error && res.error.indexOf("try pulling")) {
|
||||
const modelName = res.error.split(/"/g)[1];
|
||||
res.error += "\n!ollama run " + modelName;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e.stack);
|
||||
return "ERROR: " + e.message;
|
||||
return "ERROR: " + (res.error || e.message);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -874,10 +866,8 @@ Use radare2 to resolve user requests.
|
||||
// Command handlers
|
||||
const commands = {
|
||||
help: () => {
|
||||
let helpmsg = "";
|
||||
const msg = (m) => helpmsg += " " + COMMAND + " " + m + "\n";
|
||||
helpmsg += "Usage: " + COMMAND + " (-h) ...\n";
|
||||
helpmsg += "Version: " + VERSION + "\n";
|
||||
const msg = (m) => console.error(" " + COMMAND + " " + m);
|
||||
console.error("Usage: " + COMMAND + " (-h) ...");
|
||||
msg("-a [query] - solve query with auto mode");
|
||||
msg("-d [f1 ..] - decompile given functions");
|
||||
msg("-dd [..] - same as above, but ignoring cache");
|
||||
@@ -898,7 +888,6 @@ 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) => {
|
||||
|
||||
@@ -1,14 +1,41 @@
|
||||
SHELL=/bin/bash
|
||||
|
||||
MODELS=gpt-oss:20b gemma3:4b gemma3:12b
|
||||
MODELS+=granite3-dense:8b mannix/jan-nano:latest yi-coder:latest qwen3:latest deepseek-coder-v2:latest hhao/qwen2.5-coder-tools:latest qwen2.5-coder:latest
|
||||
PROVIDER=ollama
|
||||
MODELS=granite3-dense:8b mannix/jan-nano:latest yi-coder:latest qwen3:latest deepseek-coder-v2:latest hhao/qwen2.5-coder-tools:latest qwen2.5-coder:latest
|
||||
MODELS=gemma3:1b
|
||||
LANGS=Dart Perl Ruby Javascript PHP C
|
||||
BINARIES=files/complex1 files/complex2 files/complex3
|
||||
MODELS=gpt-4o
|
||||
PROVIDER=openai
|
||||
|
||||
all:
|
||||
for a in ${MODELS} ; do \
|
||||
declare -a ARGS; \
|
||||
for l in ${LANGS} ; do \
|
||||
ARGS+=("echo \"decai -e model=$$a;decai -e lang=$$l;s 0x100000924;af;decai -d\" | r2 -q /bin/ls"); \
|
||||
mkdir -p tmp
|
||||
for b in $(BINARIES) ; do \
|
||||
for a in $(MODELS) ; do \
|
||||
echo "$$b $$a"; \
|
||||
./run.sh $$b $$a ${PROVIDER} ${LANGS} > tmp/$${b##*/}_$${a//\//_}.html ; \
|
||||
done ; \
|
||||
./run.sh "$${ARGS[@]}" > ${a//\//_}.html ; done
|
||||
done
|
||||
|
||||
.PHONY: run index evaluate
|
||||
|
||||
compile: $(BINARIES)
|
||||
|
||||
files/complex1: files/complex1.c
|
||||
gcc -o $@ $<
|
||||
|
||||
files/complex2: files/complex2.c
|
||||
gcc -o $@ $<
|
||||
|
||||
files/complex3: files/complex3.c
|
||||
gcc -o $@ $<
|
||||
|
||||
lmstudio:
|
||||
for b in $(BINARIES) ; do \
|
||||
for a in $(MODELS) ; do \
|
||||
./run.sh $$b $$a lmstudio ${LANGS} > $${b##*/}_$${a//\//_}.html ; \
|
||||
done ; \
|
||||
done
|
||||
|
||||
clean:
|
||||
rm -rf tmp
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Evaluate decompiled outputs using mai and determine best model+lang
|
||||
|
||||
echo "Evaluating decompiled outputs..."
|
||||
|
||||
tmpfile=$(mktemp)
|
||||
MAI="mai -q -p openai -m gpt-4o"
|
||||
# MAI="mai -q"
|
||||
|
||||
for orig in files/complex1.c files/complex2.c files/complex3.c; do
|
||||
base=$(basename $orig .c)
|
||||
original_code=$(cat $orig)
|
||||
for f in tmp/${base}_*.txt; do
|
||||
if [ -f "$f" ]; then
|
||||
decompiled_code=$(cat $f)
|
||||
#echo "Comparing $orig $f"
|
||||
result=`echo n | ${MAI} -q -r "/template files/CMPROMPT.txt original=@${orig} decompiled=@${f}"`
|
||||
score=`echo "$result" | head -n 1 | grep -oE '[0-9]+'` # |head -n 1`
|
||||
perfect=`echo "$result" | grep 'No sign' && echo yes`
|
||||
[ -z "$score" ] && score=0
|
||||
if [ "$perfect" = yes ]; then
|
||||
score=100
|
||||
else
|
||||
score="$((0+${score}))"
|
||||
fi
|
||||
echo "$score\t$f"
|
||||
if [ -n "$score" ]; then
|
||||
key="${f%.*}" # remove .txt
|
||||
# Check if key exists in tmpfile
|
||||
existing=$(grep "^$key " $tmpfile | awk '{print $2}')
|
||||
if [ -n "$existing" ]; then
|
||||
new_score=$(( existing + score ))
|
||||
sed -i "s/^$key .*/$key $new_score/" $tmpfile
|
||||
else
|
||||
echo "$key $score" >> $tmpfile
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# Find the best (highest total score)
|
||||
best_key=""
|
||||
best_score=0
|
||||
while read key score; do
|
||||
if [ "$score" -gt "$best_score" ]; then
|
||||
best_score=$score
|
||||
best_key=$key
|
||||
fi
|
||||
done < $tmpfile
|
||||
|
||||
rm $tmpfile
|
||||
|
||||
if [ -n "$best_key" ]; then
|
||||
echo "Best model+language combination: $best_key with total score $best_score"
|
||||
else
|
||||
echo "No scores found"
|
||||
fi
|
||||
@@ -0,0 +1,17 @@
|
||||
Compare the two provided code in C with the decompiled counterpart. Ignore function or variable names, syntax differences, language-specific constructs, and formatting. Focus solely on what the code does: the operations performed, control flow, logic, and functional behavior. Determine if they are functionally equivalent in terms of their effects and computations.
|
||||
|
||||
Output only:
|
||||
- A numeric score from 1 to 100 (100 being identical logic and functionality)
|
||||
- Followed by a single sentence describing the most relevant semantic differences (or "No significant semantic differences" if score is 95+)
|
||||
|
||||
**Original Source Code:**
|
||||
|
||||
```C
|
||||
{original}
|
||||
```
|
||||
|
||||
**Decompiled Code:**
|
||||
|
||||
```
|
||||
{decompiled}
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int main() {
|
||||
int n = 5;
|
||||
int *arr = (int *)malloc(n * sizeof(int));
|
||||
if (arr == NULL) return 1;
|
||||
for (int i = 0; i < n; i++) {
|
||||
arr[i] = i * i;
|
||||
}
|
||||
int sum = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
sum += arr[i];
|
||||
for (int j = 0; j < i; j++) {
|
||||
sum -= j;
|
||||
}
|
||||
}
|
||||
printf("Sum: %d\n", sum);
|
||||
free(arr);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
struct Node {
|
||||
int data;
|
||||
struct Node *next;
|
||||
};
|
||||
|
||||
int main() {
|
||||
struct Node *head = NULL;
|
||||
struct Node *tail = NULL;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
|
||||
if (newNode == NULL) return 1;
|
||||
newNode->data = i * 2;
|
||||
newNode->next = NULL;
|
||||
if (head == NULL) {
|
||||
head = newNode;
|
||||
tail = newNode;
|
||||
} else {
|
||||
tail->next = newNode;
|
||||
tail = newNode;
|
||||
}
|
||||
}
|
||||
struct Node *current = head;
|
||||
int sum = 0;
|
||||
while (current != NULL) {
|
||||
sum += current->data;
|
||||
current = current->next;
|
||||
}
|
||||
printf("Sum: %d\n", sum);
|
||||
current = head;
|
||||
while (current != NULL) {
|
||||
struct Node *temp = current;
|
||||
current = current->next;
|
||||
free(temp);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
int x = 42;
|
||||
int y = 0;
|
||||
switch (x & 15) {
|
||||
case 0:
|
||||
y = x << 1;
|
||||
break;
|
||||
case 1:
|
||||
y = x >> 1;
|
||||
break;
|
||||
case 2:
|
||||
y = x ^ 255;
|
||||
break;
|
||||
case 3:
|
||||
y = ~x;
|
||||
break;
|
||||
default:
|
||||
y = x | 128;
|
||||
break;
|
||||
}
|
||||
if ((y & 1) == 0) {
|
||||
y += 10;
|
||||
} else {
|
||||
y -= 5;
|
||||
}
|
||||
for (int i = 0; i < 5; i++) {
|
||||
y = (y << 2) | (y >> 30);
|
||||
}
|
||||
printf("Result: %d\n", y);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
cp decai/BENCH.txt tmp/BENCH.txt
|
||||
# Find all directories containing BENCH.txt
|
||||
dirs=$(find . -name "BENCH.txt" -type f | xargs dirname | sort | uniq)
|
||||
|
||||
@@ -46,4 +47,4 @@ cat <<EOF >> benchmark_index.html
|
||||
<br />
|
||||
<br />
|
||||
</html>
|
||||
EOF
|
||||
EOF
|
||||
@@ -1,12 +1,55 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Generate HTML output for command executions with collapsible sections and width control
|
||||
# If PLAIN=1, output plain text instead
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: $0 command1 [command2 ...]"
|
||||
if [ $# -lt 4 ]; then
|
||||
echo "Usage: $0 <binary> <model> <provider> <lang1> [lang2 ...]"
|
||||
echo "Or: $0 command1 [command2 ...] (legacy)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if first arg is a file (binary), assume new format
|
||||
if [ -f "$1" ]; then
|
||||
binary="$1"
|
||||
model="$2"
|
||||
provider="$3"
|
||||
shift 3
|
||||
langs=("$@")
|
||||
commands=()
|
||||
for lang in "${langs[@]}"; do
|
||||
# commands+=("# $lang echo \"decai -e model=$model;decai -e lang=$lang;decai -e api=$provider;af;decai -d\" | r2 -q $binary 2> /dev/null")
|
||||
commands+=("# $lang r2 -e scr.color=0 -qcq -qc \"decai -e model=$model;decai -e lang=$lang;decai -e api=$provider;af;decai -d\" $binary")
|
||||
done
|
||||
set -- "${commands[@]}"
|
||||
else
|
||||
# Legacy mode
|
||||
commands=("$@")
|
||||
fi
|
||||
|
||||
if [ "$PLAIN" = "1" ]; then
|
||||
# In plain mode, execute each command and print raw output.
|
||||
# Also, if TXTFILE is set, save combined raw output there.
|
||||
tmp_plain=$(mktemp)
|
||||
for cmd in "$@"; do
|
||||
if [[ "$cmd" =~ ^#[[:space:]]*(.*) ]]; then
|
||||
exec_cmd="${BASH_REMATCH[1]}"
|
||||
else
|
||||
exec_cmd="$cmd"
|
||||
fi
|
||||
tmpout=$(mktemp)
|
||||
eval "$exec_cmd" > "$tmpout" 2>&1 || true
|
||||
mv "$tmpout" "$tmp_plain"
|
||||
rm -f "$tmpout"
|
||||
done
|
||||
if [ -n "$TXTFILE" ]; then
|
||||
mv "$tmp_plain" "$TXTFILE"
|
||||
else
|
||||
rm -f "$tmp_plain"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Timeout in seconds for each command (configurable via TIMEOUT environment variable)
|
||||
TIMEOUT=${TIMEOUT:-30}
|
||||
|
||||
@@ -109,24 +152,27 @@ EOF
|
||||
|
||||
# Process each command
|
||||
for cmd in "$@"; do
|
||||
# Extract title: if command starts with #, use the rest as title, else use the command
|
||||
if [[ "$cmd" =~ ^#[[:space:]]*(.*) ]]; then
|
||||
# Extract title: if command starts with #, parse title and command, else use the command
|
||||
if [[ "$cmd" =~ ^#[[:space:]]*([^[:space:]]+)[[:space:]]*(.*) ]]; then
|
||||
title="${BASH_REMATCH[1]}"
|
||||
exec_cmd="${cmd#*#}"
|
||||
exec_cmd="${exec_cmd#"${exec_cmd%%[![:space:]]*}"}" # trim leading spaces
|
||||
exec_cmd="${BASH_REMATCH[2]}"
|
||||
else
|
||||
title="$cmd"
|
||||
exec_cmd="$cmd"
|
||||
fi
|
||||
|
||||
echo "RUN $exec_cmd" > /dev/stderr
|
||||
# Execute command and time it
|
||||
start_time=$(date +%s.%N)
|
||||
tmpfile=$(mktemp)
|
||||
eval "$exec_cmd" > "$tmpfile" 2>&1 &
|
||||
# echo "eval $exec_cmd > $tmpfile" > /dev/stder
|
||||
eval "$exec_cmd" > "$tmpfile" 2> /dev/null &
|
||||
pid=$!
|
||||
(sleep $TIMEOUT && kill $pid 2>/dev/null) &
|
||||
wait $pid 2>/dev/null
|
||||
output=$(cat "$tmpfile")
|
||||
#echo "$output" > /dev/stderr
|
||||
|
||||
rm "$tmpfile"
|
||||
exit_code=$?
|
||||
end_time=$(date +%s.%N)
|
||||
@@ -139,8 +185,16 @@ for cmd in "$@"; do
|
||||
execution_time=$(printf "%.3f" "$execution_time")
|
||||
fi
|
||||
|
||||
# Save plain output to .txt file if in new format
|
||||
if [ -n "$binary" ]; then
|
||||
txtfile="tmp/${binary##*/}_${model//\//_}_${title}.txt"
|
||||
mkdir -p "$(dirname "$txtfile")" 2>/dev/null || true
|
||||
echo "$output" > "$txtfile"
|
||||
fi
|
||||
|
||||
# Escape HTML in output
|
||||
escaped_output=$(echo "$output" | sed 's/&/\&/g; s/</\</g; s/>/\>/g')
|
||||
#escaped_output=$(echo "$output" | sed 's/&/\&/g; s/</\</g; s/>/\>/g')
|
||||
escaped_output="$output"
|
||||
|
||||
cat << EOF
|
||||
<div class="command-section expanded">
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# 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
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Automatically suggest a better name for this function
|
||||
Author: pancake
|
||||
Command: afv;r2ai -d
|
||||
Prompt: Suggest a better name for this function. Output only the suggested function name (must be one alphanumeric word) prefixed with `'afn '. Check the code below:
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Augmented decompilation based on LLM
|
||||
Author: pancake
|
||||
Command: pdc
|
||||
Prompt: $(-e r2ai.prompt). Translate the code into $(-e r2ai.lang) programming language.
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Find and explain devices used
|
||||
Author: pancake
|
||||
Command: izqq~^/dev
|
||||
Prompt: explain in one line the purpose of each of these devices
|
||||
@@ -1,6 +0,0 @@
|
||||
Title: List libraries loaded with dlopen
|
||||
Description: Some libraries are loaded
|
||||
Requires: analysis
|
||||
Command: iiq~dlopen
|
||||
If-Empty: This binary is not using dlopen
|
||||
If-Command: e emu.str=true;pd--5@@c:axtq sym.imp.dlopen
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Explain the current function
|
||||
Author: pancake
|
||||
Command: r2ai -d
|
||||
Prompt: Analyze function calls, comments and strings, ignore registers and memory accesses. Considering the references and involved loops; explain the purpose of this function in one or two short sentences. Output must be only the translation of the explanation in $(-e r2ai.hlang)
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Group imports by Libraries
|
||||
Author: pancake
|
||||
Commands: iiq;il
|
||||
Query: Group the imports that belong to each library and describe its purpose and usage scope
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Suggest an improved function signature
|
||||
Author: pancake
|
||||
Command: ?e variables;afv;?e function name:;afd;?e current signature:?e afs
|
||||
Prompt: Analyze the uses of the arguments and return value to infer the signature, identify which is the correct type for the resturn. Do NOT print the function body. Output must be *ONLY* the function signature prefixed with the `'afs ` text
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Better variable names
|
||||
Author: pancake
|
||||
Command: afv
|
||||
Prompt: Output only the radare2 command without markdown, guess a better name and type for each local variable and function argument taking using. output an r2 script using afvn and afvt commands
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Find vulnerabilities or bugs in the current function
|
||||
Author: pancake
|
||||
Command: pdc
|
||||
Prompt: Find vulnerabilities, do not show the code, only a short summary of the resulting analysis report including suggested good practices to address the problem and a practical exploit example.
|
||||
@@ -0,0 +1,40 @@
|
||||
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
|
||||
@@ -1,23 +0,0 @@
|
||||
# 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
|
||||
|
||||
## Formatting Style
|
||||
|
||||
- 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
|
||||
|
||||
## Actions
|
||||
|
||||
Run the following commands to perform the action described
|
||||
|
||||
- FORMAT: `make fmt`
|
||||
- COMPILE: `make` (with no extra arguments)
|
||||
- INSTALL: `make user-install`
|
||||
+11
-30
@@ -1,20 +1,13 @@
|
||||
DOTEXE=
|
||||
# DOTEXE=.exe
|
||||
DOTLIB=.$(shell r2 -NNH R2_LIBEXT)
|
||||
DOTLIB=.$(shell r2 -H R2_LIBEXT)
|
||||
|
||||
R2SYSPLUGDIR=$(shell r2 -NNH R2_LIBR_PLUGINS)
|
||||
R2USRPLUGDIR=$(shell r2 -NNH R2_USER_PLUGINS)
|
||||
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)
|
||||
R2BINDIR=$(shell r2pm -H R2PM_BINDIR)
|
||||
|
||||
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
|
||||
@@ -38,7 +31,6 @@ ifeq ($(USE_LIBCURL),1)
|
||||
endif
|
||||
|
||||
OBJS=r2ai.o
|
||||
OBJS+=llm.o
|
||||
OBJS+=auto.o
|
||||
OBJS+=vdb.o
|
||||
OBJS+=tools.o
|
||||
@@ -47,7 +39,6 @@ OBJS+=anthropic.o
|
||||
OBJS+=r2ai_http.o
|
||||
OBJS+=openai.o
|
||||
OBJS+=markdown.o
|
||||
OBJS+=query.o
|
||||
|
||||
all: $(OBJS) r2check
|
||||
$(CC) -fPIC -shared -o r2ai$(DOTLIB) $(OBJS) $(CFLAGS) $(LDFLAGS)
|
||||
@@ -63,34 +54,24 @@ doc-install:
|
||||
|
||||
vdb.o: vdb_embed.inc.c
|
||||
|
||||
fmt format indent:
|
||||
clang-format-radare2 *.c
|
||||
indent:
|
||||
for a in *.c ; do python indent.py $$a ; done
|
||||
|
||||
user-install: user-uninstall
|
||||
mkdir -p $(R2USRPLUGDIR)
|
||||
mkdir -p $(R2BINDIR)
|
||||
mkdir -p $(HOME)/.config/r2ai/prompts
|
||||
$(MAKE) user-uninstall
|
||||
cp -f r2ai$(DOTLIB) $(R2USRPLUGDIR)/r2ai$(DOTLIB)
|
||||
cp -f r2ai$(DOTEXE) $(R2BINDIR)/r2ai$(DOTEXE)
|
||||
cp -r ../prompts/* $(HOME)/.config/r2ai/prompts/
|
||||
cp -f r2ai$(DOTLIB) $(R2USRPLUGDIR)
|
||||
cp -f r2ai$(DOTEXE) $(R2BINDIR)
|
||||
|
||||
user-uninstall:
|
||||
rm -f $(R2USRPLUGDIR)/r2ai$(DOTLIB)
|
||||
rm -f $(R2BINDIR)/r2ai$(DOTEXE)
|
||||
|
||||
install:
|
||||
$(MAKE) uninstall
|
||||
mkdir -p $(R2USRPLUGDIR)
|
||||
mkdir -p $(R2BINDIR)
|
||||
cp -f r2ai$(DOTLIB) $(R2SYSPLUGDIR)/r2ai$(DOTLIB)
|
||||
cp -f r2ai$(DOTEXE) $(R2BINDIR)/r2ai$(DOTEXE)
|
||||
cp -f r2ai$(DOTLIB) $(R2SYSPLUGDIR)
|
||||
|
||||
uninstall:
|
||||
rm -f $(R2SYSPLUGDIR)/r2ai$(DOTLIB)
|
||||
rm -f $(R2BINDIR)/r2ai$(DOTEXE)
|
||||
|
||||
r2check:
|
||||
@r2 -NN -qcq --
|
||||
@r2 -qcq --
|
||||
clean:
|
||||
rm -f *.o *.d
|
||||
+17
-40
@@ -1,10 +1,12 @@
|
||||
# R2AI
|
||||
# C-R2AI
|
||||
|
||||
The r2ai plugin for radare2.
|
||||
This directory contains the C rewrite of the r2ai plugin for radare2.
|
||||
|
||||
## Installation
|
||||
|
||||
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`.
|
||||
Requires radare2 from git if you want to use the stream HTTP APIs.
|
||||
|
||||
Otherwise the only requirement is a C compiler.
|
||||
|
||||
```console
|
||||
make
|
||||
@@ -35,53 +37,24 @@ 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 (k(=v)) Same as '-e r2ai.'
|
||||
| r2ai -e 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 -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 -n suggest a better name for the current function
|
||||
| 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 [query] query the selected model+provider with the given query
|
||||
| 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
|
||||
[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>`
|
||||
@@ -103,6 +76,7 @@ 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)
|
||||
@@ -241,6 +215,8 @@ Note: System prompt is applied automatically but not stored in history
|
||||
{"command":"pdf @ main"}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## TODO
|
||||
|
||||
* add "undo" command to drop the last message
|
||||
@@ -249,6 +225,7 @@ 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`.
|
||||
@@ -291,4 +268,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
|
||||
+7
-18
@@ -1,11 +1,10 @@
|
||||
#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(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args) {
|
||||
const char *model = args.model;
|
||||
char **error = args.error;
|
||||
const R2AI_Tools *tools = args.tools;
|
||||
@@ -41,7 +40,7 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
// Create messages JSON
|
||||
char *messages_json = NULL;
|
||||
|
||||
if (messages_input && r_list_length (messages_input->messages) > 0) {
|
||||
if (messages_input && messages_input->n_messages > 0) {
|
||||
// Convert directly to JSON without filtering
|
||||
messages_json = r2ai_msgs_to_anthropic_json (messages_input);
|
||||
if (!messages_json) {
|
||||
@@ -99,13 +98,8 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
free (messages_json);
|
||||
|
||||
// Save the full JSON for debugging
|
||||
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_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");
|
||||
R_LOG_DEBUG ("Anthropic API request data: %s", data);
|
||||
|
||||
// Make the API call
|
||||
@@ -126,13 +120,8 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
}
|
||||
|
||||
// Save the response for inspection
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
// Parse the response
|
||||
|
||||
+94
-84
@@ -1,60 +1,76 @@
|
||||
#include "r2ai.h"
|
||||
#include <time.h>
|
||||
|
||||
// Stats are now stored in R2AI_State->stats, no longer need global variable
|
||||
// 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 };
|
||||
|
||||
// Helper function to format time duration
|
||||
static char *format_time_duration(time_t seconds) {
|
||||
if (seconds < 60) {
|
||||
return r_str_newf ("%" PFMT64d "s", (long long)seconds);
|
||||
return r_str_newf ("%llds", (long long)seconds);
|
||||
}
|
||||
if (seconds < 3600) {
|
||||
return r_str_newf ("%" PFMT64d "m%" PFMT64d "s", (long long) (seconds / 60), (long long) (seconds % 60));
|
||||
return r_str_newf ("%lldm%llds", (long long)(seconds / 60), (long long)(seconds % 60));
|
||||
}
|
||||
return r_str_newf ("%" PFMT64d "h%" PFMT64d "m%" PFMT64d "s",
|
||||
(long long) (seconds / 3600),
|
||||
(long long) ((seconds % 3600) / 60),
|
||||
(long long) (seconds % 60));
|
||||
return r_str_newf ("%lldh%lldm%llds",
|
||||
(long long)(seconds / 3600),
|
||||
(long long)((seconds % 3600) / 60),
|
||||
(long long)(seconds % 60));
|
||||
}
|
||||
|
||||
// Initialize timing and cost tracking for a run
|
||||
static void r2ai_stats_init_run(R2AI_State *state, int n_run) {
|
||||
static void r2ai_stats_init_run(int n_run) {
|
||||
time_t run_start = time (NULL);
|
||||
if (n_run == 1) {
|
||||
// First run, initialize total timing
|
||||
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.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.start_time = run_start;
|
||||
stats.start_time = run_start;
|
||||
}
|
||||
|
||||
// Print a simple run indicator at the start
|
||||
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;
|
||||
static void r2ai_print_run_end(RCore *core, const R2AI_Usage *usage, int n_run, int max_runs) {
|
||||
(void)n_run;
|
||||
(void)max_runs;
|
||||
time_t run_time = time (NULL) - state->stats.start_time;
|
||||
time_t total_time = time (NULL) - state->stats.total_start_time;
|
||||
time_t run_time = time (NULL) - stats.start_time;
|
||||
time_t total_time = time (NULL) - stats.total_start_time;
|
||||
if (usage) {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
if (r_config_get_b (core->config, "r2ai.chat.show_cost") == true) {
|
||||
// TODO: calculate cost
|
||||
state->stats.run_cost = 0.0 * run_time;
|
||||
state->stats.total_cost += state->stats.run_cost;
|
||||
stats.run_cost = 0.0 * run_time;
|
||||
stats.total_cost += stats.run_cost;
|
||||
}
|
||||
|
||||
// Format times for display
|
||||
@@ -64,12 +80,12 @@ static void r2ai_print_run_end(RCorePluginSession *cps, const R2AI_Usage *usage,
|
||||
// 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_config_get (core->config, "r2ai.model"),
|
||||
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,
|
||||
stats.total_tokens,
|
||||
stats.total_prompt_tokens,
|
||||
stats.total_completion_tokens,
|
||||
stats.run_tokens,
|
||||
stats.run_prompt_tokens,
|
||||
stats.run_completion_tokens,
|
||||
run_time_str,
|
||||
total_time_str);
|
||||
R2_NEWLINE ();
|
||||
@@ -80,29 +96,27 @@ static void r2ai_print_run_end(RCorePluginSession *cps, const R2AI_Usage *usage,
|
||||
}
|
||||
|
||||
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";
|
||||
"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 process messages and handle tool calls recursively
|
||||
R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, const char *system_prompt, int n_run) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
R_API void process_messages(RCore *core, R2AI_Messages *messages, const char *system_prompt, int n_run) {
|
||||
char *error = NULL;
|
||||
bool interrupted = false;
|
||||
const int max_runs = r_config_get_i (core->config, "r2ai.auto.max_runs");
|
||||
@@ -128,7 +142,7 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
}
|
||||
}
|
||||
|
||||
r2ai_stats_init_run (state, n_run);
|
||||
r2ai_stats_init_run (n_run);
|
||||
|
||||
// Set up args for r2ai_llmcall call with tools directly
|
||||
R2AIArgs args = {
|
||||
@@ -140,7 +154,7 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
};
|
||||
|
||||
// Call r2ai_llmcall to get a response
|
||||
R2AI_ChatResponse *response = r2ai_llmcall (cps, args);
|
||||
R2AI_ChatResponse *response = r2ai_llmcall (core, args);
|
||||
|
||||
if (!response) {
|
||||
return;
|
||||
@@ -219,7 +233,7 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
// Create a tool call response message
|
||||
R2AI_Message tool_response = {
|
||||
.role = "tool",
|
||||
.tool_call_id = strdup (tool_call->id),
|
||||
.tool_call_id = tool_call->id,
|
||||
.content = cmd_output
|
||||
};
|
||||
|
||||
@@ -228,25 +242,23 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
free (cmd_output);
|
||||
}
|
||||
|
||||
r2ai_print_run_end (cps, usage, n_run, max_runs);
|
||||
r2ai_print_run_end (core, usage, n_run, max_runs);
|
||||
|
||||
// Check if we should continue with recursion
|
||||
if (!interrupted && message->tool_calls && message->n_tool_calls > 0) {
|
||||
process_messages (cps, messages, system_prompt, n_run + 1);
|
||||
process_messages (core, messages, system_prompt, n_run + 1);
|
||||
}
|
||||
} else {
|
||||
r2ai_print_run_end (cps, usage, n_run, max_runs);
|
||||
r2ai_print_run_end (core, 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(RCorePluginSession *cps, const char *user_query) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
R_IPI void cmd_r2ai_a(RCore *core, const char *user_query) {
|
||||
// Get conversation
|
||||
R2AI_Messages *messages = r2ai_conversation_get (state);
|
||||
R2AI_Messages *messages = r2ai_conversation_get ();
|
||||
if (!messages) {
|
||||
R_LOG_ERROR ("Conversation not initialized");
|
||||
return;
|
||||
@@ -254,7 +266,7 @@ R_IPI void cmd_r2ai_a(RCorePluginSession *cps, const char *user_query) {
|
||||
|
||||
// Add user query to the conversation (no system prompt)
|
||||
// If this is the first message in a new conversation, clear previous history
|
||||
if (r_list_length (messages->messages) == 0 || r_config_get_b (core->config, "r2ai.auto.reset_on_query")) {
|
||||
if (messages->n_messages == 0 || r_config_get_b (core->config, "r2ai.auto.reset_on_query")) {
|
||||
r2ai_msgs_clear (messages);
|
||||
}
|
||||
|
||||
@@ -265,13 +277,13 @@ R_IPI void cmd_r2ai_a(RCorePluginSession *cps, const char *user_query) {
|
||||
};
|
||||
r2ai_msgs_add (messages, &user_msg);
|
||||
|
||||
process_messages (cps, messages, NULL, 1);
|
||||
process_messages (core, messages, NULL, 1);
|
||||
}
|
||||
|
||||
// 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>");
|
||||
R2_PRINTF ("%s\n", empty_msg ? empty_msg : "<no content>");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -294,12 +306,10 @@ static void print_content_with_length(RCore *core, const char *content, const ch
|
||||
}
|
||||
|
||||
// Add this function right after cmd_r2ai_a
|
||||
R_IPI void cmd_r2ai_logs(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
R_IPI void cmd_r2ai_logs(RCore *core) {
|
||||
// Get conversation
|
||||
R2AI_Messages *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_length (messages->messages) == 0) {
|
||||
R2AI_Messages *messages = r2ai_conversation_get ();
|
||||
if (!messages || messages->n_messages == 0) {
|
||||
R2_PRINTF ("No conversation history available\n");
|
||||
return;
|
||||
}
|
||||
@@ -316,13 +326,13 @@ R_IPI void cmd_r2ai_logs(RCorePluginSession *cps) {
|
||||
|
||||
pj_a (pj);
|
||||
|
||||
for (int i = 0; i < r_list_length (messages->messages); i++) {
|
||||
const R2AI_Message *msg = r_list_get_n (messages->messages, i);
|
||||
for (int i = 0; i < messages->n_messages; i++) {
|
||||
const R2AI_Message *msg = &messages->messages[i];
|
||||
|
||||
pj_o (pj);
|
||||
|
||||
pj_ks (pj, "role", msg->role? msg->role: "unknown");
|
||||
pj_ks (pj, "content", msg->content? msg->content: "");
|
||||
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_ka (pj, "tool_calls");
|
||||
@@ -358,13 +368,13 @@ R_IPI void cmd_r2ai_logs(RCorePluginSession *cps) {
|
||||
}
|
||||
|
||||
R2_PRINTF ("\x1b[1;34m[r2ai] Chat Logs (%d messages)\x1b[0m\n",
|
||||
r_list_length (messages->messages));
|
||||
core->cons, messages->n_messages);
|
||||
|
||||
R2_PRINTF ("\x1b[1;33mNote: System prompt is applied automatically but not stored in history\x1b[0m\n\n");
|
||||
|
||||
// Display each message in the conversation
|
||||
for (int i = 0; i < r_list_length (messages->messages); i++) {
|
||||
const R2AI_Message *msg = r_list_get_n (messages->messages, i);
|
||||
for (int i = 0; i < messages->n_messages; i++) {
|
||||
const R2AI_Message *msg = &messages->messages[i];
|
||||
const char *role = msg->role;
|
||||
|
||||
// Format based on role
|
||||
@@ -379,7 +389,7 @@ R_IPI void cmd_r2ai_logs(RCorePluginSession *cps) {
|
||||
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>");
|
||||
tc->name ? tc->name : "<unnamed>");
|
||||
|
||||
if (tc->arguments) {
|
||||
R2_PRINTF (" %s\n", tc->arguments);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
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))
|
||||
@@ -1,353 +0,0 @@
|
||||
#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) {
|
||||
for (int i = 0; r2ai_providers[i].name; i++) {
|
||||
if (!strcmp (name, r2ai_providers[i].name)) {
|
||||
return &r2ai_providers[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
R2AI_ChatResponse *res = NULL;
|
||||
const char *provider = args.provider? args.provider: r_config_get (core->config, "r2ai.api");
|
||||
if (!provider) {
|
||||
provider = "gemini";
|
||||
}
|
||||
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->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 = 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->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;
|
||||
}
|
||||
|
||||
/* Return a malloc'd API key read from the environment or from ~/.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);
|
||||
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);
|
||||
} else {
|
||||
return r_str_newf ("%s/api", host);
|
||||
}
|
||||
}
|
||||
if (!strcmp (provider, "openai")) {
|
||||
return r_str_newf ("http://%s/v1", host);
|
||||
} else {
|
||||
return r_str_newf ("http://%s/api", host);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 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
|
||||
char *models_url = NULL;
|
||||
const R2AIProvider *prov = r2ai_get_provider (provider);
|
||||
if (prov && prov->uses_tags_endpoint) {
|
||||
models_url = r_str_newf ("%s/tags", purl);
|
||||
} else {
|
||||
models_url = r_str_newf ("%s/models", purl);
|
||||
}
|
||||
if (!models_url) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// 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 RJson *data;
|
||||
const R2AIProvider *prov = r2ai_get_provider (provider);
|
||||
if (prov && prov->uses_tags_endpoint) {
|
||||
data = r_json_get (json, "models");
|
||||
} else {
|
||||
data = r_json_get (json, "data");
|
||||
}
|
||||
|
||||
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) {
|
||||
size_t i;
|
||||
for (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 {
|
||||
R2_PRINTLN (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) {
|
||||
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);
|
||||
}
|
||||
+19
-85
@@ -1,97 +1,31 @@
|
||||
/* Copyright r2ai - 2023-2025 - pancake */
|
||||
|
||||
#include "r2ai_priv.h"
|
||||
#include <r_core.h>
|
||||
#include "r2ai.h"
|
||||
|
||||
static void show_help() {
|
||||
printf ("Usage: r2ai [-vhp:m:q:] <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");
|
||||
}
|
||||
|
||||
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;
|
||||
RList *queries = r_list_newf (free);
|
||||
|
||||
RGetopt opt;
|
||||
r_getopt_init (&opt, argc, argv, "vhp:m:q:");
|
||||
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;
|
||||
default:
|
||||
show_help ();
|
||||
r_list_free (queries);
|
||||
return 1;
|
||||
}
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 2) {
|
||||
eprintf ("Usage: r2ai <prompt>\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *prompt = argv[1];
|
||||
|
||||
RCore *core = r_core_new ();
|
||||
RCorePluginSession cps = {
|
||||
.core = core
|
||||
char *err = NULL;
|
||||
|
||||
R2AIArgs args = {
|
||||
.input = prompt,
|
||||
.error = &err,
|
||||
.dorag = false,
|
||||
};
|
||||
r2ai_init (&cps);
|
||||
|
||||
// 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) {
|
||||
show_help ();
|
||||
r_list_free (queries);
|
||||
r2ai_fini (&cps);
|
||||
r_core_free (core);
|
||||
return 1;
|
||||
}
|
||||
|
||||
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);
|
||||
char *res = r2ai (core, args);
|
||||
if (res) {
|
||||
R_LOG_INFO (res);
|
||||
free (res);
|
||||
}
|
||||
|
||||
r_list_free (queries);
|
||||
r2ai_fini (&cps);
|
||||
free (err);
|
||||
r_core_free (core);
|
||||
return 0;
|
||||
}
|
||||
+7
-7
@@ -34,13 +34,13 @@ R_API RMarkdownTheme r2ai_markdown_theme_default(void) {
|
||||
.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
|
||||
// List items
|
||||
.list_bullet = "• ", // Bullet character
|
||||
.list_number = "\x1b[1m%s.\x1b[0m ", // Bold number
|
||||
// Checkbox states
|
||||
// Checkbox states
|
||||
.checkbox_checked = "\x1b[32m[✓]\x1b[0m ", // Green checkmark
|
||||
.checkbox_unchecked = "\x1b[90m[ ]\x1b[0m ", // Gray empty box
|
||||
// Reset code
|
||||
// Reset code
|
||||
.reset = "\x1b[0m"
|
||||
};
|
||||
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 {
|
||||
|
||||
+1
-3
@@ -1,5 +1,5 @@
|
||||
project('r2ai', 'c',
|
||||
version : '1.2.0',
|
||||
version : '0.1.0',
|
||||
default_options : ['warning_level=2', 'c_std=c11']
|
||||
)
|
||||
|
||||
@@ -188,9 +188,7 @@ srcs = files(
|
||||
'r2ai.c',
|
||||
'auto.c',
|
||||
'vdb.c',
|
||||
'llm.c',
|
||||
'tools.c',
|
||||
'query.c',
|
||||
'messages.c',
|
||||
'anthropic.c',
|
||||
'r2ai_http.c',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
option('r2_prefix',
|
||||
type: 'string',
|
||||
value: '/usr/local',
|
||||
value: '',
|
||||
description: 'Path prefix where radare2 is installed (include/ and lib/).'
|
||||
)
|
||||
|
||||
+142
-103
@@ -2,8 +2,12 @@
|
||||
|
||||
#include "r2ai.h"
|
||||
|
||||
// AITODO: remove globals
|
||||
// Static global message store for session persistence
|
||||
static R2AI_Messages *conversation = NULL;
|
||||
|
||||
#define INITIAL_CAPACITY 8
|
||||
#define GROWTH_FACTOR 1.5
|
||||
#define GROWTH_FACTOR 1.5
|
||||
|
||||
R_API void r2ai_message_free(R2AI_Message *msg) {
|
||||
if (!msg) {
|
||||
@@ -11,7 +15,7 @@ R_API void r2ai_message_free(R2AI_Message *msg) {
|
||||
}
|
||||
|
||||
free (msg->role);
|
||||
free ((void *)msg->content); // maybe double free
|
||||
free (msg->content);
|
||||
free (msg->reasoning_content);
|
||||
free (msg->tool_call_id);
|
||||
|
||||
@@ -42,19 +46,24 @@ R_API void r2ai_message_free(R2AI_Message *msg) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// Initialize conversation container (call this during plugin init)
|
||||
R_API void r2ai_conversation_init(void) {
|
||||
if (conversation) {
|
||||
// Already initialized
|
||||
return;
|
||||
}
|
||||
|
||||
state->conversation = r2ai_msgs_new ();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
R_API R2AI_Messages *r2ai_conversation_get(R2AI_State *state) {
|
||||
return state? state->conversation: NULL;
|
||||
// Get the conversation instance (returns NULL if not initialized)
|
||||
R_API R2AI_Messages *r2ai_conversation_get(void) {
|
||||
return conversation;
|
||||
}
|
||||
|
||||
// Create a new temporary messages container
|
||||
@@ -63,12 +72,12 @@ R_API R2AI_Messages *r2ai_msgs_new(void) {
|
||||
if (!msgs) {
|
||||
return NULL;
|
||||
}
|
||||
msgs->messages = r_list_new ();
|
||||
msgs->cap_messages = INITIAL_CAPACITY;
|
||||
msgs->messages = R_NEWS0 (R2AI_Message, msgs->cap_messages);
|
||||
if (!msgs->messages) {
|
||||
R_FREE (msgs);
|
||||
return NULL;
|
||||
}
|
||||
msgs->messages->free = (RListFree)r2ai_message_free;
|
||||
return msgs;
|
||||
}
|
||||
|
||||
@@ -77,19 +86,38 @@ R_API void r2ai_msgs_free(R2AI_Messages *msgs) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
r_list_free (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(R2AI_State *state) {
|
||||
if (state && state->conversation) {
|
||||
r2ai_msgs_free (state->conversation);
|
||||
state->conversation = NULL;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +127,10 @@ R_API void r2ai_msgs_clear(R2AI_Messages *msgs) {
|
||||
return;
|
||||
}
|
||||
|
||||
r_list_purge (msgs->messages);
|
||||
for (int i = 0; i < msgs->n_messages; i++) {
|
||||
r2ai_message_free (&msgs->messages[i]);
|
||||
}
|
||||
msgs->n_messages = 0;
|
||||
}
|
||||
|
||||
R_API bool r2ai_msgs_add(R2AI_Messages *msgs, const R2AI_Message *msg) {
|
||||
@@ -107,79 +138,91 @@ R_API bool r2ai_msgs_add(R2AI_Messages *msgs, const R2AI_Message *msg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
R2AI_Message *new_msg = R_NEW0 (R2AI_Message);
|
||||
if (!new_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));
|
||||
}
|
||||
|
||||
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;
|
||||
// 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;
|
||||
|
||||
if (msg->content_blocks) {
|
||||
R2AI_ContentBlocks *cb = R_NEW0 (R2AI_ContentBlocks);
|
||||
if (!cb) {
|
||||
r2ai_message_free (new_msg);
|
||||
free (new_msg);
|
||||
r2ai_message_free (dest);
|
||||
msgs->n_messages--;
|
||||
return false;
|
||||
}
|
||||
cb->n_blocks = msg->content_blocks->n_blocks;
|
||||
cb->blocks = R_NEWS0 (R2AI_ContentBlock, cb->n_blocks);
|
||||
if (!cb->blocks) {
|
||||
free (cb);
|
||||
r2ai_message_free (new_msg);
|
||||
free (new_msg);
|
||||
r2ai_message_free (dest);
|
||||
msgs->n_messages--;
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < cb->n_blocks; i++) {
|
||||
R2AI_ContentBlock *src = &msg->content_blocks->blocks[i];
|
||||
R2AI_ContentBlock *dst = &cb->blocks[i];
|
||||
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;
|
||||
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;
|
||||
}
|
||||
new_msg->content_blocks = cb;
|
||||
dest->content_blocks = cb;
|
||||
}
|
||||
|
||||
new_msg->tool_call_id = msg->tool_call_id? strdup (msg->tool_call_id): NULL;
|
||||
new_msg->tool_calls = NULL;
|
||||
new_msg->n_tool_calls = 0;
|
||||
dest->tool_call_id = msg->tool_call_id ? strdup (msg->tool_call_id) : NULL;
|
||||
dest->tool_calls = NULL;
|
||||
dest->n_tool_calls = 0;
|
||||
|
||||
// Copy tool calls if any
|
||||
if (msg->tool_calls && msg->n_tool_calls > 0) {
|
||||
new_msg->tool_calls = R_NEWS0 (R2AI_ToolCall, msg->n_tool_calls);
|
||||
if (!new_msg->tool_calls) {
|
||||
r2ai_message_free (new_msg);
|
||||
free (new_msg);
|
||||
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;
|
||||
}
|
||||
|
||||
new_msg->n_tool_calls = msg->n_tool_calls;
|
||||
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 *)&new_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;
|
||||
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 (msgs->messages, new_msg);
|
||||
return true;
|
||||
}
|
||||
|
||||
R_API bool r2ai_msgs_add_tool_call(R2AI_Messages *msgs, const R2AI_ToolCall *tc) {
|
||||
if (!msgs || !tc || r_list_length (msgs->messages) == 0) {
|
||||
if (!msgs || !tc || msgs->n_messages == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
R2AI_Message *msg = r_list_get_n (msgs->messages, r_list_length (msgs->messages) - 1);
|
||||
R2AI_Message *msg = &msgs->messages[msgs->n_messages - 1];
|
||||
|
||||
// Allocate or resize the tool_calls array
|
||||
if (msg->n_tool_calls == 0) {
|
||||
@@ -190,7 +233,7 @@ R_API bool r2ai_msgs_add_tool_call(R2AI_Messages *msgs, const R2AI_ToolCall *tc)
|
||||
} else {
|
||||
R2AI_ToolCall *new_tool_calls = realloc (
|
||||
(void *)msg->tool_calls,
|
||||
sizeof (R2AI_ToolCall) *(msg->n_tool_calls + 1));
|
||||
sizeof (R2AI_ToolCall) * (msg->n_tool_calls + 1));
|
||||
if (!new_tool_calls) {
|
||||
return false;
|
||||
}
|
||||
@@ -201,9 +244,9 @@ R_API bool r2ai_msgs_add_tool_call(R2AI_Messages *msgs, const R2AI_ToolCall *tc)
|
||||
|
||||
// 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;
|
||||
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++;
|
||||
return true;
|
||||
@@ -251,8 +294,8 @@ 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;
|
||||
@@ -285,14 +328,14 @@ 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;
|
||||
}
|
||||
new_msg.content_blocks = cb;
|
||||
}
|
||||
@@ -323,32 +366,21 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the parsed message to the messages array
|
||||
if (!r2ai_msgs_add (msgs, &new_msg)) {
|
||||
r2ai_message_free (&new_msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Free the temporary message struct (strings are duplicated in add)
|
||||
r2ai_message_free (&new_msg);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
|
||||
if (!msgs || r_list_length (msgs->messages) == 0) {
|
||||
if (!msgs || msgs->n_messages == 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -359,13 +391,13 @@ R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
|
||||
|
||||
pj_a (pj); // Start array
|
||||
|
||||
for (int i = 0; i < r_list_length (msgs->messages); i++) {
|
||||
const R2AI_Message *msg = r_list_get_n (msgs->messages, i);
|
||||
for (int i = 0; i < msgs->n_messages; i++) {
|
||||
const R2AI_Message *msg = &msgs->messages[i];
|
||||
|
||||
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) {
|
||||
pj_ks (pj, "content", msg->content);
|
||||
@@ -403,7 +435,7 @@ 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) {
|
||||
@@ -427,7 +459,7 @@ R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
|
||||
}
|
||||
|
||||
R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
if (!msgs || r_list_length (msgs->messages) == 0) {
|
||||
if (!msgs || msgs->n_messages == 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -438,14 +470,14 @@ R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
|
||||
pj_a (pj); // Start array
|
||||
|
||||
for (int i = 0; i < r_list_length (msgs->messages); i++) {
|
||||
const R2AI_Message *msg = r_list_get_n (msgs->messages, i);
|
||||
for (int i = 0; i < msgs->n_messages; i++) {
|
||||
const R2AI_Message *msg = &msgs->messages[i];
|
||||
|
||||
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
|
||||
@@ -518,12 +550,12 @@ R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
const R2AI_ToolCall *tc = &msg->tool_calls[j];
|
||||
pj_o (pj); // Start tool_calls object
|
||||
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) {
|
||||
@@ -556,7 +588,7 @@ R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
|
||||
// Function to delete the last N messages from conversation history
|
||||
R_API void r2ai_delete_last_messages(R2AI_Messages *messages, int n) {
|
||||
if (!messages || r_list_length (messages->messages) == 0) {
|
||||
if (!messages || messages->n_messages == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -566,15 +598,17 @@ R_API void r2ai_delete_last_messages(R2AI_Messages *messages, int n) {
|
||||
}
|
||||
|
||||
// Make sure we don't try to delete more messages than exist
|
||||
int len = r_list_length (messages->messages);
|
||||
if (n > len) {
|
||||
n = len;
|
||||
if (n > messages->n_messages) {
|
||||
n = messages->n_messages;
|
||||
}
|
||||
|
||||
// Pop the last n messages
|
||||
for (int i = 0; i < n; i++) {
|
||||
r_list_pop (messages->messages);
|
||||
// Free the last n messages
|
||||
for (int i = messages->n_messages - n; i < messages->n_messages; i++) {
|
||||
r2ai_message_free (&messages->messages[i]);
|
||||
}
|
||||
|
||||
// Update the message count
|
||||
messages->n_messages -= n;
|
||||
}
|
||||
|
||||
// Helper function to convert RJson to PJ without draining
|
||||
@@ -583,7 +617,7 @@ R_API PJ *r_json_to_pj(const RJson *json, PJ *existing_pj) {
|
||||
return existing_pj;
|
||||
}
|
||||
|
||||
PJ *pj = existing_pj? existing_pj: pj_new ();
|
||||
PJ *pj = existing_pj ? existing_pj : pj_new ();
|
||||
if (!pj) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -730,13 +764,18 @@ R_API char *r_json_to_string(const RJson *json) {
|
||||
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 (msgs && R_STR_ISNOTEMPTY (user_message)) {
|
||||
if (R_STR_ISNOTEMPTY (user_message)) {
|
||||
R2AI_Message user_msg = {
|
||||
.role = "user",
|
||||
.content = user_message
|
||||
};
|
||||
r2ai_msgs_add (msgs, &user_msg);
|
||||
}
|
||||
|
||||
return msgs;
|
||||
}
|
||||
+145
-137
@@ -20,19 +20,37 @@ typedef struct {
|
||||
int error_flags; // Bitfield of ModelErrorFlags
|
||||
} ModelCompat;
|
||||
|
||||
// Function to add an error flag to a model
|
||||
static void model_add_error(R2AI_State *state, const char *provider, const char *model, ModelErrorFlags flag) {
|
||||
if (!state) {
|
||||
return;
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (!state->model_compat_db) {
|
||||
state->model_compat_db = ht_pp_new0 ();
|
||||
}
|
||||
|
||||
char *key = r_str_newf ("%s:%s", provider, model? model: "default");
|
||||
char *key = r_str_newf ("%s:%s", provider, model ? model : "default");
|
||||
bool found_flag = false;
|
||||
ModelCompat *compat = ht_pp_find (state->model_compat_db, key, &found_flag);
|
||||
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 ();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (found_flag && compat) {
|
||||
// Update existing entry
|
||||
@@ -42,7 +60,7 @@ static void model_add_error(R2AI_State *state, const char *provider, const char
|
||||
compat = R_NEW0 (ModelCompat);
|
||||
compat->model_id = strdup (key);
|
||||
compat->error_flags = flag;
|
||||
ht_pp_insert (state->model_compat_db, key, compat);
|
||||
ht_pp_insert (model_compat_db, key, compat);
|
||||
}
|
||||
free (key);
|
||||
}
|
||||
@@ -60,25 +78,23 @@ 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(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 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 R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
R_IPI R2AI_ChatResponse *r2ai_openai(RCore *core, R2AIArgs args) {
|
||||
// Initialize compatibility database if needed
|
||||
if (!state->model_compat_db) {
|
||||
state->model_compat_db = ht_pp_new0 ();
|
||||
if (!model_compat_db) {
|
||||
model_compat_db = ht_pp_new0 ();
|
||||
}
|
||||
|
||||
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;
|
||||
// create a temp conversation to include the system prompt and the rest of the messages
|
||||
@@ -120,15 +136,15 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
}
|
||||
}
|
||||
if (args.messages) {
|
||||
for (int i = 0; i < r_list_length (args.messages->messages); i++) {
|
||||
r2ai_msgs_add (temp_msgs, r_list_get_n (args.messages->messages, i));
|
||||
for (int i = 0; i < args.messages->n_messages; i++) {
|
||||
r2ai_msgs_add (temp_msgs, &args.messages->messages[i]);
|
||||
}
|
||||
} else {
|
||||
R_LOG_WARN ("No messages");
|
||||
}
|
||||
// Safely print debug info about first message
|
||||
if (temp_msgs && r_list_length (temp_msgs->messages) > 0 && ((R2AI_Message *)r_list_get_n (temp_msgs->messages, 0))->role) {
|
||||
R_LOG_DEBUG ("First message role: %s", ((R2AI_Message *)r_list_get_n (temp_msgs->messages, 0))->role);
|
||||
if (temp_msgs && 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 (error) {
|
||||
*error = NULL;
|
||||
@@ -145,16 +161,19 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
headers[1] = auth_header;
|
||||
}
|
||||
|
||||
const char *urlfmt = strcmp (args.provider, "ollama")
|
||||
? "%s/chat/completions"
|
||||
: "%s/chat";
|
||||
char *openai_url = r_str_newf (urlfmt, base_url);
|
||||
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);
|
||||
|
||||
// Create a messages JSON object, either from input messages or from content
|
||||
char *messages_json = NULL;
|
||||
|
||||
if (temp_msgs && r_list_length (temp_msgs->messages) > 0) {
|
||||
R_LOG_DEBUG ("Using input messages: %d messages", r_list_length (temp_msgs->messages));
|
||||
if (temp_msgs && temp_msgs->n_messages > 0) {
|
||||
R_LOG_DEBUG ("Using input messages: %d messages", temp_msgs->n_messages);
|
||||
messages_json = r2ai_msgs_to_json (temp_msgs);
|
||||
if (!messages_json) {
|
||||
if (error) {
|
||||
@@ -183,31 +202,19 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
pj_ks (pj, "model", model_name);
|
||||
pj_kb (pj, "stream", false);
|
||||
|
||||
if (strcmp (args.provider, "ollama") == 0) {
|
||||
// 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);
|
||||
@@ -258,13 +265,8 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
|
||||
// Save the full JSON to a file for inspection
|
||||
// XXX: only create request/response files when r2ai.debug is set
|
||||
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_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");
|
||||
R_LOG_DEBUG ("OpenAI API request data: %s", complete_json);
|
||||
|
||||
// Make the API call
|
||||
@@ -272,7 +274,6 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
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);
|
||||
@@ -280,7 +281,7 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, 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";
|
||||
const char *model_name = args.model ? args.model : "gpt-5-mini";
|
||||
|
||||
// Check for temperature errors
|
||||
if (strstr (res, "temperature")) {
|
||||
@@ -288,9 +289,14 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
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 (state, args.provider, model_name, error_flag);
|
||||
model_add_error (args.provider, model_name, error_flag);
|
||||
|
||||
// Clean up
|
||||
free (auth_header);
|
||||
@@ -298,8 +304,7 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
|
||||
// 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);
|
||||
RCorePluginSession retry_cps = { .core = core, .data = state };
|
||||
return r2ai_openai (&retry_cps, args);
|
||||
return r2ai_openai (core, args);
|
||||
}
|
||||
}
|
||||
free (auth_header);
|
||||
@@ -308,13 +313,8 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
}
|
||||
|
||||
// Save the response for inspection
|
||||
tmpdir = r_file_tmpdir ();
|
||||
char *res_path = r_str_newf ("%s" R_SYS_DIR "r2ai_openai_response.json", tmpdir);
|
||||
r_file_dump (res_path, (const ut8 *)res, strlen (res), 0);
|
||||
R_LOG_DEBUG ("OpenAI API response saved to %s", res_path);
|
||||
free (res_path);
|
||||
free (tmpdir);
|
||||
|
||||
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);
|
||||
|
||||
// Parse the response into our messages structure
|
||||
@@ -327,76 +327,84 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, 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 = 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");
|
||||
}
|
||||
|
||||
const RJson *usage_json = r_json_get (jres, "usage");
|
||||
if (usage_json && usage_json->type == R_JSON_OBJECT) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
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 (content && content->type == R_JSON_STRING) {
|
||||
message->content = strdup (content->str_value);
|
||||
}
|
||||
|
||||
if (reasoning_content && reasoning_content->type == R_JSON_STRING) {
|
||||
message->reasoning_content = strdup (reasoning_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 (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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
message->tool_calls = tool_calls_array;
|
||||
message->n_tool_calls = n_tool_calls;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 (content && content->type == R_JSON_STRING) {
|
||||
message->content = strdup (content->str_value);
|
||||
}
|
||||
|
||||
if (reasoning_content && reasoning_content->type == R_JSON_STRING) {
|
||||
message->reasoning_content = strdup (reasoning_content->str_value);
|
||||
}
|
||||
|
||||
// TODO: Handle tool calls if present
|
||||
}
|
||||
}
|
||||
r_json_free (jres);
|
||||
R2AI_ChatResponse *result = R_NEW0 (R2AI_ChatResponse);
|
||||
|
||||
-235
@@ -1,235 +0,0 @@
|
||||
/* 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);
|
||||
}
|
||||
|
||||
static 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;
|
||||
}
|
||||
RList *lines = r_str_split_list (content, "\n", -1);
|
||||
RListIter *iter;
|
||||
char *line;
|
||||
r_list_foreach (lines, iter, line) {
|
||||
r_str_trim (line);
|
||||
if (*line == '#' || *line == 0) {
|
||||
continue;
|
||||
}
|
||||
char *colon = strchr (line, ':');
|
||||
if (!colon) {
|
||||
continue;
|
||||
}
|
||||
*colon = 0;
|
||||
char *key = line;
|
||||
char *value = colon + 1;
|
||||
r_str_trim (key);
|
||||
r_str_trim (value);
|
||||
if (!strcmp (key, "Title")) {
|
||||
prompt->title = strdup (value);
|
||||
} else if (!strcmp (key, "Author")) {
|
||||
prompt->author = strdup (value);
|
||||
} else if (!strcmp (key, "Description")) {
|
||||
prompt->desc = strdup (value);
|
||||
} else if (!strcmp (key, "Command") || !strcmp (key, "Commands")) {
|
||||
prompt->command = strdup (value);
|
||||
} else if (!strcmp (key, "Prompt") || !strcmp (key, "Query")) {
|
||||
prompt->prompt = strdup (value);
|
||||
} else if (!strcmp (key, "Depends")) {
|
||||
prompt->
|
||||
requires
|
||||
= strdup (value);
|
||||
} else if (!strcmp (key, "If-Empty")) {
|
||||
prompt->if_empty = strdup (value);
|
||||
} else if (!strcmp (key, "If-Command")) {
|
||||
prompt->if_command = strdup (value);
|
||||
}
|
||||
}
|
||||
r_list_free (lines);
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
if (!r_file_is_directory (expanded_dir)) {
|
||||
R_LOG_ERROR ("Prompt directory does not exist: %s", expanded_dir);
|
||||
free (expanded_dir);
|
||||
return;
|
||||
}
|
||||
if (!input || !*input) {
|
||||
// list prompts
|
||||
RList *files = r_sys_dir (expanded_dir);
|
||||
RListIter *iter;
|
||||
char *file;
|
||||
r_list_foreach (files, iter, file) {
|
||||
if (r_str_endswith (file, ".r2ai.txt")) {
|
||||
char *dot = strchr (file, '.');
|
||||
char *name = dot? r_str_ndup (file, dot - file): strdup (file);
|
||||
char *filepath = r_str_newf ("%s/%s.r2ai.txt", expanded_dir, name);
|
||||
R2AIPrompt *prompt = parse_prompt_file (filepath);
|
||||
if (prompt) {
|
||||
R2_PRINTF ("%s: %s - %s\n", name, prompt->title? prompt->title: "", prompt->desc? prompt->desc: "");
|
||||
r2aiprompt_free (prompt);
|
||||
} else {
|
||||
R2_PRINTLN (name);
|
||||
}
|
||||
free (name);
|
||||
free (filepath);
|
||||
}
|
||||
}
|
||||
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 = r_str_newf ("%s/%s.r2ai.txt", expanded_dir, name);
|
||||
R2AIPrompt *prompt = parse_prompt_file (filepath);
|
||||
if (!prompt) {
|
||||
R_LOG_ERROR ("Cannot read prompt file: %s", filepath);
|
||||
free (filepath);
|
||||
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) {
|
||||
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, .error = &error, .dorag = true });
|
||||
if (error) {
|
||||
R_LOG_ERROR ("%s", error);
|
||||
free (error);
|
||||
} else if (res) {
|
||||
R2_PRINTF ("%s\n", res);
|
||||
free (res);
|
||||
}
|
||||
r2aiprompt_free (prompt);
|
||||
free (final_prompt);
|
||||
free (name);
|
||||
}
|
||||
free (expanded_dir);
|
||||
}
|
||||
+587
-128
@@ -3,7 +3,49 @@
|
||||
#define R_LOG_ORIGIN "r2ai"
|
||||
|
||||
#include "r2ai.h"
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
// External declaration for cmd_r2ai_a - implemented in auto.c
|
||||
R_IPI void cmd_r2ai_a(RCore *core, const char *user_query);
|
||||
|
||||
static R_TH_LOCAL RVdb *db = NULL;
|
||||
|
||||
#define VDBDIM 16
|
||||
|
||||
static void refresh_embeddings(RCore *core) {
|
||||
RListIter *iter, *iter2;
|
||||
char *line;
|
||||
char *file;
|
||||
// refresh embeddings database
|
||||
r_vdb_free (db);
|
||||
db = r_vdb_new (VDBDIM);
|
||||
// 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) {
|
||||
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 (db, line);
|
||||
R_LOG_DEBUG ("Insert %s", line);
|
||||
}
|
||||
r_list_free (lines);
|
||||
}
|
||||
free (filepath);
|
||||
}
|
||||
r_list_free (files);
|
||||
}
|
||||
|
||||
static RCoreHelpMessage help_msg_r2ai = {
|
||||
"Usage:",
|
||||
@@ -12,24 +54,157 @@ static RCoreHelpMessage help_msg_r2ai = {
|
||||
"r2ai", " -d [query]", "Ask a question on the current function",
|
||||
"r2ai", " -dr", "Decompile current function (+ 1 level of recursivity)",
|
||||
"r2ai", " -a [query]", "Resolve question using auto mode",
|
||||
"r2ai", " -e (k(=v))", "Same as '-e r2ai.'",
|
||||
"r2ai", " -e", "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", " -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", " -n", "suggest a better name for the current function",
|
||||
"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", " [query]", "query the selected model+provider with the given query",
|
||||
"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",
|
||||
NULL
|
||||
};
|
||||
|
||||
R_API char *r2ai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
/* Return a malloc'd API key read from the environment or from ~/.r2ai.<provider>-key
|
||||
* Caller is responsible for freeing the returned string (or NULL if not found). */
|
||||
static 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);
|
||||
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 R2AI_ChatResponse *r2ai_llmcall(RCore *core, R2AIArgs args) {
|
||||
R2AI_ChatResponse *res = NULL;
|
||||
const char *provider = r_config_get (core->config, "r2ai.api");
|
||||
if (!provider) {
|
||||
provider = "gemini";
|
||||
}
|
||||
if (!args.model) {
|
||||
const char *config_model = r_config_get (core->config, "r2ai.model");
|
||||
args.model = strdup (config_model? config_model: "");
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
char *api_key = NULL;
|
||||
if (strcmp (provider, "ollama") && strcmp (provider, "openapi")) {
|
||||
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 (strcmp (provider, "ollama")) {
|
||||
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 (!db) {
|
||||
db = r_vdb_new (VDBDIM);
|
||||
refresh_embeddings (core);
|
||||
}
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
r_strbuf_appendf (sb, "\n## Query\n\n%s\n## Context\n", args.input);
|
||||
RVdbResultSet *rs = r_vdb_query (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 = args.messages->n_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 = 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");
|
||||
|
||||
R_LOG_DEBUG ("Using provider: %s", provider);
|
||||
if (strcmp (provider, "anthropic") == 0) {
|
||||
res = r2ai_anthropic (core, args);
|
||||
} else {
|
||||
res = r2ai_openai (core, args);
|
||||
}
|
||||
if (context_pullback != -1) {
|
||||
R2AI_Message *msg = &args.messages->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;
|
||||
}
|
||||
|
||||
R_API char *r2ai(RCore *core, R2AIArgs args) {
|
||||
if (R_STR_ISEMPTY (args.input) && !args.messages) {
|
||||
if (args.error) {
|
||||
*args.error = r_str_newf ("Usage: r2ai [-h] [prompt]");
|
||||
@@ -50,7 +225,8 @@ R_API char *r2ai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
|
||||
args.messages = msgs;
|
||||
|
||||
R2AI_ChatResponse *res = r2ai_llmcall (cps, args);
|
||||
// Call the r2ai_llmcall function to get the message
|
||||
R2AI_ChatResponse *res = r2ai_llmcall (core, args);
|
||||
if (!res) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -76,8 +252,7 @@ R_API char *r2ai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
return content;
|
||||
}
|
||||
|
||||
static void cmd_r2ai_d(RCorePluginSession *cps, const char *input, const bool recursive) {
|
||||
RCore *core = cps->core;
|
||||
static void cmd_r2ai_d(RCore *core, const char *input, const bool recursive) {
|
||||
const char *prompt = r_config_get (core->config, "r2ai.prompt");
|
||||
const char *lang = r_config_get (core->config, "r2ai.lang");
|
||||
char *full_prompt;
|
||||
@@ -86,7 +261,8 @@ static void cmd_r2ai_d(RCorePluginSession *cps, const char *input, const bool re
|
||||
full_prompt = strdup (input);
|
||||
} else {
|
||||
if (!R_STR_ISEMPTY (lang)) {
|
||||
full_prompt = r_str_newf ("%s. Translate the code into %s programming language.", prompt, lang);
|
||||
full_prompt = r_str_newf (
|
||||
"%s. Translate the code into %s programming language.", prompt, lang);
|
||||
} else {
|
||||
full_prompt = strdup (prompt);
|
||||
}
|
||||
@@ -128,7 +304,7 @@ static void cmd_r2ai_d(RCorePluginSession *cps, const char *input, const bool re
|
||||
char *s = r_strbuf_drain (sb);
|
||||
char *error = NULL;
|
||||
R2AIArgs d_args = { .input = s, .error = &error, .dorag = true };
|
||||
char *res = r2ai (cps, d_args);
|
||||
char *res = r2ai (core, d_args);
|
||||
free (s);
|
||||
if (error) {
|
||||
R_LOG_ERROR (error);
|
||||
@@ -140,12 +316,45 @@ static void cmd_r2ai_d(RCorePluginSession *cps, const char *input, const bool re
|
||||
r_list_free (cmdslist);
|
||||
}
|
||||
|
||||
static void cmd_r2ai_repl(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
static void cmd_r2ai_x(RCore *core) {
|
||||
const char *hlang = r_config_get (core->config, "r2ai.hlang");
|
||||
char *explain_prompt = r_str_newf (
|
||||
"Analyze function calls, comments and strings, ignore registers and "
|
||||
"memory accesess. Considering the references and involved loops make "
|
||||
"explain the purpose of this function in one or two short sentences. "
|
||||
"Output must be only the translation of the explanation in %s",
|
||||
hlang);
|
||||
char *s = r_core_cmd_str (core, "r2ai -d");
|
||||
|
||||
// Create conversation
|
||||
R2AI_Messages *msgs = create_conversation (s);
|
||||
if (!msgs) {
|
||||
R_LOG_ERROR ("Failed to create conversation");
|
||||
free (s);
|
||||
free (explain_prompt);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process the conversation with custom system prompt (will print the result)
|
||||
process_messages (core, msgs, explain_prompt, 1);
|
||||
|
||||
// Free temporary messages
|
||||
r2ai_msgs_free (msgs);
|
||||
|
||||
free (s);
|
||||
free (explain_prompt);
|
||||
}
|
||||
|
||||
static void cmd_r2ai_repl(RCore *core) {
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
while (true) {
|
||||
#if R2_VERSION_NUMBER >= 50909
|
||||
r_line_set_prompt (core->cons->line, ">>> ");
|
||||
const char *ptr = r_line_readline (core->cons);
|
||||
#else
|
||||
r_line_set_prompt (">>> ");
|
||||
const char *ptr = r_line_readline ();
|
||||
#endif
|
||||
if (R_STR_ISEMPTY (ptr)) {
|
||||
break;
|
||||
}
|
||||
@@ -166,7 +375,7 @@ static void cmd_r2ai_repl(RCorePluginSession *cps) {
|
||||
const char *a = r_strbuf_tostring (sb);
|
||||
char *error = NULL;
|
||||
char *res =
|
||||
r2ai (cps, (R2AIArgs){ .input = a, .error = &error, .dorag = true });
|
||||
r2ai (core, (R2AIArgs){ .input = a, .error = &error, .dorag = true });
|
||||
if (error) {
|
||||
R_LOG_ERROR ("%s", error);
|
||||
free (error);
|
||||
@@ -180,23 +389,21 @@ static void cmd_r2ai_repl(RCorePluginSession *cps) {
|
||||
r_strbuf_free (sb);
|
||||
}
|
||||
|
||||
static void cmd_r2ai_R(RCorePluginSession *cps, const char *q) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
static void cmd_r2ai_R(RCore *core, const char *q) {
|
||||
if (!r_config_get_b (core->config, "r2ai.data")) {
|
||||
R_LOG_ERROR ("r2ai -e r2ai.data=true");
|
||||
return;
|
||||
}
|
||||
if (R_STR_ISEMPTY (q)) {
|
||||
if (state->db) {
|
||||
r_vdb_free (state->db);
|
||||
state->db = NULL;
|
||||
if (db) {
|
||||
r_vdb_free (db);
|
||||
db = NULL;
|
||||
}
|
||||
r2ai_refresh_embeddings (cps);
|
||||
refresh_embeddings (core);
|
||||
} else {
|
||||
r2ai_refresh_embeddings (cps);
|
||||
refresh_embeddings (core);
|
||||
const int K = r_config_get_i (core->config, "r2ai.data.nth");
|
||||
RVdbResultSet *rs = r_vdb_query (state->db, q, K);
|
||||
RVdbResultSet *rs = r_vdb_query (db, q, K);
|
||||
|
||||
if (rs) {
|
||||
R_LOG_DEBUG ("Query: \"%s\"", q);
|
||||
@@ -208,7 +415,7 @@ static void cmd_r2ai_R(RCorePluginSession *cps, const char *q) {
|
||||
float dist_sq = r->dist_sq;
|
||||
float cos_sim = 1.0f - (dist_sq * 0.5f); // for normalized vectors
|
||||
printf ("%2d) dist_sq=%.4f cos_sim=%.4f text=\"%s\"\n", i + 1, dist_sq,
|
||||
cos_sim, (n->text? n->text: "(null)"));
|
||||
cos_sim, (n->text ? n->text : "(null)"));
|
||||
}
|
||||
r_vdb_result_free (rs);
|
||||
} else {
|
||||
@@ -217,15 +424,36 @@ static void cmd_r2ai_R(RCorePluginSession *cps, const char *q) {
|
||||
}
|
||||
}
|
||||
|
||||
static void cmd_r2ai_i(RCorePluginSession *cps, const char *arg) {
|
||||
RCore *core = cps->core;
|
||||
static void cmd_r2ai_n(RCore *core) {
|
||||
char *s = r_core_cmd_str (core, "r2ai -d");
|
||||
char *q =
|
||||
r_str_newf ("output only the radare2 commands in plain text without "
|
||||
"markdown. Give me a better name for this function. the "
|
||||
"output must be: 'afn NEWNAME'. do not include the function "
|
||||
"code, only the afn line. consider: \n```c\n%s\n```",
|
||||
s);
|
||||
char *error = NULL;
|
||||
char *res =
|
||||
r2ai (core, (R2AIArgs){ .input = q, .error = &error, .dorag = true });
|
||||
free (s);
|
||||
if (error) {
|
||||
R_LOG_ERROR (error);
|
||||
free (error);
|
||||
} else {
|
||||
R2_PRINTF ("%s\n", res);
|
||||
}
|
||||
free (res);
|
||||
free (q);
|
||||
}
|
||||
|
||||
static void cmd_r2ai_i(RCore *core, const char *arg) {
|
||||
char *fname = strdup (arg);
|
||||
char *query = strchr (fname, ' ');
|
||||
if (query) {
|
||||
*query++ = 0;
|
||||
}
|
||||
char *s = r_file_slurp (fname, NULL);
|
||||
if (R_STR_ISNOTEMPTY (s)) {
|
||||
if (R_STR_ISEMPTY (s)) {
|
||||
R_LOG_ERROR ("Cannot open %s", fname);
|
||||
free (fname);
|
||||
return;
|
||||
@@ -233,7 +461,7 @@ static void cmd_r2ai_i(RCorePluginSession *cps, const char *arg) {
|
||||
char *q = r_str_newf ("%s\n```\n%s\n```\n", query, s);
|
||||
char *error = NULL;
|
||||
char *res =
|
||||
r2ai (cps, (R2AIArgs){ .input = q, .error = &error, .dorag = true });
|
||||
r2ai (core, (R2AIArgs){ .input = q, .error = &error, .dorag = true });
|
||||
free (s);
|
||||
if (error) {
|
||||
R_LOG_ERROR (error);
|
||||
@@ -246,18 +474,105 @@ static void cmd_r2ai_i(RCorePluginSession *cps, const char *arg) {
|
||||
free (q);
|
||||
}
|
||||
|
||||
static void cmd_r2ai_m(RCorePluginSession *cps, const char *input) {
|
||||
RCore *core = cps->core;
|
||||
if (R_STR_ISEMPTY (input)) {
|
||||
R2_PRINTF ("%s\n", r_config_get (core->config, "r2ai.model"));
|
||||
} else {
|
||||
r_config_set (core->config, "r2ai.model", input);
|
||||
static void cmd_r2ai_s(RCore *core) {
|
||||
char *afv = r_core_cmd_str (core, "afv");
|
||||
r_str_trim (afv);
|
||||
char *s = r_core_cmd_str (core, "r2ai -d");
|
||||
r_str_trim (s);
|
||||
if (R_STR_ISEMPTY (s)) {
|
||||
R_LOG_ERROR ("Cannot find function");
|
||||
free (afv);
|
||||
return;
|
||||
}
|
||||
char *q = r_str_newf (
|
||||
"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, ONLY output the function signature, ignore '@' "
|
||||
"in argument types because it must be used in a C header:\n```\n%s\n``` "
|
||||
"source code:\n```\n%s\n```\n",
|
||||
afv, s);
|
||||
char *error = NULL;
|
||||
char *res =
|
||||
r2ai (core, (R2AIArgs){ .input = q, .error = &error, .dorag = true });
|
||||
if (error) {
|
||||
R_LOG_ERROR (error);
|
||||
free (error);
|
||||
} else {
|
||||
char *begin = strstr (res, "```");
|
||||
if (begin) {
|
||||
char *nl = strchr (begin, '\n');
|
||||
if (nl) {
|
||||
nl++;
|
||||
char *end = strstr (nl, "```");
|
||||
if (end) {
|
||||
*end = 0;
|
||||
}
|
||||
r_str_trim (nl);
|
||||
R2_PRINTF ("'afs %s\n", nl);
|
||||
}
|
||||
}
|
||||
}
|
||||
free (afv);
|
||||
free (res);
|
||||
free (q);
|
||||
free (s);
|
||||
}
|
||||
|
||||
static void load_embeddings(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
static void cmd_r2ai_v(RCore *core) {
|
||||
char *s = r_core_cmd_str (core, "r2ai -d");
|
||||
char *afv = r_core_cmd_str (core, "afv");
|
||||
char *q = r_str_newf (
|
||||
"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:\n```\n%s```",
|
||||
afv);
|
||||
char *error = NULL;
|
||||
char *res =
|
||||
r2ai (core, (R2AIArgs){ .input = q, .error = &error, .dorag = true });
|
||||
if (error) {
|
||||
R_LOG_ERROR (error);
|
||||
free (error);
|
||||
} else {
|
||||
R2_PRINTLN (res);
|
||||
}
|
||||
free (afv);
|
||||
free (res);
|
||||
free (q);
|
||||
free (s);
|
||||
}
|
||||
|
||||
static void cmd_r2ai_V(RCore *core, bool recursive) {
|
||||
char *s = r_core_cmd_str (core, recursive ? "r2ai -d" : "r2ai -dr");
|
||||
char *q = r_str_newf (
|
||||
"find vulnerabilities, dont show the code, only show the response, "
|
||||
"provide a sample exploit and suggest good practices:\n```\n%s```",
|
||||
s);
|
||||
char *error = NULL;
|
||||
char *res =
|
||||
r2ai (core, (R2AIArgs){ .input = q, .error = &error, .dorag = true });
|
||||
if (error) {
|
||||
R_LOG_ERROR (error);
|
||||
free (error);
|
||||
} else {
|
||||
R2_PRINTF ("%s\n", res);
|
||||
}
|
||||
free (res);
|
||||
free (q);
|
||||
free (s);
|
||||
}
|
||||
|
||||
static void cmd_r2ai_m(RCore *core, const char *input) {
|
||||
if (R_STR_ISEMPTY (input)) {
|
||||
R2_PRINTF ("%s\n", r_config_get (core->config, "r2ai.model"));
|
||||
return;
|
||||
}
|
||||
r_config_lock (core->config, false);
|
||||
r_config_set (core->config, "r2ai.model", input);
|
||||
r_config_lock (core->config, true);
|
||||
R2_PRINTF ("Model set to %s\n", input);
|
||||
}
|
||||
|
||||
static void load_embeddings(RCore *core, RVdb *db) {
|
||||
RListIter *iter, *iter2;
|
||||
char *line;
|
||||
char *file;
|
||||
@@ -280,7 +595,7 @@ static void load_embeddings(RCorePluginSession *cps) {
|
||||
if (r_str_trim_head_ro (line)[0] == 0) {
|
||||
continue;
|
||||
}
|
||||
r_vdb_insert (state->db, line);
|
||||
r_vdb_insert (db, line);
|
||||
R_LOG_DEBUG ("Insert %s", line);
|
||||
}
|
||||
r_list_free (lines);
|
||||
@@ -290,9 +605,7 @@ static void load_embeddings(RCorePluginSession *cps) {
|
||||
r_list_free (files);
|
||||
}
|
||||
|
||||
static void cmd_r2ai(RCorePluginSession *cps, const char *input) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
static void cmd_r2ai(RCore *core, const char *input) {
|
||||
if (*input == '?' || r_str_startswith (input, "-h")) {
|
||||
r_core_cmd_help (core, help_msg_r2ai);
|
||||
} else if (r_str_startswith (input, "-e")) {
|
||||
@@ -303,32 +616,36 @@ static void cmd_r2ai(RCorePluginSession *cps, const char *input) {
|
||||
r_core_cmdf (core, "-e r2ai.%s", arg);
|
||||
}
|
||||
} else if (r_str_startswith (input, "-a")) {
|
||||
cmd_r2ai_a (cps, r_str_trim_head_ro (input + 2));
|
||||
cmd_r2ai_a (core, r_str_trim_head_ro (input + 2));
|
||||
} else if (r_str_startswith (input, "-L-")) {
|
||||
const char *arg = r_str_trim_head_ro (input + 3);
|
||||
const int N = atoi (arg);
|
||||
R2AI_Messages *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_length (messages->messages) == 0) {
|
||||
R2AI_Messages *messages = r2ai_conversation_get ();
|
||||
if (!messages || messages->n_messages == 0) {
|
||||
R2_PRINTF ("No conversation history available\n");
|
||||
} else {
|
||||
r2ai_delete_last_messages (messages, N);
|
||||
R2_PRINTF ("Deleted %d message%s from chat history\n", N > 0? N: 1,
|
||||
(N > 0 && N != 1)? "s": "");
|
||||
R2_PRINTF ("Deleted %d message%s from chat history\n", N > 0 ? N : 1,
|
||||
(N > 0 && N != 1) ? "s" : "");
|
||||
}
|
||||
} else if (r_str_startswith (input, "-L")) {
|
||||
cmd_r2ai_logs (cps);
|
||||
cmd_r2ai_logs (core);
|
||||
} else if (r_str_startswith (input, "-d")) {
|
||||
cmd_r2ai_d (cps, r_str_trim_head_ro (input + 2), false);
|
||||
cmd_r2ai_d (core, r_str_trim_head_ro (input + 2), false);
|
||||
} else if (r_str_startswith (input, "-dr")) {
|
||||
cmd_r2ai_d (cps, r_str_trim_head_ro (input + 2), true);
|
||||
cmd_r2ai_d (core, r_str_trim_head_ro (input + 2), true);
|
||||
} else if (r_str_startswith (input, "-x")) {
|
||||
cmd_r2ai_x (core);
|
||||
} else if (r_str_startswith (input, "-s")) {
|
||||
cmd_r2ai_s (core);
|
||||
} else if (r_str_startswith (input, "-S")) {
|
||||
if (state->db == NULL) {
|
||||
state->db = r_vdb_new (R2AI_DEFAULT_VECTORS);
|
||||
load_embeddings (cps);
|
||||
if (db == NULL) {
|
||||
db = r_vdb_new (VDBDIM);
|
||||
load_embeddings (core, db);
|
||||
}
|
||||
const char *arg = r_str_trim_head_ro (input + 2);
|
||||
const int K = 10;
|
||||
RVdbResultSet *rs = r_vdb_query (state->db, arg, K);
|
||||
RVdbResultSet *rs = r_vdb_query (db, arg, K);
|
||||
if (rs) {
|
||||
int i;
|
||||
eprintf ("Found up to %d neighbors (actual found: %d).\n", K, rs->size);
|
||||
@@ -340,33 +657,35 @@ static void cmd_r2ai(RCorePluginSession *cps, const char *input) {
|
||||
r_vdb_result_free (rs);
|
||||
}
|
||||
} else if (r_str_startswith (input, "-i")) {
|
||||
cmd_r2ai_i (cps, r_str_trim_head_ro (input + 2));
|
||||
cmd_r2ai_i (core, r_str_trim_head_ro (input + 2));
|
||||
} else if (r_str_startswith (input, "-v")) {
|
||||
cmd_r2ai_v (core);
|
||||
} else if (r_str_startswith (input, "-V")) {
|
||||
cmd_r2ai_V (core, false);
|
||||
} else if (r_str_startswith (input, "-Vr")) {
|
||||
cmd_r2ai_V (core, true);
|
||||
} else if (r_str_startswith (input, "-n")) {
|
||||
cmd_r2ai_n (core);
|
||||
} else if (r_str_startswith (input, "-r")) {
|
||||
cmd_r2ai_repl (cps);
|
||||
cmd_r2ai_repl (core);
|
||||
} else if (r_str_startswith (input, "-R")) {
|
||||
R2AI_Messages *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_length (messages->messages) == 0) {
|
||||
R2AI_Messages *messages = r2ai_conversation_get ();
|
||||
if (!messages || messages->n_messages == 0) {
|
||||
R2_PRINTF ("No conversation history to reset\n");
|
||||
} else {
|
||||
r2ai_msgs_clear (messages);
|
||||
R2_PRINTF ("Chat conversation context has been reset\n");
|
||||
}
|
||||
} else if (r_str_startswith (input, "-Rq")) {
|
||||
cmd_r2ai_R (cps, r_str_trim_head_ro (input + 3));
|
||||
cmd_r2ai_R (core, r_str_trim_head_ro (input + 3));
|
||||
} else if (r_str_startswith (input, "-m")) {
|
||||
cmd_r2ai_m (cps, r_str_trim_head_ro (input + 2));
|
||||
} else if (r_str_startswith (input, "-p")) {
|
||||
const char *provider = r_str_trim_head_ro (input + 2);
|
||||
r_config_set (core->config, "r2ai.api", provider);
|
||||
R2_PRINTF ("Provider set to %s\n", provider);
|
||||
} else if (r_str_startswith (input, "-q")) {
|
||||
r2ai_cmd_q (cps, r_str_trim_head_ro (input + 2));
|
||||
cmd_r2ai_m (core, r_str_trim_head_ro (input + 2));
|
||||
} else if (r_str_startswith (input, "-")) {
|
||||
r_core_cmd_help (core, help_msg_r2ai);
|
||||
} else {
|
||||
char *err = NULL;
|
||||
char *res =
|
||||
r2ai (cps, (R2AIArgs){ .input = input, .error = &err, .dorag = true });
|
||||
r2ai (core, (R2AIArgs){ .input = input, .error = &err, .dorag = true });
|
||||
if (err) {
|
||||
R_LOG_ERROR ("%s", err);
|
||||
R_FREE (err);
|
||||
@@ -378,20 +697,155 @@ static void cmd_r2ai(RCorePluginSession *cps, const char *input) {
|
||||
}
|
||||
}
|
||||
|
||||
R_IPI const char *r2ai_get_provider_url(RCore *core, const char *provider) {
|
||||
if (strcmp (provider, "openai") == 0) {
|
||||
const char *host = r_config_get (core->config, "r2ai.baseurl");
|
||||
if (R_STR_ISNOTEMPTY (host)) {
|
||||
if (r_str_startswith (host, "http")) {
|
||||
return r_str_newf ("%s/v1", host);
|
||||
}
|
||||
return r_str_newf ("http://%s/v1", host);
|
||||
}
|
||||
return "https://api.openai.com/v1";
|
||||
} else if (strcmp (provider, "gemini") == 0) {
|
||||
return "https://generativelanguage.googleapis.com/v1beta/openai";
|
||||
} else if (strcmp (provider, "ollama") == 0) {
|
||||
const char *host = r_config_get (core->config, "r2ai.baseurl");
|
||||
if (R_STR_ISNOTEMPTY (host)) {
|
||||
if (r_str_startswith (host, "http")) {
|
||||
return r_str_newf ("%s/api", host);
|
||||
}
|
||||
return r_str_newf ("http://%s/api", host);
|
||||
}
|
||||
return "http://localhost:11434/api";
|
||||
} else if (strcmp (provider, "xai") == 0) {
|
||||
return "https://api.x.ai/v1";
|
||||
} else if (strcmp (provider, "anthropic") == 0) {
|
||||
return "https://api.anthropic.com/v1";
|
||||
} else if (strcmp (provider, "openapi") == 0) {
|
||||
return "http://127.0.0.1:11434";
|
||||
} else if (strcmp (provider, "openrouter") == 0) {
|
||||
return "https://openrouter.ai/api/v1";
|
||||
} else if (strcmp (provider, "groq") == 0) {
|
||||
return "https://api.groq.com/openai/v1";
|
||||
} else if (strcmp (provider, "mistral") == 0) {
|
||||
return "https://api.mistral.ai/v1";
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static RList *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
|
||||
char *models_url = NULL;
|
||||
if (strcmp (provider, "ollama") != 0) {
|
||||
models_url = r_str_newf ("%s/models", purl);
|
||||
} else {
|
||||
models_url = r_str_newf ("%s/tags", purl);
|
||||
}
|
||||
if (!models_url) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get API key for authentication (except for ollama and openapi)
|
||||
char *api_key = NULL;
|
||||
if (strcmp (provider, "ollama") && strcmp (provider, "openapi")) {
|
||||
// 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;
|
||||
|
||||
if (strcmp (provider, "anthropic") == 0) {
|
||||
// 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 (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 (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 RJson *data;
|
||||
if (strcmp (provider, "ollama") != 0) {
|
||||
data = r_json_get (json, "data");
|
||||
} else {
|
||||
data = r_json_get (json, "models");
|
||||
}
|
||||
|
||||
if (data && data->type == R_JSON_ARRAY) {
|
||||
const RJson *model_item = data->children.first;
|
||||
while (model_item) {
|
||||
const RJson *id;
|
||||
if (strcmp (provider, "ollama") != 0) {
|
||||
id = r_json_get (model_item, "id");
|
||||
} else {
|
||||
id = r_json_get (model_item, "model");
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
static bool cb_r2ai_api(void *user, void *data) {
|
||||
RCore *core = (RCore *)user;
|
||||
RConfigNode *node = (RConfigNode *)data;
|
||||
if (*node->value == '?') {
|
||||
r2ai_list_providers (core, NULL);
|
||||
const char apis[] = "ollama\nopenai\nopenapi\nanthropic\ngemini\nopenrouter\nmistral\ngroq\nxai";
|
||||
R2_PRINTLN (apis);
|
||||
return false;
|
||||
}
|
||||
// Validate the provider name
|
||||
const R2AIProvider *p = r2ai_get_provider (node->value);
|
||||
if (p) {
|
||||
return true;
|
||||
}
|
||||
R_LOG_ERROR ("Invalid provider '%s'. Use '?' to list valid providers.", node->value);
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cb_r2ai_model(void *user, void *data) {
|
||||
@@ -400,7 +854,7 @@ static bool cb_r2ai_model(void *user, void *data) {
|
||||
const char *api = r_config_get (core->config, "r2ai.api");
|
||||
if (*node->value == '?') {
|
||||
// Try to fetch models dynamically first
|
||||
RList *models = r2ai_fetch_available_models (core, api);
|
||||
RList *models = fetch_available_models (core, api);
|
||||
if (models && !r_list_empty (models)) {
|
||||
RListIter *iter;
|
||||
char *model;
|
||||
@@ -410,32 +864,28 @@ static bool cb_r2ai_model(void *user, void *data) {
|
||||
r_list_free (models);
|
||||
} else {
|
||||
// Fallback to static lists if dynamic fetching fails
|
||||
const R2AIProvider *p = r2ai_get_provider (api);
|
||||
if (p) {
|
||||
if (p->uses_system_ls) {
|
||||
char *s = r_sys_cmd_str ("ollama ls", NULL, NULL);
|
||||
if (s) {
|
||||
RList *items = r_str_split_list (s, "\n", 0);
|
||||
RListIter *iter;
|
||||
char *item;
|
||||
r_list_foreach (items, iter, item) {
|
||||
if (R_STR_ISEMPTY (item) || r_str_startswith (item, "NAME")) {
|
||||
continue;
|
||||
}
|
||||
char *s = strchr (item, ' ');
|
||||
if (s) {
|
||||
*s = 0;
|
||||
}
|
||||
R2_PRINTLN (item);
|
||||
if (!strcmp (api, "gemini")) {
|
||||
R2_PRINTLN ("gemini-1.5-flash");
|
||||
R2_PRINTLN ("gemini-1.0-pro");
|
||||
} else if (!strcmp (api, "ollama")) {
|
||||
char *s = r_sys_cmd_str ("ollama ls", NULL, NULL);
|
||||
if (s) {
|
||||
RList *items = r_str_split_list (s, "\n", 0);
|
||||
RListIter *iter;
|
||||
char *item;
|
||||
r_list_foreach (items, iter, item) {
|
||||
if (R_STR_ISEMPTY (item) || r_str_startswith (item, "NAME")) {
|
||||
continue;
|
||||
}
|
||||
r_list_free (items);
|
||||
free (s);
|
||||
char *s = strchr (item, ' ');
|
||||
if (s) {
|
||||
*s = 0;
|
||||
}
|
||||
R2_PRINTLN (item);
|
||||
}
|
||||
} else {
|
||||
R_LOG_ERROR ("Cannot list models for this provider");
|
||||
r_list_free (items);
|
||||
free (s);
|
||||
}
|
||||
} else {
|
||||
R_LOG_ERROR ("Invalid provider");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -443,26 +893,22 @@ static bool cb_r2ai_model(void *user, void *data) {
|
||||
return true;
|
||||
}
|
||||
|
||||
R_IPI bool r2ai_init(RCorePluginSession *cps) {
|
||||
#if R2_VERSION_NUMBER >= 50909
|
||||
static bool r2ai_init(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
// Initialize state
|
||||
R2AI_State *state = R_NEW0 (R2AI_State);
|
||||
cps->data = state;
|
||||
#else
|
||||
static int r2ai_init(void *user, const char *input) {
|
||||
RCmd *cmd = (RCmd *)user;
|
||||
RCore *core = cmd->data;
|
||||
#endif
|
||||
|
||||
// Initialize conversation container
|
||||
r2ai_conversation_init (state);
|
||||
r2ai_conversation_init ();
|
||||
|
||||
r_config_lock (core->config, false);
|
||||
r_config_set_cb (core->config, "r2ai.api", R2AI_DEFAULT_PROVIDER, &cb_r2ai_api);
|
||||
{
|
||||
RStrBuf *sb = r_strbuf_new ("LLM provider to use (");
|
||||
r2ai_list_providers (core, sb);
|
||||
r_strbuf_append (sb, ")");
|
||||
char *desc = r_strbuf_drain (sb);
|
||||
r_config_desc (core->config, "r2ai.api", desc);
|
||||
free (desc);
|
||||
}
|
||||
r_config_set_cb (core->config, "r2ai.model", R2AI_DEFAULT_MODEL, &cb_r2ai_model);
|
||||
r_config_set_cb (core->config, "r2ai.api", "openai", &cb_r2ai_api);
|
||||
r_config_desc (core->config, "r2ai.api", "LLM provider to use (openai, gemini, anthropic, ollama, ...)");
|
||||
r_config_set_cb (core->config, "r2ai.model", "gpt-5-mini", &cb_r2ai_model);
|
||||
r_config_desc (core->config, "r2ai.model", "Model identifier for the selected provider (e.g. gpt-5-mini)");
|
||||
r_config_set (core->config, "r2ai.baseurl", "");
|
||||
r_config_desc (core->config, "r2ai.baseurl", "Base URL for provider API (overrides default endpoints)");
|
||||
@@ -497,8 +943,6 @@ R_IPI bool r2ai_init(RCorePluginSession *cps) {
|
||||
"with if/else/for, use NO explanations, NO markdown, Simplify as much as "
|
||||
"possible, use better variable names, take function arguments and "
|
||||
"strings from comments like 'string:'");
|
||||
r_config_set (core->config, "r2ai.promptdir", "~/.config/r2ai/prompts");
|
||||
r_config_desc (core->config, "r2ai.promptdir", "Directory containing .r2ai prompt files");
|
||||
r_config_set_b (core->config, "r2ai.stream", false);
|
||||
r_config_desc (core->config, "r2ai.stream", "Enable streaming responses from the LLM (true/false)");
|
||||
r_config_set_i (core->config, "r2ai.auto.max_runs", 50);
|
||||
@@ -528,8 +972,14 @@ R_IPI bool r2ai_init(RCorePluginSession *cps) {
|
||||
return true;
|
||||
}
|
||||
|
||||
R_API bool r2ai_fini(RCorePluginSession *cps) {
|
||||
#if R2_VERSION_NUMBER >= 50909
|
||||
static bool r2ai_fini(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
#else
|
||||
static int r2ai_fini(void *user, const char *input) {
|
||||
RCmd *cmd = (RCmd *)user;
|
||||
RCore *core = cmd->data;
|
||||
#endif
|
||||
r_config_lock (core->config, false);
|
||||
r_config_rm (core->config, "r2ai.api");
|
||||
r_config_rm (core->config, "r2ai.cmds");
|
||||
@@ -548,22 +998,29 @@ R_API bool r2ai_fini(RCorePluginSession *cps) {
|
||||
r_config_rm (core->config, "r2ai.http.use_files");
|
||||
r_config_lock (core->config, true);
|
||||
|
||||
R2AI_State *state = cps->data;
|
||||
r2ai_conversation_free (state);
|
||||
r2ai_openai_fini (state);
|
||||
// Free the conversation
|
||||
r2ai_conversation_free ();
|
||||
|
||||
if (state) {
|
||||
r_vdb_free (state->db);
|
||||
state->db = NULL;
|
||||
free (state);
|
||||
cps->data = NULL;
|
||||
// Free the OpenAI resources
|
||||
r2ai_openai_fini ();
|
||||
|
||||
// Free the vector database if we have one
|
||||
if (db) {
|
||||
r_vdb_free (db);
|
||||
db = NULL;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#if R2_VERSION_NUMBER >= 50909
|
||||
static bool r_cmd_r2ai_client(RCorePluginSession *cps, const char *input) {
|
||||
RCore *core = cps->core;
|
||||
#else
|
||||
static int r_cmd_r2ai_client(void *user, const char *input) {
|
||||
RCore *core = (RCore *)user;
|
||||
#endif
|
||||
if (r_str_startswith (input, "r2ai")) {
|
||||
cmd_r2ai (cps, r_str_trim_head_ro (input + 4));
|
||||
cmd_r2ai (core, r_str_trim_head_ro (input + 4));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -575,7 +1032,7 @@ RCorePlugin r_core_plugin_r2ai_client = {
|
||||
.name = "r2ai",
|
||||
.desc = "r2ai plugin in plain C",
|
||||
.author = "pancake",
|
||||
.version = R2AI_VERSION,
|
||||
.version = "1.1.1",
|
||||
.license = "MIT",
|
||||
},
|
||||
.init = r2ai_init,
|
||||
@@ -587,7 +1044,9 @@ RCorePlugin r_core_plugin_r2ai_client = {
|
||||
R_API RLibStruct radare_plugin = {
|
||||
.type = R_LIB_TYPE_CORE,
|
||||
.data = &r_core_plugin_r2ai_client,
|
||||
#if R2_VERSION_NUMBER >= 50909
|
||||
.abiversion = R2_ABIVERSION,
|
||||
#endif
|
||||
.version = R2_VERSION
|
||||
};
|
||||
#endif
|
||||
+36
-94
@@ -1,38 +1,24 @@
|
||||
#ifndef R2AI_H
|
||||
#define R2AI_H
|
||||
|
||||
#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_vdb.h"
|
||||
#include "markdown.h"
|
||||
|
||||
#define R2AI_VERSION "1.2.0"
|
||||
|
||||
#if 0
|
||||
#define R2AI_DEFAULT_MODEL "gpt-5-mini"
|
||||
#define R2AI_DEFAULT_PROVIDER "openai"
|
||||
#else
|
||||
#define R2AI_DEFAULT_MODEL "gpt-oss:20b"
|
||||
#define R2AI_DEFAULT_PROVIDER "ollama"
|
||||
#endif
|
||||
|
||||
#define R2AI_DEFAULT_VECTORS 16
|
||||
|
||||
#if R2_VERSION_NUMBER < 60000
|
||||
#error Your radare2 is too old
|
||||
#endif
|
||||
|
||||
// TODO: Deprecate those defines
|
||||
#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)
|
||||
#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 ()
|
||||
#endif
|
||||
|
||||
// Tool definition structure
|
||||
typedef struct {
|
||||
@@ -54,14 +40,14 @@ typedef struct {
|
||||
} R2AI_ToolCall;
|
||||
|
||||
typedef struct {
|
||||
char *type;
|
||||
char *id;
|
||||
char *name;
|
||||
char *input;
|
||||
char *data;
|
||||
char *thinking;
|
||||
char *signature;
|
||||
char *text;
|
||||
const char *type;
|
||||
const char *id;
|
||||
const char *name;
|
||||
const char *input;
|
||||
const char *data;
|
||||
const char *thinking;
|
||||
const char *signature;
|
||||
const char *text;
|
||||
} R2AI_ContentBlock;
|
||||
|
||||
typedef struct {
|
||||
@@ -70,26 +56,15 @@ typedef struct {
|
||||
} R2AI_ContentBlocks;
|
||||
|
||||
typedef struct {
|
||||
char *role;
|
||||
const char *role;
|
||||
const char *content;
|
||||
char *reasoning_content;
|
||||
R2AI_ContentBlocks *content_blocks;
|
||||
char *tool_call_id;
|
||||
R2AI_ToolCall *tool_calls;
|
||||
const char *reasoning_content;
|
||||
const R2AI_ContentBlocks *content_blocks;
|
||||
const char *tool_call_id;
|
||||
const R2AI_ToolCall *tool_calls;
|
||||
int n_tool_calls;
|
||||
} R2AI_Message;
|
||||
|
||||
typedef struct {
|
||||
char *title;
|
||||
char *author;
|
||||
char *desc;
|
||||
char *command;
|
||||
char *prompt;
|
||||
char *requires;
|
||||
char *if_empty;
|
||||
char *if_command;
|
||||
} R2AIPrompt;
|
||||
|
||||
typedef struct {
|
||||
uint64_t prompt_tokens;
|
||||
uint64_t completion_tokens;
|
||||
@@ -103,7 +78,9 @@ typedef struct {
|
||||
|
||||
// Messages array management
|
||||
typedef struct {
|
||||
RList *messages; // RList<R2AI_Message *>
|
||||
R2AI_Message *messages;
|
||||
int n_messages;
|
||||
int cap_messages;
|
||||
} R2AI_Messages;
|
||||
|
||||
typedef struct {
|
||||
@@ -121,32 +98,6 @@ 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 {
|
||||
R2AI_Messages *conversation; // Global conversation messages (from messages.c)
|
||||
R2AIStats stats; // Global stats (from auto.c)
|
||||
R2AI_Tools *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
|
||||
*/
|
||||
@@ -155,12 +106,12 @@ R_API R2AI_Messages *r2ai_msgs_new(void);
|
||||
/**
|
||||
* Initialize the conversation container (call during plugin init)
|
||||
*/
|
||||
R_API void r2ai_conversation_init(R2AI_State *state);
|
||||
R_API void r2ai_conversation_init(void);
|
||||
|
||||
/**
|
||||
* Get the conversation instance (returns NULL if not initialized)
|
||||
*/
|
||||
R_API R2AI_Messages *r2ai_conversation_get(R2AI_State *state);
|
||||
R_API R2AI_Messages *r2ai_conversation_get(void);
|
||||
|
||||
/**
|
||||
* Clear all messages in a container without freeing the container
|
||||
@@ -211,18 +162,13 @@ R_API void r2ai_msgs_free(R2AI_Messages *msgs);
|
||||
/**
|
||||
* Free the conversation (call during plugin unload)
|
||||
*/
|
||||
R_API void r2ai_conversation_free(R2AI_State *state);
|
||||
R_API void r2ai_conversation_free(void);
|
||||
|
||||
/**
|
||||
* Free a R2AI_Message structure
|
||||
*/
|
||||
R_API void r2ai_message_free(R2AI_Message *msg);
|
||||
|
||||
/**
|
||||
* Free a R2AIPrompt structure
|
||||
*/
|
||||
R_API void r2aiprompt_free(R2AIPrompt *prompt);
|
||||
|
||||
/**
|
||||
* Delete the last N messages from the message array
|
||||
* If n <= 0, defaults to deleting just the last message
|
||||
@@ -290,7 +236,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(RCore *core, const char *url, const char *headers[], int *code, int *rlen);
|
||||
R_API char *r2ai_http_get(const char *url, const char *headers[], int *code, int *rlen);
|
||||
|
||||
/**
|
||||
* Get the base URL for a given provider
|
||||
@@ -302,22 +248,19 @@ R_API char *r2ai_http_get(RCore *core, const char *url, const char *headers[], i
|
||||
R_IPI const char *r2ai_get_provider_url(RCore *core, const char *provider);
|
||||
|
||||
// anthropic
|
||||
R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args);
|
||||
R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args);
|
||||
|
||||
// openai
|
||||
R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args);
|
||||
R_IPI void r2ai_openai_fini(R2AI_State *state);
|
||||
R_IPI R2AI_ChatResponse *r2ai_openai(RCore *core, R2AIArgs args);
|
||||
R_IPI void r2ai_openai_fini(void);
|
||||
|
||||
// auto mode
|
||||
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 void cmd_r2ai_a(RCore *core, const char *user_query);
|
||||
R_API char *r2ai(RCore *core, R2AIArgs args);
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args);
|
||||
R_IPI R2AI_ChatResponse *r2ai_llmcall(RCore *core, R2AIArgs args);
|
||||
|
||||
R_IPI void cmd_r2ai_logs(RCorePluginSession *cps);
|
||||
R_IPI void cmd_r2ai_logs(RCore *core);
|
||||
|
||||
/**
|
||||
* Create a conversation with system prompt and optional user message
|
||||
@@ -327,7 +270,7 @@ R_API R2AI_Messages *create_conversation(const char *user_message);
|
||||
/**
|
||||
* Process messages through LLM and handle tool calls recursively
|
||||
*/
|
||||
R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, const char *system_prompt, int n_run);
|
||||
R_API void process_messages(RCore *core, R2AI_Messages *messages, const char *system_prompt, int n_run);
|
||||
|
||||
/**
|
||||
* Helper function to convert RJson to string
|
||||
@@ -338,6 +281,5 @@ 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);
|
||||
|
||||
#endif
|
||||
+159
-290
@@ -18,91 +18,56 @@
|
||||
// Global flag for tracking interrupt status
|
||||
static volatile sig_atomic_t r2ai_http_interrupted = 0;
|
||||
|
||||
// Helper function to get HTTP configuration with defaults
|
||||
static void get_http_config(RCore *core, int *timeout, int *max_retries, int *max_backoff) {
|
||||
*timeout = 120;
|
||||
*max_retries = 10;
|
||||
*max_backoff = 30;
|
||||
if (core) {
|
||||
int t = r_config_get_i (core->config, "r2ai.http.timeout");
|
||||
if (t > 0) {
|
||||
*timeout = t;
|
||||
}
|
||||
int mr = r_config_get_i (core->config, "r2ai.http.max_retries");
|
||||
if (mr >= 0) {
|
||||
*max_retries = mr;
|
||||
}
|
||||
int mb = r_config_get_i (core->config, "r2ai.http.max_backoff");
|
||||
if (mb > 0) {
|
||||
*max_backoff = mb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// Helper to append headers to PowerShell command
|
||||
static void append_headers_to_cmd(RStrBuf *cmd, const char *headers[]) {
|
||||
r_strbuf_appendf (cmd, "$ProgressPreference='SilentlyContinue';$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);
|
||||
r_strbuf_appendf (cmd, "'%s'='%s';", key, value);
|
||||
free (key);
|
||||
free (value);
|
||||
}
|
||||
free (header);
|
||||
}
|
||||
}
|
||||
r_strbuf_appendf (cmd, "};");
|
||||
}
|
||||
#endif
|
||||
|
||||
// Signal handler for SIGINT
|
||||
static void r2ai_http_sigint_handler(int sig) {
|
||||
(void)sig;
|
||||
r2ai_http_interrupted = 1;
|
||||
(void)sig;
|
||||
r2ai_http_interrupted = 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Portable install/restore helpers for SIGINT handler.
|
||||
* Some platforms may not have struct sigaction available; use signal ()
|
||||
* Some platforms may not have struct sigaction available; use signal()
|
||||
* as a fallback. We allocate storage for the old handler when
|
||||
* sigaction is available so the restore function can restore it later.
|
||||
*/
|
||||
static void install_sigint_handler_local(void **out_old, int *out_old_is_sigaction) {
|
||||
void (*old) (int) = signal (SIGINT, r2ai_http_sigint_handler);
|
||||
*out_old = (void *)old;
|
||||
*out_old_is_sigaction = 0;
|
||||
void (*old)(int) = signal (SIGINT, r2ai_http_sigint_handler);
|
||||
*out_old = (void *)old;
|
||||
*out_old_is_sigaction = 0;
|
||||
}
|
||||
|
||||
static void restore_sigint_handler_local(void *old, int old_is_sigaction) {
|
||||
(void)old_is_sigaction;
|
||||
signal (SIGINT, (void (*) (int))old);
|
||||
(void)old_is_sigaction;
|
||||
signal (SIGINT, (void (*)(int))old);
|
||||
}
|
||||
|
||||
// Exponential backoff sleep with jitter
|
||||
// Helper function to implement exponential backoff sleep
|
||||
static void r2ai_sleep_with_backoff(int retry_count, int max_sleep_seconds) {
|
||||
int base_time_ms = 1000;
|
||||
// Calculate sleep time with exponential backoff: 2^retry * base_time with jitter
|
||||
int base_time_ms = 1000; // 500ms base time
|
||||
int max_sleep_ms = max_sleep_seconds * 1000;
|
||||
|
||||
// Calculate exponential delay time with upper bound
|
||||
int delay_ms = (1 << retry_count) * base_time_ms;
|
||||
if (delay_ms > max_sleep_ms) {
|
||||
delay_ms = max_sleep_ms;
|
||||
}
|
||||
|
||||
// Add jitter (±20%)
|
||||
int jitter = delay_ms / 5;
|
||||
if (jitter > 0) {
|
||||
srand (time (NULL) + retry_count);
|
||||
delay_ms += (rand () % (jitter * 2)) - jitter;
|
||||
}
|
||||
|
||||
// Ensure delay stays positive and within max limits
|
||||
if (delay_ms <= 0) {
|
||||
delay_ms = base_time_ms;
|
||||
} else if (delay_ms > max_sleep_ms) {
|
||||
delay_ms = max_sleep_ms;
|
||||
}
|
||||
|
||||
// Sleep for the calculated time
|
||||
struct timespec ts;
|
||||
ts.tv_sec = delay_ms / 1000;
|
||||
ts.tv_nsec = (delay_ms % 1000) * 1000000;
|
||||
@@ -156,13 +121,33 @@ static char *curl_http_post(const char *url, const char *headers[], const char *
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int timeout, max_retries, max_backoff;
|
||||
get_http_config (NULL, &timeout, &max_retries, &max_backoff);
|
||||
// Get timeout and retry configuration from config if available
|
||||
int timeout = 120; // Default timeout in seconds
|
||||
int max_retries = 10; // Default max retries
|
||||
int max_backoff = 30; // Default max backoff in seconds
|
||||
|
||||
RCore *core = r_cons_singleton ()->user;
|
||||
if (core) {
|
||||
timeout = r_config_get_i (core->config, "r2ai.http.timeout");
|
||||
if (timeout <= 0) {
|
||||
timeout = 120; // Use default if invalid
|
||||
}
|
||||
|
||||
max_retries = r_config_get_i (core->config, "r2ai.http.max_retries");
|
||||
if (max_retries < 0) {
|
||||
max_retries = 10; // Use default if invalid
|
||||
}
|
||||
|
||||
max_backoff = r_config_get_i (core->config, "r2ai.http.max_backoff");
|
||||
if (max_backoff <= 0) {
|
||||
max_backoff = 30; // Use default if invalid
|
||||
}
|
||||
}
|
||||
|
||||
// Install signal handler for interruption (portable)
|
||||
void *r2ai_old_sig = NULL;
|
||||
int r2ai_old_is_sigaction = 0;
|
||||
install_sigint_handler_local (&r2ai_old_sig, &r2ai_old_is_sigaction);
|
||||
install_sigint_handler_local(&r2ai_old_sig, &r2ai_old_is_sigaction);
|
||||
|
||||
// Reset interrupt flag
|
||||
r2ai_http_interrupted = 0;
|
||||
@@ -186,7 +171,7 @@ static char *curl_http_post(const char *url, const char *headers[], const char *
|
||||
r2ai_sleep_with_backoff (retry_count, max_backoff);
|
||||
continue;
|
||||
}
|
||||
restore_sigint_handler_local (r2ai_old_sig, r2ai_old_is_sigaction); // Restore signal handler
|
||||
restore_sigint_handler_local(r2ai_old_sig, r2ai_old_is_sigaction); // Restore signal handler
|
||||
return NULL;
|
||||
}
|
||||
response.data[0] = '\0';
|
||||
@@ -200,7 +185,7 @@ static char *curl_http_post(const char *url, const char *headers[], const char *
|
||||
r2ai_sleep_with_backoff (retry_count, max_backoff);
|
||||
continue;
|
||||
}
|
||||
restore_sigint_handler_local (r2ai_old_sig, r2ai_old_is_sigaction); // Restore signal handler
|
||||
restore_sigint_handler_local(r2ai_old_sig, r2ai_old_is_sigaction); // Restore signal handler
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -295,86 +280,14 @@ static char *curl_http_post(const char *url, const char *headers[], const char *
|
||||
}
|
||||
|
||||
// Restore the original signal handler
|
||||
restore_sigint_handler_local (r2ai_old_sig, r2ai_old_is_sigaction);
|
||||
restore_sigint_handler_local(r2ai_old_sig, r2ai_old_is_sigaction);
|
||||
|
||||
return result;
|
||||
}
|
||||
#endif // HAVE_LIBCURL
|
||||
|
||||
#ifdef _WIN32
|
||||
/**
|
||||
* Windows-specific HTTP POST using PowerShell
|
||||
*/
|
||||
static char *windows_http_post(const char *url, const char *headers[], const char *data, int *code, int *rlen, int timeout) {
|
||||
RStrBuf *cmd = r_strbuf_new ("powershell -Command \"");
|
||||
append_headers_to_cmd (cmd, headers);
|
||||
r_strbuf_appendf (cmd, "$body='%s';", data);
|
||||
r_strbuf_appendf (cmd, "try{$r=Invoke-WebRequest -Method Post -Uri '%s' -Headers $headers -Body $body -TimeoutSec %d;", 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);
|
||||
char *full_response = r_sys_cmd_str (cmd_str, NULL, NULL);
|
||||
free (cmd_str);
|
||||
if (full_response) {
|
||||
char *newline = strchr (full_response, '\n');
|
||||
if (newline) {
|
||||
*newline = '\0';
|
||||
*code = atoi (full_response);
|
||||
char *body = newline + 1;
|
||||
if (rlen) {
|
||||
*rlen = strlen (body);
|
||||
}
|
||||
char *result = strdup (body);
|
||||
free (full_response);
|
||||
return result;
|
||||
}
|
||||
*code = 200;
|
||||
if (rlen) {
|
||||
*rlen = strlen (full_response);
|
||||
}
|
||||
return full_response;
|
||||
}
|
||||
*code = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows-specific HTTP GET using PowerShell
|
||||
*/
|
||||
static char *windows_http_get(const char *url, const char *headers[], int *code, int *rlen, int timeout) {
|
||||
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;", 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);
|
||||
char *full_response = r_sys_cmd_str (cmd_str, NULL, NULL);
|
||||
free (cmd_str);
|
||||
if (full_response) {
|
||||
char *newline = strchr (full_response, '\n');
|
||||
if (newline) {
|
||||
*newline = '\0';
|
||||
*code = atoi (full_response);
|
||||
char *body = newline + 1;
|
||||
if (rlen) {
|
||||
*rlen = strlen (body);
|
||||
}
|
||||
char *result = strdup (body);
|
||||
free (full_response);
|
||||
return result;
|
||||
}
|
||||
*code = 200;
|
||||
if (rlen) {
|
||||
*rlen = strlen (full_response);
|
||||
}
|
||||
return full_response;
|
||||
}
|
||||
*code = 0;
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Execute curl as a system command, sending data via temporary file
|
||||
* On Windows, use PowerShell as fallback when curl is not available
|
||||
*
|
||||
* @param url The URL to send the request to
|
||||
* @param headers Array of headers, NULL terminated
|
||||
@@ -388,13 +301,25 @@ static char *system_curl_post_file(RCore *core, const char *url, const char *hea
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int timeout, max_retries, max_backoff;
|
||||
get_http_config (core, &timeout, &max_retries, &max_backoff);
|
||||
int timeout = r_config_get_i (core->config, "r2ai.http.timeout");
|
||||
if (timeout <= 0) {
|
||||
timeout = 120; // Use default if invalid
|
||||
}
|
||||
|
||||
int max_retries = r_config_get_i (core->config, "r2ai.http.max_retries");
|
||||
if (max_retries < 0) {
|
||||
max_retries = 10; // Use default if invalid
|
||||
}
|
||||
|
||||
int max_backoff = r_config_get_i (core->config, "r2ai.http.max_backoff");
|
||||
if (max_backoff <= 0) {
|
||||
max_backoff = 30; // Use default if invalid
|
||||
}
|
||||
|
||||
// Install signal handler for interruption (portable)
|
||||
void *r2ai_old_sig = NULL;
|
||||
int r2ai_old_is_sigaction = 0;
|
||||
install_sigint_handler_local (&r2ai_old_sig, &r2ai_old_is_sigaction);
|
||||
install_sigint_handler_local(&r2ai_old_sig, &r2ai_old_is_sigaction);
|
||||
|
||||
// Reset interrupt flag
|
||||
r2ai_http_interrupted = 0;
|
||||
@@ -431,81 +356,6 @@ static char *system_curl_post_file(RCore *core, const char *url, const char *hea
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// On Windows, use PowerShell for HTTP requests
|
||||
RStrBuf *cmd = r_strbuf_new ("powershell -Command \"");
|
||||
r_strbuf_appendf (cmd, "$ProgressPreference = 'SilentlyContinue'; ");
|
||||
r_strbuf_appendf (cmd, "$headers = @{");
|
||||
|
||||
// Add headers as PowerShell hashtable
|
||||
for (int i = 0; headers[i] != NULL; 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);
|
||||
r_strbuf_appendf (cmd, "'%s'='%s';", key, value);
|
||||
free (key);
|
||||
free (value);
|
||||
}
|
||||
free (header);
|
||||
}
|
||||
r_strbuf_appendf (cmd, "}; ");
|
||||
|
||||
// Add body
|
||||
if (use_files) {
|
||||
r_strbuf_appendf (cmd, "$body = Get-Content '%s' -Raw; ", temp_file);
|
||||
} else {
|
||||
char *escaped_data = r_str_replace_all (strdup (data), "'", "''");
|
||||
r_strbuf_appendf (cmd, "$body = '%s'; ", escaped_data);
|
||||
free (escaped_data);
|
||||
}
|
||||
|
||||
r_strbuf_appendf (cmd, "try { $response = Invoke-WebRequest -Method Post -Uri '%s' -Headers $headers -Body $body -TimeoutSec %d; ", url, timeout);
|
||||
r_strbuf_appendf (cmd, "Write-Host $response.StatusCode; $response.Content } catch { Write-Host $_.Exception.Response.StatusCode.Value__; $_.Exception.Response.GetResponseStream().ReadToEnd() }\"");
|
||||
|
||||
// Execute the PowerShell command
|
||||
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);
|
||||
r_file_rm (temp_file);
|
||||
free (temp_file);
|
||||
|
||||
// Parse PowerShell response: first line is status code, rest is body
|
||||
if (full_response) {
|
||||
char *newline = strchr (full_response, '\n');
|
||||
if (newline) {
|
||||
*newline = '\0';
|
||||
*code = atoi (full_response);
|
||||
char *body = newline + 1;
|
||||
if (rlen) {
|
||||
*rlen = strlen (body);
|
||||
}
|
||||
result = strdup (body);
|
||||
} else {
|
||||
*code = 200; // Assume success if no newline
|
||||
if (rlen) {
|
||||
*rlen = strlen (full_response);
|
||||
}
|
||||
result = full_response;
|
||||
}
|
||||
free (full_response);
|
||||
success = true;
|
||||
} else {
|
||||
if (retry_count < max_retries) {
|
||||
retry_count++;
|
||||
R_LOG_INFO ("Retrying request (%d/%d) after failure...", retry_count, max_retries);
|
||||
r2ai_sleep_with_backoff (retry_count, max_backoff);
|
||||
continue;
|
||||
}
|
||||
*code = 0;
|
||||
break;
|
||||
}
|
||||
#else
|
||||
// Compose curl command
|
||||
RStrBuf *cmd = r_strbuf_new ("curl -s");
|
||||
|
||||
@@ -556,7 +406,7 @@ static char *system_curl_post_file(RCore *core, const char *url, const char *hea
|
||||
R_LOG_DEBUG ("HTTP request was interrupted by user");
|
||||
free (response);
|
||||
response = NULL;
|
||||
restore_sigint_handler_local (r2ai_old_sig, r2ai_old_is_sigaction);
|
||||
restore_sigint_handler_local(r2ai_old_sig, r2ai_old_is_sigaction);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -579,11 +429,10 @@ static char *system_curl_post_file(RCore *core, const char *url, const char *hea
|
||||
*code = 0;
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Restore the original signal handler
|
||||
restore_sigint_handler_local (r2ai_old_sig, r2ai_old_is_sigaction);
|
||||
restore_sigint_handler_local(r2ai_old_sig, r2ai_old_is_sigaction);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -603,13 +452,32 @@ static char *system_curl_get(const char *url, const char *headers[], int *code,
|
||||
}
|
||||
|
||||
// Get timeout and retry configuration from config if available
|
||||
int timeout, max_retries, max_backoff;
|
||||
get_http_config (NULL, &timeout, &max_retries, &max_backoff);
|
||||
int timeout = 120; // Default timeout in seconds
|
||||
int max_retries = 10; // Default max retries
|
||||
int max_backoff = 30; // Default max backoff in seconds
|
||||
|
||||
RCore *core = r_cons_singleton ()->user;
|
||||
if (core) {
|
||||
timeout = r_config_get_i (core->config, "r2ai.http.timeout");
|
||||
if (timeout <= 0) {
|
||||
timeout = 120; // Use default if invalid
|
||||
}
|
||||
|
||||
max_retries = r_config_get_i (core->config, "r2ai.http.max_retries");
|
||||
if (max_retries < 0) {
|
||||
max_retries = 10; // Use default if invalid
|
||||
}
|
||||
|
||||
max_backoff = r_config_get_i (core->config, "r2ai.http.max_backoff");
|
||||
if (max_backoff <= 0) {
|
||||
max_backoff = 30; // Use default if invalid
|
||||
}
|
||||
}
|
||||
|
||||
// Install signal handler for interruption (portable)
|
||||
void *r2ai_old_sig = NULL;
|
||||
int r2ai_old_is_sigaction = 0;
|
||||
install_sigint_handler_local (&r2ai_old_sig, &r2ai_old_is_sigaction);
|
||||
install_sigint_handler_local(&r2ai_old_sig, &r2ai_old_is_sigaction);
|
||||
|
||||
// Reset interrupt flag
|
||||
r2ai_http_interrupted = 0;
|
||||
@@ -620,20 +488,6 @@ static char *system_curl_get(const char *url, const char *headers[], int *code,
|
||||
bool success = false;
|
||||
|
||||
while (!success && retry_count <= max_retries && !r2ai_http_interrupted) {
|
||||
#ifdef _WIN32
|
||||
result = windows_http_get (url, headers, code, rlen, timeout);
|
||||
if (result) {
|
||||
success = true;
|
||||
} else if (retry_count < max_retries) {
|
||||
retry_count++;
|
||||
R_LOG_INFO ("Retrying request (%d/%d) after failure...", retry_count, max_retries);
|
||||
r2ai_sleep_with_backoff (retry_count, max_backoff);
|
||||
continue;
|
||||
} else {
|
||||
*code = 0;
|
||||
break;
|
||||
}
|
||||
#else
|
||||
// Compose curl command
|
||||
RStrBuf *cmd = r_strbuf_new ("curl -s");
|
||||
|
||||
@@ -674,7 +528,7 @@ static char *system_curl_get(const char *url, const char *headers[], int *code,
|
||||
if (r2ai_http_interrupted) {
|
||||
R_LOG_DEBUG ("HTTP request was interrupted by user");
|
||||
free (response);
|
||||
restore_sigint_handler_local (r2ai_old_sig, r2ai_old_is_sigaction);
|
||||
restore_sigint_handler_local(r2ai_old_sig, r2ai_old_is_sigaction);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -697,11 +551,10 @@ static char *system_curl_get(const char *url, const char *headers[], int *code,
|
||||
*code = 0;
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Restore the original signal handler
|
||||
restore_sigint_handler_local (r2ai_old_sig, r2ai_old_is_sigaction);
|
||||
restore_sigint_handler_local(r2ai_old_sig, r2ai_old_is_sigaction);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -709,13 +562,32 @@ static char *system_curl_get(const char *url, const char *headers[], int *code,
|
||||
// Socket implementation with interrupt handling and retry logic
|
||||
static char *socket_http_post_with_interrupt(const char *url, const char *headers[], const char *data, int *code, int *rlen) {
|
||||
// Get timeout and retry configuration from config if available
|
||||
int timeout, max_retries, max_backoff;
|
||||
get_http_config (NULL, &timeout, &max_retries, &max_backoff);
|
||||
int timeout = 120; // Default timeout in seconds
|
||||
int max_retries = 10; // Default max retries
|
||||
int max_backoff = 30; // Default max backoff in seconds
|
||||
|
||||
RCore *core = r_cons_singleton ()->user;
|
||||
if (core) {
|
||||
timeout = r_config_get_i (core->config, "r2ai.http.timeout");
|
||||
if (timeout <= 0) {
|
||||
timeout = 120; // Use default if invalid
|
||||
}
|
||||
|
||||
max_retries = r_config_get_i (core->config, "r2ai.http.max_retries");
|
||||
if (max_retries < 0) {
|
||||
max_retries = 10; // Use default if invalid
|
||||
}
|
||||
|
||||
max_backoff = r_config_get_i (core->config, "r2ai.http.max_backoff");
|
||||
if (max_backoff <= 0) {
|
||||
max_backoff = 30; // Use default if invalid
|
||||
}
|
||||
}
|
||||
|
||||
// Install signal handler for interruption (portable)
|
||||
void *r2ai_old_sig = NULL;
|
||||
int r2ai_old_is_sigaction = 0;
|
||||
install_sigint_handler_local (&r2ai_old_sig, &r2ai_old_is_sigaction);
|
||||
install_sigint_handler_local(&r2ai_old_sig, &r2ai_old_is_sigaction);
|
||||
|
||||
// Reset interrupt flag
|
||||
r2ai_http_interrupted = 0;
|
||||
@@ -785,7 +657,7 @@ static char *socket_http_post_with_interrupt(const char *url, const char *header
|
||||
}
|
||||
|
||||
// Restore the original signal handler
|
||||
restore_sigint_handler_local (r2ai_old_sig, r2ai_old_is_sigaction);
|
||||
restore_sigint_handler_local(r2ai_old_sig, r2ai_old_is_sigaction);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -797,13 +669,32 @@ static char *curl_http_get(const char *url, const char *headers[], int *code, in
|
||||
}
|
||||
|
||||
// Get timeout and retry configuration from config if available
|
||||
int timeout, max_retries, max_backoff;
|
||||
get_http_config (NULL, &timeout, &max_retries, &max_backoff);
|
||||
int timeout = 120; // Default timeout in seconds
|
||||
int max_retries = 10; // Default max retries
|
||||
int max_backoff = 30; // Default max backoff in seconds
|
||||
|
||||
RCore *core = r_cons_singleton ()->user;
|
||||
if (core) {
|
||||
timeout = r_config_get_i (core->config, "r2ai.http.timeout");
|
||||
if (timeout <= 0) {
|
||||
timeout = 120; // Use default if invalid
|
||||
}
|
||||
|
||||
max_retries = r_config_get_i (core->config, "r2ai.http.max_retries");
|
||||
if (max_retries < 0) {
|
||||
max_retries = 10; // Use default if invalid
|
||||
}
|
||||
|
||||
max_backoff = r_config_get_i (core->config, "r2ai.http.max_backoff");
|
||||
if (max_backoff <= 0) {
|
||||
max_backoff = 30; // Use default if invalid
|
||||
}
|
||||
}
|
||||
|
||||
// Install signal handler for interruption (portable)
|
||||
void *r2ai_old_sig = NULL;
|
||||
int r2ai_old_is_sigaction = 0;
|
||||
install_sigint_handler_local (&r2ai_old_sig, &r2ai_old_is_sigaction);
|
||||
install_sigint_handler_local(&r2ai_old_sig, &r2ai_old_is_sigaction);
|
||||
|
||||
// Reset interrupt flag
|
||||
r2ai_http_interrupted = 0;
|
||||
@@ -827,7 +718,7 @@ static char *curl_http_get(const char *url, const char *headers[], int *code, in
|
||||
r2ai_sleep_with_backoff (retry_count, max_backoff);
|
||||
continue;
|
||||
}
|
||||
restore_sigint_handler_local (r2ai_old_sig, r2ai_old_is_sigaction); // Restore signal handler
|
||||
restore_sigint_handler_local(r2ai_old_sig, r2ai_old_is_sigaction); // Restore signal handler
|
||||
return NULL;
|
||||
}
|
||||
response.data[0] = '\0';
|
||||
@@ -841,7 +732,7 @@ static char *curl_http_get(const char *url, const char *headers[], int *code, in
|
||||
r2ai_sleep_with_backoff (retry_count, max_backoff);
|
||||
continue;
|
||||
}
|
||||
restore_sigint_handler_local (r2ai_old_sig, r2ai_old_is_sigaction); // Restore signal handler
|
||||
restore_sigint_handler_local(r2ai_old_sig, r2ai_old_is_sigaction); // Restore signal handler
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -938,7 +829,7 @@ static char *curl_http_get(const char *url, const char *headers[], int *code, in
|
||||
}
|
||||
|
||||
// Restore the original signal handler
|
||||
restore_sigint_handler_local (r2ai_old_sig, r2ai_old_is_sigaction);
|
||||
restore_sigint_handler_local(r2ai_old_sig, r2ai_old_is_sigaction);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -947,13 +838,32 @@ static char *curl_http_get(const char *url, const char *headers[], int *code, in
|
||||
// Socket implementation for GET requests
|
||||
static char *socket_http_get_with_interrupt(const char *url, const char *headers[], int *code, int *rlen) {
|
||||
// Get timeout and retry configuration from config if available
|
||||
int timeout, max_retries, max_backoff;
|
||||
get_http_config (NULL, &timeout, &max_retries, &max_backoff);
|
||||
int timeout = 120; // Default timeout in seconds
|
||||
int max_retries = 10; // Default max retries
|
||||
int max_backoff = 30; // Default max backoff in seconds
|
||||
|
||||
RCore *core = r_cons_singleton ()->user;
|
||||
if (core) {
|
||||
timeout = r_config_get_i (core->config, "r2ai.http.timeout");
|
||||
if (timeout <= 0) {
|
||||
timeout = 120; // Use default if invalid
|
||||
}
|
||||
|
||||
max_retries = r_config_get_i (core->config, "r2ai.http.max_retries");
|
||||
if (max_retries < 0) {
|
||||
max_retries = 10; // Use default if invalid
|
||||
}
|
||||
|
||||
max_backoff = r_config_get_i (core->config, "r2ai.http.max_backoff");
|
||||
if (max_backoff <= 0) {
|
||||
max_backoff = 30; // Use default if invalid
|
||||
}
|
||||
}
|
||||
|
||||
// Install signal handler for interruption (portable)
|
||||
void *r2ai_old_sig = NULL;
|
||||
int r2ai_old_is_sigaction = 0;
|
||||
install_sigint_handler_local (&r2ai_old_sig, &r2ai_old_is_sigaction);
|
||||
install_sigint_handler_local(&r2ai_old_sig, &r2ai_old_is_sigaction);
|
||||
|
||||
// Reset interrupt flag
|
||||
r2ai_http_interrupted = 0;
|
||||
@@ -1026,7 +936,7 @@ static char *socket_http_get_with_interrupt(const char *url, const char *headers
|
||||
}
|
||||
|
||||
// Restore the original signal handler
|
||||
restore_sigint_handler_local (r2ai_old_sig, r2ai_old_is_sigaction);
|
||||
restore_sigint_handler_local(r2ai_old_sig, r2ai_old_is_sigaction);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1064,37 +974,17 @@ R_API char *r2ai_http_post(RCore *core, const char *url, const char *headers[],
|
||||
#if USE_LIBCURL && HAVE_LIBCURL
|
||||
return curl_http_post (url, headers, data, code, rlen);
|
||||
#else
|
||||
#ifdef _WIN32
|
||||
int timeout, max_retries, max_backoff;
|
||||
get_http_config (core, &timeout, &max_retries, &max_backoff);
|
||||
char *result = NULL;
|
||||
int retry_count = 0;
|
||||
bool success = false;
|
||||
while (!success && retry_count <= max_retries) {
|
||||
result = windows_http_post (url, headers, data, code, rlen, timeout);
|
||||
if (result) {
|
||||
success = true;
|
||||
} else if (retry_count < max_retries) {
|
||||
retry_count++;
|
||||
R_LOG_INFO ("Retrying request (%d/%d) after failure...", retry_count, max_retries);
|
||||
r2ai_sleep_with_backoff (retry_count, max_backoff);
|
||||
} else {
|
||||
*code = 0;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
#else
|
||||
#if USE_R2_CURL
|
||||
r_sys_setenv ("R2_CURL", "1");
|
||||
#endif
|
||||
return socket_http_post_with_interrupt (url, headers, data, code, rlen);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
R_API char *r2ai_http_get(RCore *core, const char *url, const char *headers[], int *code, int *rlen) {
|
||||
R_API char *r2ai_http_get(const char *url, const char *headers[], int *code, int *rlen) {
|
||||
RCore *core = r_cons_singleton ()->user;
|
||||
const char *backend = "auto";
|
||||
|
||||
if (core) {
|
||||
@@ -1119,31 +1009,10 @@ R_API char *r2ai_http_get(RCore *core, const char *url, const char *headers[], i
|
||||
#if USE_LIBCURL && HAVE_LIBCURL
|
||||
return curl_http_get (url, headers, code, rlen);
|
||||
#else
|
||||
#ifdef _WIN32
|
||||
int timeout, max_retries, max_backoff;
|
||||
get_http_config (core, &timeout, &max_retries, &max_backoff);
|
||||
char *result = NULL;
|
||||
int retry_count = 0;
|
||||
bool success = false;
|
||||
while (!success && retry_count <= max_retries) {
|
||||
result = windows_http_get (url, headers, code, rlen, timeout);
|
||||
if (result) {
|
||||
success = true;
|
||||
} else if (retry_count < max_retries) {
|
||||
retry_count++;
|
||||
R_LOG_INFO ("Retrying request (%d/%d) after failure...", retry_count, max_retries);
|
||||
r2ai_sleep_with_backoff (retry_count, max_backoff);
|
||||
} else {
|
||||
*code = 0;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
#else
|
||||
#if USE_R2_CURL
|
||||
r_sys_setenv ("R2_CURL", "1");
|
||||
#endif
|
||||
return socket_http_get_with_interrupt (url, headers, code, rlen);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
#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
|
||||
+1
-1
@@ -19,7 +19,7 @@ static R2AI_Tool r2cmd_tool = {
|
||||
|
||||
static R2AI_Tool qjs_tool = {
|
||||
.name = "execute_js",
|
||||
.description = "Execute a JavaScript script inside the radare2 environment. Use `var RESULT=r2.cmd(COMMAND)` to execute radare2 commands and `r2.log(MESSAGE)` instead of console.log`",
|
||||
.description = "Execute a JavaScript script in a quickjs environment. Only what you console.log will be returned.",
|
||||
.parameters = "{\
|
||||
\"type\": \"object\",\
|
||||
\"properties\": {\
|
||||
|
||||
@@ -19,7 +19,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 +34,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 +212,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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user