mirror of
https://github.com/radareorg/r2ai.git
synced 2026-11-10 04:01:27 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c016fc72ed | ||
|
|
779b787f2d | ||
|
|
ca210f149b | ||
|
|
99e99b1797 | ||
|
|
fb5427238b | ||
|
|
d7c2dacd7a | ||
|
|
d6c32f4b72 | ||
|
|
0a083e7231 | ||
|
|
1a067fbbfc | ||
|
|
8a724d3682 | ||
|
|
8f7d152aa3 | ||
|
|
7120c325da | ||
|
|
14c991cd10 | ||
|
|
d1ef1e065e | ||
|
|
6f8ec21a39 | ||
|
|
3db0744c91 | ||
|
|
3306e412e2 | ||
|
|
20f5e16164 | ||
|
|
660249a31f | ||
|
|
312921c2a1 | ||
|
|
2356dbef9e | ||
|
|
7612a80a9c | ||
|
|
12d8bc9e22 | ||
|
|
d4be5f8569 | ||
|
|
a9333c8c38 | ||
|
|
29bdb2fcea | ||
|
|
078376c5d2 | ||
|
|
aa24702335 | ||
|
|
9aa40216a2 | ||
|
|
96e454e0c6 | ||
|
|
219b034261 | ||
|
|
797c967fb0 | ||
|
|
e61b7db8a5 | ||
|
|
3a39f9cd9d | ||
|
|
2b5e50d8f0 | ||
|
|
bb737f5be6 | ||
|
|
3e5f91e631 | ||
|
|
a3154bdcaf | ||
|
|
a6be6e6555 | ||
|
|
ad0bdbb902 | ||
|
|
4651d3b1b3 | ||
|
|
572e02c4ec | ||
|
|
75c787ef41 | ||
|
|
731f1933a9 | ||
|
|
573755ccef | ||
|
|
ecca7825fe | ||
|
|
95117608fb | ||
|
|
b3958b5f7d | ||
|
|
d0eaf37764 | ||
|
|
65692edd54 | ||
|
|
50f55bb5d2 | ||
|
|
fe1768fcc3 | ||
|
|
f241fba17d | ||
|
|
e5252960e0 | ||
|
|
8a45dadbde | ||
|
|
53d32efbbe | ||
|
|
f06faba9d6 | ||
|
|
8b52c2f6e2 | ||
|
|
b37436fb8d | ||
|
|
2524e66825 | ||
|
|
519017a75e | ||
|
|
6f30d6ca37 | ||
|
|
08ea4d11b2 | ||
|
|
0281308434 | ||
|
|
17a59d2057 | ||
|
|
eac0b8c6ef | ||
|
|
24aee426b4 | ||
|
|
b31bc5c116 | ||
|
|
01738210f5 | ||
|
|
7b19c4ff99 | ||
|
|
1986dd88bd | ||
|
|
1ed40401dd | ||
|
|
6ac0ff6352 | ||
|
|
4158e5f157 | ||
|
|
225ff58228 | ||
|
|
a51be44c68 | ||
|
|
911e455f72 | ||
|
|
70eb975b7f | ||
|
|
a0d36cc909 | ||
|
|
030551816b | ||
|
|
88c44c871b | ||
|
|
9218e3ea27 | ||
|
|
6f7ffcae1a | ||
|
|
fe12d2f8ee | ||
|
|
ae55b2f204 | ||
|
|
7d4e949240 | ||
|
|
9b97d39c7a | ||
|
|
33f72a997e | ||
|
|
36a8f13131 | ||
|
|
5c2ded0171 | ||
|
|
d5a39816a8 | ||
|
|
5aab38ab76 | ||
|
|
9f15ec6c73 | ||
|
|
68baac9911 | ||
|
|
54a280952e | ||
|
|
efc2368af1 | ||
|
|
28ddcbf638 | ||
|
|
85e9ed5c65 |
No files matched your search
@@ -0,0 +1,8 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- buildsystem
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Meson/Ninja
|
||||
if: matrix.build_system == 'meson'
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
|
||||
- name: Upload artifacts (tags only)
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.os }}-${{ matrix.build_system }}-binary
|
||||
path: release/r2ai-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.build_system }}.zip
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Verify tag is on master
|
||||
run: |
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: release-artifacts
|
||||
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
run: cp decai/decai.r2.js release-artifacts/
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
release-artifacts/**/*.zip
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -27,6 +27,9 @@ If you are looking to use radare2 with other agents via MCP:
|
||||
* **r2mcp** - the [official radare2 mcp](https://github.com/radare2/radare2-mcp)
|
||||
* **r2copilot** - the mcp with focus on CTF [r2copilot](https://github.com/darallium/r2-copilot)
|
||||
|
||||
If you are looking for a radare2 focused autonomous agent:
|
||||
* **r2agent** - automate radare2 workflows through autonomous agents. [r2agent](https://github.com/nitanmarcel/r2agent)
|
||||
|
||||
## Features
|
||||
|
||||
* Configure different roles and customize prompts
|
||||
@@ -67,28 +70,32 @@ $ r2pm -Uci decai
|
||||
- Adds the **r2ai** command to the radare2 shell: `r2 -qc r2ai`
|
||||
- You can also run the wrapper in $PATH: `r2pm -r r2ai`
|
||||
|
||||
Drop your API keys in environment variables or files in your home:
|
||||
Drop your API keys in environment variables or use the configuration file:
|
||||
|
||||
```console
|
||||
$ cat ~/.r2ai.anthropic-key
|
||||
sk-ant-api03-CENSORED
|
||||
$ export ANTHROPIC_API_KEY=sk-ant-api03-CENSORED
|
||||
$ export OPENAI_API_KEY=sk-proj-6rlSPS-zN1v...
|
||||
```
|
||||
|
||||
| AI | API key |
|
||||
| --------- | -------------------------- |
|
||||
| OpenAI | `$HOME/.r2ai.openai-key` |
|
||||
| Gemini | `$HOME/.r2ai.gemini-key` |
|
||||
| Anthropic | `$HOME/.r2ai.anthropic-key` |
|
||||
| Mistral | `$HOME/.r2ai.mistral-key` |
|
||||
Or edit the api keys file `~/.config/r2ai/apikeys.txt` run:
|
||||
|
||||
```console
|
||||
$ r2ai -K
|
||||
```
|
||||
|
||||
## Saving settings
|
||||
|
||||
You may customize and save your configuration settings using your OS's default settings file (e.g `~/.radare2rc` on Linux).
|
||||
For example, the following configuration sets Claude 3.7 by default, with max output tokens to 64000.
|
||||
|
||||
|
||||
```console
|
||||
$ r2ai -E
|
||||
```
|
||||
|
||||
then you can type the commands you want to run when the r2ai plugin is loaded:
|
||||
|
||||
```console
|
||||
r2ai -e api=anthropic
|
||||
r2ai -e model=claude-3-7-sonnet-20250219
|
||||
r2ai -e max_tokens=64000
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
#!/bin/sh
|
||||
vim decai/decai.r2.js
|
||||
vim decai/src/constants.ts
|
||||
make -C decai/src
|
||||
vim src/r2ai.h
|
||||
vim src/meson.build
|
||||
vim VERSION
|
||||
@@ -0,0 +1,7 @@
|
||||
# Decompiler based AI
|
||||
|
||||
## Actions
|
||||
|
||||
* Build: `make -C src`
|
||||
* Install: `make user-install`
|
||||
* Test: `r2 -A -qc "decai -e api=ollamacloud;decai -e model=glm-5;decai -d @ 0x100000924" /bin/ls`
|
||||
+5
-6
@@ -1,17 +1,16 @@
|
||||
PD=$(shell r2 -H R2_USER_PLUGINS)
|
||||
SPD=$(shell r2 -H R2_LIBR_PLUGINS)
|
||||
#BF=/bin/dd
|
||||
BF=a.out
|
||||
BF=/bin/dd
|
||||
|
||||
all:
|
||||
@echo "Run: make user-install"
|
||||
@echo "Run: 'make user-install' to install it in your home"
|
||||
@echo "Run: 'make -C src' to recompile the r2.js"
|
||||
|
||||
test:
|
||||
@echo 'You can now use 'decai' command'
|
||||
r2 -i decai.r2.js -c 'decai -e deterministic=true' -caf $(BF)
|
||||
|
||||
fmt indent:
|
||||
deno fmt
|
||||
$(MAKE) -C src fmt
|
||||
|
||||
user-install:
|
||||
mkdir -p "$(PD)"
|
||||
@@ -27,4 +26,4 @@ uninstall:
|
||||
make user-uninstall PD=$(SPD)
|
||||
|
||||
vs open:
|
||||
open -a "Visual Studio Code" .
|
||||
open -a "Visual Studio Code" src
|
||||
@@ -37,6 +37,7 @@ Usage: decai (-h) ...
|
||||
decai -dd [..] - same as above, but ignoring cache
|
||||
decai -dD [query]- decompile current function with given extra query
|
||||
decai -e - display and change eval config vars
|
||||
decai -E - edit decai.txt
|
||||
decai -h - show this help
|
||||
decai -i [f] [q] - include given file and query
|
||||
decai -n - suggest better function name
|
||||
@@ -59,6 +60,7 @@ decai -e host=http://localhost
|
||||
decai -e port=11434
|
||||
decai -e prompt=Rewrite this function and respond ONLY with code, NO explanations, NO 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:'
|
||||
decai -e ctxfile=
|
||||
decai -e headers=
|
||||
decai -e cmds=pdc
|
||||
decai -e cache=false
|
||||
decai -e lang=C
|
||||
@@ -96,6 +98,10 @@ decai -e ctxfile=
|
||||
...
|
||||
```
|
||||
|
||||
Persist user defaults in `~/.config/r2ai/decai.txt` with one `key=value` entry
|
||||
per line, then edit it with `decai -E`. That file is loaded on the first
|
||||
`decai` command in a session and reloaded after `decai -E` exits.
|
||||
|
||||
List possible APIs to discuss with AI: `decai -e api=?`:
|
||||
|
||||
```
|
||||
@@ -106,6 +112,14 @@ openapi
|
||||
...
|
||||
```
|
||||
|
||||
Extra headers can be injected globally with `decai -e headers=...` or from the
|
||||
environment with `DECAI_HEADERS` / `R2AI_HEADERS`. Use one header per line, for
|
||||
example:
|
||||
|
||||
```console
|
||||
[0x00406cac]> decai -e headers=Authorization: Bearer sk-123\nUser-Agent: curl/8.7.1
|
||||
```
|
||||
|
||||
### Example using a local model and ollama
|
||||
|
||||
For example, if Ollama serves model codegeex4:latest (`ollama ls`), set decai
|
||||
|
||||
+58
-1256
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,18 @@
|
||||
PLUGIN_NAME=decai
|
||||
R2F_C_FLAGS=-c -S
|
||||
|
||||
all: build
|
||||
|
||||
build:
|
||||
r2pm -r r2frida-compile $(R2F_C_FLAGS) -o ../$(PLUGIN_NAME).r2.js main.ts
|
||||
|
||||
test: build
|
||||
r2 -i ../$(PLUGIN_NAME).r2.js -q -c 'af;decai -d' /bin/ls
|
||||
|
||||
fmt:
|
||||
deno fmt
|
||||
|
||||
clean:
|
||||
rm -rf out node_modules ../$(PLUGIN_NAME).r2.js
|
||||
|
||||
.PHONY: all build test clean
|
||||
@@ -0,0 +1,63 @@
|
||||
import { API_KEYS_PATH, DECAI_CONFIG_DIR } from "./constants";
|
||||
import { ApiKeyResult } from "./types";
|
||||
import { ensurePath, fileExists, parseEnvLikeString } from "./utils";
|
||||
|
||||
const PROVIDER_ENV_MAP: Record<string, string> = {
|
||||
mistral: "MISTRAL_API_KEY",
|
||||
anthropic: "ANTHROPIC_API_KEY",
|
||||
huggingface: "HUGGINGFACE_API_KEY",
|
||||
openai: "OPENAI_API_KEY",
|
||||
gemini: "GEMINI_API_KEY",
|
||||
deepseek: "DEEPSEEK_API_KEY",
|
||||
xai: "XAI_API_KEY",
|
||||
ollama: "OLLAMA_API_KEY",
|
||||
ollamacloud: "OLLAMA_API_KEY",
|
||||
opencode: "OPENCODE_API_KEY",
|
||||
zen: "OPENCODE_API_KEY",
|
||||
openrouter: "OPENROUTER_API_KEY",
|
||||
groq: "GROQ_API_KEY",
|
||||
};
|
||||
|
||||
export function getApiKey(provider: string, envvar: string): ApiKeyResult {
|
||||
const keyEnv = r2.cmd("'%" + envvar).trim();
|
||||
if (!keyEnv.includes("=") && keyEnv !== "") {
|
||||
return [keyEnv.trim(), null, "env"];
|
||||
}
|
||||
|
||||
const providerLower = provider.toLowerCase();
|
||||
const keysPath = API_KEYS_PATH;
|
||||
|
||||
if (fileExists(keysPath)) {
|
||||
const keyFile = r2.cmd("'cat " + keysPath);
|
||||
const kv = parseEnvLikeString(keyFile);
|
||||
if (providerLower in kv) {
|
||||
return [kv[providerLower], null, "txt"];
|
||||
}
|
||||
}
|
||||
|
||||
const keyPath = "~/.r2ai." + providerLower + "-key";
|
||||
if (fileExists(keyPath)) {
|
||||
const keyFile = r2.cmd("'cat " + keyPath);
|
||||
return keyFile === ""
|
||||
? [null, "Cannot read " + keyPath, "no"]
|
||||
: [keyFile.trim(), null, "file"];
|
||||
}
|
||||
|
||||
return [null, "Not available", "nope"];
|
||||
}
|
||||
|
||||
export function editApiKeys(): void {
|
||||
ensurePath(DECAI_CONFIG_DIR, API_KEYS_PATH);
|
||||
r2.cmd("'ed " + API_KEYS_PATH);
|
||||
}
|
||||
|
||||
export function listApiKeys(): void {
|
||||
Object.entries(PROVIDER_ENV_MAP).forEach(([key, env]) => {
|
||||
const status = getApiKey(key, env)[2];
|
||||
console.log(status, "\t", key);
|
||||
});
|
||||
}
|
||||
|
||||
export function getProviderEnvVar(provider: string): string {
|
||||
return PROVIDER_ENV_MAP[provider.toLowerCase()] || "";
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
import { AutoReply } from "./types";
|
||||
import { state } from "./state";
|
||||
import { COMMAND, DEFAULT_PROMPT, HELP_TEXT, VERSION } from "./constants";
|
||||
import {
|
||||
b64,
|
||||
debugLog,
|
||||
filterResponse,
|
||||
trimAnsi,
|
||||
trimDown,
|
||||
trimJson,
|
||||
} from "./utils";
|
||||
import { editApiKeys, listApiKeys } from "./apiKeys";
|
||||
import { editRcConfig, evalConfig, listAllConfig } from "./config";
|
||||
import { r2ai } from "./r2ai";
|
||||
|
||||
export function showHelp(): void {
|
||||
let helpmsg = "";
|
||||
const msg = (m: string) => (helpmsg += " " + COMMAND + " " + m + "\n");
|
||||
|
||||
helpmsg += "Usage: " + COMMAND + " (-h) ...\n";
|
||||
helpmsg += "Version: " + VERSION + "\n";
|
||||
msg("-a [query] - solve query with auto mode");
|
||||
msg("-b [url] - set base URL (alias for decai -e baseurl)");
|
||||
msg("-d [f1 ..] - decompile given functions");
|
||||
msg("-dd [..] - same as above, but ignoring cache");
|
||||
msg("-dD [query] - decompile current function with given extra query");
|
||||
msg("-dr - decompile function and its called ones (recursive)");
|
||||
msg("-e - display and change eval config vars");
|
||||
msg("-E - edit decai.txt");
|
||||
msg("-h - show this help");
|
||||
msg("-H - help setting up r2ai");
|
||||
msg("-i [f] [q] - include given file and query");
|
||||
msg("-k - list API key status");
|
||||
msg("-K - edit apikeys.txt");
|
||||
msg("-m [model] - use -m? or -e model=? to list the available models");
|
||||
msg("-n - suggest better function name");
|
||||
msg("-p [provider] - same as decai -e api (will be provider)");
|
||||
msg("-q [text] - query language model with given text");
|
||||
msg("-Q [text] - query on top of the last output");
|
||||
msg("-r [prompt] - change role prompt (same as: decai -e prompt)");
|
||||
msg("-R - reset role prompt to default prompt");
|
||||
msg("-s - function signature");
|
||||
msg("-v - show local variables");
|
||||
msg("-V - find vulnerabilities");
|
||||
msg("-x[*] - eXplain current function (-x* for r2 script)");
|
||||
|
||||
r2.log(helpmsg.trim());
|
||||
}
|
||||
|
||||
export function decompile(
|
||||
args: string,
|
||||
extraQuery: boolean,
|
||||
useCache: boolean,
|
||||
recursiveCalls: boolean,
|
||||
): string | undefined {
|
||||
if (useCache) {
|
||||
const cachedAnnotation = r2.cmd("anos").trim();
|
||||
if (cachedAnnotation.length > 0) {
|
||||
return cachedAnnotation;
|
||||
}
|
||||
}
|
||||
|
||||
let context = "";
|
||||
if (recursiveCalls) {
|
||||
const at = r2.cmd("s");
|
||||
context += "## Context functions:\n";
|
||||
const funcs = r2.cmdAt("axff~^C[2]~$$", at);
|
||||
for (const addr of funcs.split(/\n/g)) {
|
||||
context += r2.cmd("pdc@" + addr);
|
||||
}
|
||||
r2.cmd("s " + at);
|
||||
}
|
||||
|
||||
const appendQuery = extraQuery ? " " + args : "";
|
||||
const origColor = r2.cmd("e scr.color");
|
||||
|
||||
try {
|
||||
const parsedArgs = args.slice(2).trim();
|
||||
let count = 0;
|
||||
let text = "";
|
||||
|
||||
if (
|
||||
state.contextFile !== "" &&
|
||||
r2.cmd2("test -f " + state.contextFile).value === 0
|
||||
) {
|
||||
text += "## Context:\n[START]\n" +
|
||||
r2.cmd("cat " + state.contextFile) +
|
||||
"\n[END]\n";
|
||||
}
|
||||
|
||||
r2.cmd("e scr.color=0");
|
||||
let body = "## Before:\n";
|
||||
|
||||
for (const c of state.commands.split(",")) {
|
||||
if (c.trim() === "") continue;
|
||||
|
||||
const oneliner = extraQuery || parsedArgs.trim().length === 0
|
||||
? c
|
||||
: c + "@@= " + parsedArgs;
|
||||
const output = r2.cmd(oneliner);
|
||||
|
||||
if (output.length > 5) {
|
||||
body += "Output of " + c + ":\n[START]\n" + output + "\n[END]\n";
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
body += "## After:\n";
|
||||
r2.cmd("e scr.color=" + origColor);
|
||||
|
||||
if (count === 0) {
|
||||
console.error("Nothing to do.");
|
||||
return;
|
||||
}
|
||||
|
||||
let out = "";
|
||||
if (state.decopipe.use) {
|
||||
const dpipe = state.decopipe[state.decopipe.default as string] as {
|
||||
pipeline: Array<{ model?: string; query: string }>;
|
||||
globalQuery: string;
|
||||
};
|
||||
const origModel = state.model;
|
||||
let code = text + body;
|
||||
|
||||
for (const dp of dpipe.pipeline) {
|
||||
if (dp.model) state.model = dp.model;
|
||||
const query = dp.query + ". " + dpipe.globalQuery;
|
||||
out = r2ai(query, code, true);
|
||||
if (state.debug) {
|
||||
console.log("QUERY\n", query, "\nINPUT\n", code, "\nOUTPUT\n", out);
|
||||
}
|
||||
code = out;
|
||||
}
|
||||
out = code;
|
||||
state.model = origModel;
|
||||
} else {
|
||||
const query = appendQuery;
|
||||
text += body + context;
|
||||
out = r2ai(query, text, false);
|
||||
state.lastOutput = out;
|
||||
}
|
||||
|
||||
if (useCache && out.length > 1) {
|
||||
r2.call("ano=base64:" + b64(out));
|
||||
}
|
||||
|
||||
if (out.startsWith("```")) {
|
||||
out = out.replace(/```.*\n/, "").replace(/```$/, "");
|
||||
}
|
||||
|
||||
return out.trim();
|
||||
} catch (e) {
|
||||
r2.cmd("e scr.color=" + origColor);
|
||||
const err = e as Error;
|
||||
console.error(err, err.stack);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export function explainFunction(): string {
|
||||
const origColor = r2.cmd("e scr.color");
|
||||
r2.cmd("e scr.color=0");
|
||||
const hints = "[START]" +
|
||||
state.commands.split(",").map((c) => r2.cmd(c)).join("\n") + "[END]";
|
||||
r2.cmd("e scr.color=" + origColor);
|
||||
|
||||
const res = r2ai(
|
||||
"Analyze function calls, references, comments and strings, loops and ignore registers and memory accesses. Explain the purpose of this function in a single short sentence. /no_think Do not introduce or argue the response, translation of the explanation in " +
|
||||
state.humanLanguage,
|
||||
hints,
|
||||
true,
|
||||
);
|
||||
|
||||
const lines = res.trim().split(/\n/g);
|
||||
return lines[lines.length - 1].trim();
|
||||
}
|
||||
|
||||
export function suggestSignature(): string {
|
||||
const tmp = state.language;
|
||||
const code = r2.cmd("afv;pdc");
|
||||
state.language = "C";
|
||||
|
||||
let out = "'afs " +
|
||||
r2ai(
|
||||
"analyze the uses of the arguments and return value to infer the signature, identify which is the correct type for the return. Do NOT print the function body, ONLY output the function signature, like if it was going to be used in a C header",
|
||||
code,
|
||||
false,
|
||||
);
|
||||
|
||||
const brace = out.indexOf("{");
|
||||
if (brace !== -1) {
|
||||
out = out.substring(0, brace);
|
||||
}
|
||||
|
||||
state.language = tmp;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function autoMode(
|
||||
queryText: string,
|
||||
mainHandler: (args: string) => boolean,
|
||||
): void {
|
||||
const replies: string[] = [];
|
||||
|
||||
while (true) {
|
||||
let q = HELP_TEXT.auto;
|
||||
|
||||
if (replies.length > 0) {
|
||||
q += "## Command Results\n\n";
|
||||
for (const rep of replies) {
|
||||
const rp = JSON.parse(rep);
|
||||
q += "### " + rp.command + "\n\n```\n" + rp.response + "\n```\n";
|
||||
}
|
||||
}
|
||||
|
||||
q += "\n\n## User Prompt\n\n" + queryText;
|
||||
|
||||
if (state.debug) {
|
||||
console.log("#### input\n", q, "\n#### /input");
|
||||
}
|
||||
|
||||
console.log("Thinking...");
|
||||
const out = r2ai("", q, true);
|
||||
|
||||
if (state.debug) {
|
||||
console.log("#### output\n", out, "\n#### /output");
|
||||
}
|
||||
|
||||
try {
|
||||
const o: AutoReply = JSON.parse(
|
||||
trimJson(trimDown(filterResponse(out))),
|
||||
);
|
||||
|
||||
if (
|
||||
o.action === "r2cmd" ||
|
||||
o.action === "response" ||
|
||||
o.action === o.command
|
||||
) {
|
||||
const ocmd = o.command || "";
|
||||
|
||||
if (o.reason) {
|
||||
console.log("[r2cmd] Reasoning: " + o.reason);
|
||||
if (state.tts) {
|
||||
r2.syscmd("pkill say");
|
||||
r2.syscmd(
|
||||
"say -v Alex -r 250 '" + o.reason.replace(/'/g, "") + "' &",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[r2cmd] Action: " + o.description);
|
||||
console.log("[r2cmd] Command: " + ocmd);
|
||||
|
||||
let cmd = ocmd;
|
||||
if (!state.yolo) {
|
||||
cmd = autoRepl(ocmd, mainHandler);
|
||||
}
|
||||
|
||||
console.log("[r2cmd] Running: " + cmd);
|
||||
const obj = r2.cmd2(cmd);
|
||||
const logs = obj.logs
|
||||
? obj.logs.map((x: { type: string; message: string }) =>
|
||||
x.type + ": " + x.message
|
||||
).join("\n")
|
||||
: "";
|
||||
const res = (obj.res + logs).trim();
|
||||
|
||||
console.log(res);
|
||||
const cleanRes = trimAnsi(res);
|
||||
|
||||
if (state.debug) {
|
||||
console.log("<r2output>\n", cleanRes, "\n<(r2output>");
|
||||
}
|
||||
|
||||
replies.push(
|
||||
JSON.stringify({
|
||||
action: "response",
|
||||
command: cmd,
|
||||
description: o.description,
|
||||
response: cleanRes,
|
||||
}),
|
||||
);
|
||||
} else if (o.action === "reply") {
|
||||
console.log("Done\n", o.response);
|
||||
break;
|
||||
} else {
|
||||
console.log("Unknown response\n", JSON.stringify(out));
|
||||
}
|
||||
} catch (e) {
|
||||
const response = out.indexOf('response": "');
|
||||
if (response !== -1) {
|
||||
const res = out
|
||||
.slice(response + 12)
|
||||
.replace(/\\n/g, "\n")
|
||||
.replace(/\\/g, "");
|
||||
console.log(res);
|
||||
} else {
|
||||
console.log(out);
|
||||
console.error(e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function autoRepl(
|
||||
ocmd: string,
|
||||
mainHandler: (args: string) => boolean,
|
||||
): string {
|
||||
while (true) {
|
||||
const cmd = r2.cmd("'?ie Tweak command? ('?' for help)").trim();
|
||||
|
||||
if (cmd === "q!") {
|
||||
console.error("Break!");
|
||||
break;
|
||||
}
|
||||
if (cmd === "?") {
|
||||
autoHelp();
|
||||
continue;
|
||||
} else if (cmd.startsWith(":")) {
|
||||
console.log(r2.cmd(cmd.slice(1)));
|
||||
continue;
|
||||
} else if (cmd.startsWith("-e")) {
|
||||
mainHandler(cmd);
|
||||
continue;
|
||||
} else if (cmd === "!") {
|
||||
return "?e do NOT execute '" + ocmd + "' again, continue without it";
|
||||
} else if (cmd.startsWith("!")) {
|
||||
console.log(r2.syscmd(cmd.slice(1)));
|
||||
continue;
|
||||
} else if (cmd === "q") {
|
||||
return "?e All data collected!. Do not call more commands, reply the solutions";
|
||||
} else if (!cmd) {
|
||||
return ocmd;
|
||||
} else {
|
||||
const comment = cmd.indexOf("#");
|
||||
if (comment !== -1) {
|
||||
const command = cmd.slice(0, comment).trim();
|
||||
return command;
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
return ocmd;
|
||||
}
|
||||
|
||||
function autoHelp(): void {
|
||||
console.log(" '!' do not run this command");
|
||||
console.log(" '!c' run system command");
|
||||
console.log(" 'q' to quit auto and try to solve");
|
||||
console.log(" 'q!' quit auto without solving");
|
||||
console.log(" 'c # C' use given command with comment");
|
||||
console.log(" ':c' run r2 command without feeding auto");
|
||||
console.log(" '-e' set decai configuration variables");
|
||||
}
|
||||
|
||||
function getFlagValue(args: string): string {
|
||||
return args.slice(2).trim();
|
||||
}
|
||||
|
||||
export function handleCommand(
|
||||
args: string,
|
||||
mainHandler: (args: string) => boolean,
|
||||
): string | undefined {
|
||||
if (args === "" || !args.startsWith("-")) {
|
||||
showHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
let output = "";
|
||||
const flag = args[1];
|
||||
const argValue = getFlagValue(args);
|
||||
|
||||
switch (flag) {
|
||||
case "H":
|
||||
console.log(HELP_TEXT.decai);
|
||||
break;
|
||||
|
||||
case "a":
|
||||
autoMode(argValue, mainHandler);
|
||||
break;
|
||||
|
||||
case "m": {
|
||||
if (argValue === "=") {
|
||||
evalConfig("model=");
|
||||
} else if (argValue) {
|
||||
evalConfig("model=" + argValue);
|
||||
} else {
|
||||
evalConfig("model");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "n":
|
||||
case "f": {
|
||||
output = r2.cmd("axff~$[3]");
|
||||
const considerations = r2
|
||||
.cmd("fd.")
|
||||
.trim()
|
||||
.split(/\n/)
|
||||
.filter((x: string) => !x.startsWith("secti"))
|
||||
.join(",");
|
||||
|
||||
output = r2ai(
|
||||
"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: " +
|
||||
considerations,
|
||||
output,
|
||||
false,
|
||||
).trim();
|
||||
output += " @ " + r2.cmd("?v $FB").trim();
|
||||
break;
|
||||
}
|
||||
|
||||
case "v":
|
||||
output = r2.cmd("afv;pdc");
|
||||
output = r2ai(
|
||||
"guess a better name and type for each local variable and function argument taking using. output an r2 script using afvn and afvt commands",
|
||||
output,
|
||||
false,
|
||||
);
|
||||
break;
|
||||
|
||||
case "i": {
|
||||
const parts = argValue.split(/\s+/).filter(Boolean);
|
||||
if (parts.length >= 2) {
|
||||
const file = parts[0];
|
||||
const query = parts.slice(1).join(" ");
|
||||
const fileData = r2.cmd("cat " + file);
|
||||
console.log(r2ai(query, fileData, true));
|
||||
} else {
|
||||
console.log("Use: decai -i [file] [query ...]");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "p": {
|
||||
if (argValue) {
|
||||
evalConfig("api=" + argValue);
|
||||
} else {
|
||||
listApiKeys();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "r": {
|
||||
if (argValue) {
|
||||
state.prompt = argValue;
|
||||
} else {
|
||||
console.log(state.prompt);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "R":
|
||||
state.prompt = DEFAULT_PROMPT;
|
||||
break;
|
||||
|
||||
case "s":
|
||||
output = suggestSignature();
|
||||
break;
|
||||
|
||||
case "V":
|
||||
output = r2ai(
|
||||
"find vulnerabilities, dont show the code, only show the response, provide a sample exploit",
|
||||
state.lastOutput,
|
||||
false,
|
||||
);
|
||||
break;
|
||||
|
||||
case "K":
|
||||
editApiKeys();
|
||||
break;
|
||||
|
||||
case "E":
|
||||
editRcConfig();
|
||||
break;
|
||||
|
||||
case "k":
|
||||
listApiKeys();
|
||||
break;
|
||||
|
||||
case "b": {
|
||||
if (argValue) {
|
||||
evalConfig("baseurl=" + argValue);
|
||||
} else {
|
||||
console.log(state.baseurl);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "e": {
|
||||
if (argValue) {
|
||||
evalConfig(argValue);
|
||||
} else {
|
||||
listAllConfig();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "q":
|
||||
try {
|
||||
output = r2ai(argValue, null, true);
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
console.error(err, err.stack);
|
||||
}
|
||||
break;
|
||||
|
||||
case "Q":
|
||||
output = r2ai(argValue, state.lastOutput, false);
|
||||
break;
|
||||
|
||||
case "x":
|
||||
output = explainFunction();
|
||||
if (args[2] === "*" || args[2] === "r") {
|
||||
output = "'CC " + output;
|
||||
}
|
||||
break;
|
||||
|
||||
case "d":
|
||||
if (args[2] === "r") {
|
||||
output = decompile(args.slice(2), true, state.cache, true) || "";
|
||||
} else if (args[2] === "d") {
|
||||
output = decompile(args, false, false, false) || "";
|
||||
} else if (args[2] === "D") {
|
||||
output = decompile(args, true, false, false) || "";
|
||||
} else {
|
||||
output = decompile(args, false, state.cache, false) || "";
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
showHelp();
|
||||
break;
|
||||
}
|
||||
|
||||
return output || undefined;
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import { ConfigHandlers } from "./types";
|
||||
import { DECAI_CONFIG_DIR, DECAI_CONFIG_PATH } from "./constants";
|
||||
import { defaultState, state } from "./state";
|
||||
import { listModelsFor } from "./models";
|
||||
import { listProviders } from "./providers";
|
||||
import { formatHeaders, parseHeaders } from "./headers";
|
||||
import { ensurePath, fileExists } from "./utils";
|
||||
|
||||
let rcConfigLoaded = false;
|
||||
|
||||
function parseBoolean(value: string): boolean {
|
||||
return value === "true" || value === "1";
|
||||
}
|
||||
|
||||
export const configHandlers: ConfigHandlers = {
|
||||
pipeline: {
|
||||
get: () => state.pipeline,
|
||||
set: (v: string) => {
|
||||
state.pipeline = v;
|
||||
try {
|
||||
state.decopipe = JSON.parse(r2.cmd("cat " + v));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
},
|
||||
model: {
|
||||
get: () => state.model,
|
||||
set: (v: string) => {
|
||||
if (v === "?") {
|
||||
listModelsFor(state.api);
|
||||
} else {
|
||||
state.model = v.trim();
|
||||
}
|
||||
},
|
||||
},
|
||||
deterministic: {
|
||||
get: () => state.deterministic,
|
||||
set: (v: string) => {
|
||||
state.deterministic = parseBoolean(v);
|
||||
},
|
||||
},
|
||||
files: {
|
||||
get: () => state.useFiles,
|
||||
set: (v: string) => {
|
||||
state.useFiles = parseBoolean(v);
|
||||
},
|
||||
},
|
||||
think: {
|
||||
get: () => state.think,
|
||||
set: (v: string) => {
|
||||
state.think = v === "true" ? 1 : v === "false" ? 0 : +v;
|
||||
},
|
||||
},
|
||||
debug: {
|
||||
get: () => state.debug,
|
||||
set: (v: string) => {
|
||||
state.debug = parseBoolean(v);
|
||||
},
|
||||
},
|
||||
timeout: {
|
||||
get: () => state.timeout,
|
||||
set: (v: string) => {
|
||||
state.timeout = Math.max(0, parseInt(v, 10) || 0);
|
||||
},
|
||||
},
|
||||
api: {
|
||||
get: () => state.api,
|
||||
set: (v: string) => {
|
||||
if (v === "?") {
|
||||
const providersList = listProviders().join("\n");
|
||||
console.error(providersList);
|
||||
} else {
|
||||
state.api = v;
|
||||
}
|
||||
},
|
||||
},
|
||||
lang: {
|
||||
get: () => state.language,
|
||||
set: (v: string) => {
|
||||
state.language = v;
|
||||
},
|
||||
},
|
||||
hlang: {
|
||||
get: () => state.humanLanguage,
|
||||
set: (v: string) => {
|
||||
state.humanLanguage = v;
|
||||
},
|
||||
},
|
||||
cache: {
|
||||
get: () => state.cache,
|
||||
set: (v: string) => {
|
||||
state.cache = parseBoolean(v);
|
||||
},
|
||||
},
|
||||
cmds: {
|
||||
get: () => state.commands,
|
||||
set: (v: string) => {
|
||||
state.commands = v;
|
||||
},
|
||||
},
|
||||
tts: {
|
||||
get: () => state.tts,
|
||||
set: (v: string) => {
|
||||
state.tts = parseBoolean(v);
|
||||
},
|
||||
},
|
||||
yolo: {
|
||||
get: () => state.yolo,
|
||||
set: (v: string) => {
|
||||
state.yolo = parseBoolean(v);
|
||||
},
|
||||
},
|
||||
prompt: {
|
||||
get: () => state.prompt,
|
||||
set: (v: string) => {
|
||||
state.prompt = v;
|
||||
},
|
||||
},
|
||||
ctxfile: {
|
||||
get: () => state.contextFile,
|
||||
set: (v: string) => {
|
||||
state.contextFile = v;
|
||||
},
|
||||
},
|
||||
baseurl: {
|
||||
get: () => state.baseurl,
|
||||
set: (v: string) => {
|
||||
state.baseurl = v;
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
get: () => formatHeaders(state.extraHeaders),
|
||||
set: (v: string) => {
|
||||
state.extraHeaders = parseHeaders(v);
|
||||
},
|
||||
},
|
||||
maxtokens: {
|
||||
get: () => state.maxInputTokens,
|
||||
set: (v: string) => {
|
||||
state.maxInputTokens = parseInt(v, 10) || -1;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function resetRcConfigState(): void {
|
||||
const keep = { host: state.host, port: state.port, lastOutput: state.lastOutput };
|
||||
Object.assign(state, defaultState, keep);
|
||||
state.decopipe = { ...defaultState.decopipe };
|
||||
state.extraHeaders = [...defaultState.extraHeaders];
|
||||
}
|
||||
|
||||
function normalizeRcLine(line: string): string | null {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) {
|
||||
return null;
|
||||
}
|
||||
return trimmed.startsWith("decai -e ")
|
||||
? trimmed.slice("decai -e ".length).trim()
|
||||
: trimmed;
|
||||
}
|
||||
|
||||
export function evalConfig(arg: string): void {
|
||||
const eqIndex = arg.indexOf("=");
|
||||
const k = eqIndex === -1 ? arg : arg.slice(0, eqIndex);
|
||||
const v = eqIndex === -1 ? undefined : arg.slice(eqIndex + 1);
|
||||
const handler = configHandlers[k];
|
||||
|
||||
if (!handler) {
|
||||
console.error("Unknown config key");
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof v !== "undefined") {
|
||||
handler.set(v);
|
||||
} else {
|
||||
console.log(handler.get());
|
||||
}
|
||||
}
|
||||
|
||||
export function listAllConfig(): void {
|
||||
Object.entries(configHandlers).forEach(([key, handler]) => {
|
||||
const value = handler.get();
|
||||
console.log("decai -e " + key + "=" + value);
|
||||
});
|
||||
}
|
||||
|
||||
export function loadRcConfig(): void {
|
||||
resetRcConfigState();
|
||||
if (!fileExists(DECAI_CONFIG_PATH)) {
|
||||
return;
|
||||
}
|
||||
const rcFile = r2.cmd("'cat " + DECAI_CONFIG_PATH);
|
||||
for (const line of rcFile.split(/\r?\n/)) {
|
||||
const rcLine = normalizeRcLine(line);
|
||||
if (rcLine) {
|
||||
evalConfig(rcLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureRcConfigLoaded(): void {
|
||||
if (rcConfigLoaded) {
|
||||
return;
|
||||
}
|
||||
loadRcConfig();
|
||||
rcConfigLoaded = true;
|
||||
}
|
||||
|
||||
export function editRcConfig(): void {
|
||||
ensurePath(DECAI_CONFIG_DIR, DECAI_CONFIG_PATH);
|
||||
r2.cmd("'ed " + DECAI_CONFIG_PATH);
|
||||
loadRcConfig();
|
||||
rcConfigLoaded = true;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
export const VERSION = "1.3.4";
|
||||
export const COMMAND = "decai";
|
||||
export const DECAI_CONFIG_DIR = "~/.config/r2ai";
|
||||
export const API_KEYS_PATH = DECAI_CONFIG_DIR + "/apikeys.txt";
|
||||
export const DECAI_CONFIG_PATH = DECAI_CONFIG_DIR + "/decai.txt";
|
||||
|
||||
export const DEFAULT_PROMPT =
|
||||
"Rewrite this pseudocode into concise and clean code. Output only the provided function. Do not add wrappers, helper examples, test code, or main-like functions. Replace goto with structured control flow, simplify as much as possible, infer types and use better names for variables and parameters, some strings may be appearing as comments, preserve only what is implied by the input, and remove dead code.";
|
||||
|
||||
export const HELP_TEXT = {
|
||||
decai: `# Using Decai
|
||||
|
||||
Decai is the radare2 plugin for decompiling functions with the help of language models.
|
||||
|
||||
By default uses a local ollama server, but can you can pick any other service by using 'decai -e api=?'.
|
||||
|
||||
[0x00000000]> decai -e api=?
|
||||
r2ai deepseek anthropic claude gemini groq hf lmstudio mistral ollama ollamacloud openapi opencode openai openrouter vllm xai zen
|
||||
|
||||
## Using Ollama
|
||||
|
||||
* Visit https://ollama.com to install it.
|
||||
* Download the model of choice: 'ollama run llama3.3'
|
||||
* Configure decai to use the given model with: 'decai -e model=?'
|
||||
|
||||
These are the most recommended models for decompiling in local:
|
||||
|
||||
* hhao/qwen2.5-coder-tools:latest (18GB of ram)
|
||||
* hhao/qwen2.5-coder-tools:32b (24GB of ram required)
|
||||
|
||||
## Common Options
|
||||
* 'decai -e baseurl=<url>' override default host and port for API endpoint (e.g., 'http://localhost:11434')
|
||||
* 'decai -e headers=Authorization: Bearer ...\\nUser-Agent: curl/8.7.1' add or override HTTP headers
|
||||
* "export DECAI_HEADERS='Authorization: Bearer ...\\nUser-Agent: curl/8.7.1'" set extra headers from the environment
|
||||
* 'decai -e timeout=0' disable curl timeouts, or set a positive number of seconds
|
||||
|
||||
* 'decai -e deterministic=true' to remove randomness from decompilation responses
|
||||
* 'decai -e lang=Python' to output the decompilation in Python instead of C
|
||||
* 'decai -e hlang=Catalan' to add comments or explanations in that language (instead of English)
|
||||
* 'decai -e cmds=pdd,pdg' use r2dec and r2ghidra instead of r2's pdc as input for decompiling
|
||||
* 'decai -e prompt=..' default prompt must be fine for most models and binaries, feel free to tweak it
|
||||
|
||||
## API Keys
|
||||
|
||||
Remove services like OpenAI, Mistral, Anthropic, Grok, Gemini, .. require API keys to work.
|
||||
|
||||
See 'decai -k' to list the status of available APIkeys
|
||||
Use 'decai -K' to edit apikeys.txt and 'decai -E' to edit decai.txt.
|
||||
|
||||
Decai will pick them from the environment or the config files in your home:
|
||||
|
||||
* echo KEY > ~/.r2ai.openai-key
|
||||
* export OPENAI_API_KEY=...
|
||||
* put one 'key=value' pair per line in ~/.config/r2ai/decai.txt
|
||||
|
||||
## Using the R2AI Server:
|
||||
|
||||
Install r2ai or r2ai-server with r2pm:
|
||||
|
||||
[0x0000000]> decai -e api=r2ai
|
||||
[0x0000000]> r2pm -ci r2ai
|
||||
|
||||
Choose one of the recommended models (after r2pm -r r2ai):
|
||||
|
||||
* -m ibm-granite/granite-20b-code-instruct-8k-GGUF
|
||||
* -m QuantFactory/granite-8b-code-instruct-4k-GGUF
|
||||
* -m TheBloke/Mistral-7B-Instruct-v0.2-GGUF
|
||||
|
||||
Start the webserver:
|
||||
|
||||
$ r2pm -r r2ai-server -l r2ai -m granite-8...
|
||||
`,
|
||||
auto: `# Radare2 Auto Mode
|
||||
|
||||
Use function calling to execute radare2 commands in order to resolve the user request defined in the "User Prompt" section, analyze the responses attached in the "Command Results" section.
|
||||
|
||||
## Function Calling
|
||||
|
||||
Respond ONLY using plain JSON. Process user query and decide which function calls are necessary to solve the task.
|
||||
|
||||
1. Analyze the user request to determine if we need to run commands to extend the knowledge and context of the problem.
|
||||
2. If function call is needed, construct the JSON like this:
|
||||
- Fill the "action" key with the "r2cmd" value.
|
||||
- Specify the "command" as a string.
|
||||
- Optionally, provide a "reason" and "description"
|
||||
3. If the answer can be provided and no more function calls are required:
|
||||
- Use the key "action": "reply".
|
||||
- Include "response" with the direct answer to the user query.
|
||||
|
||||
Return the result as a JSON object.
|
||||
|
||||
### Sample Function Calling Communication
|
||||
|
||||
Command Results: already performed actions with their responses
|
||||
User Prompt: "Count how many functions we have here."
|
||||
Response:
|
||||
{
|
||||
"action": "r2cmd",
|
||||
"command": "aflc",
|
||||
"description": "Count functions"
|
||||
"reason": "Evaluate if the program is analyzed before running aaa"
|
||||
}
|
||||
|
||||
## Rules
|
||||
|
||||
Use radare2 to resolve user requests.
|
||||
|
||||
* Explain each step in the "reason" field of the JSON.
|
||||
* Follow the initial analysis instructions.
|
||||
* Output only valid JSON as specified.
|
||||
* Decompile and inspect functions, starting from main.
|
||||
* Run only the needed commands to gather info.
|
||||
* Use "@ (address|symbol)" to seek temporarily.
|
||||
* Output should be a verbose markdown report.
|
||||
* Use "sym." or "fcn." prefixes if "pdc" is empty.
|
||||
* If a seek fails, use "f~name" to find the symbol's address.
|
||||
|
||||
### Initial Analysis
|
||||
|
||||
1. Run "aflc" to count the number of functions
|
||||
2. If the output of "aflc" is "0" run "aaa" once, then "aflc" again
|
||||
3. Run only one command at a time (do not use ";")
|
||||
|
||||
### Special cases
|
||||
|
||||
* On Swift binaries run...
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import { HttpResponse } from "./types";
|
||||
import { state } from "./state";
|
||||
import { mergeHeaders } from "./headers";
|
||||
import { debugLog } from "./utils";
|
||||
|
||||
interface HttpRequestOptions {
|
||||
method: "GET" | "POST";
|
||||
url: string;
|
||||
headers: string[];
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
function shellEscape(arg: string): string {
|
||||
return `'${arg.replace(/'/g, `'"'"'`)}'`;
|
||||
}
|
||||
|
||||
function executeCurl(options: HttpRequestOptions): string {
|
||||
const { method, url, headers, payload } = options;
|
||||
const cmdParts = ["curl", "-s"];
|
||||
if (state.timeout > 0) {
|
||||
cmdParts.push("--max-time", String(state.timeout));
|
||||
}
|
||||
const requestHeaders = mergeHeaders(
|
||||
["Content-Type: application/json"],
|
||||
headers,
|
||||
);
|
||||
requestHeaders.forEach((h) => cmdParts.push("-H", shellEscape(h)));
|
||||
|
||||
if (method === "POST") {
|
||||
if (!payload) throw new Error("Payload required for POST requests");
|
||||
const tmpfile = r2.fdump(payload);
|
||||
cmdParts.push("--data-binary", "@-", shellEscape(url));
|
||||
const cmd = cmdParts.join(" ") + " < " + shellEscape(tmpfile) +
|
||||
" && rm " + shellEscape(tmpfile);
|
||||
debugLog(cmd);
|
||||
return r2.syscmds(cmd);
|
||||
} else {
|
||||
cmdParts.push(shellEscape(url));
|
||||
const cmd = cmdParts.join(" ");
|
||||
debugLog(cmd);
|
||||
return r2.syscmds(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
export function httpRequest(options: HttpRequestOptions): HttpResponse {
|
||||
try {
|
||||
const output = executeCurl(options).trim();
|
||||
if (output === "") {
|
||||
return { error: "empty response" };
|
||||
}
|
||||
try {
|
||||
return JSON.parse(output) as HttpResponse;
|
||||
} catch {
|
||||
return { error: output, rawOutput: output };
|
||||
}
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
return { error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
export function httpGet(url: string, headers: string[]): HttpResponse {
|
||||
return httpRequest({ method: "GET", url, headers });
|
||||
}
|
||||
|
||||
export function httpPost(
|
||||
url: string,
|
||||
headers: string[],
|
||||
payload: string,
|
||||
): HttpResponse {
|
||||
return httpRequest({ method: "POST", url, headers, payload });
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
### 3. HTTP Request Patterns
|
||||
|
||||
**Location**: `http.ts`, used throughout providers and models
|
||||
|
||||
**Problems**:
|
||||
|
||||
- Two different curl methods (`curlArgs` vs `curlFile`) with similar but
|
||||
separate logic
|
||||
- Error handling in HTTP layer vs. application layer
|
||||
- No centralized request/response abstraction
|
||||
@@ -0,0 +1,34 @@
|
||||
import { COMMAND } from "./constants";
|
||||
import { handleCommand } from "./commands";
|
||||
import { ensureRcConfigLoaded } from "./config";
|
||||
|
||||
function main(args: string): boolean {
|
||||
ensureRcConfigLoaded();
|
||||
const output = handleCommand(args, main);
|
||||
if (output) {
|
||||
r2.log(output);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function installPlugin(): void {
|
||||
r2.unload("core", COMMAND);
|
||||
r2.plugin("core", function () {
|
||||
function coreCall(cmd: string): boolean {
|
||||
if (cmd.startsWith(COMMAND)) {
|
||||
const args = cmd.slice(COMMAND.length).trim();
|
||||
return main(args);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
name: COMMAND,
|
||||
license: "MIT",
|
||||
desc: "r2 decompiler based on r2ai",
|
||||
call: coreCall,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
installPlugin();
|
||||
@@ -0,0 +1,34 @@
|
||||
import { getProvider, listModels } from "./providers";
|
||||
|
||||
export function listModelsFor(api: string): void {
|
||||
const providerConfig = getProvider(api);
|
||||
|
||||
if (!providerConfig) {
|
||||
console.error(`Unknown provider: ${api}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const models = listModels(api);
|
||||
|
||||
if (models) {
|
||||
console.log(models);
|
||||
}
|
||||
|
||||
if (providerConfig.hardcodedModels) {
|
||||
providerConfig.hardcodedModels.forEach((model) => console.log(model));
|
||||
}
|
||||
|
||||
if (api === "mistral") {
|
||||
console.log("ministral-8b-latest");
|
||||
}
|
||||
|
||||
if (!models && !providerConfig.hardcodedModels) {
|
||||
console.log(providerConfig.defaultModel);
|
||||
}
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
console.error(`Error listing models for ${api}:`, err.message);
|
||||
console.log(providerConfig.defaultModel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,546 @@
|
||||
import {
|
||||
ApiKeyResult,
|
||||
ApiStyle,
|
||||
AuthStyle,
|
||||
HttpResponse,
|
||||
JsonObject,
|
||||
ModelDataEntry,
|
||||
OllamaModelEntry,
|
||||
ProviderConfig,
|
||||
ProviderRegistry,
|
||||
} from "./types";
|
||||
import { state } from "./state";
|
||||
import { filterResponse, padRight } from "./utils";
|
||||
import { getApiKey } from "./apiKeys";
|
||||
import { getConfiguredHeaders, mergeHeaders } from "./headers";
|
||||
import { httpGet, httpPost } from "./http";
|
||||
|
||||
interface ProviderRuntime {
|
||||
buildPayload: (model: string, query: string) => JsonObject;
|
||||
parseResponse: (response: HttpResponse) => string;
|
||||
buildUrl: (baseUrl: string, model: string, apiKey?: string) => string;
|
||||
requiresUrlApiKey?: boolean;
|
||||
}
|
||||
|
||||
function getApiKeyName(authKey: string): string {
|
||||
return authKey.split("_")[0].toLowerCase();
|
||||
}
|
||||
|
||||
function readProviderKey(provider: ProviderConfig): ApiKeyResult | undefined {
|
||||
if (!provider.authKey) {
|
||||
return undefined;
|
||||
}
|
||||
const keyNames = Array.from(
|
||||
new Set(
|
||||
[provider.keyName, getApiKeyName(provider.authKey)].filter(Boolean),
|
||||
),
|
||||
) as string[];
|
||||
|
||||
let fallback: ApiKeyResult | undefined;
|
||||
for (const keyName of keyNames) {
|
||||
const result = getApiKey(keyName, provider.authKey);
|
||||
if (result[0]) {
|
||||
return result;
|
||||
}
|
||||
if (!fallback && result[2] !== "nope") {
|
||||
fallback = result;
|
||||
}
|
||||
}
|
||||
|
||||
return fallback ||
|
||||
getApiKey(getApiKeyName(provider.authKey), provider.authKey);
|
||||
}
|
||||
|
||||
function getErrorMessage(error: HttpResponse["error"]): string | undefined {
|
||||
if (!error) {
|
||||
return undefined;
|
||||
}
|
||||
return typeof error === "string" ? error : error.message;
|
||||
}
|
||||
|
||||
function getProviderBaseUrl(provider: ProviderConfig): string {
|
||||
return state.baseurl || provider.defaultBaseUrl;
|
||||
}
|
||||
|
||||
function getProviderHeaders(
|
||||
providerHeaders: string[],
|
||||
extraHeaders: string[] = getConfiguredHeaders(),
|
||||
): string[] {
|
||||
return mergeHeaders(providerHeaders, extraHeaders);
|
||||
}
|
||||
|
||||
function buildBearerHeaders(apiKey: string | null): string[] {
|
||||
return apiKey ? ["Authorization: Bearer " + apiKey] : [];
|
||||
}
|
||||
|
||||
function buildAnthropicHeaders(apiKey: string | null): string[] {
|
||||
const headers = ["anthropic-version: 2023-06-01"];
|
||||
return apiKey ? mergeHeaders(headers, ["x-api-key: " + apiKey]) : headers;
|
||||
}
|
||||
|
||||
function buildAuthHeaders(
|
||||
provider: ProviderConfig,
|
||||
apiKey: string | null,
|
||||
): string[] {
|
||||
const authStyle: AuthStyle = provider.authStyle || "none";
|
||||
switch (authStyle) {
|
||||
case "bearer":
|
||||
return buildBearerHeaders(apiKey);
|
||||
case "anthropic":
|
||||
return buildAnthropicHeaders(apiKey);
|
||||
case "none":
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function listDataModels(
|
||||
url: string,
|
||||
headers: string[],
|
||||
mapper: (model: ModelDataEntry) => string,
|
||||
): string {
|
||||
const response = httpGet(url, headers);
|
||||
const error = getErrorMessage(response.error);
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return "error invalid response";
|
||||
}
|
||||
return response.data?.map(mapper).join("\n") || "";
|
||||
}
|
||||
|
||||
function uniqueBy<T>(items: T[], getKey: (item: T) => string): T[] {
|
||||
const seen = new Set<string>();
|
||||
return items.filter((item) => {
|
||||
const key = getKey(item);
|
||||
if (seen.has(key)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function listOpenAIModels(provider: ProviderConfig): string {
|
||||
const apiKey = readProviderKey(provider);
|
||||
const baseUrl = getProviderBaseUrl(provider);
|
||||
const headers = getProviderHeaders(
|
||||
buildAuthHeaders(provider, apiKey?.[0] || null),
|
||||
);
|
||||
return listDataModels(
|
||||
baseUrl + "/v1/models",
|
||||
headers,
|
||||
(model) => model.id,
|
||||
);
|
||||
}
|
||||
|
||||
function listAnthropicModels(provider: ProviderConfig): string {
|
||||
const apiKey = readProviderKey(provider);
|
||||
const baseUrl = getProviderBaseUrl(provider);
|
||||
const headers = getProviderHeaders(
|
||||
buildAuthHeaders(provider, apiKey?.[0] || null),
|
||||
);
|
||||
return listDataModels(
|
||||
baseUrl + "/v1/models",
|
||||
headers,
|
||||
(model) => model.id,
|
||||
);
|
||||
}
|
||||
|
||||
function listOllamaModels(provider: ProviderConfig): string {
|
||||
const response = httpGet(
|
||||
getProviderBaseUrl(provider) + "/api/tags",
|
||||
getProviderHeaders(
|
||||
buildAuthHeaders(provider, readProviderKey(provider)?.[0] || null),
|
||||
),
|
||||
);
|
||||
const error = getErrorMessage(response.error);
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return "error invalid response";
|
||||
}
|
||||
return response.models?.map((model: OllamaModelEntry) => model.name).join(
|
||||
"\n",
|
||||
) || "";
|
||||
}
|
||||
|
||||
function listMistralModels(provider: ProviderConfig): string {
|
||||
const apiKey = readProviderKey(provider);
|
||||
const response = httpGet(
|
||||
getProviderBaseUrl(provider) + "/v1/models",
|
||||
getProviderHeaders(buildAuthHeaders(provider, apiKey?.[0] || null)),
|
||||
);
|
||||
if (response.data) {
|
||||
return uniqueBy(response.data, (model) => model.name || model.id)
|
||||
.map((model) =>
|
||||
[
|
||||
padRight(model.name || model.id, 30),
|
||||
padRight("" + (model.max_context_length || ""), 10),
|
||||
model.description || "",
|
||||
].join(" ")
|
||||
)
|
||||
.join("\n");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
const providerRuntimes: Record<ApiStyle, ProviderRuntime> = {
|
||||
openai: {
|
||||
buildPayload: (model, query) => ({
|
||||
stream: false,
|
||||
model,
|
||||
messages: [{ role: "user", content: query }],
|
||||
}),
|
||||
parseResponse: (response) => {
|
||||
if (response.error) {
|
||||
const error = typeof response.error === "object"
|
||||
? response.error.message
|
||||
: response.error;
|
||||
throw new Error(error || "Unknown error");
|
||||
}
|
||||
if (response.choices && response.choices[0]?.message?.content) {
|
||||
return filterResponse(response.choices[0].message.content);
|
||||
}
|
||||
throw new Error("Invalid response format");
|
||||
},
|
||||
buildUrl: (baseUrl) => baseUrl + "/v1/chat/completions",
|
||||
},
|
||||
anthropic: {
|
||||
buildPayload: (model, query) => {
|
||||
const payload: {
|
||||
model: string;
|
||||
max_tokens: number;
|
||||
messages: Array<{ role: string; content: string }>;
|
||||
temperature?: number;
|
||||
top_p?: number;
|
||||
top_k?: number;
|
||||
} = {
|
||||
model,
|
||||
max_tokens: 5128,
|
||||
messages: [{ role: "user", content: query }],
|
||||
};
|
||||
if (state.deterministic) {
|
||||
Object.assign(payload, { temperature: 0, top_p: 0, top_k: 1 });
|
||||
}
|
||||
return payload;
|
||||
},
|
||||
parseResponse: (response) => {
|
||||
if (response.content && response.content[0]?.text) {
|
||||
return filterResponse(response.content[0].text);
|
||||
}
|
||||
if (response.error) {
|
||||
const error = typeof response.error === "object"
|
||||
? response.error.message
|
||||
: response.error;
|
||||
throw new Error(error || "Unknown error");
|
||||
}
|
||||
throw new Error("Invalid response format");
|
||||
},
|
||||
buildUrl: (baseUrl) => baseUrl + "/v1/messages",
|
||||
},
|
||||
ollama: {
|
||||
buildPayload: (model, query) => {
|
||||
const payload: {
|
||||
stream: boolean;
|
||||
model: string;
|
||||
messages: Array<{ role: string; content: string }>;
|
||||
options?: {
|
||||
repeat_last_n: number;
|
||||
top_p: number;
|
||||
top_k: number;
|
||||
temperature: number;
|
||||
repeat_penalty: number;
|
||||
seed: number;
|
||||
};
|
||||
} = {
|
||||
stream: false,
|
||||
model,
|
||||
messages: [{ role: "user", content: query }],
|
||||
};
|
||||
if (state.deterministic) {
|
||||
payload.options = {
|
||||
repeat_last_n: 0,
|
||||
top_p: 1.0,
|
||||
top_k: 1.0,
|
||||
temperature: 0.0,
|
||||
repeat_penalty: 1.0,
|
||||
seed: 123,
|
||||
};
|
||||
}
|
||||
return payload;
|
||||
},
|
||||
parseResponse: (response) => {
|
||||
if (response.error) {
|
||||
const error = typeof response.error === "string"
|
||||
? response.error
|
||||
: JSON.stringify(response.error);
|
||||
throw new Error(error);
|
||||
}
|
||||
if (response.message?.content) {
|
||||
return filterResponse(response.message.content);
|
||||
}
|
||||
throw new Error(JSON.stringify(response));
|
||||
},
|
||||
buildUrl: (baseUrl) => baseUrl + "/api/chat",
|
||||
},
|
||||
gemini: {
|
||||
buildPayload: (model, query) => {
|
||||
const payload: {
|
||||
contents: Array<{ parts: Array<{ text: string }> }>;
|
||||
generationConfig?: {
|
||||
temperature: number;
|
||||
topP: number;
|
||||
topK: number;
|
||||
};
|
||||
} = {
|
||||
contents: [{ parts: [{ text: query }] }],
|
||||
};
|
||||
if (state.deterministic) {
|
||||
payload.generationConfig = {
|
||||
temperature: 0.0,
|
||||
topP: 1.0,
|
||||
topK: 1,
|
||||
};
|
||||
}
|
||||
return payload;
|
||||
},
|
||||
parseResponse: (response) => {
|
||||
if (
|
||||
response.candidates &&
|
||||
response.candidates[0]?.content?.parts?.[0]?.text
|
||||
) {
|
||||
return filterResponse(response.candidates[0].content.parts[0].text);
|
||||
}
|
||||
if (response.error) {
|
||||
throw new Error(
|
||||
typeof response.error === "string"
|
||||
? response.error
|
||||
: JSON.stringify(response.error),
|
||||
);
|
||||
}
|
||||
console.log(JSON.stringify(response));
|
||||
throw new Error("Invalid response format");
|
||||
},
|
||||
buildUrl: (baseUrl, model, apiKey) =>
|
||||
`${baseUrl}/v1beta/models/${model}:generateContent?key=${apiKey}`,
|
||||
requiresUrlApiKey: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const providerRegistry: ProviderRegistry = {
|
||||
anthropic: {
|
||||
defaultModel: "claude-3-7-sonnet-20250219",
|
||||
defaultBaseUrl: "https://api.anthropic.com",
|
||||
authKey: "ANTHROPIC_API_KEY",
|
||||
keyName: "anthropic",
|
||||
authStyle: "anthropic",
|
||||
apiStyle: "anthropic",
|
||||
},
|
||||
claude: {
|
||||
defaultModel: "claude-3-7-sonnet-20250219",
|
||||
defaultBaseUrl: "https://api.anthropic.com",
|
||||
authKey: "ANTHROPIC_API_KEY",
|
||||
keyName: "claude",
|
||||
authStyle: "anthropic",
|
||||
apiStyle: "anthropic",
|
||||
},
|
||||
openai: {
|
||||
defaultModel: "gpt-4o-mini",
|
||||
defaultBaseUrl: "https://api.openai.com",
|
||||
authKey: "OPENAI_API_KEY",
|
||||
authStyle: "bearer",
|
||||
apiStyle: "openai",
|
||||
},
|
||||
ollama: {
|
||||
defaultModel: "qwen2.5-coder:latest",
|
||||
defaultBaseUrl: "http://localhost:11434",
|
||||
authStyle: "none",
|
||||
apiStyle: "ollama",
|
||||
},
|
||||
ollamacloud: {
|
||||
defaultModel: "gpt-oss:120b",
|
||||
defaultBaseUrl: "https://ollama.com",
|
||||
authKey: "OLLAMA_API_KEY",
|
||||
keyName: "ollamacloud",
|
||||
authStyle: "bearer",
|
||||
apiStyle: "ollama",
|
||||
},
|
||||
opencode: {
|
||||
defaultModel: "big-pickle",
|
||||
defaultBaseUrl: "https://opencode.ai/zen",
|
||||
authKey: "OPENCODE_API_KEY",
|
||||
keyName: "opencode",
|
||||
authStyle: "bearer",
|
||||
apiStyle: "openai",
|
||||
hardcodedModels: ["big-pickle", "glm-5", "kimi-k2.5"],
|
||||
},
|
||||
zen: {
|
||||
defaultModel: "big-pickle",
|
||||
defaultBaseUrl: "https://opencode.ai/zen",
|
||||
authKey: "OPENCODE_API_KEY",
|
||||
keyName: "zen",
|
||||
authStyle: "bearer",
|
||||
apiStyle: "openai",
|
||||
hardcodedModels: ["big-pickle", "glm-5", "kimi-k2.5"],
|
||||
},
|
||||
gemini: {
|
||||
defaultModel: "gemini-2.5-flash",
|
||||
defaultBaseUrl: "https://generativelanguage.googleapis.com",
|
||||
authKey: "GEMINI_API_KEY",
|
||||
authStyle: "none",
|
||||
apiStyle: "gemini",
|
||||
hardcodedModels: [
|
||||
"gemini-2.0-flash",
|
||||
"gemini-2.0-flash-lite",
|
||||
"gemini-2.5-pro",
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-flash-lite",
|
||||
],
|
||||
},
|
||||
mistral: {
|
||||
defaultModel: "codestral-latest",
|
||||
defaultBaseUrl: "https://api.mistral.ai",
|
||||
authKey: "MISTRAL_API_KEY",
|
||||
authStyle: "bearer",
|
||||
apiStyle: "openai",
|
||||
},
|
||||
xai: {
|
||||
defaultModel: "grok-beta",
|
||||
defaultBaseUrl: "https://api.x.ai",
|
||||
authKey: "XAI_API_KEY",
|
||||
authStyle: "bearer",
|
||||
apiStyle: "openai",
|
||||
hardcodedModels: ["grok-2", "grok-beta"],
|
||||
},
|
||||
lmstudio: {
|
||||
defaultModel: "local-model",
|
||||
defaultBaseUrl: "http://127.0.0.1:1234",
|
||||
authStyle: "none",
|
||||
apiStyle: "openai",
|
||||
},
|
||||
deepseek: {
|
||||
defaultModel: "deepseek-coder",
|
||||
defaultBaseUrl: "https://api.deepseek.com",
|
||||
authKey: "DEEPSEEK_API_KEY",
|
||||
authStyle: "bearer",
|
||||
apiStyle: "openai",
|
||||
hardcodedModels: ["deepseek-coder", "deepseek-chat"],
|
||||
},
|
||||
openrouter: {
|
||||
defaultModel: "openai/gpt-4o-mini",
|
||||
defaultBaseUrl: "https://openrouter.ai/api",
|
||||
authKey: "OPENROUTER_API_KEY",
|
||||
authStyle: "bearer",
|
||||
apiStyle: "openai",
|
||||
},
|
||||
groq: {
|
||||
defaultModel: "llama-3.3-70b-versatile",
|
||||
defaultBaseUrl: "https://api.groq.com/openai",
|
||||
authKey: "GROQ_API_KEY",
|
||||
authStyle: "bearer",
|
||||
apiStyle: "openai",
|
||||
},
|
||||
};
|
||||
|
||||
export function getProvider(api: string): ProviderConfig | undefined {
|
||||
return providerRegistry[api];
|
||||
}
|
||||
|
||||
export function listProviders(): string[] {
|
||||
return Object.keys(providerRegistry);
|
||||
}
|
||||
|
||||
export function listModels(providerName: string): string {
|
||||
const provider = getProvider(providerName);
|
||||
if (!provider) {
|
||||
throw new Error(`Unknown provider: ${providerName}`);
|
||||
}
|
||||
|
||||
if (providerName === "mistral") {
|
||||
return listMistralModels(provider);
|
||||
}
|
||||
|
||||
switch (provider.apiStyle) {
|
||||
case "openai":
|
||||
return listOpenAIModels(provider);
|
||||
case "anthropic":
|
||||
return listAnthropicModels(provider);
|
||||
case "ollama":
|
||||
return listOllamaModels(provider);
|
||||
case "gemini":
|
||||
return "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function buildQuery(msg: string, hideprompt: boolean): string {
|
||||
let query = msg;
|
||||
|
||||
if (state.think >= 0) {
|
||||
if (state.think === 0) {
|
||||
query +=
|
||||
' Answers directly and concisely, without showing any thinking steps or internal reasoning. Never include phrases like "Let me think".';
|
||||
query += " /no_think";
|
||||
} else if (state.think > 0) {
|
||||
query =
|
||||
"Think step by step and explain the reasoning process, When answering, first output your reasoning inside <think> and </think> tags, then give the final answer." +
|
||||
query;
|
||||
}
|
||||
}
|
||||
|
||||
return hideprompt ? query : state.prompt + languagePrompt() + query;
|
||||
}
|
||||
|
||||
function languagePrompt(): string {
|
||||
return "\n.Translate the code into " + state.language +
|
||||
" programming language\n";
|
||||
}
|
||||
|
||||
function callRuntime(
|
||||
provider: ProviderConfig,
|
||||
runtime: ProviderRuntime,
|
||||
msg: string,
|
||||
hideprompt: boolean,
|
||||
): string {
|
||||
const model = state.model || provider.defaultModel;
|
||||
const query = buildQuery(msg, hideprompt);
|
||||
const apiKey = readProviderKey(provider);
|
||||
|
||||
if (runtime.requiresUrlApiKey && apiKey?.[1] && provider.authKey) {
|
||||
return `Cannot read ~/.r2ai.${getApiKeyName(provider.authKey)}-key`;
|
||||
}
|
||||
|
||||
const payload = runtime.buildPayload(model, query);
|
||||
const headers = getProviderHeaders(
|
||||
buildAuthHeaders(provider, apiKey?.[0] || null),
|
||||
);
|
||||
const url = runtime.buildUrl(
|
||||
getProviderBaseUrl(provider),
|
||||
model,
|
||||
apiKey?.[0] || undefined,
|
||||
);
|
||||
|
||||
try {
|
||||
return runtime.parseResponse(
|
||||
httpPost(url, headers, JSON.stringify(payload)),
|
||||
);
|
||||
} catch (error) {
|
||||
return "ERROR: " + (error as Error).message;
|
||||
}
|
||||
}
|
||||
|
||||
export function callProvider(msg: string, hideprompt: boolean): string {
|
||||
const provider = getProvider(state.api);
|
||||
if (!provider) {
|
||||
return `Unknown value for 'decai -e api'. Available: ${
|
||||
listProviders().join(", ")
|
||||
}`;
|
||||
}
|
||||
|
||||
return callRuntime(
|
||||
provider,
|
||||
providerRuntimes[provider.apiStyle],
|
||||
msg,
|
||||
hideprompt,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { state } from "./state";
|
||||
import { fileDump, tmpdir } from "./utils";
|
||||
import { callProvider } from "./providers";
|
||||
import { httpGet } from "./http";
|
||||
|
||||
export function r2ai(
|
||||
queryText: string,
|
||||
fileData: string | null,
|
||||
hideprompt: boolean = false,
|
||||
): string {
|
||||
const cleanFileData = (fileData || "").replace(/`/g, "");
|
||||
const cleanQuery = queryText.replace(/'/g, "");
|
||||
|
||||
// Handle r2ai server mode
|
||||
if (state.api === "r2" || state.api === "r2ai") {
|
||||
const fileName = tmpdir(".pdc.txt");
|
||||
fileDump(fileName, cleanFileData);
|
||||
|
||||
const q = cleanQuery.startsWith("-")
|
||||
? cleanQuery
|
||||
: ["-i", fileName, cleanQuery].join(" ");
|
||||
|
||||
const host = state.baseurl
|
||||
? state.baseurl + "/cmd"
|
||||
: state.host + ":" + state.port + "/cmd";
|
||||
|
||||
const url = host + "/" + encodeURIComponent(q);
|
||||
const response = httpGet(url, []);
|
||||
if (response.error) {
|
||||
return `Error: ${response.error}`;
|
||||
}
|
||||
return response.result || JSON.stringify(response) ||
|
||||
"Cannot curl, use r2ai-server or r2ai -w";
|
||||
}
|
||||
|
||||
if (cleanQuery.startsWith("-")) return "";
|
||||
|
||||
let q = cleanQuery + ":\n" + cleanFileData;
|
||||
if (state.maxInputTokens > 0 && q.length > state.maxInputTokens) {
|
||||
q = q.slice(0, state.maxInputTokens);
|
||||
}
|
||||
|
||||
return callProvider(q, hideprompt);
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
export type R2Address = number | string | unknown;
|
||||
|
||||
export interface R2LogEntry {
|
||||
type: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface R2CommandResult {
|
||||
res?: string;
|
||||
value?: number;
|
||||
logs?: R2LogEntry[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface R2Plugin {
|
||||
name: string;
|
||||
license: string;
|
||||
desc: string;
|
||||
call: (command: string) => boolean;
|
||||
}
|
||||
|
||||
export type R2PluginFactory = () => R2Plugin;
|
||||
|
||||
export class R2PipeSyncFromSync {
|
||||
r2p: R2Pipe;
|
||||
constructor(r2p: R2Pipe) {
|
||||
this.r2p = r2p;
|
||||
}
|
||||
/**
|
||||
* Run a command in the associated instance of radare2 and return the output as a string
|
||||
*
|
||||
* @param {string} command to be executed inside radare2.
|
||||
* @returns {string} The output of the command execution
|
||||
*/
|
||||
cmd(command: string): string {
|
||||
return this.r2p.cmd(command);
|
||||
}
|
||||
cmd2(command: string): R2CommandResult {
|
||||
const output = this.r2p.cmd(command);
|
||||
return typeof output === "string" ? { res: output } : output;
|
||||
}
|
||||
cmdAt(
|
||||
command: string,
|
||||
address: R2Address,
|
||||
): string {
|
||||
return this.r2p.cmdAt(command, address);
|
||||
}
|
||||
cmdj(cmd: string): unknown {
|
||||
return this.r2p.cmdj(cmd);
|
||||
}
|
||||
call(command: string): string {
|
||||
return this.r2p.call(command);
|
||||
}
|
||||
callj(cmd: string): unknown {
|
||||
return this.r2p.cmdj(cmd);
|
||||
}
|
||||
callAt(
|
||||
command: string,
|
||||
address: R2Address,
|
||||
): string {
|
||||
return this.r2p.cmdAt(command, address);
|
||||
}
|
||||
syscmd(command: string): string {
|
||||
return this.r2p.syscmd(command);
|
||||
}
|
||||
syscmds(command: string): string {
|
||||
return this.r2p.syscmds(command);
|
||||
}
|
||||
fdump(data: string): string {
|
||||
return this.r2p.fdump(data);
|
||||
}
|
||||
log(msg: string): void {
|
||||
return this.r2p.log(msg);
|
||||
}
|
||||
plugin(type: string, maker: R2PluginFactory): boolean {
|
||||
return this.r2p.plugin(type, maker);
|
||||
}
|
||||
unload(type: string, name: string): boolean {
|
||||
return this.r2p.unload(type, name);
|
||||
}
|
||||
}
|
||||
|
||||
export function newAsyncR2PipeFromSync(r2p: R2Pipe): R2PipeSync {
|
||||
const asyncR2Pipe = new R2PipeSyncFromSync(r2p);
|
||||
return asyncR2Pipe as R2PipeSync;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic interface to interact with radare2, abstracts the access to the associated
|
||||
* instance of the tool, which could be native via rlang or remote via pipes or tcp/http.
|
||||
*
|
||||
* @typedef R2Pipe
|
||||
*/
|
||||
export interface R2Pipe {
|
||||
/**
|
||||
* Run a command in the associated instance of radare2 and return the output as a string
|
||||
*
|
||||
* @param {string} command to be executed inside radare2.
|
||||
* @returns {string} The output of the command execution
|
||||
*/
|
||||
cmd(cmd: string): string;
|
||||
|
||||
/**
|
||||
* Run a radare2 command in a different address. Same as `.cmd(x + '@ ' + a)`
|
||||
*
|
||||
* @param {string} command to be executed inside radare2.
|
||||
* @param {number|string|NativePointer} command to be executed inside radare2.
|
||||
* @returns {string} The output of the command execution
|
||||
*/
|
||||
cmd2(cmd: string): R2CommandResult;
|
||||
cmdAt(cmd: string, address: R2Address): string;
|
||||
|
||||
/**
|
||||
* Run a radare2 command expecting the output to be JSON
|
||||
*
|
||||
* @param {string} command to be executed inside radare2. The given command should end with `j`
|
||||
* @returns {object} the JSON decoded object from the output of the command
|
||||
*/
|
||||
cmdj(cmd: string): unknown;
|
||||
|
||||
/**
|
||||
* Call a radare2 command. This is similar to `R2Pipe.cmd`, but skips all the command parsing rules,
|
||||
* which is safer and faster but you cannot use any special modifier like `@`, `~`, ...
|
||||
*
|
||||
* See R2Pipe.callAt() to call a command on a different address
|
||||
*
|
||||
* @param {string} command to be executed inside radare2. The given command should end with `j`
|
||||
* @returns {object} the JSON decoded object from the output of the command
|
||||
*/
|
||||
call(cmd: string): string;
|
||||
|
||||
/**
|
||||
* Call a radare2 command in a different address
|
||||
*
|
||||
* @param {string} command to be executed inside radare2. The given command should end with `j`
|
||||
* @param {NativePointer|string|number} where to seek to execute this command (previous offset is restored after executing it)
|
||||
* @returns {string} the string containing the output of the command
|
||||
*/
|
||||
callAt(cmd: string, address: R2Address): string;
|
||||
|
||||
syscmd(cmd: string): string;
|
||||
syscmds(cmd: string): string;
|
||||
fdump(data: string): string;
|
||||
|
||||
/**
|
||||
* Same as cmdj but using .call which avoids command injection problems
|
||||
*
|
||||
* @param {string} command to be executed inside radare2. The given command should end with `j`
|
||||
* @returns {object} the JSON decoded object from the command output
|
||||
*/
|
||||
callj(cmd: string): unknown;
|
||||
|
||||
/**
|
||||
* Log a string to the associated console. This is used internally by `console.log` in some implementations.
|
||||
*
|
||||
* @param {string} text to be displayed
|
||||
*/
|
||||
log(msg: string): void;
|
||||
|
||||
/**
|
||||
* Instantiate a new radare2 plugin with the given type and constructor method.
|
||||
*
|
||||
* @param {string} type of plugin ("core", "io", "arch", ...)
|
||||
* @param {string} function that returns the plugin definition
|
||||
* @returns {boolean} true if successful
|
||||
*/
|
||||
plugin(type: string, maker: R2PluginFactory): boolean;
|
||||
|
||||
/**
|
||||
* Unload the plugin associated with a `type` and a `name`.
|
||||
*
|
||||
* @param {string} type of plugin ("core", "io", "arch", ...)
|
||||
* @param {string} name of the plugin
|
||||
* @returns {boolean} true if successful
|
||||
*/
|
||||
unload(type: string, name: string): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic interface to interact with radare2, abstracts the access to the associated
|
||||
* instance of the tool, which could be native via rlang or remote via pipes or tcp/http.
|
||||
*
|
||||
* @typedef R2PipeSync
|
||||
*/
|
||||
export interface R2PipeSync {
|
||||
/**
|
||||
* Run a command in the associated instance of radare2
|
||||
*
|
||||
* @param {string} command to be executed inside radare2.
|
||||
* @returns {string} The output of the command execution
|
||||
*/
|
||||
cmd(cmd: string): string;
|
||||
|
||||
/**
|
||||
* Run a radare2 command in a different address. Same as `.cmd(x + '@ ' + a)`
|
||||
*
|
||||
* @param {string} command to be executed inside radare2.
|
||||
* @param {number|string|NativePointer} command to be executed inside radare2.
|
||||
* @returns {string} The output of the command execution
|
||||
*/
|
||||
cmd2(cmd: string): R2CommandResult;
|
||||
cmdAt(cmd: string, address: R2Address): string;
|
||||
|
||||
/**
|
||||
* Run a radare2 command expecting the output to be JSON
|
||||
*
|
||||
* @param {string} command to be executed inside radare2. The given command should end with `j`
|
||||
* @returns {object} the JSON decoded object from the output of the command
|
||||
*/
|
||||
cmdj(cmd: string): unknown;
|
||||
|
||||
/**
|
||||
* Call a radare2 command. This is similar to `R2Pipe.cmd`, but skips all the command parsing rules,
|
||||
* which is safer and faster but you cannot use any special modifier like `@`, `~`, ...
|
||||
*
|
||||
* See R2Pipe.callAt() to call a command on a different address
|
||||
*
|
||||
* @param {string} command to be executed inside radare2. The given command should end with `j`
|
||||
* @returns {object} the JSON decoded object from the output of the command
|
||||
*/
|
||||
call(cmd: string): string;
|
||||
|
||||
/**
|
||||
* Call a radare2 command in a different address
|
||||
*
|
||||
* @param {string} command to be executed inside radare2. The given command should end with `j`
|
||||
* @param {NativePointer|string|number} where to seek to execute this command (previous offset is restored after executing it)
|
||||
* @returns {string} the string containing the output of the command
|
||||
*/
|
||||
callAt(cmd: string, address: R2Address): string;
|
||||
|
||||
syscmd(cmd: string): string;
|
||||
syscmds(cmd: string): string;
|
||||
fdump(data: string): string;
|
||||
|
||||
/**
|
||||
* Same as cmdj but using .call which avoids command injection problems
|
||||
*
|
||||
* @param {string} command to be executed inside radare2. The given command should end with `j`
|
||||
* @returns {object} the JSON decoded object from the command output
|
||||
*/
|
||||
callj(cmd: string): unknown;
|
||||
|
||||
/**
|
||||
* Log a string to the associated console. This is used internally by `console.log` in some implementations.
|
||||
*
|
||||
* @param {string} text to be displayed
|
||||
*/
|
||||
log(msg: string): void;
|
||||
|
||||
/**
|
||||
* Instantiate a new radare2 plugin with the given type and constructor method.
|
||||
*
|
||||
* @param {string} type of plugin ("core", "io", "arch", ...)
|
||||
* @param {string} function that returns the plugin definition
|
||||
* @returns {boolean} true if successful
|
||||
*/
|
||||
plugin(type: string, maker: R2PluginFactory): boolean;
|
||||
|
||||
/**
|
||||
* Unload the plugin associated with a `type` and a `name`.
|
||||
*
|
||||
* @param {string} type of plugin ("core", "io", "arch", ...)
|
||||
* @param {string} name of the plugin
|
||||
* @returns {boolean} true if successful
|
||||
*/
|
||||
unload(type: string, name: string): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A global instance of R2Pipe associated with the current instance of radare2
|
||||
*
|
||||
* @type {R2PipeSync}
|
||||
*/
|
||||
export declare const r2: R2Pipe;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { AppState } from "./types";
|
||||
import { DEFAULT_PROMPT } from "./constants";
|
||||
|
||||
export const defaultState: AppState = {
|
||||
decopipe: { use: false },
|
||||
host: "http://localhost",
|
||||
port: "11434",
|
||||
baseurl: "",
|
||||
extraHeaders: [],
|
||||
api: "ollama",
|
||||
pipeline: "",
|
||||
commands: "pdc",
|
||||
yolo: false,
|
||||
tts: false,
|
||||
language: "C",
|
||||
humanLanguage: "English",
|
||||
deterministic: true,
|
||||
debug: false,
|
||||
timeout: 180,
|
||||
think: -1,
|
||||
useFiles: false,
|
||||
contextFile: "",
|
||||
model: "",
|
||||
cache: false,
|
||||
maxInputTokens: -1,
|
||||
prompt: DEFAULT_PROMPT,
|
||||
lastOutput: "",
|
||||
};
|
||||
|
||||
export const state: AppState = {
|
||||
...defaultState,
|
||||
decopipe: { ...defaultState.decopipe },
|
||||
extraHeaders: [...defaultState.extraHeaders],
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"lib": ["esnext"],
|
||||
"module": "commonjs",
|
||||
"outDir": "out",
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { R2PipeSync } from "./r2pipe";
|
||||
|
||||
interface ConsoleLike {
|
||||
log: (...args: unknown[]) => void;
|
||||
error: (...args: unknown[]) => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
const r2: R2PipeSync;
|
||||
const console: ConsoleLike;
|
||||
function btoa(str: string): string;
|
||||
}
|
||||
|
||||
export type ApiStyle = "openai" | "anthropic" | "ollama" | "gemini";
|
||||
export type AuthStyle = "none" | "bearer" | "anthropic";
|
||||
|
||||
export interface ProviderConfig {
|
||||
defaultModel: string;
|
||||
defaultBaseUrl: string;
|
||||
authKey?: string;
|
||||
authStyle?: AuthStyle;
|
||||
keyName?: string;
|
||||
apiStyle: ApiStyle;
|
||||
hardcodedModels?: string[];
|
||||
}
|
||||
|
||||
export interface ProviderRegistry {
|
||||
[key: string]: ProviderConfig;
|
||||
}
|
||||
|
||||
export interface DecoPipeConfig {
|
||||
use: boolean;
|
||||
default?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
decopipe: DecoPipeConfig;
|
||||
host: string;
|
||||
port: string;
|
||||
baseurl: string;
|
||||
extraHeaders: string[];
|
||||
api: string;
|
||||
pipeline: string;
|
||||
commands: string;
|
||||
yolo: boolean;
|
||||
tts: boolean;
|
||||
language: string;
|
||||
humanLanguage: string;
|
||||
deterministic: boolean;
|
||||
debug: boolean;
|
||||
timeout: number;
|
||||
think: number;
|
||||
useFiles: boolean;
|
||||
contextFile: string;
|
||||
model: string;
|
||||
cache: boolean;
|
||||
maxInputTokens: number;
|
||||
prompt: string;
|
||||
lastOutput: string;
|
||||
}
|
||||
|
||||
export interface ConfigHandler {
|
||||
get: () => string | number | boolean;
|
||||
set: (v: string) => void;
|
||||
}
|
||||
|
||||
export interface ConfigHandlers {
|
||||
[key: string]: ConfigHandler;
|
||||
}
|
||||
|
||||
export type ApiKeyResult = [string | null, string | null, string];
|
||||
|
||||
export interface ApiError {
|
||||
message?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface OpenAIChoice {
|
||||
message?: {
|
||||
content?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AnthropicContentBlock {
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export interface OllamaMessage {
|
||||
content?: string;
|
||||
}
|
||||
|
||||
export interface GeminiPart {
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export interface GeminiCandidate {
|
||||
content?: {
|
||||
parts?: GeminiPart[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ModelDataEntry {
|
||||
id: string;
|
||||
name?: string;
|
||||
max_context_length?: number;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface OllamaModelEntry {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface HttpResponse {
|
||||
error?: ApiError | string;
|
||||
rawOutput?: string;
|
||||
choices?: OpenAIChoice[];
|
||||
content?: AnthropicContentBlock[];
|
||||
message?: OllamaMessage;
|
||||
candidates?: GeminiCandidate[];
|
||||
data?: ModelDataEntry[];
|
||||
models?: OllamaModelEntry[];
|
||||
result?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export type JsonObject = Record<string, unknown>;
|
||||
|
||||
export interface AutoReply {
|
||||
action: string;
|
||||
command?: string;
|
||||
description?: string;
|
||||
reason?: string;
|
||||
response?: string;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { state } from "./state";
|
||||
|
||||
export function tmpdir(path: string): string {
|
||||
const dir = r2.cmd("-e dir.tmp").trim() ?? ".";
|
||||
return dir + "/" + path;
|
||||
}
|
||||
|
||||
export function ensurePath(dir: string, file: string): void {
|
||||
r2.cmd("'mkdir -p " + dir);
|
||||
r2.cmd("'touch " + file);
|
||||
}
|
||||
|
||||
export function fileExists(path: string): boolean {
|
||||
const helpMessage = r2.cmd2("test -h").logs?.[0]?.message ?? "";
|
||||
if (helpMessage.includes("-fdx")) {
|
||||
return true; // r2 is old
|
||||
}
|
||||
return r2.cmd("'test -vf " + path).startsWith("found");
|
||||
}
|
||||
|
||||
export function padRight(str: string, length: number): string {
|
||||
return str + " ".repeat(Math.max(0, length - str.length));
|
||||
}
|
||||
|
||||
export function trimAnsi(str: string): string {
|
||||
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
||||
}
|
||||
|
||||
export function trimDown(out: string): string {
|
||||
const jsonMatch = out.match(/```json\s*([\s\S]*?)```/);
|
||||
return jsonMatch && jsonMatch[1] ? jsonMatch[1].trim() : out;
|
||||
}
|
||||
|
||||
export function trimJson(out: string): string {
|
||||
let result = out;
|
||||
const bob = result.indexOf("{");
|
||||
if (bob !== -1) result = result.slice(bob);
|
||||
const eob = result.lastIndexOf("}");
|
||||
if (eob !== -1) result = result.slice(0, eob + 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function b64(str: string): string {
|
||||
return btoa(str);
|
||||
}
|
||||
|
||||
export function fileDump(fileName: string, fileData: string): void {
|
||||
const d = b64(fileData);
|
||||
r2.cmd("p6ds " + d + " > " + fileName);
|
||||
}
|
||||
|
||||
export function filterResponse(msg: string): string {
|
||||
let result = msg;
|
||||
if (state.think !== 2) {
|
||||
result = result.replace(/<think>[\s\S]*?<\/think>/gi, "");
|
||||
}
|
||||
return result
|
||||
.split("\n")
|
||||
.filter((line) => !line.trim().startsWith("```"))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function debugLog(msg: string): void {
|
||||
if (state.debug) {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseEnvLikeString(input: string): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
for (const line of input.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
const [key, ...rest] = trimmed.split("=");
|
||||
if (!key || rest.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const value = rest.join("=").trim();
|
||||
// Normalize key: strip trailing _API_KEY (case-insensitive)
|
||||
const normKey = key.toLowerCase().replace(/_api_key$/i, "");
|
||||
result[normKey] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="210mm"
|
||||
height="297mm"
|
||||
viewBox="0 0 210 297"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (ebf0e940, 2025-05-08)"
|
||||
sodipodi:docname="decai.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="1.0482005"
|
||||
inkscape:cx="318.64133"
|
||||
inkscape:cy="426.92214"
|
||||
inkscape:window-width="1512"
|
||||
inkscape:window-height="960"
|
||||
inkscape:window-x="222"
|
||||
inkscape:window-y="44"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
id="rect2"
|
||||
width="186.28354"
|
||||
height="176.18687"
|
||||
x="1.5145004"
|
||||
y="23.979588"
|
||||
ry="0.047831953" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#1a1a1a;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="path1"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="10"
|
||||
sodipodi:cx="71.751205"
|
||||
sodipodi:cy="102.09375"
|
||||
sodipodi:r1="53.804771"
|
||||
sodipodi:r2="49.845222"
|
||||
sodipodi:arg1="0.65927727"
|
||||
sodipodi:arg2="0.97343657"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 114.28039,135.05163 -14.49317,8.25532 -13.001341,10.44831 -16.577574,-1.84017 -16.659669,0.81086 -12.329911,-11.23279 -13.95457,-9.1363 -3.37264,-16.33486 -5.919299,-15.59371 6.872864,-15.197565 4.376943,-16.094859 14.493168,-8.255319 13.001341,-10.448311 16.577574,1.840177 16.659669,-0.810865 12.329915,11.232788 13.95457,9.136304 3.37264,16.334856 5.91929,15.593714 -6.87286,15.19757 z"
|
||||
transform="matrix(1.2188073,0,0,1.2188073,1.600691,-15.424646)" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#37662e;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="path1-5"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="10"
|
||||
sodipodi:cx="71.751205"
|
||||
sodipodi:cy="102.09375"
|
||||
sodipodi:r1="35.902484"
|
||||
sodipodi:r2="49.845222"
|
||||
sodipodi:arg1="0.32368816"
|
||||
sodipodi:arg2="0.63784749"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 105.78923,113.51309 6.0066,18.26187 -19.219401,-0.43572 -5.874621,18.30475 L 71.4091,137.9946 55.897164,149.35045 50.372443,130.93707 31.148226,131.00645 37.501747,112.86236 21.908246,101.61878 37.71318,90.674414 31.706582,72.412538 l 19.2194,0.435725 5.874621,-18.304757 15.292708,11.649389 15.511935,-11.355844 5.524722,18.413382 19.224212,-0.06938 -6.35352,18.144089 15.5935,11.243582 z"
|
||||
transform="matrix(1.2188073,0,0,1.2188073,0.97362416,-14.941195)" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#800000;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="path2"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="10"
|
||||
sodipodi:cx="69.252403"
|
||||
sodipodi:cy="183.84013"
|
||||
sodipodi:r1="76.18972"
|
||||
sodipodi:r2="39.770834"
|
||||
sodipodi:arg1="0.94510027"
|
||||
sodipodi:arg2="1.2659605"
|
||||
inkscape:rounded="0.029345222"
|
||||
inkscape:randomized="0"
|
||||
d="m 113.8738,245.59614 c -0.96661,0.6886 -31.555039,-24.18254 -32.684714,-23.81876 -1.120974,0.36099 -10.958876,38.2632 -12.136488,38.25221 -1.186753,-0.0111 -11.314419,-38.11167 -12.442172,-38.48137 -1.119068,-0.36685 -31.356457,24.51411 -32.302709,23.81304 -0.953595,-0.70651 13.247923,-37.48344 12.552854,-38.44541 -0.689714,-0.95456 -39.7769375,1.40147 -40.1303916,0.27809 -0.3561976,-1.13209 32.7500076,-22.5378 32.7531166,-23.7246 0.0031,-1.17766 -33.0039804,-22.24649 -32.6296302,-23.36307 0.3772559,-1.12525 39.7427032,1.0165 40.4428022,0.0582 0.694707,-0.95093 -13.624624,-37.39704 -12.665459,-38.08034 0.966611,-0.68859 31.555037,24.18255 32.684712,23.81876 1.120974,-0.36098 10.958875,-38.26319 12.136488,-38.2522 1.186752,0.0111 11.314419,38.11167 12.442172,38.48137 1.119067,0.36685 31.356459,-24.51411 32.302709,-23.81304 0.95359,0.70651 -13.24792,37.48344 -12.55285,38.44541 0.68971,0.95456 39.77693,-1.40147 40.13039,-0.2781 0.35619,1.13209 -32.75001,22.53781 -32.75312,23.72461 -0.003,1.17766 33.00398,22.24649 32.62963,23.36307 -0.37726,1.12524 -39.7427,-1.01651 -40.4428,-0.0582 -0.69471,0.95093 13.62462,37.39704 12.66546,38.08033 z"
|
||||
transform="translate(19.276442,-75.320913)" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#d45500;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
id="path2-0"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="10"
|
||||
sodipodi:cx="69.252403"
|
||||
sodipodi:cy="183.84013"
|
||||
sodipodi:r1="3.2911112"
|
||||
sodipodi:r2="71.359137"
|
||||
sodipodi:arg1="1.3521275"
|
||||
sodipodi:arg2="1.5760036"
|
||||
inkscape:rounded="0.029345222"
|
||||
inkscape:randomized="0"
|
||||
d="m 69.966345,187.05287 c -1.9053,0.60813 0.905346,68.33625 -1.085527,68.14543 -1.996473,-0.19135 0.964985,-67.70967 -0.939225,-68.33936 -1.898868,-0.62792 -39.434599,55.81734 -40.933089,54.49275 -1.502705,-1.3283 40.579437,-54.21107 39.409022,-55.83977 -1.167132,-1.62412 -64.7118681,21.9781 -65.1456023,20.0257 -0.43495449,-1.95789 64.6939243,-20.00568 64.7043623,-22.01128 0.01041,-1.99997 -65.27140303,-20.25603 -64.4747102,-22.09049 0.7989341,-1.83963 64.0975312,21.84119 65.2848352,20.22477 1.183973,-1.61189 -40.89948,-54.75304 -39.17667,-55.76887 1.727657,-1.01869 39.01806,55.34548 40.92872,54.73564 1.905301,-0.60812 -0.905346,-68.33625 1.085527,-68.14543 1.996474,0.19136 -0.964984,67.70968 0.939225,68.33937 1.898868,0.62792 39.434597,-55.81734 40.933087,-54.49276 1.50271,1.32831 -40.579435,54.21108 -39.409019,55.83977 1.167131,1.62413 64.711869,-21.97809 65.145599,-20.0257 0.43496,1.9579 -64.693921,20.00569 -64.704359,22.01129 -0.01041,1.99997 65.271399,20.25602 64.474709,22.09049 -0.79893,1.83963 -64.09753,-21.8412 -65.284834,-20.22477 -1.183973,1.61189 40.899484,54.75304 39.176674,55.76887 -1.72766,1.01869 -39.018064,-55.34548 -40.928725,-54.73565 z"
|
||||
transform="translate(19.25264,-74.484719)" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#ffb380;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
id="path2-0-7"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="10"
|
||||
sodipodi:cx="69.252403"
|
||||
sodipodi:cy="183.84013"
|
||||
sodipodi:r1="2.0329688"
|
||||
sodipodi:r2="36.653175"
|
||||
sodipodi:arg1="1.3051581"
|
||||
sodipodi:arg2="1.5885803"
|
||||
inkscape:rounded="0.029345222"
|
||||
inkscape:randomized="0"
|
||||
d="m 69.786109,185.8018 c -0.974051,0.29744 -0.168255,34.73512 -1.18551,34.68571 -1.01845,-0.0495 0.894533,-34.41437 -0.06946,-34.74665 -0.962858,-0.3319 -20.552916,28.00241 -21.346851,27.36451 -0.794867,-0.63865 20.951954,-27.31602 20.367382,-28.15147 -0.583886,-0.83446 -33.087061,10.57373 -33.354419,9.59099 -0.267673,-0.98389 33.006439,-9.78387 33.024571,-10.80336 0.01811,-1.01829 -32.983073,-10.89376 -32.621734,-11.84596 0.361764,-0.95332 32.453587,11.48538 33.067497,10.67125 0.61319,-0.81317 -20.280673,-28.2002 -19.428655,-28.75816 0.853019,-0.55861 19.504568,28.36761 20.479763,28.06981 0.974051,-0.29744 0.168255,-34.73512 1.18551,-34.68572 1.01845,0.0495 -0.894534,34.41438 0.06946,34.74666 0.962858,0.3319 20.552915,-28.00241 21.34685,-27.36451 0.794868,0.63865 -20.951953,27.31602 -20.367382,28.15146 0.583886,0.83446 33.087059,-10.57372 33.354419,-9.59099 0.26767,0.98389 -33.006439,9.78388 -33.024571,10.80337 -0.01811,1.01829 32.983071,10.89376 32.621731,11.84596 -0.36176,0.95332 -32.453584,-11.48538 -33.067494,-10.67125 -0.613189,0.81317 20.280674,28.2002 19.428655,28.75816 C 89.412852,214.43022 70.761304,185.504 69.786109,185.8018 Z"
|
||||
transform="rotate(-17.984043,-157.18948,86.992365)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.3 KiB |
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'Automatically suggest a better name for this function'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: afv;r2ai -d
|
||||
if-empty: exit
|
||||
---
|
||||
Suggest a better name for this function. Output only the suggested function name (must be one alphanumeric word) prefixed with `'afn '. Check the code below:
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Automatically suggest a better name for this function
|
||||
Author: pancake
|
||||
Command: afv;r2ai -d
|
||||
Prompt: Suggest a better name for this function. Output only the suggested function name (must be one alphanumeric word) prefixed with `'afn '. Check the code below:
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'Compact conversation history into a summary'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: r2ai -C
|
||||
if-empty: exit
|
||||
---
|
||||
Create a compact summary of this conversation that preserves all key information, insights, and context for future reference. Focus on essential details about the binary analysis, tools used, findings, and any important conclusions.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'Augmented decompilation based on LLM'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: pdc
|
||||
if-empty: exit
|
||||
---
|
||||
$(-e r2ai.prompt). Translate the code into $(-e r2ai.lang) programming language.
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Augmented decompilation based on LLM
|
||||
Author: pancake
|
||||
Command: pdc
|
||||
Prompt: $(-e r2ai.prompt). Translate the code into $(-e r2ai.lang) programming language.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'Find and explain devices used'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: izqq~^/dev
|
||||
if-empty: exit
|
||||
---
|
||||
explain in one line the purpose of each of these devices
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Find and explain devices used
|
||||
Author: pancake
|
||||
Command: izqq~^/dev
|
||||
Prompt: explain in one line the purpose of each of these devices
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'List libraries loaded with dlopen'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: iiq~dlopen
|
||||
if-empty: This binary is not using dlopen
|
||||
---
|
||||
Some libraries are loaded
|
||||
@@ -1,6 +0,0 @@
|
||||
Title: List libraries loaded with dlopen
|
||||
Description: Some libraries are loaded
|
||||
Requires: analysis
|
||||
Command: iiq~dlopen
|
||||
If-Empty: This binary is not using dlopen
|
||||
If-Command: e emu.str=true;pd--5@@c:axtq sym.imp.dlopen
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'Explain the current function'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: r2ai -d
|
||||
if-empty: exit
|
||||
---
|
||||
Analyze function calls, comments and strings, ignore registers and memory accesses. Considering the references and involved loops; explain the purpose of this function in one or two short sentences. Output must be only the translation of the explanation in $(-e r2ai.hlang)
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Explain the current function
|
||||
Author: pancake
|
||||
Command: r2ai -d
|
||||
Prompt: Analyze function calls, comments and strings, ignore registers and memory accesses. Considering the references and involved loops; explain the purpose of this function in one or two short sentences. Output must be only the translation of the explanation in $(-e r2ai.hlang)
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'Group imports by Libraries'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: iiq;il
|
||||
if-empty: exit
|
||||
---
|
||||
Group the imports that belong to each library and describe its purpose and usage scope
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Group imports by Libraries
|
||||
Author: pancake
|
||||
Commands: iiq;il
|
||||
Query: Group the imports that belong to each library and describe its purpose and usage scope
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'Suggest an improved function signature'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: ?e variables;afv;?e function name:;afd;?e current signature:?e afs
|
||||
if-empty: exit
|
||||
---
|
||||
Analyze the uses of the arguments and return value to infer the signature, identify which is the correct type for the resturn. Do NOT print the function body. Output must be *ONLY* the function signature prefixed with the `'afs ` text
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Suggest an improved function signature
|
||||
Author: pancake
|
||||
Command: ?e variables;afv;?e function name:;afd;?e current signature:?e afs
|
||||
Prompt: Analyze the uses of the arguments and return value to infer the signature, identify which is the correct type for the resturn. Do NOT print the function body. Output must be *ONLY* the function signature prefixed with the `'afs ` text
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'Better variable names'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: afv
|
||||
if-empty: exit
|
||||
---
|
||||
Output only the radare2 command without markdown, guess a better name and type for each local variable and function argument taking using. output an r2 script using afvn and afvt commands
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Better variable names
|
||||
Author: pancake
|
||||
Command: afv
|
||||
Prompt: Output only the radare2 command without markdown, guess a better name and type for each local variable and function argument taking using. output an r2 script using afvn and afvt commands
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: 'Find vulnerabilities or bugs in the current function'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: pdc
|
||||
if-empty: exit
|
||||
---
|
||||
Find vulnerabilities, do not show the code, only a short summary of the resulting analysis report including suggested good practices to address the problem and a practical exploit example.
|
||||
@@ -1,4 +0,0 @@
|
||||
Title: Find vulnerabilities or bugs in the current function
|
||||
Author: pancake
|
||||
Command: pdc
|
||||
Prompt: Find vulnerabilities, do not show the code, only a short summary of the resulting analysis report including suggested good practices to address the problem and a practical exploit example.
|
||||
+2
-1
@@ -5,8 +5,9 @@
|
||||
- All the source code of the plugin is in the current directory
|
||||
- Radare2 headers use to be in /usr/local/include/libr
|
||||
|
||||
## Formatting Style
|
||||
## Coding Style
|
||||
|
||||
- `R_NEW` and `R_NEW0` calls never return NULL, so there's no need to check after calling them
|
||||
- Use the radare2 coding style rules
|
||||
- Always add a whitespace before `(` in function calls
|
||||
- Indent the code with tabs
|
||||
|
||||
+9
-2
@@ -6,6 +6,9 @@ R2SYSPLUGDIR=$(shell r2 -NNH R2_LIBR_PLUGINS)
|
||||
R2USRPLUGDIR=$(shell r2 -NNH R2_USER_PLUGINS)
|
||||
R2BINDIR=$(shell r2pm -H R2PM_BINDIR)
|
||||
R2MANDIR=$(shell r2pm -H R2PM_MANDIR)
|
||||
ifeq ($(R2MANDIR),)
|
||||
R2MANDIR=$(shell r2 -NNH R2_PREFIX)/share/man
|
||||
endif
|
||||
|
||||
R2_CFLAGS=$(shell pkg-config --cflags r_core)
|
||||
R2_LDFLAGS=$(shell pkg-config --libs r_core)
|
||||
@@ -39,17 +42,21 @@ ifeq ($(USE_LIBCURL),1)
|
||||
endif
|
||||
|
||||
OBJS=r2ai.o
|
||||
OBJS+=apikeys.o
|
||||
OBJS+=llm.o
|
||||
OBJS+=auto.o
|
||||
OBJS+=json.o
|
||||
OBJS+=vdb.o
|
||||
OBJS+=tools.o
|
||||
OBJS+=messages.o
|
||||
OBJS+=anthropic.o
|
||||
OBJS+=r2ai_http.o
|
||||
OBJS+=gemini.o
|
||||
OBJS+=http.o
|
||||
OBJS+=openai.o
|
||||
OBJS+=markdown.o
|
||||
OBJS+=query.o
|
||||
OBJS+=rawtools.o
|
||||
OBJS+=wizard.o
|
||||
|
||||
all: $(OBJS) r2check
|
||||
$(CC) -fPIC -shared -o r2ai$(DOTLIB) $(OBJS) $(CFLAGS) $(LDFLAGS)
|
||||
@@ -66,7 +73,7 @@ doc-install:
|
||||
vdb.o: vdb_embed.inc.c
|
||||
|
||||
fmt format indent:
|
||||
clang-format-radare2 *.c
|
||||
clang-format-radare2 *.c http/*.c
|
||||
|
||||
user-install: user-uninstall
|
||||
mkdir -p $(R2USRPLUGDIR)
|
||||
|
||||
+4
-1
@@ -43,6 +43,7 @@ Usage: r2ai [-args] [...]
|
||||
| r2ai -q list available query prompts
|
||||
| r2ai -q [name] (inst) run predefined prompt with optional instructions
|
||||
| r2ai -r enter the chat repl
|
||||
| r2ai -w launch the interactive setup wizard
|
||||
| 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
|
||||
@@ -97,8 +98,10 @@ These can be set with `r2ai -e <keyname>=<value>`
|
||||
| r2ai.lang | Tells LLM which programming language to use for decompilation result. Only works for `r2ai -d` |
|
||||
| r2ai.hlang | Tells LLM in which language to speak |
|
||||
| r2ai.prompt | User prompt to send to LLM with `r2ai -d` |
|
||||
| r2ai.clippy | Display chat replies using `r2clippy` instead of plain text |
|
||||
| r2ai.wizard | Auto-start the setup wizard on the first interactive chat session |
|
||||
| r2ai.auto.max_runs | Maximum loops when using auto mode `r2ai -a` |
|
||||
| r2ai.auto.hide_tool_output | Only for auto mode `r2ai -a`. Will not show the output of the tool which ran locally |
|
||||
| r2ai.auto.verbose | Only for auto mode `r2ai -a`. Will show the output of the tool which ran locally |
|
||||
| r2ai.auto.yolo | Set this to true if you don't want r2ai to ask you for approval before running commands sent by the LLM. This is **dangerous**. Recommendation: **leave this to false** unless you fully trust your LLM not to create havoc! |
|
||||
| r2ai.http.use_files | If you get CURL argument too long error, set this to true and arguments will be stored in a file.|
|
||||
|
||||
|
||||
+32
-36
@@ -8,8 +8,8 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
RCore *core = cps->core;
|
||||
const char *model = args.model;
|
||||
char **error = args.error;
|
||||
const R2AI_Tools *tools = args.tools;
|
||||
R2AI_Messages *messages_input = args.messages;
|
||||
RList *tools = args.tools;
|
||||
RList *messages_input = args.messages;
|
||||
|
||||
if (error) {
|
||||
*error = NULL;
|
||||
@@ -41,7 +41,7 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
// Create messages JSON
|
||||
char *messages_json = NULL;
|
||||
|
||||
if (messages_input && r_list_length (messages_input->messages) > 0) {
|
||||
if (messages_input && !r_list_empty (messages_input)) {
|
||||
// Convert directly to JSON without filtering
|
||||
messages_json = r2ai_msgs_to_anthropic_json (messages_input);
|
||||
if (!messages_json) {
|
||||
@@ -61,7 +61,7 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
|
||||
// Convert tools to Anthropic format if available
|
||||
char *anthropic_tools_json = NULL;
|
||||
if (tools && tools->n_tools > 0) {
|
||||
if (tools && !r_list_empty (tools)) {
|
||||
anthropic_tools_json = r2ai_tools_to_anthropic_json (tools);
|
||||
}
|
||||
|
||||
@@ -108,6 +108,17 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
|
||||
R_LOG_DEBUG ("Anthropic API request data: %s", data);
|
||||
|
||||
if (r_config_get_b (core->config, "r2ai.debug")) {
|
||||
// Generate curl command for debugging
|
||||
RStrBuf *curl_cmd = r_strbuf_new ("curl -X POST");
|
||||
for (int i = 0; headers[i]; i++) {
|
||||
r_strbuf_appendf (curl_cmd, " -H \"%s\"", headers[i]);
|
||||
}
|
||||
r_strbuf_appendf (curl_cmd, " -d '%s' \"%s\"", data, anthropic_url);
|
||||
eprintf ("Curl command: %s\n", r_strbuf_get (curl_cmd));
|
||||
r_strbuf_free (curl_cmd);
|
||||
}
|
||||
|
||||
// Make the API call
|
||||
int code = 0;
|
||||
char *res = r2ai_http_post (core, anthropic_url, headers, data, &code, NULL);
|
||||
@@ -133,17 +144,12 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
free (res_path);
|
||||
free (tmpdir);
|
||||
|
||||
R_LOG_DEBUG ("Anthropic API response: %s", res);
|
||||
if (r_config_get_b (core->config, "r2ai.debug")) {
|
||||
R_LOG_DEBUG ("Anthropic API response: %s", res);
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
R2AI_ChatResponse *result = R_NEW0 (R2AI_ChatResponse);
|
||||
if (!result) {
|
||||
free (res);
|
||||
if (error) {
|
||||
*error = strdup ("Failed to allocate memory for response");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
R2AI_Message *message = NULL;
|
||||
R2AI_Usage *usage = NULL;
|
||||
@@ -195,9 +201,9 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate tool_calls array if needed
|
||||
// Allocate tool_calls list if needed
|
||||
if (has_tool_use && n_tool_calls > 0) {
|
||||
message->tool_calls = R_NEWS0 (R2AI_ToolCall, n_tool_calls);
|
||||
message->tool_calls = r_list_new ();
|
||||
if (!message->tool_calls) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to allocate memory for tool calls");
|
||||
@@ -208,13 +214,13 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
free (res);
|
||||
return NULL;
|
||||
}
|
||||
message->n_tool_calls = n_tool_calls;
|
||||
message->tool_calls->free = (RListFree)r2ai_tool_call_free;
|
||||
}
|
||||
|
||||
// Process each content item
|
||||
int tool_idx = 0;
|
||||
if (content_array && content_array->type == R_JSON_ARRAY) {
|
||||
R2AI_ContentBlocks *cb = R_NEW0 (R2AI_ContentBlocks);
|
||||
RList *cb = r2ai_content_blocks_new ();
|
||||
if (!cb) {
|
||||
r_json_free (jres);
|
||||
free (response_copy);
|
||||
@@ -222,22 +228,11 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
free (res);
|
||||
return NULL;
|
||||
}
|
||||
cb->n_blocks = content_array->children.count;
|
||||
cb->blocks = R_NEWS0 (R2AI_ContentBlock, cb->n_blocks);
|
||||
if (!cb->blocks) {
|
||||
free (cb);
|
||||
r_json_free (jres);
|
||||
free (response_copy);
|
||||
r2ai_message_free (message);
|
||||
free (res);
|
||||
return NULL;
|
||||
}
|
||||
int block_idx = 0;
|
||||
const RJson *content_item = content_array->children.first;
|
||||
while (content_item && block_idx < cb->n_blocks) {
|
||||
while (content_item) {
|
||||
const RJson *type = r_json_get (content_item, "type");
|
||||
if (type && type->type == R_JSON_STRING) {
|
||||
R2AI_ContentBlock *block = &cb->blocks[block_idx];
|
||||
R2AI_ContentBlock *block = R_NEW0 (R2AI_ContentBlock);
|
||||
if (!strcmp (type->str_value, "text")) {
|
||||
// Text content
|
||||
const RJson *text = r_json_get (content_item, "text");
|
||||
@@ -253,29 +248,30 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
const RJson *input = r_json_get (content_item, "input");
|
||||
|
||||
block->type = strdup ("tool_use");
|
||||
R2AI_ToolCall *tc = R_NEW0 (R2AI_ToolCall);
|
||||
if (name && name->type == R_JSON_STRING) {
|
||||
block->name = strdup (name->str_value);
|
||||
R2AI_ToolCall *tc = (R2AI_ToolCall *)&message->tool_calls[tool_idx];
|
||||
tc->name = strdup (name->str_value);
|
||||
}
|
||||
|
||||
if (id && id->type == R_JSON_STRING) {
|
||||
block->id = strdup (id->str_value);
|
||||
R2AI_ToolCall *tc = (R2AI_ToolCall *)&message->tool_calls[tool_idx];
|
||||
tc->id = strdup (id->str_value);
|
||||
}
|
||||
|
||||
if (input && input->type == R_JSON_OBJECT) {
|
||||
#if 1
|
||||
char *input_str = r_json_to_string (input);
|
||||
if (input_str) {
|
||||
R_LOG_DEBUG ("Input string: %s", input_str);
|
||||
block->input = strdup (input_str);
|
||||
R2AI_ToolCall *tc = (R2AI_ToolCall *)&message->tool_calls[tool_idx];
|
||||
tc->arguments = strdup (input_str);
|
||||
free (input_str);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
r_list_append (message->tool_calls, tc);
|
||||
tool_idx++;
|
||||
} else if (!strcmp (type->str_value, "thinking")) {
|
||||
const RJson *data = r_json_get (content_item, "data");
|
||||
@@ -292,11 +288,11 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
}
|
||||
block->type = strdup ("thinking");
|
||||
|
||||
r_strbuf_append (content_buf, "\n\x1b[90m<thinking>\n");
|
||||
r_strbuf_append (content_buf, "\n" Color_GRAY "<thinking>\n");
|
||||
r_strbuf_append (content_buf, block->thinking);
|
||||
r_strbuf_append (content_buf, "\n</thinking>\x1b[0m\n");
|
||||
r_strbuf_append (content_buf, "\n</thinking>" Color_RESET "\n");
|
||||
}
|
||||
block_idx++;
|
||||
r_list_append (cb, block);
|
||||
}
|
||||
content_item = content_item->next;
|
||||
}
|
||||
@@ -307,7 +303,7 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
message->content = r_strbuf_drain (content_buf);
|
||||
|
||||
// If there's no content and no tool calls, clean up
|
||||
if (!message->content && !message->n_tool_calls) {
|
||||
if (!message->content && (!message->tool_calls || r_list_empty (message->tool_calls))) {
|
||||
r2ai_message_free (message);
|
||||
message = NULL;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/* r2ai - Copyright 2023-2025 pancake */
|
||||
|
||||
#define R_LOG_ORIGIN "r2ai"
|
||||
|
||||
#include "r2ai.h"
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
/* Return the path to the apikeys.txt file */
|
||||
R_API char *r2ai_apikeys_path(bool *exists) {
|
||||
char *path = r_file_home (".config/r2ai/apikeys.txt");
|
||||
if (exists) {
|
||||
*exists = r_file_exists (path);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/* Edit the API keys file */
|
||||
R_API void r2ai_apikeys_edit(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
bool exists = false;
|
||||
char *keys_path = r2ai_apikeys_path (&exists);
|
||||
char *config_dir = r_file_home (".config/r2ai");
|
||||
r_sys_mkdirp (config_dir);
|
||||
free (config_dir);
|
||||
if (!exists) {
|
||||
const char *template = "# API Keys configuration for r2ai\n"
|
||||
"\n"
|
||||
"# Specify your API keys in the format: PROVIDER=KEY\n"
|
||||
"# OpenAI=sk-your-openai-api-key-here\n"
|
||||
"# MISTRAL=your-mistral-api-key-here\n";
|
||||
r_file_dump (keys_path, (const ut8 *)template, strlen (template), 0);
|
||||
}
|
||||
r_cons_editor (core->cons, keys_path, NULL);
|
||||
free (keys_path);
|
||||
}
|
||||
|
||||
/* Return a malloc'd API key read from ~/.config/r2ai/apikeys.txt
|
||||
* Provider matching is case-insensitive */
|
||||
R_API char *r2ai_apikeys_get(const char *provider) {
|
||||
if (!provider) {
|
||||
return NULL;
|
||||
}
|
||||
bool exists = false;
|
||||
char *config_file = r2ai_apikeys_path (&exists);
|
||||
if (!exists) {
|
||||
free (config_file);
|
||||
return NULL;
|
||||
}
|
||||
char *content = r_file_slurp (config_file, NULL);
|
||||
free (config_file);
|
||||
if (!content) {
|
||||
return NULL;
|
||||
}
|
||||
RList *lines = r_str_split_list (content, "\n", 0);
|
||||
char *key = NULL;
|
||||
if (lines) {
|
||||
RListIter *iter;
|
||||
char *line;
|
||||
r_list_foreach (lines, iter, line) {
|
||||
const char *trimmed = r_str_trim_head_ro (line);
|
||||
if (R_STR_ISEMPTY (trimmed) || *trimmed == '#') {
|
||||
continue;
|
||||
}
|
||||
char *eq = strchr (trimmed, '=');
|
||||
if (eq) {
|
||||
*eq = 0;
|
||||
char *lower = strdup (trimmed);
|
||||
r_str_case (lower, false); /* lowercase */
|
||||
char *tail = strstr (lower, "_api_key");
|
||||
if (tail) {
|
||||
*tail = 0;
|
||||
}
|
||||
if (r_str_casecmp (lower, provider) == 0) {
|
||||
key = strdup (eq + 1);
|
||||
r_str_trim (key);
|
||||
free (lower);
|
||||
break;
|
||||
}
|
||||
free (lower);
|
||||
} else if (*trimmed) {
|
||||
R_LOG_WARN ("Invalid syntax in apikeys.txt: %s", line);
|
||||
}
|
||||
}
|
||||
r_list_free (lines);
|
||||
}
|
||||
free (content);
|
||||
return key;
|
||||
}
|
||||
+337
-142
@@ -1,3 +1,5 @@
|
||||
/* Copyright r2ai - 2023-2025 - pancake */
|
||||
|
||||
#include "r2ai.h"
|
||||
|
||||
// Stats are now stored in R2AI_State->stats, no longer need global variable
|
||||
@@ -5,24 +7,22 @@
|
||||
// Helper function to format time duration
|
||||
static char *format_time_duration(time_t seconds) {
|
||||
if (seconds < 60) {
|
||||
return r_str_newf ("%" PFMT64d "s", (long long)seconds);
|
||||
return r_str_newf ("%" PFMT64d "s", (ut64)seconds);
|
||||
}
|
||||
if (seconds < 3600) {
|
||||
return r_str_newf ("%" PFMT64d "m%" PFMT64d "s", (long long) (seconds / 60), (long long) (seconds % 60));
|
||||
return r_str_newf ("%" PFMT64d "m%" PFMT64d "s", (ut64) (seconds / 60), (ut64) (seconds % 60));
|
||||
}
|
||||
return r_str_newf ("%" PFMT64d "h%" PFMT64d "m%" PFMT64d "s",
|
||||
(long long) (seconds / 3600),
|
||||
(long long) ((seconds % 3600) / 60),
|
||||
(long long) (seconds % 60));
|
||||
(ut64) (seconds / 3600),
|
||||
(ut64) ((seconds % 3600) / 60),
|
||||
(ut64) (seconds % 60));
|
||||
}
|
||||
|
||||
// Initialize timing and cost tracking for a run
|
||||
// Initialize timing tracking for a run
|
||||
static void r2ai_stats_init_run(R2AI_State *state, int n_run) {
|
||||
time_t run_start = time (NULL);
|
||||
if (n_run == 1) {
|
||||
// First run, initialize total timing
|
||||
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;
|
||||
@@ -51,18 +51,12 @@ static void r2ai_print_run_end(RCorePluginSession *cps, const R2AI_Usage *usage,
|
||||
state->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;
|
||||
}
|
||||
|
||||
// Format times for display
|
||||
char *run_time_str = format_time_duration (run_time);
|
||||
char *total_time_str = format_time_duration (total_time);
|
||||
|
||||
// Print detailed stats
|
||||
r_cons_printf (core->cons, "\x1b[1;34m%s | total: %d in: %d out: %d | run: %d in: %d out: %d | %s / %s\x1b[0m\n",
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_BLUE "%s | total: %d in: %d out: %d | run: %d in: %d out: %d | %s / %s" Color_RESET "\n",
|
||||
r_config_get (core->config, "r2ai.model"),
|
||||
state->stats.total_tokens,
|
||||
state->stats.total_prompt_tokens,
|
||||
@@ -79,28 +73,122 @@ static void r2ai_print_run_end(RCorePluginSession *cps, const R2AI_Usage *usage,
|
||||
free (total_time_str);
|
||||
}
|
||||
|
||||
const char *Gprompt_auto = "You are a reverse engineer and you are using radare2 to analyze a binary.\n"
|
||||
"The user will ask questions about the binary and you will respond with the answer to the best of your ability.\n"
|
||||
"\n"
|
||||
"# Guidelines\n"
|
||||
"- Understand the Task: Grasp the main objective, goals, requirements, constraints, and expected output.\n"
|
||||
"- Reasoning Before Conclusions**: Encourage reasoning steps before any conclusions are reached.\n"
|
||||
"- Assume the user is always asking you about the binary, unless they're specifically asking you for radare2 help.\n"
|
||||
"- The binary has already been loaded. You can interact with the binary using the r2cmd tool.\n"
|
||||
"- `this` or `here` might refer to the current address in the binary or the binary itself.\n"
|
||||
"- If you need more information, try to use the r2cmd tool to run commands before answering.\n"
|
||||
"- You can use the r2cmd tool multiple times if you need or you can pass a command with pipes if you need to chain commands.\n"
|
||||
"- If you're asked to decompile a function, make sure to return the code in the language you think it was originally written and rewrite it to be as easy as possible to be understood. Make sure you use descriptive variable and function names and add comments.\n"
|
||||
"- Don't just regurgitate the same code, figure out what it's doing and rewrite it to be more understandable.\n"
|
||||
"- If you need to run a command in r2 before answering, you can use the r2cmd tool\n"
|
||||
"- Do not repeat commands if you already know the answer.\n"
|
||||
"- Formulate a plan. Think step by step. Analyze the binary as much as possible before answering.\n"
|
||||
"- You must keep going until you have a final answer.\n"
|
||||
"- Double check that final answer. Make sure you didn't miss anything.\n"
|
||||
"- Make sure you call tools and functions correctly.\n";
|
||||
// Helper function to handle final response attempt without tools
|
||||
static void handle_final_response_attempt(RCorePluginSession *cps, RList *messages, const char *effective_prompt, char **error) {
|
||||
RCore *core = cps->core;
|
||||
if (r_cons_yesno (core->cons, 1, "Try to produce response without tool calling with collected information? (Y/n)")) {
|
||||
char *final_system_prompt = r_str_newf ("%s\n\nIMPORTANT: Do not use any tools. Provide a direct answer based on the collected information.", effective_prompt);
|
||||
R2AIArgs args_final = {
|
||||
.messages = messages,
|
||||
.error = error,
|
||||
.dorag = true,
|
||||
.tools = NULL, // No tools for final response
|
||||
.system_prompt = final_system_prompt
|
||||
};
|
||||
|
||||
R2AI_ChatResponse *final_response = r2ai_llmcall (cps, args_final);
|
||||
if (final_response && final_response->message) {
|
||||
const R2AI_Message *final_msg = final_response->message;
|
||||
r_cons_printf (core->cons, Color_RED "[Assistant]" Color_RESET);
|
||||
if (final_msg->reasoning_content) {
|
||||
r_cons_printf (core->cons, Color_GRAY "<thinking>\n%s\n</thinking>" Color_RESET "\n", final_msg->reasoning_content);
|
||||
r_cons_newline (core->cons);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
if (final_msg->content) {
|
||||
r_cons_printf (core->cons, "%s", final_msg->content);
|
||||
r_cons_newline (core->cons);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
// Add final response to messages for completeness
|
||||
r2ai_msgs_add (messages, final_msg);
|
||||
free (final_response);
|
||||
}
|
||||
free (final_system_prompt);
|
||||
} else {
|
||||
r_cons_printf (core->cons, "Auto mode interrupted without final response.\n");
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
}
|
||||
|
||||
static void update_tool_call_argument(R2AI_Message *msg, const char *tool_call_id, const char *field_name, const char *field_value) {
|
||||
R_RETURN_IF_FAIL (msg && tool_call_id && field_name && field_value);
|
||||
if (!msg->tool_calls || r_list_empty (msg->tool_calls)) {
|
||||
return;
|
||||
}
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (msg->tool_calls, iter, tc) {
|
||||
if (!tc->id || strcmp (tc->id, tool_call_id) || !tc->arguments) {
|
||||
continue;
|
||||
}
|
||||
char *args_dup = strdup (tc->arguments);
|
||||
if (!args_dup) {
|
||||
return;
|
||||
}
|
||||
RJson *args_json = r_json_parse (args_dup);
|
||||
if (!args_json) {
|
||||
free (args_dup);
|
||||
return;
|
||||
}
|
||||
RJson *field_json = (RJson *)r_json_get (args_json, field_name);
|
||||
if (field_json && field_json->str_value) {
|
||||
const char *orig_value = field_json->str_value;
|
||||
field_json->str_value = field_value;
|
||||
char *new_args = r_json_to_string (args_json);
|
||||
field_json->str_value = orig_value;
|
||||
if (new_args) {
|
||||
free ((void *)tc->arguments);
|
||||
tc->arguments = new_args;
|
||||
}
|
||||
}
|
||||
r_json_free (args_json);
|
||||
free (args_dup);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#if 0
|
||||
static const char *Gprompt_auto =
|
||||
"You are a reverse engineer using radare2.\n"
|
||||
"Answer questions about the loaded binary using r2cmd tool calls.\n"
|
||||
"\n"
|
||||
"Rules:\n"
|
||||
"- Use r2cmd tool to run radare2 commands when you need information\n"
|
||||
"- Make tool calls immediately when you need data\n"
|
||||
"- Provide final answer only after gathering needed information\n"
|
||||
"- Keep responses short and direct\n"
|
||||
;
|
||||
#else
|
||||
const char *Gprompt_auto =
|
||||
"You are a reverse engineer and you are using radare2 to analyze a binary.\n"
|
||||
"The user will ask questions about the binary and you will respond with the answer to the best of your ability.\n"
|
||||
"\n"
|
||||
"# Guidelines\n"
|
||||
"- Understand the Task: Grasp the main objective, goals, requirements, constraints, and expected output.\n"
|
||||
"- Reasoning Before Conclusions**: Encourage reasoning steps before any conclusions are reached.\n"
|
||||
"- Assume the user is always asking you about the binary, unless they're specifically asking you for radare2 help.\n"
|
||||
"- The binary has already been loaded. You can interact with the binary using the r2cmd tool.\n"
|
||||
"- `this` or `here` might refer to the current address in the binary or the binary itself.\n"
|
||||
"- If you need more information, try to use the r2cmd tool to run commands before answering.\n"
|
||||
"- You can use the r2cmd tool multiple times if you need or you can pass a command with pipes if you need to chain commands.\n"
|
||||
"- IMPORTANT: You must make only ONE tool call per response. Do not make multiple tool calls in a single response.\n"
|
||||
"- Keep your reasoning concise and avoid repeating the same thoughts. If you have a clear next step, execute it immediately with a tool call.\n"
|
||||
"- If you're asked to decompile a function, make sure to return the code in the language you think it was originally written "
|
||||
"and rewrite it to be as easy as possible to be understood. Make sure you use descriptive variable and function names and add comments.\n"
|
||||
"- Don't just regurgitate the same code, figure out what it's doing and rewrite it to be more understandable.\n"
|
||||
"- If you need to run a command in r2 before answering, you can use the r2cmd tool\n"
|
||||
"- Do not repeat commands if you already know the answer.\n"
|
||||
"- Formulate a plan. Think step by step. Analyze the binary as much as possible before answering.\n"
|
||||
"- You must keep going until you have a final answer.\n"
|
||||
"- Double check that final answer. Make sure you didn't miss anything.\n"
|
||||
"- Make sure you call tools and functions correctly.\n"
|
||||
"- When calling tools, use the exact format: {\"tool_calls\": [{\"id\": \"call_123\", \"type\": \"function\", \"function\": {\"name\": \"r2cmd\", \"arguments\": \"{\\\"command\\\":\\\"aa\\\"}\"}}]}\n"
|
||||
"- Arguments must be a JSON string, not an object.\n";
|
||||
#endif
|
||||
|
||||
// Helper function to process messages and handle tool calls recursively
|
||||
R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, const char *system_prompt, int n_run) {
|
||||
R_API void process_messages(RCorePluginSession *cps, RList *messages, const char *system_prompt, int n_run) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
char *error = NULL;
|
||||
@@ -108,32 +196,42 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
const int max_runs = r_config_get_i (core->config, "r2ai.auto.max_runs");
|
||||
|
||||
if (n_run > max_runs) {
|
||||
r_cons_printf (core->cons, "\x1b[1;31m[r2ai] Max runs reached\x1b[0m\n");
|
||||
R_LOG_WARN ("Max runs reached");
|
||||
r_cons_flush (core->cons);
|
||||
return;
|
||||
}
|
||||
|
||||
const char *effective_prompt = system_prompt? system_prompt: Gprompt_auto;
|
||||
if (!system_prompt) {
|
||||
system_prompt = Gprompt_auto;
|
||||
const char *init_commands = r_config_get (core->config, "r2ai.auto.init_commands");
|
||||
if (R_STR_ISNOTEMPTY (init_commands)) {
|
||||
char *edited_command = NULL;
|
||||
char *cmd_output = execute_tool (core, "r2cmd", r_str_newf ("{\"command\":\"%s\"}", init_commands), &edited_command);
|
||||
char *comment = NULL;
|
||||
char *cmd_output = execute_tool (core, "r2cmd", r_str_newf ("{\"command\":\"%s\"}", init_commands), &edited_command, &comment);
|
||||
if (R_STR_ISNOTEMPTY (cmd_output)) {
|
||||
char *display_command = strip_command_comment (init_commands, NULL);
|
||||
char *content = r_str_newf ("Here is some information about the binary to get you started:\n>%s\n%s", display_command, cmd_output);
|
||||
if (comment && *comment) {
|
||||
char *new_content = r_str_newf ("%s\nHINT: %s", content, comment);
|
||||
free (content);
|
||||
content = new_content;
|
||||
}
|
||||
R2AI_Message init_msg = {
|
||||
.role = "system",
|
||||
.content = r_str_newf ("Here is some information about the binary to get you started:\n>%s\n%s", edited_command, cmd_output)
|
||||
.content = content
|
||||
};
|
||||
r2ai_msgs_add (messages, &init_msg);
|
||||
free (display_command);
|
||||
free (cmd_output);
|
||||
}
|
||||
free (edited_command);
|
||||
free (comment);
|
||||
}
|
||||
}
|
||||
|
||||
r2ai_stats_init_run (state, n_run);
|
||||
|
||||
r_cons_printf (core->cons, "\x1b[35m[DEBUG] About to call r2ai_llmcall with n_run=%d\x1b[0m\n", n_run);
|
||||
r_cons_printf (core->cons, Color_BLUE "About to call r2ai_llmcall with n_run=%d%s\n", n_run, Color_RESET);
|
||||
r_cons_flush (core->cons);
|
||||
|
||||
// Set up args for r2ai_llmcall call with tools directly
|
||||
@@ -141,13 +239,14 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
.messages = messages,
|
||||
.error = &error,
|
||||
.dorag = true,
|
||||
.tools = r2ai_get_tools (), // Always send tools in auto mode
|
||||
.system_prompt = system_prompt
|
||||
.tools = r2ai_get_tools (core, cps->data), // Always send tools in auto mode
|
||||
.system_prompt = effective_prompt
|
||||
};
|
||||
|
||||
R2AI_ChatResponse *response = r2ai_llmcall (cps, args);
|
||||
|
||||
if (!response) {
|
||||
R_LOG_ERROR ("No response from llmcall");
|
||||
handle_final_response_attempt (cps, messages, effective_prompt, &error);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -157,30 +256,34 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
if (!message) {
|
||||
R_LOG_ERROR ("No message in response");
|
||||
free (response);
|
||||
handle_final_response_attempt (cps, messages, effective_prompt, &error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Debug logging for rawtools
|
||||
R_LOG_DEBUG ("[DEBUG] Model response - Role: %s", message->role? message->role: "null");
|
||||
R_LOG_DEBUG ("Model response - Role: %s", message->role? message->role: "null");
|
||||
if (message->content) {
|
||||
R_LOG_DEBUG ("[DEBUG] Content: %s", message->content);
|
||||
R_LOG_DEBUG ("Content: %s", message->content);
|
||||
}
|
||||
if (message->reasoning_content) {
|
||||
R_LOG_DEBUG ("[DEBUG] Reasoning: %s", message->reasoning_content);
|
||||
R_LOG_DEBUG ("Reasoning: %s", message->reasoning_content);
|
||||
}
|
||||
if (message->tool_calls && message->n_tool_calls > 0) {
|
||||
R_LOG_DEBUG ("[DEBUG] Tool calls: %d", message->n_tool_calls);
|
||||
for (int i = 0; i < message->n_tool_calls; i++) {
|
||||
const R2AI_ToolCall *tc = &message->tool_calls[i];
|
||||
R_LOG_DEBUG ("[DEBUG] Tool %d: %s - %s", i, tc->name? tc->name: "null", tc->arguments? tc->arguments: "null");
|
||||
if (message->tool_calls && r_list_length (message->tool_calls) > 0) {
|
||||
R_LOG_DEBUG ("Tool calls: %d", r_list_length (message->tool_calls));
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tc;
|
||||
int i = 0;
|
||||
r_list_foreach (message->tool_calls, iter, tc) {
|
||||
R_LOG_DEBUG ("Tool %d: %s - %s", i, tc->name? tc->name: "null", tc->arguments? tc->arguments: "null");
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Process the response - we need to add it to our messages array
|
||||
if (message->content || message->reasoning_content) {
|
||||
r_cons_printf (core->cons, "\x1b[31m[Assistant]\x1b[0m\n\n");
|
||||
r_cons_printf (core->cons, Color_RED "[Assistant]" Color_RESET);
|
||||
if (message->reasoning_content) {
|
||||
r_cons_printf (core->cons, "\x1b[90m<thinking>\n%s\n</thinking>\x1b[0m\n", message->reasoning_content);
|
||||
r_cons_printf (core->cons, Color_GRAY "<thinking>\n%s\n</thinking>" Color_RESET "\n", message->reasoning_content);
|
||||
r_cons_newline (core->cons);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
@@ -196,18 +299,21 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
r2ai_msgs_add (messages, message);
|
||||
|
||||
// Check for tool calls and process them
|
||||
if (message->tool_calls && message->n_tool_calls > 0) {
|
||||
R_LOG_DEBUG ("Found %d tool call(s)", message->n_tool_calls);
|
||||
if (message->tool_calls && r_list_length (message->tool_calls) > 0) {
|
||||
R_LOG_DEBUG ("Found %d tool call(s)", r_list_length (message->tool_calls));
|
||||
|
||||
// Process each tool call
|
||||
for (int i = 0; i < message->n_tool_calls; i++) {
|
||||
const R2AI_ToolCall *tool_call = &message->tool_calls[i];
|
||||
// Process only the first tool call
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tool_call;
|
||||
int i = 0;
|
||||
r_list_foreach (message->tool_calls, iter, tool_call) {
|
||||
|
||||
if (!tool_call->name || !tool_call->arguments || !tool_call->id) {
|
||||
R_LOG_DEBUG ("Skipping invalid tool call %d", i);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
R_LOG_DEBUG ("Tool call %d: %s with args: %s", i, tool_call->name, tool_call->arguments);
|
||||
R_LOG_DEBUG ("Processing first tool call %d: %s with args: %s", i, tool_call->name, tool_call->arguments);
|
||||
// Don't log the full arguments which might get truncated
|
||||
char *tool_name = strdup (tool_call->name);
|
||||
char *tool_args = strdup (tool_call->arguments);
|
||||
@@ -219,40 +325,20 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
}
|
||||
|
||||
char *cmd_output = NULL;
|
||||
char *comment = NULL;
|
||||
if (interrupted) {
|
||||
cmd_output = strdup ("<user interrupted>");
|
||||
} else {
|
||||
char *edited_command = NULL;
|
||||
cmd_output = execute_tool (core, tool_name, tool_args, &edited_command);
|
||||
cmd_output = execute_tool (core, tool_name, tool_args, &edited_command, &comment);
|
||||
if (edited_command) {
|
||||
// Update the last message's tool call arguments with the edited command
|
||||
R2AI_Message *last_msg = r_list_get_n (messages->messages, r_list_length (messages->messages) - 1);
|
||||
if (last_msg && last_msg->tool_calls && last_msg->n_tool_calls > 0) {
|
||||
for (int j = 0; j < last_msg->n_tool_calls; j++) {
|
||||
R2AI_ToolCall *tc = (R2AI_ToolCall *)&last_msg->tool_calls[j];
|
||||
if (tc->id && strcmp (tc->id, tool_call->id) == 0) {
|
||||
// For r2cmd, update the command in arguments
|
||||
if (strcmp (tool_name, "r2cmd") == 0) {
|
||||
char *args_dup = strdup (tc->arguments);
|
||||
RJson *args_json = r_json_parse (args_dup);
|
||||
if (args_json) {
|
||||
RJson *cmd_json = (RJson *)r_json_get (args_json, "command");
|
||||
if (cmd_json && cmd_json->str_value) {
|
||||
// Update the command field
|
||||
free ((char *)cmd_json->str_value);
|
||||
cmd_json->str_value = strdup (edited_command);
|
||||
// Serialize back to JSON
|
||||
char *new_args = r_json_to_string (args_json);
|
||||
if (new_args) {
|
||||
free ((void *)tc->arguments);
|
||||
tc->arguments = new_args;
|
||||
}
|
||||
}
|
||||
r_json_free (args_json);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
R2AI_Message *last_msg = r_list_get_n (messages, r_list_length (messages) - 1);
|
||||
if (last_msg) {
|
||||
if (!strcmp (tool_name, "r2cmd")) {
|
||||
update_tool_call_argument (last_msg, tool_call->id, "command", edited_command);
|
||||
} else if (!strcmp (tool_name, "execute_js")) {
|
||||
update_tool_call_argument (last_msg, tool_call->id, "script", edited_command);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,12 +348,20 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
free (tool_name);
|
||||
free (tool_args);
|
||||
if (strcmp (cmd_output, "R2AI_SIGINT") == 0) {
|
||||
r_cons_printf (core->cons, "\n\n\x1b[1;31m[r2ai] Processing interrupted after tool execution\x1b[0m\n\n");
|
||||
r_cons_printf (core->cons, "\n\n\x1b[1" Color_RED "[r2ai] Processing interrupted after tool execution" Color_RESET "\n\n");
|
||||
r_cons_flush (core->cons);
|
||||
free (cmd_output);
|
||||
cmd_output = strdup ("<user interrupted>");
|
||||
interrupted = true;
|
||||
|
||||
handle_final_response_attempt (cps, messages, effective_prompt, &error);
|
||||
}
|
||||
if (comment) {
|
||||
char *msg = r_str_newf ("HINT: %s\n%s", comment, cmd_output);
|
||||
free (cmd_output);
|
||||
cmd_output = msg;
|
||||
}
|
||||
free (comment);
|
||||
|
||||
// Create a tool call response message
|
||||
R2AI_Message tool_response = {
|
||||
@@ -278,19 +372,24 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
|
||||
// Add the tool response to our messages array
|
||||
r2ai_msgs_add (messages, &tool_response);
|
||||
R_LOG_DEBUG ("[DEBUG] Added tool response to messages: %s", cmd_output? cmd_output: "null");
|
||||
r_cons_printf (core->cons, "\x1b[32m[DEBUG] Tool result: %s\x1b[0m\n", cmd_output? cmd_output: "no output");
|
||||
R_LOG_DEBUG ("Added tool response to messages: %s", cmd_output? cmd_output: "null");
|
||||
r_cons_printf (core->cons, Color_GREEN "Tool result: %s" Color_RESET, cmd_output? cmd_output: "no output");
|
||||
free (cmd_output);
|
||||
i++;
|
||||
#if 0
|
||||
// Only process the first valid tool call
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
|
||||
r2ai_print_run_end (cps, usage, n_run, max_runs);
|
||||
|
||||
// Check if we should continue with recursion
|
||||
if (!interrupted && message->tool_calls && message->n_tool_calls > 0) {
|
||||
R_LOG_DEBUG ("[DEBUG] Recursing to process_messages with n_run=%d", n_run + 1);
|
||||
process_messages (cps, messages, system_prompt, n_run + 1);
|
||||
if (!interrupted && message->tool_calls && r_list_length (message->tool_calls) > 0) {
|
||||
R_LOG_DEBUG ("Recursing to process_messages with n_run=%d", n_run + 1);
|
||||
process_messages (cps, messages, effective_prompt, n_run + 1);
|
||||
} else {
|
||||
R_LOG_DEBUG ("[DEBUG] Auto mode loop ending - no more tool calls or interrupted");
|
||||
R_LOG_DEBUG ("Auto mode loop ending - no more tool calls or interrupted");
|
||||
}
|
||||
} else {
|
||||
r2ai_print_run_end (cps, usage, n_run, max_runs);
|
||||
@@ -303,10 +402,10 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
R_IPI void cmd_r2ai_a(RCorePluginSession *cps, const char *user_query) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
r_cons_printf (core->cons, "\x1b[35m[DEBUG] cmd_r2ai_a called with query: %s\x1b[0m\n", user_query);
|
||||
r_cons_printf (core->cons, Color_CYAN "cmd_r2ai_a called with query: %s" Color_RESET "\n", user_query);
|
||||
r_cons_flush (core->cons);
|
||||
// Get conversation
|
||||
R2AI_Messages *messages = r2ai_conversation_get (state);
|
||||
RList *messages = r2ai_conversation_get (state);
|
||||
if (!messages) {
|
||||
R_LOG_ERROR ("Conversation not initialized");
|
||||
return;
|
||||
@@ -314,7 +413,7 @@ R_IPI void cmd_r2ai_a(RCorePluginSession *cps, const char *user_query) {
|
||||
|
||||
// Add user query to the conversation (no system prompt)
|
||||
// If this is the first message in a new conversation, clear previous history
|
||||
if (r_list_length (messages->messages) == 0 || r_config_get_b (core->config, "r2ai.auto.reset_on_query")) {
|
||||
if (r_list_empty (messages) || r_config_get_b (core->config, "r2ai.auto.reset_on_query")) {
|
||||
r2ai_msgs_clear (messages);
|
||||
}
|
||||
|
||||
@@ -325,31 +424,22 @@ R_IPI void cmd_r2ai_a(RCorePluginSession *cps, const char *user_query) {
|
||||
};
|
||||
r2ai_msgs_add (messages, &user_msg);
|
||||
|
||||
process_messages (cps, messages, NULL, 1);
|
||||
const char *system_prompt = NULL;
|
||||
if (!r_config_get_b (core->config, "r2ai.auto.think")) {
|
||||
system_prompt = r_str_newf ("/no_think\nReasoning: Low\n%s", Gprompt_auto);
|
||||
}
|
||||
process_messages (cps, messages, system_prompt, 1);
|
||||
if (system_prompt) {
|
||||
free ((char *)system_prompt);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to display content with length indication for long content
|
||||
static void print_content_with_length(RCore *core, const char *content, const char *empty_msg, bool always_show_length) {
|
||||
if (!content || *content == '\0') {
|
||||
r_cons_printf (core->cons, "%s\n", empty_msg? empty_msg: "<no content>");
|
||||
return;
|
||||
}
|
||||
|
||||
size_t content_len = strlen (content);
|
||||
const size_t max_display = 200;
|
||||
|
||||
if (content_len > max_display) {
|
||||
// Truncate long content and show length
|
||||
char *truncated = r_str_ndup (content, max_display);
|
||||
r_cons_printf (core->cons, "%s... \x1b[1;37m(length: %zu chars)\x1b[0m\n",
|
||||
truncated, content_len);
|
||||
free (truncated);
|
||||
} else if (always_show_length) {
|
||||
// Always show length for certain types (like tool responses)
|
||||
r_cons_printf (core->cons, "%s \x1b[1;37m(length: %zu chars)\x1b[0m\n",
|
||||
content, content_len);
|
||||
static void print_content_with_length(RCore *core, const char *content, const char *empty_msg) {
|
||||
if (R_STR_ISEMPTY (content)) {
|
||||
r_cons_println (core->cons, empty_msg? empty_msg: "<no content>");
|
||||
} else {
|
||||
r_cons_printf (core->cons, "%s\n", content);
|
||||
r_cons_println (core->cons, content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,8 +448,8 @@ R_IPI void cmd_r2ai_logs(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
// Get conversation
|
||||
R2AI_Messages *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_length (messages->messages) == 0) {
|
||||
RList *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_empty (messages)) {
|
||||
r_cons_printf (core->cons, "No conversation history available\n");
|
||||
return;
|
||||
}
|
||||
@@ -376,15 +466,17 @@ R_IPI void cmd_r2ai_logs(RCorePluginSession *cps) {
|
||||
|
||||
pj_a (pj);
|
||||
|
||||
for (int i = 0; i < r_list_length (messages->messages); i++) {
|
||||
const R2AI_Message *msg = r_list_get_n (messages->messages, i);
|
||||
RListIter *iter;
|
||||
const R2AI_Message *msg;
|
||||
r_list_foreach (messages, iter, msg) {
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "role", msg->role? msg->role: "unknown");
|
||||
pj_ks (pj, "content", msg->content? msg->content: "");
|
||||
if (msg->tool_calls && msg->n_tool_calls > 0) {
|
||||
if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
|
||||
pj_ka (pj, "tool_calls");
|
||||
for (int j = 0; j < msg->n_tool_calls; j++) {
|
||||
const R2AI_ToolCall *tc = &msg->tool_calls[j];
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (msg->tool_calls, iter, tc) {
|
||||
pj_o (pj);
|
||||
if (tc->name) {
|
||||
pj_ks (pj, "name", tc->name);
|
||||
@@ -408,28 +500,30 @@ R_IPI void cmd_r2ai_logs(RCorePluginSession *cps) {
|
||||
return;
|
||||
}
|
||||
|
||||
r_cons_printf (core->cons, "\x1b[1;34m[r2ai] Chat Logs (%d messages)\x1b[0m\n",
|
||||
r_list_length (messages->messages));
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_BLUE "[r2ai] Chat Logs (%d messages)" Color_RESET "\n",
|
||||
r_list_length (messages));
|
||||
|
||||
r_cons_printf (core->cons, "\x1b[1;33mNote: System prompt is applied automatically but not stored in history\x1b[0m\n\n");
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_YELLOW "Note: System prompt is applied automatically but not stored in history" Color_RESET "\n\n");
|
||||
|
||||
// Display each message in the conversation
|
||||
for (int i = 0; i < r_list_length (messages->messages); i++) {
|
||||
const R2AI_Message *msg = r_list_get_n (messages->messages, i);
|
||||
RListIter *iter;
|
||||
const R2AI_Message *msg;
|
||||
r_list_foreach (messages, iter, msg) {
|
||||
const char *role = msg->role;
|
||||
|
||||
// Format based on role
|
||||
if (!strcmp (role, "user")) {
|
||||
r_cons_printf (core->cons, "\x1b[1;32m[user]:\x1b[0m ");
|
||||
print_content_with_length (core, msg->content, "<no content>", false);
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_GREEN "[user]:" Color_RESET " ");
|
||||
print_content_with_length (core, msg->content, "<no content>");
|
||||
} else if (!strcmp (role, "assistant")) {
|
||||
r_cons_printf (core->cons, "\x1b[1;36m[assistant]:\x1b[0m ");
|
||||
print_content_with_length (core, msg->content, "<no content>", false);
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_CYAN "[assistant]:" Color_RESET " ");
|
||||
print_content_with_length (core, msg->content, "<no content>");
|
||||
// Show tool calls if present
|
||||
if (msg->tool_calls && msg->n_tool_calls > 0) {
|
||||
for (int j = 0; j < msg->n_tool_calls; j++) {
|
||||
const R2AI_ToolCall *tc = &msg->tool_calls[j];
|
||||
r_cons_printf (core->cons, " \x1b[1;35m[tool call]:\x1b[0m %s\n",
|
||||
if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (msg->tool_calls, iter, tc) {
|
||||
r_cons_printf (core->cons, " \x1b[1" Color_MAGENTA "[tool call]:" Color_RESET " %s\n",
|
||||
tc->name? tc->name: "<unnamed>");
|
||||
|
||||
if (tc->arguments) {
|
||||
@@ -438,17 +532,118 @@ R_IPI void cmd_r2ai_logs(RCorePluginSession *cps) {
|
||||
}
|
||||
}
|
||||
} else if (!strcmp (role, "tool")) {
|
||||
r_cons_printf (core->cons, "\x1b[1;35m[tool]:\x1b[0m ");
|
||||
print_content_with_length (core, msg->content, "<no result>", true);
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_MAGENTA "[tool]:" Color_RESET " ");
|
||||
print_content_with_length (core, msg->content, "<no result>");
|
||||
|
||||
// Don't show the tool call ID as requested
|
||||
} else {
|
||||
// Other roles (system, etc.)
|
||||
r_cons_printf (core->cons, "\x1b[1;37m[%s]:\x1b[0m ", role);
|
||||
print_content_with_length (core, msg->content, "<no content>", false);
|
||||
r_cons_printf (core->cons, "\x1b[1" Color_WHITE "[%s]:" Color_RESET " ", role);
|
||||
print_content_with_length (core, msg->content, "<no content>");
|
||||
}
|
||||
|
||||
r_cons_newline (core->cons);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to format conversation log as string
|
||||
static char *format_conversation_log(RList *messages) {
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
r_strbuf_append (sb, "Conversation Log:\n");
|
||||
|
||||
RListIter *iter;
|
||||
const R2AI_Message *msg;
|
||||
r_list_foreach (messages, iter, msg) {
|
||||
const char *role = msg->role;
|
||||
|
||||
if (!strcmp (role, "user")) {
|
||||
r_strbuf_appendf (sb, "[user]: %s\n", msg->content? msg->content: "<no content>");
|
||||
} else if (!strcmp (role, "assistant")) {
|
||||
r_strbuf_appendf (sb, "[assistant]: %s\n", msg->content? msg->content: "<no content>");
|
||||
// Include tool calls if present
|
||||
if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
|
||||
RListIter *iter_tc;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (msg->tool_calls, iter_tc, tc) {
|
||||
r_strbuf_appendf (sb, " [tool call]: %s\n", tc->name? tc->name: "<unnamed>");
|
||||
if (tc->arguments) {
|
||||
r_strbuf_appendf (sb, " %s\n", tc->arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!strcmp (role, "tool")) {
|
||||
r_strbuf_appendf (sb, "[tool]: %s\n", msg->content? msg->content: "<no result>");
|
||||
} else {
|
||||
r_strbuf_appendf (sb, "[%s]: %s\n", role, msg->content? msg->content: "<no content>");
|
||||
}
|
||||
}
|
||||
|
||||
return r_strbuf_drain (sb);
|
||||
}
|
||||
|
||||
// Helper function to process conversation with LLM and handle result
|
||||
static void process_conversation_with_llm(RCorePluginSession *cps, bool compact) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
// Get conversation
|
||||
RList *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_empty (messages)) {
|
||||
r_cons_printf (core->cons, "No conversation history available\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Format conversation as string
|
||||
char *log_str = format_conversation_log (messages);
|
||||
|
||||
// Load the compact prompt from file
|
||||
char *prompt_text = r2ai_load_prompt_text (core, "compact");
|
||||
if (!prompt_text) {
|
||||
// Fallback to hardcoded prompt
|
||||
prompt_text = strdup (compact
|
||||
? "Create a compact summary of this conversation that preserves all key information, insights, and context for future reference."
|
||||
"Focus on essential details about the binary analysis, tools used, findings, and any important conclusions."
|
||||
: "mai create a summary of all the information retrieved from the binary that is relevant for future work");
|
||||
}
|
||||
|
||||
// Combine log and prompt
|
||||
char *full_input = r_str_newf ("%s\n\n%s", log_str, prompt_text);
|
||||
|
||||
char *error = NULL;
|
||||
char *res = r2ai (cps, (R2AIArgs){ .input = full_input, .error = &error, .dorag = false });
|
||||
|
||||
free (log_str);
|
||||
free (full_input);
|
||||
free (prompt_text);
|
||||
|
||||
if (error) {
|
||||
R_LOG_ERROR ("%s", error);
|
||||
free (error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (res) {
|
||||
if (compact) {
|
||||
// Clear the conversation and add summary as system message
|
||||
r2ai_msgs_clear (messages);
|
||||
R2AI_Message summary_msg = {
|
||||
.role = "system",
|
||||
.content = res
|
||||
};
|
||||
r2ai_msgs_add (messages, &summary_msg);
|
||||
r_cons_printf (core->cons, "Conversation compacted. Summary added to history.\n");
|
||||
} else {
|
||||
// Just print the result
|
||||
r_cons_printf (core->cons, "%s\n", res);
|
||||
}
|
||||
free (res);
|
||||
}
|
||||
}
|
||||
|
||||
R_IPI void cmd_r2ai_c(RCorePluginSession *cps) {
|
||||
process_conversation_with_llm (cps, true);
|
||||
}
|
||||
|
||||
R_IPI void cmd_r2ai_lr(RCorePluginSession *cps) {
|
||||
process_conversation_with_llm (cps, false);
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
/* r2ai - Copyright 2023-2025 pancake, dnakov */
|
||||
|
||||
#include "r2ai.h"
|
||||
|
||||
/* Gemini API implementation */
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_gemini(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
args.model = r_config_get (core->config, "r2ai.model");
|
||||
|
||||
const char *base_url = r2ai_get_provider_url (core, args.provider);
|
||||
const char *model_name = args.model && strstr (args.model, "gemini")? args.model: "gemini-2.0-flash-exp";
|
||||
char **error = args.error;
|
||||
|
||||
// Create a temp conversation to include the system prompt and the rest of the messages
|
||||
RList *temp_msgs = r2ai_msgs_new ();
|
||||
if (!temp_msgs) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to create temporary messages array");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get system prompt
|
||||
const char *system_prompt = NULL;
|
||||
if (R_STR_ISNOTEMPTY (args.system_prompt)) {
|
||||
system_prompt = args.system_prompt;
|
||||
} else {
|
||||
system_prompt = r_config_get (core->config, "r2ai.system");
|
||||
}
|
||||
|
||||
if (args.messages) {
|
||||
RListIter *iter;
|
||||
R2AI_Message *msg;
|
||||
r_list_foreach (args.messages, iter, msg) {
|
||||
r2ai_msgs_add (temp_msgs, msg);
|
||||
}
|
||||
} else {
|
||||
R_LOG_WARN ("No messages");
|
||||
}
|
||||
|
||||
if (error) {
|
||||
*error = NULL;
|
||||
}
|
||||
|
||||
// Setup headers for Gemini API
|
||||
const char **headers = NULL;
|
||||
char *auth_header = NULL;
|
||||
char *api_key_header = NULL;
|
||||
|
||||
if (R_STR_ISNOTEMPTY (args.api_key)) {
|
||||
static const char *static_headers[] = { NULL, NULL, NULL };
|
||||
headers = static_headers;
|
||||
headers[0] = "Content-Type: application/json";
|
||||
|
||||
// Check if it's an OAuth token (starts with "ya29.")
|
||||
if (r_str_startswith (args.api_key, "ya29.")) {
|
||||
auth_header = r_str_newf ("Authorization: Bearer %s", args.api_key);
|
||||
headers[1] = auth_header;
|
||||
} else {
|
||||
api_key_header = r_str_newf ("x-goog-api-key: %s", args.api_key);
|
||||
headers[1] = api_key_header;
|
||||
}
|
||||
}
|
||||
|
||||
// Build Gemini API URL
|
||||
char *gemini_url = r_str_newf ("%s/models/%s:generateContent?key=%s",
|
||||
base_url, model_name, args.api_key);
|
||||
|
||||
// Create Gemini-style request JSON
|
||||
PJ *pj = pj_new ();
|
||||
pj_o (pj);
|
||||
|
||||
// Build contents array
|
||||
pj_ka (pj, "contents");
|
||||
RListIter *iter;
|
||||
R2AI_Message *msg;
|
||||
int msg_index = 0;
|
||||
r_list_foreach (temp_msgs, iter, msg) {
|
||||
pj_o (pj);
|
||||
// Gemini uses "model" for assistant, "user" for user
|
||||
const char *role = "user";
|
||||
if (strcmp (msg->role, "assistant") == 0) {
|
||||
role = "model";
|
||||
}
|
||||
pj_ks (pj, "role", role);
|
||||
pj_ka (pj, "parts");
|
||||
pj_o (pj);
|
||||
// Prepend system prompt to first user message
|
||||
char *content = NULL;
|
||||
if (system_prompt && strcmp (msg->role, "user") == 0 && msg_index == 0) {
|
||||
content = r_str_newf ("System: %s\n\n%s", system_prompt, msg->content? msg->content: "");
|
||||
} else {
|
||||
content = msg->content? strdup (msg->content): strdup ("");
|
||||
}
|
||||
pj_ks (pj, "text", content);
|
||||
free (content);
|
||||
pj_end (pj); // end parts object
|
||||
pj_end (pj); // end parts array
|
||||
pj_end (pj); // end content object
|
||||
msg_index++;
|
||||
}
|
||||
pj_end (pj); // end contents array
|
||||
|
||||
// Add generation config
|
||||
pj_ko (pj, "generationConfig");
|
||||
if (args.max_tokens > 0) {
|
||||
pj_kn (pj, "maxOutputTokens", args.max_tokens);
|
||||
}
|
||||
if (args.temperature > 0) {
|
||||
pj_kd (pj, "temperature", args.temperature);
|
||||
}
|
||||
pj_end (pj); // end generationConfig
|
||||
|
||||
pj_end (pj); // end root object
|
||||
|
||||
char *request_json = pj_drain (pj);
|
||||
|
||||
// Debug: save request
|
||||
char *tmpdir = r_file_tmpdir ();
|
||||
char *req_path = r_str_newf ("%s" R_SYS_DIR "r2ai_gemini_request.json", tmpdir);
|
||||
r_file_dump (req_path, (const ut8 *)request_json, strlen (request_json), 0);
|
||||
free (req_path);
|
||||
free (tmpdir);
|
||||
|
||||
// Make the API call
|
||||
char *response = NULL;
|
||||
int code = 0;
|
||||
response = r2ai_http_post (core, gemini_url, headers, request_json, &code, NULL);
|
||||
|
||||
free (request_json);
|
||||
free (gemini_url);
|
||||
free (auth_header);
|
||||
free (api_key_header);
|
||||
|
||||
if (code != 200) {
|
||||
R_LOG_ERROR ("Gemini API error %d", code);
|
||||
if (response) {
|
||||
R_LOG_ERROR ("Gemini API error response: %s", response);
|
||||
}
|
||||
free (response);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Debug: save response
|
||||
tmpdir = r_file_tmpdir ();
|
||||
char *res_path = r_str_newf ("%s" R_SYS_DIR "r2ai_gemini_response.json", tmpdir);
|
||||
r_file_dump (res_path, (const ut8 *)response, strlen (response), 0);
|
||||
free (res_path);
|
||||
free (tmpdir);
|
||||
|
||||
// Parse Gemini response
|
||||
RJson *jres = r_json_parse (response);
|
||||
if (!jres) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to parse Gemini response JSON");
|
||||
}
|
||||
free (response);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Extract response content
|
||||
const RJson *candidates = r_json_get (jres, "candidates");
|
||||
if (!candidates || candidates->type != R_JSON_ARRAY || candidates->children.count == 0) {
|
||||
r_json_free (jres);
|
||||
free (response);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const RJson *candidate = r_json_item (candidates, 0);
|
||||
const RJson *content = r_json_get (candidate, "content");
|
||||
const RJson *parts = r_json_get (content, "parts");
|
||||
|
||||
if (!parts || parts->type != R_JSON_ARRAY || parts->children.count == 0) {
|
||||
r_json_free (jres);
|
||||
free (response);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const RJson *part = r_json_item (parts, 0);
|
||||
const RJson *text = r_json_get (part, "text");
|
||||
|
||||
char *response_text = NULL;
|
||||
if (text && text->type == R_JSON_STRING) {
|
||||
response_text = strdup (text->str_value);
|
||||
}
|
||||
|
||||
r_json_free (jres);
|
||||
free (response);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
|
||||
if (!response_text) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Create response
|
||||
R2AI_Message *message = R_NEW0 (R2AI_Message);
|
||||
R2AI_Usage *usage = R_NEW0 (R2AI_Usage);
|
||||
|
||||
if (message) {
|
||||
message->role = strdup ("assistant");
|
||||
message->content = response_text;
|
||||
}
|
||||
|
||||
R2AI_ChatResponse *result = R_NEW0 (R2AI_ChatResponse);
|
||||
if (result) {
|
||||
result->message = message;
|
||||
result->usage = usage;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
/* Copyright r2ai - 2023-2025 - pancake */
|
||||
|
||||
#define _GNU_SOURCE
|
||||
// #define _POSIX_C_SOURCE 200809L
|
||||
#include <signal.h>
|
||||
#include "r2ai.h"
|
||||
|
||||
// Global flag for tracking interrupt status
|
||||
static volatile sig_atomic_t r2ai_http_interrupted = 0;
|
||||
|
||||
// Helper function to get HTTP configuration with defaults
|
||||
static R2AI_HttpConfig get_http_config(RCore *core) {
|
||||
R2AI_HttpConfig config = { 120, 10, 30 };
|
||||
if (core) {
|
||||
int t = r_config_get_i (core->config, "r2ai.http.timeout");
|
||||
if (t > 0) {
|
||||
config.timeout = t;
|
||||
}
|
||||
int mr = r_config_get_i (core->config, "r2ai.http.max_retries");
|
||||
if (mr >= 0) {
|
||||
config.max_retries = mr;
|
||||
}
|
||||
int mb = r_config_get_i (core->config, "r2ai.http.max_backoff");
|
||||
if (mb > 0) {
|
||||
config.max_backoff = mb;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
#if R2__UNIX__
|
||||
// Signal handler for timeout (SIGALRM)
|
||||
static void r2ai_http_sigint_handler(int sig) {
|
||||
if (sig == SIGALRM) {
|
||||
r2ai_http_interrupted = 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Portable break callback for r_cons_break (handles SIGINT)
|
||||
static void r2ai_http_break_callback(void *user) {
|
||||
(void)user;
|
||||
r2ai_http_interrupted = 1;
|
||||
}
|
||||
|
||||
// Function pointer type for HTTP request implementations
|
||||
typedef HttpResponse(*HttpRequestFunc)(const HTTPRequest *request);
|
||||
|
||||
// Simple retry sleep
|
||||
static void sleep_retry(int retry_count, int max_sleep_seconds) {
|
||||
int delay_seconds = retry_count + 1; // sleep 1, 2, 3, ... seconds
|
||||
if (delay_seconds > max_sleep_seconds) {
|
||||
delay_seconds = max_sleep_seconds;
|
||||
}
|
||||
r_sys_sleep (delay_seconds);
|
||||
}
|
||||
|
||||
// Generic HTTP request with retry logic
|
||||
static HttpResponse r2ai_http_request_with_retry(HttpRequestFunc func, const HTTPRequest *request, RCore *core) {
|
||||
// Install portable break handler for interruption
|
||||
if (core && core->cons) {
|
||||
r_cons_break_push (core->cons, r2ai_http_break_callback, NULL);
|
||||
}
|
||||
|
||||
// Reset interrupt flag
|
||||
r2ai_http_interrupted = 0;
|
||||
|
||||
#if R2__UNIX__
|
||||
// Ignore SIGWINCH to prevent window resize from interrupting child processes
|
||||
struct sigaction old_winch_act;
|
||||
sigaction (SIGWINCH, &(struct sigaction){ .sa_handler = SIG_IGN }, &old_winch_act);
|
||||
#endif
|
||||
|
||||
// Retry loop
|
||||
HttpResponse result = { 0 };
|
||||
int retry_count = 0;
|
||||
bool success = false;
|
||||
|
||||
while (!success && retry_count <= request->config.max_retries && !r2ai_http_interrupted) {
|
||||
result = func (request);
|
||||
|
||||
// Check if we were interrupted
|
||||
if (r2ai_http_interrupted) {
|
||||
R_LOG_DEBUG ("HTTP request was interrupted by user");
|
||||
free (result.body);
|
||||
result.body = NULL;
|
||||
result.code = -1;
|
||||
break; // Exit the retry loop
|
||||
}
|
||||
|
||||
// Check for rate limiting or server errors (429, 500, 502, 503, 504)
|
||||
if (result.code > 0) {
|
||||
if (result.code == 429 || (result.code >= 500 && result.code < 600)) {
|
||||
R_LOG_WARN ("Server returned %d response code", result.code);
|
||||
free (result.body);
|
||||
result.body = NULL;
|
||||
result.code = 0;
|
||||
|
||||
if (retry_count < request->config.max_retries) {
|
||||
retry_count++;
|
||||
R_LOG_INFO ("Retrying request (%d/%d) after error...", retry_count, request->config.max_retries);
|
||||
sleep_retry (retry_count, request->config.max_backoff);
|
||||
continue;
|
||||
}
|
||||
result.code = -1;
|
||||
break; // Exit the retry loop after max retries
|
||||
}
|
||||
}
|
||||
|
||||
// Check for other failures
|
||||
if (result.code <= 0 || !result.body) {
|
||||
free (result.body);
|
||||
result.body = NULL;
|
||||
result.code = -1;
|
||||
if (retry_count < request->config.max_retries) {
|
||||
retry_count++;
|
||||
R_LOG_INFO ("Retrying request (%d/%d) after failure...", retry_count, request->config.max_retries);
|
||||
sleep_retry (retry_count, request->config.max_backoff);
|
||||
continue;
|
||||
}
|
||||
break; // Exit the retry loop after max retries
|
||||
}
|
||||
success = true;
|
||||
}
|
||||
|
||||
// Restore the original break handler
|
||||
if (core && core->cons) {
|
||||
r_cons_break_pop (core->cons);
|
||||
}
|
||||
|
||||
#if R2__UNIX__
|
||||
// Restore SIGWINCH handler
|
||||
sigaction (SIGWINCH, &old_winch_act, NULL);
|
||||
#endif
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#include "http/libcurl.inc.c"
|
||||
#include "http/pwsh.inc.c"
|
||||
#include "http/curl.inc.c"
|
||||
#include "http/r2.inc.c"
|
||||
|
||||
static HttpRequestFunc select_backend(const char *backend, bool is_post) {
|
||||
HttpRequestFunc func = NULL;
|
||||
if (!strcmp (backend, "auto")) {
|
||||
#if R2__WINDOWS__
|
||||
// TODO: we should use pwsh or r2 by default, but for not it seems the only backend that works on Windows is r2curl+system
|
||||
r_sys_setenv ("R2_CURL", "1");
|
||||
backend = "system";
|
||||
#elif USE_LIBCURL && HAVE_LIBCURL
|
||||
backend = "libcurl";
|
||||
#else
|
||||
backend = "system";
|
||||
#endif
|
||||
R_LOG_DEBUG ("Auto-selected %s backend", backend);
|
||||
}
|
||||
if (!strcmp (backend, "system") || !strcmp (backend, "curl")) {
|
||||
func = is_post? system_curl_post: system_curl_get;
|
||||
} else if (!strcmp (backend, "libcurl")) {
|
||||
#if USE_LIBCURL && HAVE_LIBCURL
|
||||
func = is_post? curl_http_post: curl_http_get;
|
||||
#else
|
||||
R_LOG_WARN ("LibCurl requested but not available, falling back to socket implementation");
|
||||
func = is_post? socket_http_post_with_interrupt: socket_http_get_with_interrupt;
|
||||
#endif
|
||||
} else if (!strcmp (backend, "socket")) {
|
||||
func = is_post? socket_http_post_with_interrupt: socket_http_get_with_interrupt;
|
||||
} else if (!strcmp (backend, "pwsh") || !strcmp (backend, "powershell")) {
|
||||
#if R2__WINDOWS__
|
||||
func = is_post? windows_http_post: windows_http_get;
|
||||
#else
|
||||
R_LOG_WARN ("powershell is only available on Windows");
|
||||
func = is_post? socket_http_post_with_interrupt: socket_http_get_with_interrupt;
|
||||
#endif
|
||||
} else if (!strcmp (backend, "r2curl")) {
|
||||
r_sys_setenv ("R2_CURL", "1");
|
||||
func = is_post? socket_http_post_with_interrupt: socket_http_get_with_interrupt;
|
||||
}
|
||||
return func;
|
||||
}
|
||||
|
||||
// Generic HTTP request function that handles backend selection
|
||||
static HttpResponse r2ai_http_request(const char *method, RCore *core, const char *url, const char *headers[], const char *data) {
|
||||
(void)method;
|
||||
bool is_post = (data != NULL);
|
||||
|
||||
HTTPRequest request = {
|
||||
.config = get_http_config (core),
|
||||
.url = url,
|
||||
.data = data,
|
||||
.headers = headers
|
||||
};
|
||||
|
||||
const char *backend = r_config_get (core->config, "r2ai.http.backend");
|
||||
HttpRequestFunc func = select_backend (backend, is_post);
|
||||
if (func) {
|
||||
return r2ai_http_request_with_retry (func, &request, core);
|
||||
}
|
||||
R_LOG_ERROR ("Cannot find a valid http backend");
|
||||
return (HttpResponse){ .body = NULL, .code = -1, .length = 0 };
|
||||
}
|
||||
|
||||
R_API char *r2ai_http_post(RCore *core, const char *url, const char *headers[], const char *data, int *code, int *rlen) {
|
||||
HttpResponse response = r2ai_http_request ("POST", core, url, headers, data);
|
||||
if (response.code <= 0) {
|
||||
free (response.body);
|
||||
if (code) {
|
||||
*code = response.code;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
if (code) {
|
||||
*code = response.code;
|
||||
}
|
||||
if (rlen) {
|
||||
*rlen = response.length;
|
||||
}
|
||||
return response.body;
|
||||
}
|
||||
|
||||
R_API char *r2ai_http_get(RCore *core, const char *url, const char *headers[], int *code, int *rlen) {
|
||||
HttpResponse response = r2ai_http_request ("GET", core, url, headers, NULL);
|
||||
if (response.code <= 0) {
|
||||
if (response.body) {
|
||||
R_FREE (response.body);
|
||||
}
|
||||
if (code) {
|
||||
*code = response.code;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
if (code) {
|
||||
*code = response.code;
|
||||
}
|
||||
if (rlen) {
|
||||
*rlen = response.length;
|
||||
}
|
||||
return response.body;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Helper to escape double quotes for cmd double-quoted strings
|
||||
static char *escape_cmd_double_quotes(const char *str) {
|
||||
if (!str) {
|
||||
return NULL;
|
||||
}
|
||||
size_t len = strlen (str);
|
||||
size_t count = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (str[i] == '"') {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count == 0) {
|
||||
return strdup (str);
|
||||
}
|
||||
char *escaped = malloc (len + count + 1);
|
||||
if (!escaped) {
|
||||
return NULL;
|
||||
}
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (str[i] == '"') {
|
||||
escaped[j++] = '\\';
|
||||
escaped[j++] = '"';
|
||||
} else {
|
||||
escaped[j++] = str[i];
|
||||
}
|
||||
}
|
||||
escaped[j] = '\0';
|
||||
return escaped;
|
||||
}
|
||||
|
||||
static HttpResponse build_and_execute_curl(const char *cmd_start, const HTTPRequest *request, const char *input_data) {
|
||||
HttpResponse error = { .code = -1 };
|
||||
int timeout = request->config.timeout;
|
||||
if (!request->url) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Compose curl command
|
||||
RStrBuf *cmd = r_strbuf_new (cmd_start);
|
||||
|
||||
// Add timeout
|
||||
r_strbuf_appendf (cmd, " --connect-timeout %d --max-time %d", 10, timeout);
|
||||
|
||||
// Add headers
|
||||
if (request->headers) {
|
||||
for (int i = 0; request->headers[i] != NULL; i++) {
|
||||
char *escaped_header = escape_cmd_double_quotes (request->headers[i]);
|
||||
r_strbuf_appendf (cmd, " -H \"%s\"", escaped_header);
|
||||
free (escaped_header);
|
||||
}
|
||||
}
|
||||
char *escaped_url = escape_cmd_double_quotes (request->url);
|
||||
r_strbuf_appendf (cmd, " -w \"\\n%%{http_code}\" \"%s\"", escaped_url);
|
||||
free (escaped_url);
|
||||
char *cmd_str = r_strbuf_drain (cmd);
|
||||
R_LOG_DEBUG ("Running system curl: %s", cmd_str);
|
||||
char *response = r_sys_cmd_str (cmd_str, input_data, NULL);
|
||||
|
||||
free (cmd_str);
|
||||
|
||||
if (!response) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Parse the response: body\ncode
|
||||
char *last_nl = (char *)r_str_rchr (response, NULL, '\n');
|
||||
int code = -1;
|
||||
char *body = response;
|
||||
if (last_nl) {
|
||||
*last_nl = '\0';
|
||||
code = atoi (last_nl + 1);
|
||||
if (code == 0) {
|
||||
code = -1; // Invalid code
|
||||
}
|
||||
} else {
|
||||
// No newline, assume 200 if response
|
||||
code = 200;
|
||||
}
|
||||
|
||||
return (HttpResponse){ .body = body, .code = code, .length = strlen (body) };
|
||||
}
|
||||
|
||||
HttpResponse system_curl_get(const HTTPRequest *request) {
|
||||
R_LOG_DEBUG ("Using system curl GET");
|
||||
return build_and_execute_curl ("curl -s", request, NULL);
|
||||
}
|
||||
|
||||
HttpResponse system_curl_post(const HTTPRequest *request) {
|
||||
HttpResponse error = { .code = -1 };
|
||||
if (!request->url || !request->data) {
|
||||
return error;
|
||||
}
|
||||
|
||||
R_LOG_DEBUG ("Using system curl POST without files");
|
||||
HttpResponse res = build_and_execute_curl ("curl -s -X POST -d @-", request, request->data);
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
#if HAVE_LIBCURL
|
||||
#include <curl/curl.h>
|
||||
|
||||
// Struct to store response data
|
||||
typedef struct {
|
||||
char *data;
|
||||
size_t size;
|
||||
} CurlResponse;
|
||||
|
||||
// Callback function for curl to write response data
|
||||
static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) {
|
||||
// Check if we've been interrupted
|
||||
if (r2ai_http_interrupted) {
|
||||
return 0; // Return 0 to abort the transfer
|
||||
}
|
||||
|
||||
size_t realsize = size * nmemb;
|
||||
CurlResponse *mem = (CurlResponse *)userp;
|
||||
|
||||
char *ptr = realloc (mem->data, mem->size + realsize + 1);
|
||||
if (!ptr) {
|
||||
R_LOG_ERROR ("Not enough memory for curl response");
|
||||
return 0; // Out of memory
|
||||
}
|
||||
|
||||
mem->data = ptr;
|
||||
memcpy (&(mem->data[mem->size]), contents, realsize);
|
||||
mem->size += realsize;
|
||||
mem->data[mem->size] = 0;
|
||||
|
||||
return realsize;
|
||||
}
|
||||
|
||||
// Curl progress callback function to handle interrupts
|
||||
static int progress_callback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
|
||||
return r2ai_http_interrupted? 1: 0;
|
||||
}
|
||||
|
||||
static HttpResponse curl_http_request(const HTTPRequest *request, bool is_post) {
|
||||
HttpResponse error = { .code = -1 };
|
||||
if (!request->url || (is_post && !request->data)) {
|
||||
return error;
|
||||
}
|
||||
|
||||
CURL *curl;
|
||||
CURLcode res;
|
||||
struct curl_slist *curl_headers = NULL;
|
||||
CurlResponse response = { 0 };
|
||||
|
||||
// Initialize response
|
||||
response.data = malloc (1);
|
||||
if (!response.data) {
|
||||
return error;
|
||||
}
|
||||
response.data[0] = '\0';
|
||||
response.size = 0;
|
||||
|
||||
curl = curl_easy_init ();
|
||||
if (!curl) {
|
||||
free (response.data);
|
||||
return error;
|
||||
}
|
||||
|
||||
// Set URL
|
||||
curl_easy_setopt (curl, CURLOPT_URL, request->url);
|
||||
|
||||
// Set method-specific options
|
||||
if (is_post) {
|
||||
curl_easy_setopt (curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt (curl, CURLOPT_POSTFIELDS, request->data);
|
||||
curl_easy_setopt (curl, CURLOPT_POSTFIELDSIZE, (long)strlen (request->data));
|
||||
} else {
|
||||
curl_easy_setopt (curl, CURLOPT_HTTPGET, 1L);
|
||||
}
|
||||
|
||||
// Set headers if provided
|
||||
if (request->headers) {
|
||||
for (int i = 0; request->headers[i] != NULL; i++) {
|
||||
curl_headers = curl_slist_append (curl_headers, request->headers[i]);
|
||||
}
|
||||
curl_easy_setopt (curl, CURLOPT_HTTPHEADER, curl_headers);
|
||||
}
|
||||
|
||||
// Set write callback
|
||||
curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt (curl, CURLOPT_WRITEDATA, (void *)&response);
|
||||
|
||||
// Set progress callback to handle interrupts
|
||||
curl_easy_setopt (curl, CURLOPT_NOPROGRESS, 0L);
|
||||
curl_easy_setopt (curl, CURLOPT_XFERINFOFUNCTION, progress_callback);
|
||||
curl_easy_setopt (curl, CURLOPT_XFERINFODATA, NULL);
|
||||
|
||||
// Set timeout options
|
||||
curl_easy_setopt (curl, CURLOPT_CONNECTTIMEOUT, 10L); // 10 seconds connect timeout
|
||||
curl_easy_setopt (curl, CURLOPT_TIMEOUT, (long)request->config.timeout); // Use configured timeout
|
||||
|
||||
// Follow redirects
|
||||
curl_easy_setopt (curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
|
||||
// Perform the request
|
||||
res = curl_easy_perform (curl);
|
||||
|
||||
// Check for interruption
|
||||
if (r2ai_http_interrupted) {
|
||||
R_LOG_DEBUG ("HTTP request was interrupted by user");
|
||||
free (response.data);
|
||||
curl_slist_free_all (curl_headers);
|
||||
curl_easy_cleanup (curl);
|
||||
return error;
|
||||
}
|
||||
|
||||
// Get response code
|
||||
long http_code;
|
||||
curl_easy_getinfo (curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
|
||||
// Check for errors
|
||||
if (res != CURLE_OK) {
|
||||
R_LOG_ERROR ("curl_easy_perform () failed: %s", curl_easy_strerror (res));
|
||||
free (response.data);
|
||||
curl_slist_free_all (curl_headers);
|
||||
curl_easy_cleanup (curl);
|
||||
return error;
|
||||
}
|
||||
|
||||
// If we get here, the request was successful
|
||||
// Cleanup
|
||||
curl_slist_free_all (curl_headers);
|
||||
curl_easy_cleanup (curl);
|
||||
|
||||
return (HttpResponse){ .body = response.data, .code = (int)http_code, .length = response.size };
|
||||
}
|
||||
|
||||
HttpResponse curl_http_get(const HTTPRequest *request) {
|
||||
return curl_http_request (request, false);
|
||||
}
|
||||
|
||||
HttpResponse curl_http_post(const HTTPRequest *request) {
|
||||
return curl_http_request (request, true);
|
||||
}
|
||||
#endif // HAVE_LIBCURL
|
||||
@@ -0,0 +1,118 @@
|
||||
#if R2__WINDOWS__
|
||||
|
||||
// Helper to escape single quotes for PowerShell single-quoted strings
|
||||
static char *escape_single_quotes(const char *str) {
|
||||
if (!str) {
|
||||
return NULL;
|
||||
}
|
||||
size_t len = strlen (str);
|
||||
size_t count = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (str[i] == '\'') {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count == 0) {
|
||||
return strdup (str);
|
||||
}
|
||||
char *escaped = malloc (len + count + 1);
|
||||
if (!escaped) {
|
||||
return NULL;
|
||||
}
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (str[i] == '\'') {
|
||||
escaped[j++] = '\'';
|
||||
escaped[j++] = '\'';
|
||||
} else {
|
||||
escaped[j++] = str[i];
|
||||
}
|
||||
}
|
||||
escaped[j] = '\0';
|
||||
return escaped;
|
||||
}
|
||||
|
||||
// Helper to append headers to PowerShell command
|
||||
static void append_headers_to_cmd(RStrBuf *cmd, const char *const *headers) {
|
||||
r_strbuf_appendf (cmd, "$headers=@{");
|
||||
if (headers) {
|
||||
for (int i = 0; headers[i]; i++) {
|
||||
char *header = strdup (headers[i]);
|
||||
char *colon = strchr (header, ':');
|
||||
if (colon) {
|
||||
*colon = '\0';
|
||||
char *key = r_str_trim_dup (header);
|
||||
char *value = r_str_trim_dup (colon + 1);
|
||||
char *escaped_key = escape_single_quotes (key);
|
||||
char *escaped_value = escape_single_quotes (value);
|
||||
r_strbuf_appendf (cmd, "'%s'='%s';", escaped_key, escaped_value);
|
||||
free (escaped_key);
|
||||
free (escaped_value);
|
||||
free (key);
|
||||
free (value);
|
||||
}
|
||||
free (header);
|
||||
}
|
||||
}
|
||||
r_strbuf_appendf (cmd, "};");
|
||||
}
|
||||
|
||||
// Helper to parse PowerShell response
|
||||
static HttpResponse parse_powershell_response(char *full_response) {
|
||||
if (full_response) {
|
||||
char *newline = strchr (full_response, '\n');
|
||||
if (newline) {
|
||||
*newline = '\0';
|
||||
int code = atoi (full_response);
|
||||
char *body = newline + 1;
|
||||
char *body_copy = strdup (body);
|
||||
size_t length = strlen (body);
|
||||
free (full_response);
|
||||
return (HttpResponse){ .body = body_copy, .code = code, .length = length };
|
||||
} else {
|
||||
return (HttpResponse){ .body = full_response, .code = 200, .length = strlen (full_response) };
|
||||
}
|
||||
}
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
/**
|
||||
* Windows-specific HTTP POST using PowerShell
|
||||
*/
|
||||
HttpResponse windows_http_post(const HTTPRequest *request) {
|
||||
const char *url = request->url;
|
||||
const char *const *headers = request->headers;
|
||||
const char *data = request->data;
|
||||
int timeout = request->config.timeout;
|
||||
char *escaped_url = escape_single_quotes (url);
|
||||
char *escaped_data = escape_single_quotes (data);
|
||||
RStrBuf *cmd = r_strbuf_new ("powershell -Command \"");
|
||||
append_headers_to_cmd (cmd, headers);
|
||||
r_strbuf_appendf (cmd, "$body='%s';", escaped_data);
|
||||
r_strbuf_appendf (cmd, "try{$r=Invoke-WebRequest -Method Post -Uri '%s' -Headers $headers -Body $body -TimeoutSec %d;", escaped_url, timeout);
|
||||
r_strbuf_appendf (cmd, "Write-Host $r.StatusCode;$r.Content}catch{Write-Host 0;$_.Exception.Message}\"");
|
||||
char *cmd_str = r_strbuf_drain (cmd);
|
||||
R_LOG_DEBUG ("Running PowerShell: %s", cmd_str);
|
||||
char *full_response = r_sys_cmd_str (cmd_str, NULL, NULL);
|
||||
free (cmd_str);
|
||||
return parse_powershell_response (full_response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows-specific HTTP GET using PowerShell
|
||||
*/
|
||||
HttpResponse windows_http_get(const HTTPRequest *request) {
|
||||
const char *url = request->url;
|
||||
const char *const *headers = request->headers;
|
||||
int timeout = request->config.timeout;
|
||||
char *escaped_url = escape_single_quotes (url);
|
||||
RStrBuf *cmd = r_strbuf_new ("powershell -Command \"");
|
||||
append_headers_to_cmd (cmd, headers);
|
||||
r_strbuf_appendf (cmd, "try{$r=Invoke-WebRequest -Method Get -Uri '%s' -Headers $headers -TimeoutSec %d;", escaped_url, timeout);
|
||||
r_strbuf_appendf (cmd, "Write-Host $r.StatusCode;$r.Content}catch{Write-Host 0;$_.Exception.Message}\"");
|
||||
char *cmd_str = r_strbuf_drain (cmd);
|
||||
R_LOG_DEBUG ("Running PowerShell: %s", cmd_str);
|
||||
char *full_response = r_sys_cmd_str (cmd_str, NULL, NULL);
|
||||
free (cmd_str);
|
||||
return parse_powershell_response (full_response);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,49 @@
|
||||
extern volatile sig_atomic_t r2ai_http_interrupted;
|
||||
extern void r2ai_http_sigint_handler(int sig);
|
||||
|
||||
// Socket implementation with interrupt handling and retry logic
|
||||
static HttpResponse socket_http_post_with_interrupt(const HTTPRequest *request) {
|
||||
#if R2__WINDOWS__
|
||||
r_sys_setenv ("R2_CURL", "1");
|
||||
#endif
|
||||
#if R2__UNIX__
|
||||
signal (SIGALRM, r2ai_http_sigint_handler);
|
||||
alarm (request->config.timeout); // Use configured timeout
|
||||
#endif
|
||||
int code = 0;
|
||||
int rlen = 0;
|
||||
char *result = r_socket_http_post (request->url, (const char **)request->headers, request->data, &code, &rlen);
|
||||
#if R2__UNIX__
|
||||
alarm (0);
|
||||
#endif
|
||||
if (r2ai_http_interrupted) {
|
||||
R_LOG_DEBUG ("HTTP request was interrupted by user");
|
||||
free (result);
|
||||
return (HttpResponse){ .body = NULL, .code = -1, .length = 0 };
|
||||
}
|
||||
return (HttpResponse){ .body = result, .code = code, .length = rlen };
|
||||
}
|
||||
|
||||
// Socket implementation for GET requests
|
||||
static HttpResponse socket_http_get_with_interrupt(const HTTPRequest *request) {
|
||||
#if R2__WINDOWS__
|
||||
r_sys_setenv ("R2_CURL", "1");
|
||||
#endif
|
||||
#if R2__UNIX__
|
||||
signal (SIGALRM, r2ai_http_sigint_handler);
|
||||
alarm (request->config.timeout); // Use configured timeout
|
||||
#endif
|
||||
// Make the request - use r_socket_http_get if available
|
||||
int code = 0;
|
||||
int rlen = 0;
|
||||
char *result = r_socket_http_get (request->url, (const char **)request->headers, &code, &rlen);
|
||||
#if R2__UNIX__
|
||||
alarm (0);
|
||||
#endif
|
||||
if (r2ai_http_interrupted) {
|
||||
R_LOG_DEBUG ("HTTP request was interrupted by user");
|
||||
free (result);
|
||||
return (HttpResponse){ .body = NULL, .code = -1, .length = 0 };
|
||||
}
|
||||
return (HttpResponse){ .body = result, .code = code, .length = rlen };
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
#include "r2ai.h"
|
||||
// TODO: move into r2/libr/util/json.c
|
||||
//
|
||||
// Helper function to convert RJson to PJ without draining
|
||||
R_API PJ *r_json_to_pj(const RJson *json, PJ *existing_pj) {
|
||||
if (!json) {
|
||||
return existing_pj;
|
||||
}
|
||||
|
||||
PJ *pj = existing_pj? existing_pj: pj_new ();
|
||||
if (!pj) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
switch (json->type) {
|
||||
case R_JSON_STRING:
|
||||
pj_s (pj, json->str_value);
|
||||
break;
|
||||
case R_JSON_INTEGER:
|
||||
pj_n (pj, json->num.u_value);
|
||||
break;
|
||||
case R_JSON_DOUBLE:
|
||||
pj_d (pj, json->num.dbl_value);
|
||||
break;
|
||||
case R_JSON_BOOLEAN:
|
||||
pj_b (pj, json->num.u_value);
|
||||
break;
|
||||
case R_JSON_NULL:
|
||||
pj_null (pj);
|
||||
break;
|
||||
case R_JSON_OBJECT:
|
||||
if (!existing_pj) {
|
||||
pj_o (pj);
|
||||
}
|
||||
// Handle object's properties
|
||||
const RJson *prop = json->children.first;
|
||||
while (prop) {
|
||||
if (prop->key) {
|
||||
switch (prop->type) {
|
||||
case R_JSON_STRING:
|
||||
pj_ks (pj, prop->key, prop->str_value);
|
||||
break;
|
||||
case R_JSON_INTEGER:
|
||||
pj_kn (pj, prop->key, prop->num.u_value);
|
||||
break;
|
||||
case R_JSON_DOUBLE:
|
||||
pj_kd (pj, prop->key, prop->num.dbl_value);
|
||||
break;
|
||||
case R_JSON_BOOLEAN:
|
||||
pj_kb (pj, prop->key, prop->num.u_value);
|
||||
break;
|
||||
case R_JSON_NULL:
|
||||
pj_knull (pj, prop->key);
|
||||
break;
|
||||
case R_JSON_OBJECT:
|
||||
pj_ko (pj, prop->key);
|
||||
if (!r_json_to_pj (prop, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
case R_JSON_ARRAY:
|
||||
pj_ka (pj, prop->key);
|
||||
if (!r_json_to_pj (prop, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
prop = prop->next;
|
||||
}
|
||||
if (!existing_pj) {
|
||||
pj_end (pj);
|
||||
}
|
||||
break;
|
||||
case R_JSON_ARRAY:
|
||||
if (!existing_pj) {
|
||||
pj_a (pj);
|
||||
}
|
||||
// Handle array items
|
||||
const RJson *item = json->children.first;
|
||||
while (item) {
|
||||
switch (item->type) {
|
||||
case R_JSON_STRING:
|
||||
pj_s (pj, item->str_value);
|
||||
break;
|
||||
case R_JSON_INTEGER:
|
||||
pj_n (pj, item->num.u_value);
|
||||
break;
|
||||
case R_JSON_DOUBLE:
|
||||
pj_d (pj, item->num.dbl_value);
|
||||
break;
|
||||
case R_JSON_BOOLEAN:
|
||||
pj_b (pj, item->num.u_value);
|
||||
break;
|
||||
case R_JSON_NULL:
|
||||
pj_null (pj);
|
||||
break;
|
||||
case R_JSON_OBJECT:
|
||||
pj_o (pj);
|
||||
if (!r_json_to_pj (item, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
case R_JSON_ARRAY:
|
||||
pj_a (pj);
|
||||
if (!r_json_to_pj (item, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
item = item->next;
|
||||
}
|
||||
if (!existing_pj) {
|
||||
pj_end (pj);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return pj;
|
||||
}
|
||||
|
||||
// Helper function to clone RJson to string
|
||||
R_API char *r_json_to_string(const RJson *json) {
|
||||
PJ *pj = r_json_to_pj (json, NULL);
|
||||
return pj? pj_drain (pj): NULL;
|
||||
}
|
||||
@@ -5,16 +5,22 @@
|
||||
#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
|
||||
{ "openai", "https://api.openai.com/v1", R2AI_API_OPENAI_COMPATIBLE, true, true },
|
||||
{ "gemini", "https://generativelanguage.googleapis.com/v1beta", R2AI_API_GEMINI, true, false },
|
||||
{ "anthropic", "https://api.anthropic.com/v1", R2AI_API_ANTHROPIC, true, false },
|
||||
{ "claude", "https://api.anthropic.com/v1", R2AI_API_ANTHROPIC, true, false },
|
||||
{ "ollama", "http://localhost:11434/api", R2AI_API_OLLAMA, false, true },
|
||||
{ "ollamacloud", "https://ollama.com/api", R2AI_API_OLLAMA, true, true },
|
||||
{ "openapi", "http://127.0.0.1:11434", R2AI_API_OPENAI_COMPATIBLE, false, false },
|
||||
{ "opencode", "https://opencode.ai/zen/v1", R2AI_API_OPENAI_COMPATIBLE, true, true },
|
||||
{ "zen", "https://opencode.ai/zen/v1", R2AI_API_OPENAI_COMPATIBLE, true, true },
|
||||
{ "xai", "https://api.x.ai/v1", R2AI_API_OPENAI_COMPATIBLE, true, true },
|
||||
{ "openrouter", "https://openrouter.ai/api/v1", R2AI_API_OPENAI_COMPATIBLE, true, true },
|
||||
{ "groq", "https://api.groq.com/openai/v1", R2AI_API_OPENAI_COMPATIBLE, true, true },
|
||||
{ "mistral", "https://api.mistral.ai/v1", R2AI_API_OPENAI_COMPATIBLE, true, true },
|
||||
{ "lmstudio", "http://127.0.0.1:1234/v1", R2AI_API_OPENAI_COMPATIBLE, false, true },
|
||||
{ "deepseek", "https://api.deepseek.com/v1", R2AI_API_OPENAI_COMPATIBLE, true, true },
|
||||
{ NULL, NULL, R2AI_API_OPENAI_COMPATIBLE, false, false } // sentinel
|
||||
};
|
||||
|
||||
R_IPI const R2AIProvider *r2ai_get_provider(const char *name) {
|
||||
@@ -39,7 +45,7 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
if (!provider) {
|
||||
provider = "gemini";
|
||||
}
|
||||
if (rawtools_enabled && args.tools && args.tools->n_tools > 0) {
|
||||
if (rawtools_enabled && args.tools && r_list_length (args.tools) > 0) {
|
||||
return r2ai_rawtools_llmcall (cps, args);
|
||||
}
|
||||
R2AI_State *state = cps->data;
|
||||
@@ -68,7 +74,7 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
|
||||
char *api_key = NULL;
|
||||
if (prov->requires_api_key) {
|
||||
api_key = r2ai_get_api_key (core, provider);
|
||||
api_key = r2ai_apikeys_get (provider);
|
||||
if (api_key) {
|
||||
args.api_key = api_key;
|
||||
}
|
||||
@@ -78,9 +84,7 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
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);
|
||||
R_LOG_ERROR ("No API key found for the %s provider. Use r2ai -K", provider);
|
||||
free (Provider);
|
||||
return NULL;
|
||||
}
|
||||
@@ -115,7 +119,7 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
}
|
||||
char *m = r_strbuf_drain (sb);
|
||||
R2AI_Message msg = { .role = "user", .content = m };
|
||||
context_pullback = r_list_length (args.messages->messages);
|
||||
context_pullback = r_list_length (args.messages);
|
||||
r2ai_msgs_add (args.messages, &msg);
|
||||
free (m);
|
||||
// TODO: we can save the msg without context
|
||||
@@ -131,13 +135,25 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
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) {
|
||||
if (!p) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
switch (p->api_type) {
|
||||
case R2AI_API_ANTHROPIC:
|
||||
res = r2ai_anthropic (cps, args);
|
||||
} else {
|
||||
break;
|
||||
case R2AI_API_GEMINI:
|
||||
res = r2ai_gemini (cps, args);
|
||||
break;
|
||||
case R2AI_API_OPENAI_COMPATIBLE:
|
||||
case R2AI_API_OLLAMA:
|
||||
default:
|
||||
res = r2ai_openai (cps, args);
|
||||
break;
|
||||
}
|
||||
if (context_pullback != -1) {
|
||||
R2AI_Message *msg = r_list_get_n (args.messages->messages, context_pullback);
|
||||
R2AI_Message *msg = r_list_get_n (args.messages, context_pullback);
|
||||
free ((char *)msg->content);
|
||||
msg->content = strdup (args.input);
|
||||
}
|
||||
@@ -150,33 +166,6 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
return res;
|
||||
}
|
||||
|
||||
/* Return a malloc'd API key read from the environment or from ~/.r2ai.<provider>-key
|
||||
* 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) {
|
||||
@@ -184,41 +173,33 @@ R_IPI const char *r2ai_get_provider_url(RCore *core, const char *provider) {
|
||||
}
|
||||
|
||||
// Handle providers that support custom baseurl
|
||||
if (!strcmp (provider, "openai") || !strcmp (provider, "ollama")) {
|
||||
if (p->supports_custom_baseurl) {
|
||||
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")) {
|
||||
if (p->api_type == R2AI_API_OPENAI_COMPATIBLE) {
|
||||
if (r_str_endswith (host, "/v1")) {
|
||||
return r_str_newf ("%s", host);
|
||||
}
|
||||
return r_str_newf ("%s/v1", host);
|
||||
} else {
|
||||
return r_str_newf ("%s/api", host);
|
||||
}
|
||||
return r_str_newf ("%s/api", host);
|
||||
}
|
||||
if (!strcmp (provider, "openai")) {
|
||||
if (p->api_type == R2AI_API_OPENAI_COMPATIBLE) {
|
||||
return r_str_newf ("http://%s/v1", host);
|
||||
} else {
|
||||
return r_str_newf ("http://%s/api", host);
|
||||
}
|
||||
return r_str_newf ("http://%s/api", host);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!provider) {
|
||||
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) {
|
||||
const char *purl = r2ai_get_provider_url (core, provider);
|
||||
if (!purl) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -227,42 +208,60 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
|
||||
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);
|
||||
api_key = r2ai_apikeys_get (provider);
|
||||
}
|
||||
|
||||
char *models_url = NULL;
|
||||
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;
|
||||
// Special handling for Gemini
|
||||
if (!strcmp (provider, "gemini")) {
|
||||
if (!api_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// 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
|
||||
models_url = r_str_newf ("%s/models?key=%s", purl, api_key);
|
||||
const char *headers[2] = { "Content-Type: application/json", NULL };
|
||||
R_LOG_DEBUG ("GET %s", models_url);
|
||||
response = r2ai_http_get (core, models_url, NULL, &code, NULL);
|
||||
response = r2ai_http_get (core, models_url, headers, &code, NULL);
|
||||
} else {
|
||||
// Create models endpoint URL
|
||||
const R2AIProvider *prov = r2ai_get_provider (provider);
|
||||
const bool usetags = (prov && prov->api_type == R2AI_API_OLLAMA);
|
||||
models_url = r_str_newf ("%s/%s", purl, usetags? "tags": "models");
|
||||
|
||||
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->api_type == R2AI_API_ANTHROPIC) {
|
||||
// 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);
|
||||
} 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);
|
||||
free (api_key);
|
||||
|
||||
if (!response || code != 200) {
|
||||
R_LOG_DEBUG ("Failed to fetch models from %s (code: %d)", provider, code);
|
||||
@@ -279,26 +278,57 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
|
||||
|
||||
RJson *json = r_json_parse (response);
|
||||
if (json) {
|
||||
const RJson *data;
|
||||
const R2AIProvider *prov = r2ai_get_provider (provider);
|
||||
if (prov && prov->uses_tags_endpoint) {
|
||||
const RJson *data = NULL;
|
||||
|
||||
if (!strcmp (provider, "gemini")) {
|
||||
// Gemini has "models" array directly
|
||||
data = r_json_get (json, "models");
|
||||
} else {
|
||||
data = r_json_get (json, "data");
|
||||
const R2AIProvider *prov = r2ai_get_provider (provider);
|
||||
const bool usetags = (prov && prov->api_type == R2AI_API_OLLAMA);
|
||||
data = r_json_get (json, usetags? "models": "data");
|
||||
}
|
||||
|
||||
if (data && data->type == R_JSON_ARRAY) {
|
||||
const RJson *model_item = data->children.first;
|
||||
while (model_item) {
|
||||
const RJson *id;
|
||||
if (prov && prov->uses_tags_endpoint) {
|
||||
id = r_json_get (model_item, "model");
|
||||
const RJson *id = NULL;
|
||||
char *model_id = NULL;
|
||||
|
||||
if (!strcmp (provider, "gemini")) {
|
||||
// Gemini: extract model ID from "name" field (e.g., "models/gemini-1.5-flash" -> "gemini-1.5-flash")
|
||||
const RJson *name = r_json_get (model_item, "name");
|
||||
if (name && name->type == R_JSON_STRING && R_STR_ISNOTEMPTY (name->str_value)) {
|
||||
char *s = strdup (name->str_value);
|
||||
RList *parts = r_str_split_list (s, "/", 0);
|
||||
if (parts && r_list_length (parts) > 1) {
|
||||
model_id = strdup ((char *)r_list_get_n (parts, r_list_length (parts) - 1));
|
||||
} else {
|
||||
model_id = strdup (name->str_value);
|
||||
}
|
||||
r_list_free (parts);
|
||||
// Only include Gemini models
|
||||
if (!strstr (model_id, "gemini")) {
|
||||
free (model_id);
|
||||
model_id = NULL;
|
||||
}
|
||||
free (s);
|
||||
}
|
||||
} else {
|
||||
id = r_json_get (model_item, "id");
|
||||
const R2AIProvider *prov = r2ai_get_provider (provider);
|
||||
if (prov && prov->api_type == R2AI_API_OLLAMA) {
|
||||
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)) {
|
||||
model_id = strdup (id->str_value);
|
||||
}
|
||||
}
|
||||
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));
|
||||
|
||||
if (model_id) {
|
||||
R_LOG_DEBUG ("Model: %s", model_id);
|
||||
r_list_append (models, model_id);
|
||||
}
|
||||
model_item = model_item->next;
|
||||
}
|
||||
@@ -311,8 +341,7 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
|
||||
}
|
||||
|
||||
R_IPI void r2ai_list_providers(RCore *core, RStrBuf *sb) {
|
||||
size_t i;
|
||||
for (i = 0; r2ai_providers[i].name; i++) {
|
||||
for (size_t i = 0; r2ai_providers[i].name; i++) {
|
||||
if (sb) {
|
||||
if (i > 0) {
|
||||
r_strbuf_append (sb, ", ");
|
||||
|
||||
+203
-55
@@ -2,38 +2,120 @@
|
||||
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
#define CLIPPY "?E"
|
||||
// #define CLIPPY "?EC" // pico le croco
|
||||
|
||||
static void show_help() {
|
||||
printf ("Usage: r2ai [-vhp:m:q:E] <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"
|
||||
" -E Edit the r2ai rc file\n");
|
||||
printf (
|
||||
"Usage: r2ai [-vhp:m:q:Eb:Kc:f:iew] <prompt>\n"
|
||||
" -v Show version information\n"
|
||||
" -h Show this help message\n"
|
||||
" -p <provider> Select LLM provider\n"
|
||||
" -m <model> Select LLM model\n"
|
||||
" -q <query> Execute predefined prompt query (can be used multiple times)\n"
|
||||
" -b <url> Set base URL for provider API\n"
|
||||
" -c <command> Execute command after loading file (can be used multiple times)\n"
|
||||
" -f <file> Load file with r2 API\n"
|
||||
" -i <script> Load and interpret script file before executing commands\n"
|
||||
" -e <var=value> Set configuration variable\n"
|
||||
" -E Edit the r2ai rc file\n"
|
||||
" -K Edit the API keys file\n"
|
||||
" -w Launch interactive setup wizard\n");
|
||||
}
|
||||
|
||||
static void show_version() {
|
||||
printf ("r2ai " R2AI_VERSION "\n");
|
||||
}
|
||||
|
||||
static char *build_conversation(RList *conversation) {
|
||||
RListIter *iter;
|
||||
char *msg;
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
r_list_foreach (conversation, iter, msg) {
|
||||
r_strbuf_appendf (sb, "%s\n", msg);
|
||||
}
|
||||
return r_strbuf_drain (sb);
|
||||
}
|
||||
|
||||
static void r2ai_repl(RCorePluginSession *cps, const char *provider, const char *model, RList *conversation) {
|
||||
RCore *core = cps->core;
|
||||
r2ai_wizard_autorun (core);
|
||||
r_line_set_prompt (core->cons->line, "[r2ai]> ");
|
||||
while (true) {
|
||||
const char *input = r_line_readline (core->cons);
|
||||
if (r_cons_is_breaked (core->cons) || R_STR_ISEMPTY (input)) {
|
||||
break;
|
||||
}
|
||||
if (input[0] == '!') {
|
||||
// Execute shell command
|
||||
system (input + 1);
|
||||
} else if (input[0] == ':') {
|
||||
// Run radare2 command
|
||||
char *res = r_core_cmd_str (core, input + 1);
|
||||
if (res) {
|
||||
r_cons_println (core->cons, res);
|
||||
free (res);
|
||||
}
|
||||
} else if (input[0] == 'q') {
|
||||
if (r_cons_yesno (core->cons, 'y', "Do you want to quit? (Y/n)")) {
|
||||
break;
|
||||
}
|
||||
} else if (input[0] == '-') {
|
||||
cmd_r2ai (cps, input);
|
||||
} else {
|
||||
// Send message to LLM as part of conversation
|
||||
r_list_append (conversation, r_str_newf ("User: %s", input));
|
||||
char *full_prompt = build_conversation (conversation);
|
||||
char *err = NULL;
|
||||
R2AIArgs args = {
|
||||
.input = full_prompt,
|
||||
.provider = provider,
|
||||
.model = model,
|
||||
.error = &err,
|
||||
.dorag = false,
|
||||
};
|
||||
char *res = r2ai (cps, args);
|
||||
if (res) {
|
||||
if (r_config_get_b (core->config, "r2ai.clippy")) {
|
||||
char *cmd = r_str_newf (CLIPPY " %s", res);
|
||||
r_core_cmd_call (core, cmd);
|
||||
free (cmd);
|
||||
} else {
|
||||
r_cons_println (core->cons, res);
|
||||
r_list_append (conversation, r_str_newf ("Assistant: %s", res));
|
||||
}
|
||||
free (res);
|
||||
}
|
||||
if (err) {
|
||||
r_cons_println (core->cons, err);
|
||||
free (err);
|
||||
}
|
||||
free (full_prompt);
|
||||
}
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, const char **argv) {
|
||||
int c;
|
||||
const char *provider = NULL;
|
||||
const char *model = NULL;
|
||||
const char *baseurl = NULL;
|
||||
const char *filename = NULL;
|
||||
const char *scriptfile = NULL;
|
||||
RList *conversation = r_list_newf (free);
|
||||
RList *queries = r_list_newf (free);
|
||||
RList *commands = r_list_newf (free);
|
||||
RCore *core = r_core_new ();
|
||||
RCorePluginSession cps = {
|
||||
.core = core
|
||||
};
|
||||
r2ai_init (&cps);
|
||||
|
||||
RGetopt opt;
|
||||
r_getopt_init (&opt, argc, argv, "vhp:m:q:E");
|
||||
r_getopt_init (&opt, argc, argv, "vhp:m:q:Eb:Kc:f:ie:w");
|
||||
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;
|
||||
@@ -43,33 +125,101 @@ int main(int argc, const char **argv) {
|
||||
case 'q':
|
||||
r_list_append (queries, strdup (opt.arg));
|
||||
break;
|
||||
case 'b':
|
||||
baseurl = opt.arg;
|
||||
break;
|
||||
case 'c':
|
||||
r_list_append (commands, strdup (opt.arg));
|
||||
break;
|
||||
case 'f':
|
||||
filename = opt.arg;
|
||||
break;
|
||||
case 'i':
|
||||
scriptfile = opt.arg;
|
||||
break;
|
||||
case 'e':
|
||||
{
|
||||
char *var_value = strdup (opt.arg);
|
||||
char *equals = strchr (var_value, '=');
|
||||
if (equals) {
|
||||
*equals = '\0';
|
||||
const char *var = var_value;
|
||||
const char *value = equals + 1;
|
||||
r_config_set (core->config, var, value);
|
||||
} else {
|
||||
r_cons_println (core->cons, "Invalid format for -e. Use var=value.");
|
||||
}
|
||||
free (var_value);
|
||||
}
|
||||
break;
|
||||
case 'E':
|
||||
{
|
||||
RCore *core = r_core_new ();
|
||||
RCorePluginSession cps = {
|
||||
.core = core
|
||||
};
|
||||
r2ai_init (&cps);
|
||||
char *rc_path = r_file_home (".config/r2ai/rc");
|
||||
r_cons_editor (core->cons, rc_path, NULL);
|
||||
free (rc_path);
|
||||
r_list_free (queries);
|
||||
r2ai_fini (&cps);
|
||||
r_core_free (core);
|
||||
return 0;
|
||||
goto beach;
|
||||
}
|
||||
break;
|
||||
case 'K':
|
||||
{
|
||||
r2ai_apikeys_edit (&cps);
|
||||
goto beach;
|
||||
}
|
||||
break;
|
||||
case 'w':
|
||||
{
|
||||
r2ai_wizard (core);
|
||||
goto beach;
|
||||
}
|
||||
break;
|
||||
case 'v':
|
||||
show_version ();
|
||||
goto beach;
|
||||
case 'h':
|
||||
show_help ();
|
||||
goto beach;
|
||||
default:
|
||||
show_help ();
|
||||
r_list_free (queries);
|
||||
return 1;
|
||||
goto beach;
|
||||
}
|
||||
}
|
||||
|
||||
RCore *core = r_core_new ();
|
||||
RCorePluginSession cps = {
|
||||
.core = core
|
||||
};
|
||||
r2ai_init (&cps);
|
||||
if (baseurl) {
|
||||
r_config_set (core->config, "r2ai.baseurl", baseurl);
|
||||
}
|
||||
if (filename) {
|
||||
char *cmd = r_str_newf ("o %s", filename);
|
||||
r_core_cmd_call (core, cmd);
|
||||
free (cmd);
|
||||
// TODO: use the C api instead of the 'o' command and check for error if file exists etc
|
||||
} else {
|
||||
r_core_cmd0 (core, CLIPPY " Interesting, no files to analyse. Use -f or :o");
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
if (scriptfile) {
|
||||
char *cmd = r_str_newf (". %s", scriptfile);
|
||||
r_core_cmd_call (core, cmd);
|
||||
free (cmd);
|
||||
}
|
||||
if (!r_list_empty (commands)) {
|
||||
RListIter *iter;
|
||||
char *cmd;
|
||||
r_list_foreach (commands, iter, cmd) {
|
||||
char *res = r_core_cmd_str (core, cmd);
|
||||
if (res) {
|
||||
r_cons_println (core->cons, res);
|
||||
free (res);
|
||||
}
|
||||
}
|
||||
}
|
||||
r_config_set_b (core->config, "r2ai.utf8", false);
|
||||
r_config_set_b (core->config, "r2ai.clippy", true);
|
||||
if (provider) {
|
||||
r_config_set (core->config, "r2ai.api", provider);
|
||||
}
|
||||
if (model) {
|
||||
r_config_set (core->config, "r2ai.model", model);
|
||||
}
|
||||
|
||||
// Process queries if any
|
||||
if (!r_list_empty (queries)) {
|
||||
@@ -80,34 +230,32 @@ int main(int argc, const char **argv) {
|
||||
}
|
||||
} else {
|
||||
if (opt.ind >= argc) {
|
||||
// List available prompts
|
||||
r2ai_cmd_q (&cps, "");
|
||||
r_list_free (queries);
|
||||
r2ai_fini (&cps);
|
||||
r_core_free (core);
|
||||
return 0;
|
||||
}
|
||||
r2ai_repl (&cps, provider, model, conversation);
|
||||
} else {
|
||||
const char *prompt = argv[opt.ind];
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
free (err);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
|
||||
beach:
|
||||
r_list_free (conversation);
|
||||
r_list_free (queries);
|
||||
r_list_free (commands);
|
||||
r2ai_fini (&cps);
|
||||
r_core_free (core);
|
||||
return 0;
|
||||
|
||||
+13
-13
@@ -28,20 +28,20 @@ R_API RMarkdownTheme r2ai_markdown_theme_default(void) {
|
||||
.code_inline = "\x1b[48;5;236m", // Dark gray background for inline code
|
||||
.code_block = "\x1b[48;5;235m", // Slightly darker gray for code blocks
|
||||
// Heading colors
|
||||
.heading1 = "\x1b[1;4;31m", // Bold, underlined, red
|
||||
.heading2 = "\x1b[1;4;32m", // Bold, underlined, green
|
||||
.heading3 = "\x1b[1;4;33m", // Bold, underlined, yellow
|
||||
.heading4 = "\x1b[1;4;34m", // Bold, underlined, blue
|
||||
.heading5 = "\x1b[1;4;35m", // Bold, underlined, magenta
|
||||
.heading6 = "\x1b[1;4;36m", // Bold, underlined, cyan
|
||||
// List items
|
||||
.heading1 = "\x1b[1;4" Color_RED, // Bold, underlined, red
|
||||
.heading2 = "\x1b[1;4" Color_GREEN, // Bold, underlined, green
|
||||
.heading3 = "\x1b[1;4" Color_YELLOW, // Bold, underlined, yellow
|
||||
.heading4 = "\x1b[1;4" Color_BLUE, // Bold, underlined, blue
|
||||
.heading5 = "\x1b[1;4" Color_MAGENTA, // Bold, underlined, magenta
|
||||
.heading6 = "\x1b[1;4" Color_CYAN, // Bold, underlined, cyan
|
||||
// List items
|
||||
.list_bullet = "• ", // Bullet character
|
||||
.list_number = "\x1b[1m%s.\x1b[0m ", // Bold number
|
||||
// Checkbox states
|
||||
.checkbox_checked = "\x1b[32m[✓]\x1b[0m ", // Green checkmark
|
||||
.checkbox_unchecked = "\x1b[90m[ ]\x1b[0m ", // Gray empty box
|
||||
// Reset code
|
||||
.reset = "\x1b[0m"
|
||||
.list_number = "\x1b[1m%s." Color_RESET " ", // Bold number
|
||||
// Checkbox states
|
||||
.checkbox_checked = Color_GREEN "[✓]" Color_RESET " ", // Green checkmark
|
||||
.checkbox_unchecked = Color_GRAY "[ ]" Color_RESET " ", // Gray empty box
|
||||
// Reset code
|
||||
.reset = Color_RESET
|
||||
};
|
||||
return theme;
|
||||
}
|
||||
|
||||
+63
-190
@@ -1,219 +1,92 @@
|
||||
project('r2ai', 'c',
|
||||
version : '1.2.2',
|
||||
version : '1.3.4',
|
||||
default_options : ['warning_level=2', 'c_std=c11']
|
||||
)
|
||||
|
||||
cc = meson.get_compiler('c')
|
||||
r2_prefix = get_option('r2_prefix')
|
||||
|
||||
# radare2 core dependency: use pkg-config when available. Do not fall back
|
||||
# to CMake-based dependency discovery; instead, if pkg-config fails, try
|
||||
# locating the library under user-provided prefixes (see -Dr2_prefix).
|
||||
r_core_dep = dependency('r_core', method: 'pkg-config', required: false)
|
||||
|
||||
# Centralized resolution results: when we locate `r_core` manually below we
|
||||
# record the library and include search directories in `r2_libdirs` and
|
||||
# `r2_incdirs`. Later lookups for other `r_*` libraries will assume they
|
||||
# live in the same locations, avoiding repeating complex prefix search
|
||||
# logic.
|
||||
r2_libdirs = []
|
||||
r2_incdirs = []
|
||||
|
||||
# If pkg-config didn't yield an r_core dependency, fall back to manual
|
||||
# discovery logic below (search common prefixes / extracted zips).
|
||||
# If pkg-config fails, set up fallback search paths from r2_prefix
|
||||
r_core_dep = dependency('r_core', method: 'pkg-config', required: false)
|
||||
if not r_core_dep.found()
|
||||
# On Windows prefer explicit discovery like other third_party projects do:
|
||||
# 1) ask an installed `radare2` binary for its lib/include/plugin dirs
|
||||
# (via `radare2 -H R2_LIBDIR` / `R2_INCDIR`),
|
||||
# 2) fall back to -Dr2_prefix provided by CI, and
|
||||
# 3) try common locations such as C:\\radare2\\lib and <prefix>/bin.
|
||||
# This avoids Meson's CMake-based dependency fallback which doesn't work
|
||||
# reliably with radare2 release zips on Windows.
|
||||
is_windows_like = host_machine.system() == 'windows' or cc.get_id() == 'msvc' or cc.get_id() == 'clang-cl'
|
||||
r2_libdirs = [
|
||||
join_paths(r2_prefix, 'lib'),
|
||||
join_paths(r2_prefix, 'lib64'),
|
||||
join_paths(r2_prefix, 'radare2', 'lib'),
|
||||
join_paths(r2_prefix, 'radare2', 'lib64'),
|
||||
join_paths(r2_prefix, 'mingw64', 'lib'),
|
||||
join_paths(r2_prefix, 'bin'),
|
||||
]
|
||||
r2_incdirs = [
|
||||
join_paths(r2_prefix, 'include'),
|
||||
join_paths(r2_prefix, 'include', 'libr'),
|
||||
join_paths(r2_prefix, 'include', 'radare2'),
|
||||
join_paths(r2_prefix, 'radare2', 'include'),
|
||||
]
|
||||
endif
|
||||
|
||||
# Simpler, robust Windows behavior mirroring third_party projects:
|
||||
# Try to find r_core by checking a small list of likely library dirs.
|
||||
if is_windows_like
|
||||
r2libdirs = []
|
||||
# prefer explicit prefix if given
|
||||
if get_option('r2_prefix') != ''
|
||||
p = get_option('r2_prefix')
|
||||
r2libdirs += [ join_paths(p, 'lib'), join_paths(p, 'radare2', 'lib') ]
|
||||
endif
|
||||
# repository-local radare2 (used by some subprojects) and conventional path
|
||||
r2libdirs += [ meson.current_source_dir() + '/radare2/lib', 'C:/radare2/lib']
|
||||
# Filter include dirs to only those that exist
|
||||
fs = import('fs')
|
||||
r2_valid_incdirs = []
|
||||
foreach d : r2_incdirs
|
||||
if fs.is_dir(d)
|
||||
r2_valid_incdirs += [d]
|
||||
endif
|
||||
endforeach
|
||||
|
||||
# try to find r_core in the candidate dirs (MSVC import libs or mingw import libs)
|
||||
rcore_lib = cc.find_library('r_core', dirs: r2libdirs, required: false)
|
||||
if not rcore_lib.found()
|
||||
rcore_lib = cc.find_library('libr_core', dirs: r2libdirs, required: false)
|
||||
endif
|
||||
|
||||
if rcore_lib.found()
|
||||
# basic include candidates
|
||||
incdirs = []
|
||||
if get_option('r2_prefix') != ''
|
||||
p = get_option('r2_prefix')
|
||||
incdirs += [ join_paths(p, 'include', 'libr'), join_paths(p, 'include') ]
|
||||
endif
|
||||
rcore_inc = include_directories(incdirs)
|
||||
r_core_dep = declare_dependency(dependencies: [rcore_lib], include_directories: rcore_inc)
|
||||
# Remember resolved dirs for other r_* lookups
|
||||
r2_libdirs = r2libdirs
|
||||
r2_incdirs = incdirs
|
||||
else
|
||||
error('Windows dependency lookup for r_core failed: ensure radare2 is installed and pass -Dr2_prefix=<prefix> (e.g. C:/radare2)')
|
||||
endif
|
||||
else
|
||||
# Non-Windows: try locating radare2 from environment-provided prefixes. This
|
||||
# helps CI on Unix where pkg-config may not be installed.
|
||||
r2_prefixes = []
|
||||
if get_option('r2_prefix') != ''
|
||||
r2_prefixes += [get_option('r2_prefix')]
|
||||
endif
|
||||
|
||||
found = false
|
||||
foreach p : r2_prefixes
|
||||
# Common library/include locations under a provided prefix.
|
||||
libdirs = [
|
||||
join_paths(p, 'lib'),
|
||||
join_paths(p, 'lib64'),
|
||||
join_paths(p, 'radare2', 'lib'),
|
||||
join_paths(p, 'radare2', 'lib64'),
|
||||
join_paths(p, 'mingw64', 'lib'),
|
||||
]
|
||||
incdirs = [
|
||||
join_paths(p, 'include'),
|
||||
join_paths(p, 'radare2', 'include'),
|
||||
join_paths(p, 'include', 'radare2'),
|
||||
]
|
||||
extra_libdirs = libdirs + [ join_paths(p, 'bin') ]
|
||||
candidate_names = [ 'r_core', 'r_core-6', 'r_core6', 'r_core64', 'libr_core' ]
|
||||
foreach name : candidate_names
|
||||
rcore_lib = cc.find_library(name, dirs: extra_libdirs, required: false)
|
||||
if rcore_lib.found()
|
||||
rcore_inc = include_directories(incdirs + [ join_paths(p, 'include', 'libr') ])
|
||||
r_core_dep = declare_dependency(dependencies: [rcore_lib], include_directories: rcore_inc)
|
||||
# Record the successful prefix and candidate lib/include dirs so
|
||||
# other r_* dependencies can be looked up in the same locations.
|
||||
r2_libdirs = extra_libdirs
|
||||
r2_incdirs = incdirs + [ join_paths(p, 'include', 'libr') ]
|
||||
found = true
|
||||
break
|
||||
endif
|
||||
endforeach
|
||||
if found
|
||||
# Resolve all radare2 dependencies: pkg-config first, then manual search
|
||||
r2_deps = []
|
||||
foreach name : ['r_core', 'r_util', 'r_cons', 'r_config', 'r_socket']
|
||||
dep = dependency(name, method: 'pkg-config', required: false)
|
||||
if not dep.found() and r2_libdirs.length() > 0
|
||||
foreach libname : [name, 'lib' + name]
|
||||
lib = cc.find_library(libname, dirs: r2_libdirs, required: false)
|
||||
if lib.found()
|
||||
dep = declare_dependency(
|
||||
dependencies: [lib],
|
||||
include_directories: include_directories(r2_valid_incdirs),
|
||||
)
|
||||
break
|
||||
endif
|
||||
endforeach
|
||||
|
||||
if not found
|
||||
error('Dependency lookup for r_core failed: install radare2 development files or provide pkg-config paths or set the meson option -Dr2_prefix=<prefix>')
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
if name == 'r_core' and not dep.found()
|
||||
error('r_core not found: install radare2 dev files, set PKG_CONFIG_PATH, or use -Dr2_prefix=<prefix>')
|
||||
endif
|
||||
r2_deps += [dep]
|
||||
endforeach
|
||||
|
||||
# Resolve other r_* dependencies assuming same paths as r_core
|
||||
r_util_dep = dependency('r_util', method: 'pkg-config', required: false)
|
||||
if not r_util_dep.found() and r2_libdirs.length() > 0
|
||||
foreach name : ['r_util', 'libr_util']
|
||||
ulib = cc.find_library(name, dirs: r2_libdirs, required: false)
|
||||
if ulib.found()
|
||||
uinc = include_directories(r2_incdirs)
|
||||
r_util_dep = declare_dependency(dependencies: [ulib], include_directories: uinc)
|
||||
break
|
||||
endif
|
||||
endforeach
|
||||
endif
|
||||
if not r_util_dep.found()
|
||||
warning('r_util dependency not found via pkg-config or same paths as r_core; build may fail on systems without radare2 dev files')
|
||||
endif
|
||||
|
||||
r_cons_dep = dependency('r_cons', method: 'pkg-config', required: false)
|
||||
if not r_cons_dep.found() and r2_libdirs.length() > 0
|
||||
foreach name : ['r_cons', 'libr_cons']
|
||||
clib = cc.find_library(name, dirs: r2_libdirs, required: false)
|
||||
if clib.found()
|
||||
cinc = include_directories(r2_incdirs)
|
||||
r_cons_dep = declare_dependency(dependencies: [clib], include_directories: cinc)
|
||||
break
|
||||
endif
|
||||
endforeach
|
||||
endif
|
||||
if not r_cons_dep.found()
|
||||
warning('r_cons dependency not found via pkg-config or same paths as r_core; console helpers may be unavailable')
|
||||
endif
|
||||
|
||||
r_config_dep = dependency('r_config', method: 'pkg-config', required: false)
|
||||
if not r_config_dep.found() and r2_libdirs.length() > 0
|
||||
foreach name : ['r_config', 'libr_config']
|
||||
clib = cc.find_library(name, dirs: r2_libdirs, required: false)
|
||||
if clib.found()
|
||||
cinc = include_directories(r2_incdirs)
|
||||
r_config_dep = declare_dependency(dependencies: [clib], include_directories: cinc)
|
||||
break
|
||||
endif
|
||||
endforeach
|
||||
endif
|
||||
if not r_config_dep.found()
|
||||
warning('r_config dependency not found via pkg-config or same paths as r_core; console helpers may be unavailable')
|
||||
endif
|
||||
|
||||
r_socket_dep = dependency('r_socket', method: 'pkg-config', required: false)
|
||||
if not r_socket_dep.found() and r2_libdirs.length() > 0
|
||||
foreach name : ['r_socket', 'libr_socket']
|
||||
slib = cc.find_library(name, dirs: r2_libdirs, required: false)
|
||||
if slib.found()
|
||||
sinc = include_directories(r2_incdirs)
|
||||
r_socket_dep = declare_dependency(dependencies: [slib], include_directories: sinc)
|
||||
break
|
||||
endif
|
||||
endforeach
|
||||
endif
|
||||
if not r_socket_dep.found()
|
||||
warning('r_socket dependency not found via pkg-config or same paths as r_core; socket helpers may be unavailable')
|
||||
endif
|
||||
|
||||
# Math library dependency (needed for log, sqrt, etc.)
|
||||
m_dep = cc.find_library('m', required: false)
|
||||
|
||||
# Common compile flags similar to the Makefile
|
||||
c_args = []
|
||||
if cc.get_id() == 'gcc' or cc.get_id() == 'clang'
|
||||
c_args += ['-Wall', '-g', '-fPIC']
|
||||
elif cc.get_id() == 'msvc'
|
||||
c_args += ['/Zi']
|
||||
endif
|
||||
common_deps = r2_deps + [cc.find_library('m', required: false)]
|
||||
|
||||
srcs = files(
|
||||
'r2ai.c',
|
||||
'auto.c',
|
||||
'vdb.c',
|
||||
'llm.c',
|
||||
'tools.c',
|
||||
'query.c',
|
||||
'rawtools.c',
|
||||
'messages.c',
|
||||
'anthropic.c',
|
||||
'r2ai_http.c',
|
||||
'openai.c',
|
||||
'auto.c',
|
||||
'apikeys.c',
|
||||
'gemini.c',
|
||||
'http.c',
|
||||
'json.c',
|
||||
'wizard.c',
|
||||
'llm.c',
|
||||
'markdown.c',
|
||||
'messages.c',
|
||||
'openai.c',
|
||||
'query.c',
|
||||
'r2ai.c',
|
||||
'rawtools.c',
|
||||
'tools.c',
|
||||
'vdb.c',
|
||||
)
|
||||
|
||||
# Shared library (plugin)
|
||||
shared_lib = shared_library(
|
||||
'r2ai',
|
||||
srcs,
|
||||
shared_lib = shared_library('r2ai', srcs,
|
||||
install: true,
|
||||
c_args: c_args,
|
||||
dependencies: [r_core_dep, r_util_dep, r_cons_dep, r_config_dep, r_socket_dep, m_dep]
|
||||
dependencies: common_deps,
|
||||
)
|
||||
|
||||
# Command-line executable (links the same objects plus main.c)
|
||||
exe = executable(
|
||||
'r2ai',
|
||||
[ 'main.c' ],
|
||||
executable('r2ai', 'main.c',
|
||||
link_with: shared_lib,
|
||||
install: true,
|
||||
c_args: c_args,
|
||||
dependencies: [r_core_dep, r_util_dep, r_cons_dep, r_config_dep, r_socket_dep, m_dep]
|
||||
dependencies: common_deps,
|
||||
)
|
||||
+139
-343
@@ -2,14 +2,35 @@
|
||||
|
||||
#include "r2ai.h"
|
||||
|
||||
#define INITIAL_CAPACITY 8
|
||||
#define GROWTH_FACTOR 1.5
|
||||
R_API void r2ai_tool_call_free(R2AI_ToolCall *tc) {
|
||||
if (tc) {
|
||||
free ((void *)tc->id);
|
||||
free ((void *)tc->name);
|
||||
free ((void *)tc->arguments);
|
||||
free (tc);
|
||||
}
|
||||
}
|
||||
|
||||
R_API RList *r2ai_content_blocks_new(void) {
|
||||
RList *cb = r_list_new ();
|
||||
if (!cb) {
|
||||
return NULL;
|
||||
}
|
||||
cb->free = (RListFree)free; // ContentBlocks contain pointers to structs, not structs themselves
|
||||
return cb;
|
||||
}
|
||||
|
||||
R_API void r2ai_content_blocks_free(RList *cb) {
|
||||
if (!cb) {
|
||||
return;
|
||||
}
|
||||
r_list_free (cb);
|
||||
}
|
||||
|
||||
R_API void r2ai_message_free(R2AI_Message *msg) {
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
|
||||
free (msg->role);
|
||||
free ((void *)msg->content); // maybe double free
|
||||
free (msg->reasoning_content);
|
||||
@@ -17,29 +38,13 @@ R_API void r2ai_message_free(R2AI_Message *msg) {
|
||||
|
||||
// Free tool calls
|
||||
if (msg->tool_calls) {
|
||||
for (int i = 0; i < msg->n_tool_calls; i++) {
|
||||
free ((void *)msg->tool_calls[i].id);
|
||||
free ((void *)msg->tool_calls[i].name);
|
||||
free ((void *)msg->tool_calls[i].arguments);
|
||||
}
|
||||
free ((void *)msg->tool_calls);
|
||||
r_list_free (msg->tool_calls);
|
||||
}
|
||||
|
||||
if (msg->content_blocks) {
|
||||
for (int i = 0; i < msg->content_blocks->n_blocks; i++) {
|
||||
R2AI_ContentBlock *block = &msg->content_blocks->blocks[i];
|
||||
free (block->type);
|
||||
free (block->data);
|
||||
free (block->thinking);
|
||||
free (block->signature);
|
||||
free (block->text);
|
||||
free (block->id);
|
||||
free (block->name);
|
||||
free (block->input);
|
||||
}
|
||||
free (msg->content_blocks->blocks);
|
||||
free (msg->content_blocks);
|
||||
r2ai_content_blocks_free (msg->content_blocks);
|
||||
}
|
||||
free (msg);
|
||||
}
|
||||
|
||||
// Conversation is now stored in R2AI_State
|
||||
@@ -53,36 +58,24 @@ R_API void r2ai_conversation_init(R2AI_State *state) {
|
||||
state->conversation = r2ai_msgs_new ();
|
||||
}
|
||||
|
||||
R_API R2AI_Messages *r2ai_conversation_get(R2AI_State *state) {
|
||||
R_API RList *r2ai_conversation_get(R2AI_State *state) {
|
||||
return state? state->conversation: NULL;
|
||||
}
|
||||
|
||||
// Create a new temporary messages container
|
||||
R_API R2AI_Messages *r2ai_msgs_new(void) {
|
||||
R2AI_Messages *msgs = R_NEW0 (R2AI_Messages);
|
||||
R_API RList *r2ai_msgs_new(void) {
|
||||
RList *msgs = r_list_new ();
|
||||
if (!msgs) {
|
||||
return NULL;
|
||||
}
|
||||
msgs->messages = r_list_new ();
|
||||
if (!msgs->messages) {
|
||||
R_FREE (msgs);
|
||||
return NULL;
|
||||
}
|
||||
msgs->messages->free = (RListFree)r2ai_message_free;
|
||||
msgs->free = (RListFree)r2ai_message_free;
|
||||
return msgs;
|
||||
}
|
||||
|
||||
R_API void r2ai_msgs_free(R2AI_Messages *msgs) {
|
||||
if (!msgs) {
|
||||
return;
|
||||
R_API void r2ai_msgs_free(RList *msgs) {
|
||||
if (msgs) {
|
||||
r_list_free (msgs);
|
||||
}
|
||||
|
||||
// Normal free for non-static instances
|
||||
if (msgs->messages) {
|
||||
r_list_free (msgs->messages);
|
||||
}
|
||||
|
||||
R_FREE (msgs);
|
||||
}
|
||||
|
||||
// Free the conversation when plugin is unloaded
|
||||
@@ -94,46 +87,34 @@ R_API void r2ai_conversation_free(R2AI_State *state) {
|
||||
}
|
||||
|
||||
// Clear messages in a container without freeing the container itself
|
||||
R_API void r2ai_msgs_clear(R2AI_Messages *msgs) {
|
||||
R_API void r2ai_msgs_clear(RList *msgs) {
|
||||
if (!msgs) {
|
||||
return;
|
||||
}
|
||||
|
||||
r_list_purge (msgs->messages);
|
||||
r_list_purge (msgs);
|
||||
}
|
||||
|
||||
R_API bool r2ai_msgs_add(R2AI_Messages *msgs, const R2AI_Message *msg) {
|
||||
R_API bool r2ai_msgs_add(RList *msgs, const R2AI_Message *msg) {
|
||||
if (!msgs || !msg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
R2AI_Message *new_msg = R_NEW0 (R2AI_Message);
|
||||
if (!new_msg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
new_msg->role = msg->role? strdup (msg->role): NULL;
|
||||
new_msg->content = msg->content? strdup (msg->content): NULL;
|
||||
new_msg->reasoning_content = msg->reasoning_content? strdup (msg->reasoning_content): NULL;
|
||||
|
||||
if (msg->content_blocks) {
|
||||
R2AI_ContentBlocks *cb = R_NEW0 (R2AI_ContentBlocks);
|
||||
RList *cb = r2ai_content_blocks_new ();
|
||||
if (!cb) {
|
||||
r2ai_message_free (new_msg);
|
||||
free (new_msg);
|
||||
return false;
|
||||
}
|
||||
cb->n_blocks = msg->content_blocks->n_blocks;
|
||||
cb->blocks = R_NEWS0 (R2AI_ContentBlock, cb->n_blocks);
|
||||
if (!cb->blocks) {
|
||||
free (cb);
|
||||
r2ai_message_free (new_msg);
|
||||
free (new_msg);
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < cb->n_blocks; i++) {
|
||||
R2AI_ContentBlock *src = &msg->content_blocks->blocks[i];
|
||||
R2AI_ContentBlock *dst = &cb->blocks[i];
|
||||
RListIter *iter;
|
||||
R2AI_ContentBlock *src;
|
||||
r_list_foreach (msg->content_blocks, iter, src) {
|
||||
R2AI_ContentBlock *dst = R_NEW0 (R2AI_ContentBlock);
|
||||
dst->type = src->type? strdup (src->type): NULL;
|
||||
dst->data = src->data? strdup (src->data): NULL;
|
||||
dst->thinking = src->thinking? strdup (src->thinking): NULL;
|
||||
@@ -142,90 +123,79 @@ R_API bool r2ai_msgs_add(R2AI_Messages *msgs, const R2AI_Message *msg) {
|
||||
dst->id = src->id? strdup (src->id): NULL;
|
||||
dst->name = src->name? strdup (src->name): NULL;
|
||||
dst->input = src->input? strdup (src->input): NULL;
|
||||
r_list_append (cb, dst);
|
||||
}
|
||||
new_msg->content_blocks = cb;
|
||||
}
|
||||
|
||||
new_msg->tool_call_id = msg->tool_call_id? strdup (msg->tool_call_id): NULL;
|
||||
new_msg->tool_calls = NULL;
|
||||
new_msg->n_tool_calls = 0;
|
||||
new_msg->tool_calls = r_list_new ();
|
||||
if (!new_msg->tool_calls) {
|
||||
r2ai_message_free (new_msg);
|
||||
free (new_msg);
|
||||
return false;
|
||||
}
|
||||
new_msg->tool_calls->free = (RListFree)r2ai_tool_call_free;
|
||||
|
||||
// Copy tool calls if any
|
||||
if (msg->tool_calls && msg->n_tool_calls > 0) {
|
||||
new_msg->tool_calls = R_NEWS0 (R2AI_ToolCall, msg->n_tool_calls);
|
||||
if (!new_msg->tool_calls) {
|
||||
r2ai_message_free (new_msg);
|
||||
free (new_msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
new_msg->n_tool_calls = msg->n_tool_calls;
|
||||
for (int i = 0; i < msg->n_tool_calls; i++) {
|
||||
const R2AI_ToolCall *src_tc = &msg->tool_calls[i];
|
||||
R2AI_ToolCall *dst_tc = (R2AI_ToolCall *)&new_msg->tool_calls[i];
|
||||
|
||||
if (msg->tool_calls) {
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *src_tc;
|
||||
r_list_foreach (msg->tool_calls, iter, src_tc) {
|
||||
R2AI_ToolCall *dst_tc = R_NEW0 (R2AI_ToolCall);
|
||||
dst_tc->name = src_tc->name? strdup (src_tc->name): NULL;
|
||||
dst_tc->arguments = src_tc->arguments? strdup (src_tc->arguments): NULL;
|
||||
dst_tc->id = src_tc->id? strdup (src_tc->id): NULL;
|
||||
r_list_append (new_msg->tool_calls, dst_tc);
|
||||
}
|
||||
}
|
||||
|
||||
r_list_append (msgs->messages, new_msg);
|
||||
r_list_append (msgs, new_msg);
|
||||
return true;
|
||||
}
|
||||
|
||||
R_API bool r2ai_msgs_add_tool_call(R2AI_Messages *msgs, const R2AI_ToolCall *tc) {
|
||||
if (!msgs || !tc || r_list_length (msgs->messages) == 0) {
|
||||
R_API bool r2ai_msgs_add_tool_call(RList *msgs, const R2AI_ToolCall *tc) {
|
||||
if (!msgs || !tc || r_list_empty (msgs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
R2AI_Message *msg = r_list_get_n (msgs->messages, r_list_length (msgs->messages) - 1);
|
||||
R2AI_Message *msg = r_list_get_n (msgs, r_list_length (msgs) - 1);
|
||||
|
||||
// Allocate or resize the tool_calls array
|
||||
if (msg->n_tool_calls == 0) {
|
||||
msg->tool_calls = R_NEWS0 (R2AI_ToolCall, 1);
|
||||
// Ensure tool_calls list exists
|
||||
if (!msg->tool_calls) {
|
||||
msg->tool_calls = r_list_new ();
|
||||
if (!msg->tool_calls) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
R2AI_ToolCall *new_tool_calls = realloc (
|
||||
(void *)msg->tool_calls,
|
||||
sizeof (R2AI_ToolCall) *(msg->n_tool_calls + 1));
|
||||
if (!new_tool_calls) {
|
||||
return false;
|
||||
}
|
||||
msg->tool_calls = new_tool_calls;
|
||||
// Zero the new element
|
||||
memset ((void *)&msg->tool_calls[msg->n_tool_calls], 0, sizeof (R2AI_ToolCall));
|
||||
msg->tool_calls->free = (RListFree)r2ai_tool_call_free;
|
||||
}
|
||||
|
||||
// Copy the tool call
|
||||
R2AI_ToolCall *dst_tc = (R2AI_ToolCall *)&msg->tool_calls[msg->n_tool_calls];
|
||||
R2AI_ToolCall *dst_tc = R_NEW0 (R2AI_ToolCall);
|
||||
dst_tc->name = tc->name? strdup (tc->name): NULL;
|
||||
dst_tc->arguments = tc->arguments? strdup (tc->arguments): NULL;
|
||||
dst_tc->id = tc->id? strdup (tc->id): NULL;
|
||||
|
||||
msg->n_tool_calls++;
|
||||
r_list_append (msg->tool_calls, dst_tc);
|
||||
return true;
|
||||
}
|
||||
|
||||
R_API bool r2ai_msgs_from_response(R2AI_Messages *msgs, const char *json_str) {
|
||||
R_API bool r2ai_msgs_from_response(RList *msgs, const char *json_str) {
|
||||
if (!msgs || !json_str) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// r_json_parse expects non-const char*, so we need to cast it
|
||||
bool result = false;
|
||||
// r_json_parse expects (and modifies) non-const char*, so we need to cast it
|
||||
RJson *json = r_json_parse ((char *)json_str);
|
||||
if (!json) {
|
||||
return false;
|
||||
if (json) {
|
||||
result = r2ai_msgs_from_json (msgs, json);
|
||||
r_json_free (json);
|
||||
}
|
||||
|
||||
bool result = r2ai_msgs_from_json (msgs, json);
|
||||
r_json_free (json);
|
||||
return result;
|
||||
}
|
||||
|
||||
R_API bool r2ai_msgs_from_json(R2AI_Messages *msgs, const RJson *json) {
|
||||
R_API bool r2ai_msgs_from_json(RList *msgs, const RJson *json) {
|
||||
if (!msgs || !json) {
|
||||
return false;
|
||||
}
|
||||
@@ -255,27 +225,24 @@ R_API bool r2ai_msgs_from_json(R2AI_Messages *msgs, const RJson *json) {
|
||||
new_msg.content = (content && content->type == R_JSON_STRING)? strdup (content->str_value): NULL;
|
||||
new_msg.tool_call_id = NULL;
|
||||
new_msg.tool_calls = NULL;
|
||||
new_msg.n_tool_calls = 0;
|
||||
|
||||
if (content_blocks && content_blocks->type == R_JSON_ARRAY && content_blocks->children.count > 0) {
|
||||
R2AI_ContentBlocks *cb = R_NEW0 (R2AI_ContentBlocks);
|
||||
RList *cb = r2ai_content_blocks_new ();
|
||||
if (!cb) {
|
||||
r2ai_message_free (&new_msg);
|
||||
return false;
|
||||
}
|
||||
cb->n_blocks = content_blocks->children.count;
|
||||
cb->blocks = R_NEWS0 (R2AI_ContentBlock, cb->n_blocks);
|
||||
if (!cb->blocks) {
|
||||
free (cb);
|
||||
r2ai_message_free (&new_msg);
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < cb->n_blocks; i++) {
|
||||
for (size_t i = 0; i < content_blocks->children.count; i++) {
|
||||
const RJson *block = r_json_item (content_blocks, i);
|
||||
if (!block) {
|
||||
continue;
|
||||
}
|
||||
R2AI_ContentBlock *dst = &cb->blocks[i];
|
||||
R2AI_ContentBlock *dst = R_NEW0 (R2AI_ContentBlock);
|
||||
if (!dst) {
|
||||
r2ai_content_blocks_free (cb);
|
||||
r2ai_message_free (&new_msg);
|
||||
return false;
|
||||
}
|
||||
const RJson *type = r_json_get (block, "type");
|
||||
const RJson *data = r_json_get (block, "data");
|
||||
const RJson *thinking = r_json_get (block, "thinking");
|
||||
@@ -293,6 +260,7 @@ R_API bool r2ai_msgs_from_json(R2AI_Messages *msgs, const RJson *json) {
|
||||
dst->id = (id && id->type == R_JSON_STRING)? strdup (id->str_value): NULL;
|
||||
dst->name = (name && name->type == R_JSON_STRING)? strdup (name->str_value): NULL;
|
||||
dst->input = (input && input->type == R_JSON_STRING)? strdup (input->str_value): NULL;
|
||||
r_list_append (cb, dst);
|
||||
}
|
||||
new_msg.content_blocks = cb;
|
||||
}
|
||||
@@ -328,27 +296,17 @@ R_API bool r2ai_msgs_from_json(R2AI_Messages *msgs, const RJson *json) {
|
||||
tc.id = (id && id->type == R_JSON_STRING)? id->str_value: NULL;
|
||||
|
||||
if (!r2ai_msgs_add_tool_call (msgs, &tc)) {
|
||||
break;
|
||||
r2ai_message_free (&new_msg);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the parsed message to the messages array
|
||||
if (!r2ai_msgs_add (msgs, &new_msg)) {
|
||||
r2ai_message_free (&new_msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Free the temporary message struct (strings are duplicated in add)
|
||||
r2ai_message_free (&new_msg);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
|
||||
if (!msgs || r_list_length (msgs->messages) == 0) {
|
||||
R_API char *r2ai_msgs_to_json(const RList *msgs) {
|
||||
if (!msgs || r_list_empty (msgs)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -359,16 +317,22 @@ R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
|
||||
|
||||
pj_a (pj); // Start array
|
||||
|
||||
for (int i = 0; i < r_list_length (msgs->messages); i++) {
|
||||
const R2AI_Message *msg = r_list_get_n (msgs->messages, i);
|
||||
RListIter *iter;
|
||||
const R2AI_Message *msg;
|
||||
r_list_foreach (msgs, iter, msg) {
|
||||
|
||||
pj_o (pj); // Start message object
|
||||
|
||||
// Add role
|
||||
pj_ks (pj, "role", msg->role? msg->role: "user");
|
||||
|
||||
if (msg->content) {
|
||||
// Content is required for OpenAI API
|
||||
if (msg->content && *msg->content) {
|
||||
pj_ks (pj, "content", msg->content);
|
||||
} else if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
|
||||
pj_knull (pj, "content");
|
||||
} else {
|
||||
pj_ks (pj, "content", msg->content? msg->content: "");
|
||||
}
|
||||
|
||||
if (msg->reasoning_content) {
|
||||
@@ -381,13 +345,13 @@ R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
|
||||
}
|
||||
|
||||
// Add tool_calls if present
|
||||
if (msg->tool_calls && msg->n_tool_calls > 0) {
|
||||
if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
|
||||
pj_k (pj, "tool_calls");
|
||||
pj_a (pj); // Start tool_calls array
|
||||
|
||||
for (int j = 0; j < msg->n_tool_calls; j++) {
|
||||
const R2AI_ToolCall *tc = &msg->tool_calls[j];
|
||||
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (msg->tool_calls, iter, tc) {
|
||||
pj_o (pj); // Start tool call object
|
||||
|
||||
// Add id if present
|
||||
@@ -405,10 +369,8 @@ R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
|
||||
// Add name
|
||||
pj_ks (pj, "name", tc->name? tc->name: "");
|
||||
|
||||
// Add arguments if present
|
||||
if (tc->arguments) {
|
||||
pj_ks (pj, "arguments", tc->arguments);
|
||||
}
|
||||
// Add arguments (required by OpenAI API)
|
||||
pj_ks (pj, "arguments", tc->arguments? tc->arguments: "{}");
|
||||
|
||||
pj_end (pj); // End function object
|
||||
pj_end (pj); // End tool call object
|
||||
@@ -426,8 +388,8 @@ R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
|
||||
return result;
|
||||
}
|
||||
|
||||
R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
if (!msgs || r_list_length (msgs->messages) == 0) {
|
||||
R_API char *r2ai_msgs_to_anthropic_json(const RList *msgs) {
|
||||
if (!msgs || r_list_empty (msgs)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -438,19 +400,24 @@ R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
|
||||
pj_a (pj); // Start array
|
||||
|
||||
for (int i = 0; i < r_list_length (msgs->messages); i++) {
|
||||
const R2AI_Message *msg = r_list_get_n (msgs->messages, i);
|
||||
|
||||
RListIter *iter;
|
||||
const R2AI_Message *msg;
|
||||
r_list_foreach (msgs, iter, msg) {
|
||||
pj_o (pj); // Start message object
|
||||
|
||||
// Add role
|
||||
// Circumvent Anthropic's allergy to system role messages
|
||||
const char *role = msg->role? msg->role: "user";
|
||||
bool is_system_message = !strcmp (role, "system");
|
||||
if (is_system_message) {
|
||||
role = "user";
|
||||
}
|
||||
pj_ks (pj, "role", strcmp (role, "tool") == 0? "user": role);
|
||||
|
||||
if (msg->content_blocks) {
|
||||
pj_ka (pj, "content"); // Start content array
|
||||
for (int j = 0; j < msg->content_blocks->n_blocks; j++) {
|
||||
const R2AI_ContentBlock *block = &msg->content_blocks->blocks[j];
|
||||
RListIter *iter;
|
||||
R2AI_ContentBlock *block;
|
||||
r_list_foreach (msg->content_blocks, iter, block) {
|
||||
pj_o (pj); // Start content block object
|
||||
if (R_STR_ISNOTEMPTY (block->type)) {
|
||||
pj_ks (pj, "type", block->type);
|
||||
@@ -474,22 +441,8 @@ R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
pj_ks (pj, "name", block->name);
|
||||
}
|
||||
if (R_STR_ISNOTEMPTY (block->input)) {
|
||||
// Try to parse the input as JSON first
|
||||
char *input_str = strdup (block->input);
|
||||
RJson *input_json = r_json_parse (input_str);
|
||||
if (input_json) {
|
||||
// If it's valid JSON, add it directly
|
||||
pj_ko (pj, "input");
|
||||
r_json_to_pj (input_json, pj);
|
||||
pj_end (pj);
|
||||
r_json_free (input_json);
|
||||
} else {
|
||||
// If not valid JSON, create a basic object with command
|
||||
pj_ko (pj, "input");
|
||||
pj_ks (pj, "command", block->input);
|
||||
pj_end (pj);
|
||||
}
|
||||
free (input_str);
|
||||
pj_k (pj, "input");
|
||||
pj_raw (pj, block->input);
|
||||
}
|
||||
pj_end (pj); // End content block object
|
||||
}
|
||||
@@ -505,43 +458,35 @@ R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
pj_ks (pj, "content", msg->content);
|
||||
} else {
|
||||
pj_ks (pj, "type", "text");
|
||||
pj_ks (pj, "text", msg->content);
|
||||
if (is_system_message) {
|
||||
char *prefixed = r_str_newf ("SYSTEM INSTRUCTIONS: %s", msg->content);
|
||||
pj_ks (pj, "text", prefixed);
|
||||
free (prefixed);
|
||||
} else {
|
||||
pj_ks (pj, "text", msg->content);
|
||||
}
|
||||
}
|
||||
pj_end (pj); // End content block object
|
||||
}
|
||||
|
||||
if (msg->tool_calls && msg->n_tool_calls > 0) {
|
||||
pj_k (pj, "tool_calls");
|
||||
pj_a (pj); // Start tool_calls array
|
||||
|
||||
for (int j = 0; j < msg->n_tool_calls; j++) {
|
||||
const R2AI_ToolCall *tc = &msg->tool_calls[j];
|
||||
pj_o (pj); // Start tool_calls object
|
||||
if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (msg->tool_calls, iter, tc) {
|
||||
pj_o (pj); // Start tool_use content block
|
||||
pj_ks (pj, "type", "tool_use");
|
||||
pj_ks (pj, "id", tc->id? tc->id: "");
|
||||
pj_ks (pj, "name", tc->name? tc->name: "");
|
||||
|
||||
// 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;
|
||||
|
||||
pj_ko (pj, "input"); // Start input object
|
||||
if (arguments) {
|
||||
for (size_t k = 0; k < arguments->children.count; k++) {
|
||||
const RJson *arg = r_json_item (arguments, k);
|
||||
if (arg && arg->type == R_JSON_STRING) {
|
||||
pj_ks (pj, arg->key, arg->str_value);
|
||||
}
|
||||
}
|
||||
r_json_free (arguments);
|
||||
// Insert the arguments directly as the input object
|
||||
pj_k (pj, "input");
|
||||
if (tc->arguments) {
|
||||
pj_raw (pj, tc->arguments);
|
||||
} else {
|
||||
pj_raw (pj, "{}");
|
||||
}
|
||||
free (arguments_copy);
|
||||
|
||||
pj_end (pj); // End input object
|
||||
pj_end (pj); // End tool_calls object
|
||||
pj_end (pj); // End tool_use content block
|
||||
}
|
||||
|
||||
pj_end (pj); // End tool_calls array
|
||||
}
|
||||
pj_end (pj); // End content array
|
||||
}
|
||||
@@ -555,8 +500,8 @@ R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
}
|
||||
|
||||
// Function to delete the last N messages from conversation history
|
||||
R_API void r2ai_delete_last_messages(R2AI_Messages *messages, int n) {
|
||||
if (!messages || r_list_length (messages->messages) == 0) {
|
||||
R_API void r2ai_delete_last_messages(RList *messages, int n) {
|
||||
if (!messages || r_list_length (messages) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -566,162 +511,13 @@ R_API void r2ai_delete_last_messages(R2AI_Messages *messages, int n) {
|
||||
}
|
||||
|
||||
// Make sure we don't try to delete more messages than exist
|
||||
int len = r_list_length (messages->messages);
|
||||
int len = r_list_length (messages);
|
||||
if (n > len) {
|
||||
n = len;
|
||||
}
|
||||
|
||||
// Pop the last n messages
|
||||
for (int i = 0; i < n; i++) {
|
||||
r_list_pop (messages->messages);
|
||||
r_list_pop (messages);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert RJson to PJ without draining
|
||||
R_API PJ *r_json_to_pj(const RJson *json, PJ *existing_pj) {
|
||||
if (!json) {
|
||||
return existing_pj;
|
||||
}
|
||||
|
||||
PJ *pj = existing_pj? existing_pj: pj_new ();
|
||||
if (!pj) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
switch (json->type) {
|
||||
case R_JSON_STRING:
|
||||
pj_s (pj, json->str_value);
|
||||
break;
|
||||
case R_JSON_INTEGER:
|
||||
pj_n (pj, json->num.u_value);
|
||||
break;
|
||||
case R_JSON_DOUBLE:
|
||||
pj_d (pj, json->num.dbl_value);
|
||||
break;
|
||||
case R_JSON_BOOLEAN:
|
||||
pj_b (pj, json->num.u_value);
|
||||
break;
|
||||
case R_JSON_NULL:
|
||||
pj_null (pj);
|
||||
break;
|
||||
case R_JSON_OBJECT:
|
||||
if (!existing_pj) {
|
||||
pj_o (pj);
|
||||
}
|
||||
// Handle object's properties
|
||||
const RJson *prop = json->children.first;
|
||||
while (prop) {
|
||||
if (prop->key) {
|
||||
switch (prop->type) {
|
||||
case R_JSON_STRING:
|
||||
pj_ks (pj, prop->key, prop->str_value);
|
||||
break;
|
||||
case R_JSON_INTEGER:
|
||||
pj_kn (pj, prop->key, prop->num.u_value);
|
||||
break;
|
||||
case R_JSON_DOUBLE:
|
||||
pj_kd (pj, prop->key, prop->num.dbl_value);
|
||||
break;
|
||||
case R_JSON_BOOLEAN:
|
||||
pj_kb (pj, prop->key, prop->num.u_value);
|
||||
break;
|
||||
case R_JSON_NULL:
|
||||
pj_knull (pj, prop->key);
|
||||
break;
|
||||
case R_JSON_OBJECT:
|
||||
pj_ko (pj, prop->key);
|
||||
if (!r_json_to_pj (prop, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
case R_JSON_ARRAY:
|
||||
pj_ka (pj, prop->key);
|
||||
if (!r_json_to_pj (prop, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
prop = prop->next;
|
||||
}
|
||||
if (!existing_pj) {
|
||||
pj_end (pj);
|
||||
}
|
||||
break;
|
||||
case R_JSON_ARRAY:
|
||||
if (!existing_pj) {
|
||||
pj_a (pj);
|
||||
}
|
||||
// Handle array items
|
||||
const RJson *item = json->children.first;
|
||||
while (item) {
|
||||
switch (item->type) {
|
||||
case R_JSON_STRING:
|
||||
pj_s (pj, item->str_value);
|
||||
break;
|
||||
case R_JSON_INTEGER:
|
||||
pj_n (pj, item->num.u_value);
|
||||
break;
|
||||
case R_JSON_DOUBLE:
|
||||
pj_d (pj, item->num.dbl_value);
|
||||
break;
|
||||
case R_JSON_BOOLEAN:
|
||||
pj_b (pj, item->num.u_value);
|
||||
break;
|
||||
case R_JSON_NULL:
|
||||
pj_null (pj);
|
||||
break;
|
||||
case R_JSON_OBJECT:
|
||||
pj_o (pj);
|
||||
if (!r_json_to_pj (item, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
case R_JSON_ARRAY:
|
||||
pj_a (pj);
|
||||
if (!r_json_to_pj (item, pj)) {
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
item = item->next;
|
||||
}
|
||||
if (!existing_pj) {
|
||||
pj_end (pj);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!existing_pj) {
|
||||
pj_free (pj);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return pj;
|
||||
}
|
||||
|
||||
// Helper function to clone RJson to string
|
||||
R_API char *r_json_to_string(const RJson *json) {
|
||||
PJ *pj = r_json_to_pj (json, NULL);
|
||||
return pj? pj_drain (pj): NULL;
|
||||
}
|
||||
+97
-124
@@ -1,90 +1,21 @@
|
||||
/* r2ai - Copyright 2023-2025 pancake, dnakov */
|
||||
|
||||
#include <r_core.h>
|
||||
#include <r_util/r_json.h>
|
||||
#include "r2ai.h"
|
||||
|
||||
// Bit flags for different types of model errors/incompatibilities
|
||||
typedef enum {
|
||||
MODEL_ERROR_NONE = 0,
|
||||
MODEL_ERROR_TEMPERATURE = 1 << 0,
|
||||
// Can add more error types here as needed
|
||||
// MODEL_ERROR_TOP_P = 1 << 1,
|
||||
// MODEL_ERROR_MAX_TOKENS = 1 << 2,
|
||||
// etc.
|
||||
} ModelErrorFlags;
|
||||
|
||||
// Structure to store model compatibility info
|
||||
typedef struct {
|
||||
char *model_id; // Provider:model string
|
||||
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;
|
||||
}
|
||||
|
||||
if (!state->model_compat_db) {
|
||||
state->model_compat_db = ht_pp_new0 ();
|
||||
}
|
||||
|
||||
char *key = r_str_newf ("%s:%s", provider, model? model: "default");
|
||||
bool found_flag = false;
|
||||
ModelCompat *compat = ht_pp_find (state->model_compat_db, key, &found_flag);
|
||||
|
||||
if (found_flag && compat) {
|
||||
// Update existing entry
|
||||
compat->error_flags |= flag;
|
||||
} else {
|
||||
// Create new entry
|
||||
compat = R_NEW0 (ModelCompat);
|
||||
compat->model_id = strdup (key);
|
||||
compat->error_flags = flag;
|
||||
ht_pp_insert (state->model_compat_db, key, compat);
|
||||
}
|
||||
free (key);
|
||||
}
|
||||
|
||||
// Free a ModelCompat item (for hash table)
|
||||
static bool model_compat_free_cb(void *user, const void *k, const void *v) {
|
||||
(void)user;
|
||||
(void)k;
|
||||
ModelCompat *compat = (ModelCompat *)v;
|
||||
if (compat) {
|
||||
free (compat->model_id);
|
||||
free (compat);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
// Initialize compatibility database if needed
|
||||
if (!state->model_compat_db) {
|
||||
state->model_compat_db = ht_pp_new0 ();
|
||||
}
|
||||
args.provider = r_config_get (core->config, "r2ai.api");
|
||||
args.model = r_config_get (core->config, "r2ai.model");
|
||||
|
||||
const R2AIProvider *provider_info = r2ai_get_provider (args.provider);
|
||||
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";
|
||||
char **error = args.error;
|
||||
const R2AI_Tools *tools = args.tools;
|
||||
RList *tools = args.tools;
|
||||
// create a temp conversation to include the system prompt and the rest of the messages
|
||||
R2AI_Messages *temp_msgs = r2ai_msgs_new ();
|
||||
RList *temp_msgs = r2ai_msgs_new ();
|
||||
if (!temp_msgs) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to create temporary messages array");
|
||||
@@ -122,15 +53,17 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
}
|
||||
}
|
||||
if (args.messages) {
|
||||
for (int i = 0; i < r_list_length (args.messages->messages); i++) {
|
||||
r2ai_msgs_add (temp_msgs, r_list_get_n (args.messages->messages, i));
|
||||
RListIter *iter;
|
||||
R2AI_Message *msg;
|
||||
r_list_foreach (args.messages, iter, msg) {
|
||||
r2ai_msgs_add (temp_msgs, msg);
|
||||
}
|
||||
} else {
|
||||
R_LOG_WARN ("No messages");
|
||||
}
|
||||
// Safely print debug info about first message
|
||||
if (temp_msgs && r_list_length (temp_msgs->messages) > 0 && ((R2AI_Message *)r_list_get_n (temp_msgs->messages, 0))->role) {
|
||||
R_LOG_DEBUG ("First message role: %s", ((R2AI_Message *)r_list_get_n (temp_msgs->messages, 0))->role);
|
||||
if (temp_msgs && !r_list_empty (temp_msgs) && ((R2AI_Message *)r_list_get_n (temp_msgs, 0))->role) {
|
||||
R_LOG_DEBUG ("First message role: %s", ((R2AI_Message *)r_list_get_n (temp_msgs, 0))->role);
|
||||
}
|
||||
if (error) {
|
||||
*error = NULL;
|
||||
@@ -146,16 +79,16 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
headers[0] = "Content-Type: application/json";
|
||||
headers[1] = auth_header;
|
||||
}
|
||||
const char *urlfmt = strcmp (args.provider, "ollama")
|
||||
? "%s/chat/completions"
|
||||
: "%s/chat";
|
||||
const char *urlfmt = (provider_info && provider_info->api_type == R2AI_API_OLLAMA)
|
||||
? "%s/chat"
|
||||
: "%s/chat/completions";
|
||||
char *openai_url = r_str_newf (urlfmt, base_url);
|
||||
|
||||
// Create a messages JSON object, either from input messages or from content
|
||||
char *messages_json = NULL;
|
||||
|
||||
if (temp_msgs && r_list_length (temp_msgs->messages) > 0) {
|
||||
R_LOG_DEBUG ("Using input messages: %d messages", r_list_length (temp_msgs->messages));
|
||||
if (temp_msgs && !r_list_empty (temp_msgs)) {
|
||||
R_LOG_DEBUG ("Using input messages: %d messages", r_list_length (temp_msgs));
|
||||
messages_json = r2ai_msgs_to_json (temp_msgs);
|
||||
if (!messages_json) {
|
||||
if (error) {
|
||||
@@ -174,7 +107,7 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
|
||||
// Convert tools to OpenAI format if available
|
||||
char *openai_tools_json = NULL;
|
||||
if (tools && tools->n_tools > 0) {
|
||||
if (tools && !r_list_empty (tools)) {
|
||||
openai_tools_json = r2ai_tools_to_openai_json (tools);
|
||||
}
|
||||
|
||||
@@ -184,7 +117,7 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
pj_ks (pj, "model", model_name);
|
||||
pj_kb (pj, "stream", false);
|
||||
|
||||
if (strcmp (args.provider, "ollama") == 0) {
|
||||
if (provider_info && provider_info->api_type == R2AI_API_OLLAMA) {
|
||||
// Ollama uses "options" object for parameters
|
||||
pj_ko (pj, "options");
|
||||
if (args.max_tokens) {
|
||||
@@ -215,17 +148,6 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
|
||||
// Get the JSON for model settings
|
||||
char *model_json = pj_drain (pj);
|
||||
if (!model_json) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to create model settings JSON");
|
||||
}
|
||||
free (auth_header);
|
||||
free (messages_json);
|
||||
if (openai_tools_json) {
|
||||
free (openai_tools_json);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Manually create the final JSON by combining parts
|
||||
// Remove the closing brace from model_json
|
||||
@@ -268,6 +190,17 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
|
||||
R_LOG_DEBUG ("OpenAI API request data: %s", complete_json);
|
||||
|
||||
if (r_config_get_b (core->config, "r2ai.debug")) {
|
||||
// Generate curl command for debugging
|
||||
RStrBuf *curl_cmd = r_strbuf_new ("curl -X POST");
|
||||
for (int i = 0; headers[i]; i++) {
|
||||
r_strbuf_appendf (curl_cmd, " -H \"%s\"", headers[i]);
|
||||
}
|
||||
r_strbuf_appendf (curl_cmd, " -d '%s' \"%s\"", complete_json, openai_url);
|
||||
eprintf ("Curl command: %s\n", r_strbuf_get (curl_cmd));
|
||||
r_strbuf_free (curl_cmd);
|
||||
}
|
||||
|
||||
// Make the API call
|
||||
char *res = NULL;
|
||||
int code = 0;
|
||||
@@ -279,28 +212,6 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
R_LOG_ERROR ("OpenAI API error %d", code);
|
||||
if (res) {
|
||||
R_LOG_ERROR ("OpenAI API error response: %s", res);
|
||||
// Check for specific error types in the response
|
||||
ModelErrorFlags error_flag = MODEL_ERROR_NONE;
|
||||
|
||||
// Check for temperature errors
|
||||
if (strstr (res, "temperature")) {
|
||||
R_LOG_DEBUG ("Detected temperature error for %s model %s", args.provider, args.model);
|
||||
error_flag |= MODEL_ERROR_TEMPERATURE;
|
||||
}
|
||||
|
||||
if (error_flag != MODEL_ERROR_NONE) {
|
||||
// Record the error flags for this provider/model
|
||||
model_add_error (state, args.provider, args.model, error_flag);
|
||||
|
||||
// Clean up
|
||||
free (auth_header);
|
||||
free (res);
|
||||
|
||||
// Retry the call (it will skip problematic parameters this time)
|
||||
R_LOG_INFO ("Retrying request with adjusted parameters for %s/%s", args.provider, args.model);
|
||||
RCorePluginSession retry_cps = { .core = core, .data = state };
|
||||
return r2ai_openai (&retry_cps, args);
|
||||
}
|
||||
}
|
||||
free (auth_header);
|
||||
free (res);
|
||||
@@ -311,12 +222,12 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
tmpdir = r_file_tmpdir ();
|
||||
char *res_path = r_str_newf ("%s" R_SYS_DIR "r2ai_openai_response.json", tmpdir);
|
||||
r_file_dump (res_path, (const ut8 *)res, strlen (res), 0);
|
||||
R_LOG_DEBUG ("OpenAI API response saved to %s", res_path);
|
||||
if (r_config_get_b (core->config, "r2ai.debug")) {
|
||||
eprintf ("OpenAI API response: %s\n", res);
|
||||
}
|
||||
free (res_path);
|
||||
free (tmpdir);
|
||||
|
||||
R_LOG_DEBUG ("OpenAI API response: %s", res);
|
||||
|
||||
// Parse the response into our messages structure
|
||||
|
||||
char *res_copy = strdup (res);
|
||||
@@ -328,7 +239,7 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
if (message) {
|
||||
// Process the response using our r2ai_msgs_from_json logic
|
||||
const RJson *usage_json = NULL;
|
||||
if (strcmp (args.provider, "ollama") == 0) {
|
||||
if (provider_info && provider_info->api_type == R2AI_API_OLLAMA) {
|
||||
// Ollama has usage info at top level
|
||||
usage_json = jres;
|
||||
} else {
|
||||
@@ -336,7 +247,7 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
}
|
||||
|
||||
if (usage_json && usage_json->type == R_JSON_OBJECT) {
|
||||
if (strcmp (args.provider, "ollama") == 0) {
|
||||
if (provider_info && provider_info->api_type == R2AI_API_OLLAMA) {
|
||||
// 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");
|
||||
@@ -365,7 +276,7 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
}
|
||||
}
|
||||
const RJson *message_json = NULL;
|
||||
if (strcmp (args.provider, "ollama") == 0) {
|
||||
if (provider_info && provider_info->api_type == R2AI_API_OLLAMA) {
|
||||
// Ollama returns message directly
|
||||
message_json = r_json_get (jres, "message");
|
||||
} else {
|
||||
@@ -383,6 +294,9 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
const RJson *role = r_json_get (message_json, "role");
|
||||
const RJson *content = r_json_get (message_json, "content");
|
||||
const RJson *reasoning_content = r_json_get (message_json, "reasoning_content");
|
||||
const RJson *thinking = r_json_get (message_json, "thinking");
|
||||
const RJson *tool_call_id = r_json_get (message_json, "tool_call_id");
|
||||
const RJson *tool_calls = r_json_get (message_json, "tool_calls");
|
||||
|
||||
// Set the basic message properties
|
||||
message->role = (role && role->type == R_JSON_STRING)? strdup (role->str_value): strdup ("assistant");
|
||||
@@ -391,8 +305,67 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
message->content = strdup (content->str_value);
|
||||
}
|
||||
|
||||
if (reasoning_content && reasoning_content->type == R_JSON_STRING) {
|
||||
if (reasoning_content && reasoning_content->type == R_JSON_STRING && R_STR_ISNOTEMPTY (reasoning_content->str_value)) {
|
||||
message->reasoning_content = strdup (reasoning_content->str_value);
|
||||
} else if (thinking && thinking->type == R_JSON_STRING && R_STR_ISNOTEMPTY (thinking->str_value)) {
|
||||
message->reasoning_content = strdup (thinking->str_value);
|
||||
}
|
||||
|
||||
if (tool_call_id && tool_call_id->type == R_JSON_STRING && R_STR_ISNOTEMPTY (tool_call_id->str_value)) {
|
||||
message->tool_call_id = strdup (tool_call_id->str_value);
|
||||
}
|
||||
|
||||
if (tool_calls && tool_calls->type == R_JSON_ARRAY && tool_calls->children.count > 0) {
|
||||
message->tool_calls = r_list_new ();
|
||||
if (message->tool_calls) {
|
||||
message->tool_calls->free = (RListFree)r2ai_tool_call_free;
|
||||
for (size_t i = 0; i < tool_calls->children.count; i++) {
|
||||
const RJson *tool_call = r_json_item (tool_calls, i);
|
||||
if (!tool_call || tool_call->type != R_JSON_OBJECT) {
|
||||
continue;
|
||||
}
|
||||
R2AI_ToolCall *tc = R_NEW0 (R2AI_ToolCall);
|
||||
if (!tc) {
|
||||
continue;
|
||||
}
|
||||
const RJson *tc_id = r_json_get (tool_call, "id");
|
||||
if (tc_id && tc_id->type == R_JSON_STRING && R_STR_ISNOTEMPTY (tc_id->str_value)) {
|
||||
tc->id = strdup (tc_id->str_value);
|
||||
}
|
||||
const RJson *function = r_json_get (tool_call, "function");
|
||||
const RJson *name_json = NULL;
|
||||
const RJson *arguments = NULL;
|
||||
if (function && function->type == R_JSON_OBJECT) {
|
||||
name_json = r_json_get (function, "name");
|
||||
arguments = r_json_get (function, "arguments");
|
||||
} else {
|
||||
name_json = r_json_get (tool_call, "name");
|
||||
arguments = r_json_get (tool_call, "arguments");
|
||||
}
|
||||
if (name_json && name_json->type == R_JSON_STRING && R_STR_ISNOTEMPTY (name_json->str_value)) {
|
||||
tc->name = strdup (name_json->str_value);
|
||||
}
|
||||
if (arguments) {
|
||||
if (arguments->type == R_JSON_STRING && R_STR_ISNOTEMPTY (arguments->str_value)) {
|
||||
tc->arguments = strdup (arguments->str_value);
|
||||
} else {
|
||||
char *arguments_json = r_json_to_string (arguments);
|
||||
if (arguments_json) {
|
||||
tc->arguments = arguments_json;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!tc->name && !tc->arguments && !tc->id) {
|
||||
r2ai_tool_call_free (tc);
|
||||
continue;
|
||||
}
|
||||
r_list_append (message->tool_calls, tc);
|
||||
}
|
||||
if (r_list_empty (message->tool_calls)) {
|
||||
r_list_free (message->tool_calls);
|
||||
message->tool_calls = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Handle tool calls if present?
|
||||
}
|
||||
|
||||
Loaded 100 of 110 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user