Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1e0b4739a | ||
|
|
ed1269e66d | ||
|
|
b375e89b16 | ||
|
|
8f91fb1452 | ||
|
|
350f4d8f2b | ||
|
|
7632448dc0 | ||
|
|
2a244bbace |
No files matched your search
@@ -1,151 +1,47 @@
|
||||
name: CI and Release
|
||||
# TODO: add windows
|
||||
name: ci
|
||||
|
||||
env:
|
||||
R2V: 6.0.4
|
||||
R2V: 5.9.0
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags:
|
||||
- '[0-9]*'
|
||||
# pull_request:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build (${{ matrix.os }} / ${{ matrix.build_system }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-24.04, macos-latest, windows-latest]
|
||||
build_system: [make, meson]
|
||||
exclude:
|
||||
- os: windows-latest
|
||||
build_system: make
|
||||
|
||||
build-linux:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Meson/Ninja
|
||||
if: matrix.build_system == 'meson'
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install meson ninja
|
||||
|
||||
- name: Install radare2 on Linux
|
||||
if: matrix.os == 'ubuntu-24.04'
|
||||
run: |
|
||||
wget "https://github.com/radareorg/radare2/releases/download/${R2V}/radare2_${R2V}_amd64.deb"
|
||||
wget "https://github.com/radareorg/radare2/releases/download/${R2V}/radare2-dev_${R2V}_amd64.deb"
|
||||
sudo dpkg -i "radare2_${R2V}_amd64.deb"
|
||||
sudo dpkg -i "radare2-dev_${R2V}_amd64.deb"
|
||||
|
||||
- name: Install radare2 on macOS
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
git clone --depth=1 --branch "${{env.R2V}}" https://github.com/radareorg/radare2
|
||||
cd radare2
|
||||
sys/install.sh
|
||||
|
||||
- name: Install radare2 on Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
shell: powershell
|
||||
run: |
|
||||
$url = "https://github.com/radareorg/radare2/releases/download/${{ env.R2V }}/radare2-${{ env.R2V }}-w64.zip"
|
||||
$dest = "$env:TEMP\radare2.zip"
|
||||
Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing
|
||||
$prefix = 'C:\radare2'
|
||||
if (Test-Path $prefix) { Remove-Item -Path $prefix -Recurse -Force }
|
||||
Expand-Archive -Path $dest -DestinationPath $prefix -Force
|
||||
# Move contents from subdirectory if present
|
||||
$subdirs = Get-ChildItem -Path $prefix -Directory
|
||||
if ($subdirs.Count -eq 1) {
|
||||
$subdir = $subdirs[0]
|
||||
Get-ChildItem -Path $subdir.FullName | Move-Item -Destination $prefix -Force
|
||||
Remove-Item -Path $subdir.FullName -Recurse -Force
|
||||
}
|
||||
|
||||
- name: Build with Make
|
||||
if: matrix.build_system == 'make'
|
||||
run: make -C src
|
||||
|
||||
- name: Build with Meson (Unix)
|
||||
if: matrix.build_system == 'meson' && matrix.os != 'windows-latest'
|
||||
run: |
|
||||
cd src
|
||||
meson setup build -Dr2_prefix=${{ secrets.R2_PREFIX }} || meson configure build
|
||||
meson compile -C build
|
||||
|
||||
- name: Build with Meson (Windows)
|
||||
if: matrix.build_system == 'meson' && matrix.os == 'windows-latest'
|
||||
shell: powershell
|
||||
run: |
|
||||
cd src
|
||||
$prefix = 'C:\radare2'
|
||||
meson setup build --backend ninja --prefix="$prefix" -Dr2_prefix="$prefix"
|
||||
if ($LASTEXITCODE -ne 0) { meson configure build }
|
||||
meson compile -C build
|
||||
|
||||
- name: Create release artifacts (tags only)
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p release
|
||||
cp -rf src/r2ai.dll src/r2ai.exe src/r2ai src/r2ai.dylib src/r2ai.so release/ || true
|
||||
cp -rf src/build/r2ai src/build/r2ai.dylib src/build/r2ai.so release/ || true
|
||||
cp -f decai/decai.r2.js release/ || true
|
||||
if command -v zip >/dev/null 2>&1; then
|
||||
zip -j "release/r2ai-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.build_system }}.zip" release/*
|
||||
else
|
||||
powershell.exe -Command "Compress-Archive -Path release/* -DestinationPath 'release/r2ai-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.build_system }}.zip'"
|
||||
fi
|
||||
|
||||
- name: Upload artifacts (tags only)
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.os }}-${{ matrix.build_system }}-binary
|
||||
path: release/r2ai-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.build_system }}.zip
|
||||
|
||||
# - name: Smoke test
|
||||
# run: |
|
||||
# if command -v r2 >/dev/null 2>&1; then
|
||||
# r2 -q -c 'r2ai -h' -- || true
|
||||
# fi
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Verify tag is on master
|
||||
run: |
|
||||
git fetch origin master:refs/remotes/origin/master
|
||||
if ! git branch -r --contains ${GITHUB_SHA} | grep -q "origin/master"; then
|
||||
echo "Tag commit is not on origin/master — skipping release."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: release-artifacts
|
||||
|
||||
- name: Add decai.r2.js to release artifacts
|
||||
run: cp decai/decai.r2.js release-artifacts/
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
release-artifacts/**/*.zip
|
||||
release-artifacts/decai.r2.js
|
||||
generate_release_notes: true
|
||||
- uses: actions/checkout@v4
|
||||
- name: Installing radare2
|
||||
run: |
|
||||
wget "https://github.com/radareorg/radare2/releases/download/${R2V}/radare2_${R2V}_amd64.deb"
|
||||
wget "https://github.com/radareorg/radare2/releases/download/${R2V}/radare2-dev_${R2V}_amd64.deb"
|
||||
sudo dpkg -i "radare2_${R2V}_amd64.deb"
|
||||
sudo dpkg -i "radare2-dev_${R2V}_amd64.deb"
|
||||
- name: Setting up the Python virtual environment
|
||||
run: |
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install pylint
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
- name: Linting
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
make cilint
|
||||
- name: Running r2ai from the shell
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python main.py -h
|
||||
- name: Running r2ai from r2pipe
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
r2 -q -c '#!pipe python main.py -h' /bin/ls
|
||||
- name: Running r2ai from r2
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
r2 -q -i main.py -c r2ai /bin/ls
|
||||
@@ -1,14 +1,3 @@
|
||||
build
|
||||
b
|
||||
src/b
|
||||
src/r2ai
|
||||
src/r2ai.dSYM
|
||||
venv
|
||||
**/__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.o
|
||||
*.so
|
||||
*.dylib
|
||||
*.a
|
||||
third_party
|
||||
**/__pycache__
|
||||
@@ -1,28 +0,0 @@
|
||||
from transformers import AutoTokenizer
|
||||
import transformers
|
||||
import torch
|
||||
|
||||
model = "facebook/llm-compiler-13b"
|
||||
model = "QuantFactory/llm-compiler-7b-GGUF"
|
||||
# model = "file:///tmp/llm-compiler.gguf"
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained(model)
|
||||
pipeline = transformers.pipeline(
|
||||
"text-generation",
|
||||
model=model,
|
||||
torch_dtype=torch.float16,
|
||||
device_map="auto",
|
||||
)
|
||||
|
||||
sequences = pipeline(
|
||||
'%3 = alloca i32, align 4',
|
||||
do_sample=True,
|
||||
top_k=10,
|
||||
temperature=0.1,
|
||||
top_p=0.95,
|
||||
num_return_sequences=1,
|
||||
eos_token_id=tokenizer.eos_token_id,
|
||||
max_length=200,
|
||||
)
|
||||
for seq in sequences:
|
||||
print(f"Result: {seq['generated_text']}")
|
||||
@@ -1,249 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
// Token representation
|
||||
struct Token {
|
||||
var token: String
|
||||
var count: Int
|
||||
var df: Int
|
||||
}
|
||||
|
||||
// KD-Tree Node
|
||||
class KDNodeData {
|
||||
let point: [Float]
|
||||
let text: String
|
||||
var left: KDNodeData?
|
||||
var right: KDNodeData?
|
||||
let splitDim: Int
|
||||
|
||||
init(point: [Float], text: String, left: KDNodeData? = nil, right: KDNodeData? = nil, splitDim: Int) {
|
||||
self.point = point
|
||||
self.text = text
|
||||
self.left = left
|
||||
self.right = right
|
||||
self.splitDim = splitDim
|
||||
}
|
||||
}
|
||||
|
||||
// Result for K-Nearest Neighbors
|
||||
struct KNNResult {
|
||||
let node: KDNodeData
|
||||
let distanceSq: Float
|
||||
}
|
||||
|
||||
class VectorDB {
|
||||
// Core database structures
|
||||
private let dimension: Int
|
||||
private var root: KDNodeData?
|
||||
private var tokens: [Token] = []
|
||||
private var totalDocs: Int = 0
|
||||
private var size: Int = 0
|
||||
|
||||
init(dimension: Int) {
|
||||
self.dimension = dimension
|
||||
}
|
||||
|
||||
// Validate token (similar to C implementation)
|
||||
private func isValidToken(_ token: String) -> Bool {
|
||||
let invalidTokens = ["pancake", "author", "radare2"]
|
||||
return !invalidTokens.contains(token)
|
||||
}
|
||||
|
||||
// Find a token in the token list
|
||||
private func findToken(_ tokens: [Token], _ token: String) -> Token? {
|
||||
return tokens.first { $0.token == token }
|
||||
}
|
||||
|
||||
// Compute embedding using TF-IDF approach
|
||||
private func computeEmbedding(_ text: String) -> [Float] {
|
||||
// Zero the embedding vector
|
||||
var embedding = [Float](repeating: 0.0, count: dimension)
|
||||
|
||||
// Tokenize and normalize
|
||||
let words = text
|
||||
.lowercased()
|
||||
.replacingOccurrences(of: "[^a-z0-9\\s]", with: " ", options: .regularExpression)
|
||||
.split(separator: " ")
|
||||
.map { String($0) }
|
||||
.filter { !$0.isEmpty }
|
||||
|
||||
// Local document token frequency
|
||||
var docTokens: [Token] = []
|
||||
for token in words {
|
||||
if let existingToken = findToken(docTokens, token) {
|
||||
let index = docTokens.firstIndex { $0.token == token }!
|
||||
docTokens[index].count += 1
|
||||
} else {
|
||||
docTokens.append(Token(token: token, count: 1, df: 0))
|
||||
}
|
||||
}
|
||||
|
||||
// Increment total documents
|
||||
totalDocs += 1
|
||||
|
||||
// Update global document frequencies
|
||||
for docToken in docTokens {
|
||||
if let globalToken = findToken(self.tokens, docToken.token) {
|
||||
if isValidToken(docToken.token) {
|
||||
let index = self.tokens.firstIndex { $0.token == docToken.token }!
|
||||
self.tokens[index].count += 1
|
||||
self.tokens[index].df += 1
|
||||
}
|
||||
} else {
|
||||
self.tokens.append(Token(token: docToken.token, count: docToken.count, df: 1))
|
||||
}
|
||||
}
|
||||
|
||||
// Compute TF-IDF and update embedding
|
||||
for docToken in docTokens {
|
||||
// Compute term frequency
|
||||
let tf = 1 + log(Float(docToken.count))
|
||||
|
||||
// Find global token to get document frequency
|
||||
let dfValue = findToken(self.tokens, docToken.token)?.df ?? 1
|
||||
|
||||
// Compute inverse document frequency
|
||||
let idf = log(Float(totalDocs + 1) / Float(dfValue + 1)) + 1
|
||||
|
||||
// Compute weight
|
||||
let weight = tf * idf
|
||||
|
||||
// Hash-based index assignment
|
||||
let hash = simpleHash(docToken.token)
|
||||
let index = hash % dimension
|
||||
|
||||
// Add weighted value to embedding
|
||||
embedding[index] += weight
|
||||
}
|
||||
|
||||
// L2 Normalize the embedding
|
||||
return normalizeVector(embedding)
|
||||
}
|
||||
|
||||
// Simple string hash function
|
||||
private func simpleHash(_ str: String) -> Int {
|
||||
var hash = 0
|
||||
for char in str.unicodeScalars {
|
||||
hash = ((hash << 5) - hash) + Int(char.value)
|
||||
hash = hash & hash // Convert to 32-bit integer
|
||||
}
|
||||
return abs(hash)
|
||||
}
|
||||
|
||||
// Normalize vector to unit length
|
||||
private func normalizeVector(_ vec: [Float]) -> [Float] {
|
||||
let normSq = vec.reduce(0) { $0 + $1 * $1 }
|
||||
if normSq > 0 {
|
||||
let norm = sqrt(normSq)
|
||||
return vec.map { $0 / norm }
|
||||
}
|
||||
return vec
|
||||
}
|
||||
|
||||
// Compute squared Euclidean distance
|
||||
private func squaredDistance(_ a: [Float], _ b: [Float]) -> Float {
|
||||
guard a.count == b.count else { return Float.greatestFiniteMagnitude }
|
||||
return zip(a, b).reduce(0) { $0 + pow($1.0 - $1.1, 2) }
|
||||
}
|
||||
|
||||
// Recursive KD-Tree insertion
|
||||
private func insertRecursive(_ node: KDNodeData?, point: [Float], text: String, depth: Int) -> KDNodeData {
|
||||
guard let currentNode = node else {
|
||||
return KDNodeData(point: point, text: text, splitDim: depth % dimension)
|
||||
}
|
||||
|
||||
let axis = currentNode.splitDim
|
||||
if point[axis] < currentNode.point[axis] {
|
||||
currentNode.left = insertRecursive(currentNode.left, point: point, text: text, depth: depth + 1)
|
||||
} else {
|
||||
currentNode.right = insertRecursive(currentNode.right, point: point, text: text, depth: depth + 1)
|
||||
}
|
||||
|
||||
return currentNode
|
||||
}
|
||||
|
||||
// Insert a document into the vector database
|
||||
func insert(_ text: String) {
|
||||
guard !text.isEmpty else { return }
|
||||
|
||||
// Compute embedding
|
||||
let embedding = computeEmbedding(text)
|
||||
|
||||
// Insert into KD-Tree
|
||||
root = insertRecursive(root, point: embedding, text: text, depth: 0)
|
||||
size += 1
|
||||
}
|
||||
|
||||
// K-Nearest Neighbors search
|
||||
func query(_ text: String, k: Int) -> [String] {
|
||||
guard !text.isEmpty, root != nil else { return [] }
|
||||
|
||||
// Compute query embedding
|
||||
let queryEmbedding = computeEmbedding(text)
|
||||
|
||||
// Initialize result set
|
||||
var results: [KNNResult] = []
|
||||
|
||||
// Recursive KNN search
|
||||
func searchRecursive(_ node: KDNodeData?, query: [Float], depth: Int) {
|
||||
guard let node = node else { return }
|
||||
|
||||
// Compute distance and potentially insert into results
|
||||
let distSq = squaredDistance(query, node.point)
|
||||
|
||||
if results.count < k {
|
||||
results.append(KNNResult(node: node, distanceSq: distSq))
|
||||
// Maintain max-heap property
|
||||
results.sort { $0.distanceSq > $1.distanceSq }
|
||||
} else if distSq < results[0].distanceSq {
|
||||
results[0] = KNNResult(node: node, distanceSq: distSq)
|
||||
results.sort { $0.distanceSq > $1.distanceSq }
|
||||
}
|
||||
|
||||
// Decide which subtree to search first
|
||||
let axis = node.splitDim
|
||||
let diff = query[axis] - node.point[axis]
|
||||
let first = diff < 0 ? node.left : node.right
|
||||
let second = diff < 0 ? node.right : node.left
|
||||
|
||||
// Recursively search
|
||||
searchRecursive(first, query: query, depth: depth + 1)
|
||||
|
||||
// Potentially search the other subtree
|
||||
if results.count < k || diff * diff < results[0].distanceSq {
|
||||
searchRecursive(second, query: query, depth: depth + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the search
|
||||
searchRecursive(root, query: queryEmbedding, depth: 0)
|
||||
|
||||
// Sort results and return texts
|
||||
return results
|
||||
.sorted { $0.distanceSq < $1.distanceSq }
|
||||
.map { $0.node.text }
|
||||
}
|
||||
|
||||
// Get total number of documents
|
||||
func getSize() -> Int {
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage
|
||||
func main() {
|
||||
// Create a vector database with 64-dimensional embeddings
|
||||
let db = VectorDB(dimension: 64)
|
||||
|
||||
// Insert some sample documents
|
||||
db.insert("The quick brown fox jumps over the lazy dog")
|
||||
db.insert("A quick brown fox is very quick")
|
||||
db.insert("The author of radare2 is name / named pancake")
|
||||
db.insert("The lazy dog sleeps all day")
|
||||
|
||||
// Query for similar documents
|
||||
let results = db.query("who is the author of radare2?", k: 2)
|
||||
print("Similar documents:", results)
|
||||
}
|
||||
|
||||
// Run example
|
||||
main()
|
||||
@@ -1,81 +0,0 @@
|
||||
R2_USER_PLUGINS=$(shell r2 -H R2_USER_PLUGINS)
|
||||
|
||||
PWD=$(shell pwd)
|
||||
R2PM_BINDIR=$(shell r2pm -H R2PM_BINDIR)
|
||||
PV=3.12
|
||||
ifeq ($(shell which python${PV} > /dev/null && echo ok),ok)
|
||||
PYTHON?=python${PV}
|
||||
else
|
||||
PYTHON?=python3
|
||||
endif
|
||||
PIP=$(PYTHON) -m pip
|
||||
|
||||
ifeq ($(R2PM_BINDIR),)
|
||||
MISSING RADARE2
|
||||
endif
|
||||
|
||||
LINTED=r2ai/code_block.py
|
||||
LINTED+=r2ai/bubble.py
|
||||
LINTED+=r2ai/const.py
|
||||
LINTED+=r2ai/backend/kobaldcpp.py
|
||||
# LINTED+=r2ai/index.py
|
||||
# LINTED+=r2ai/voice.py
|
||||
# LINTED+=r2ai/anthropic.py
|
||||
r2ai-python py python r2aipy: venv
|
||||
@./r2ai.sh
|
||||
|
||||
large:
|
||||
. venv/bin/activate ; $(PYTHON) -m r2ai.cli -l
|
||||
|
||||
all.old:
|
||||
@test -n "${VIRTUAL_ENV}" || (echo "Run:"; echo ". venv/bin/activate" ; exit 1)
|
||||
$(PYTHON) main.py || $(MAKE) deps
|
||||
|
||||
venv:
|
||||
$(PYTHON) -m venv venv
|
||||
if [ -z "`find venv | grep llama_cpp`" ]; then . venv/bin/activate ; pip install . ; fi
|
||||
|
||||
deps: venv
|
||||
#test -n "${VIRTUAL_ENV}" || (echo "Run: . venv/bin/activate" ; exit 1)
|
||||
. venv/bin/activate && export CMAKE_ARGS="-DLLAMA_METAL=on -DLLAMA_METAL_EMBED_LIBRARY=ON" && \
|
||||
pip install --force-reinstall -U --no-cache-dir .
|
||||
clean:
|
||||
rm -rf venv
|
||||
rm -rf build
|
||||
find . -name "*.egg-info" -exec rm -rf {} +
|
||||
|
||||
user-install:
|
||||
rm -f $(R2PM_BINDIR)/r2ai
|
||||
ln -fs $(PWD)/r2ai.sh $(R2PM_BINDIR)/r2ai
|
||||
|
||||
install: user-install
|
||||
-mkdir -p /usr/local/share/man/man1/r2ai.1
|
||||
-cp doc/usage/r2ai.1 /usr/local/share/man/man1/r2ai.1
|
||||
|
||||
install-plugin user-install-plugin:
|
||||
ln -fs $(PWD)/r2ai/plugin.py $(R2_USER_PLUGINS)/r2ai.py
|
||||
|
||||
uninstall user-uninstall:
|
||||
rm -f $(R2PM_BINDIR)/r2ai
|
||||
-rm -f /usr/local/share/man/man1/r2ai.1
|
||||
|
||||
user-uninstall-plugin uninstall-plugin:
|
||||
rm -f $(R2_USER_PLUGINS)/r2ai.py
|
||||
|
||||
pub:
|
||||
$(PYTHON) -m build
|
||||
twine check dist/*
|
||||
twine upload -u __token__ --repository-url https://upload.pypi.org/legacy/ --verbose dist/*
|
||||
|
||||
pylint lint cilint:
|
||||
. venv/bin/activate ; pylint $(LINTED)
|
||||
|
||||
lintall:
|
||||
pylint *.py r2ai/*.py
|
||||
|
||||
.PHONY: lint lintall
|
||||
|
||||
deps-global:
|
||||
export CMAKE_ARGS="-DLLAMA_METAL=on -DLLAMA_METAL_EMBED_LIBRARY=ON" && \
|
||||
$(PIP) install --force-reinstall -U --break-system-packages --no-cache-dir .
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
# r2ai-python
|
||||
|
||||
This directory contains the original implementation of the AI support for radare2 written in Python.
|
||||
|
||||
It is considered _deprecated_, but it's still useful and aims to be maintained, we recommend using decai or the C rewrite as alternatives.
|
||||
|
||||
The Python implementation consists in a module can be used in 3 different ways:
|
||||
|
||||
* commandline repl using r2pipe to spawn or connect to remote radare2 instances
|
||||
* radare2 core plugin that is instantiated when placing it in the radare2 plugin's directory
|
||||
* Python API to be used for writing your own scripts
|
||||
|
||||
## API Providers
|
||||
|
||||
The Python implementation focused on supporting the LlamaCpp Python module, which is somehow heavy to load in many environments, but it also supports litellm which provides access to many external connectors like ollama, openai, anthropic, gemini, etc.
|
||||
|
||||
## Deprecation Reasons
|
||||
|
||||
There are several reasons why this implementation is considered deprecated and not recommended to use:
|
||||
|
||||
1) It's Python.
|
||||
|
||||
- Aka, it's slow, heavy loads in same process
|
||||
- Your system probably have multiple versions installed
|
||||
- The shell and radare2 python bindings are probably not the same
|
||||
- Required venv to be created (at least an extra 1GB)
|
||||
- Not typed, regressions and bugs appear at runtime
|
||||
|
||||
2) Goto 1
|
||||
|
||||
### Windows
|
||||
|
||||
On Windows you may follow the same instructions, just ensure you have the right python environment ready and create the venv to use
|
||||
|
||||
```cmd
|
||||
git clone https://github.com/radareorg/r2ai
|
||||
cd r2ai
|
||||
set PATH=C:\Users\YOURUSERNAME\Local\Programs\Python\Python39\;%PATH%
|
||||
python3 -m pip install .
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
### Selecting the model
|
||||
|
||||
- List all downloaded models: `-m`
|
||||
- Get a short list of models: `-MM`
|
||||
- Help: `-h`
|
||||
|
||||
**Example selecting a remote models:**
|
||||
|
||||
```
|
||||
[r2ai:0x00006aa0]> -m anthropic:claude-3-7-sonnet-20250219
|
||||
[r2ai:0x00006aa0]> -m openai:gpt-4
|
||||
```
|
||||
|
||||
**Example downloading a free local AI: Mistral 7B v0.2:**
|
||||
|
||||
Launch r2ai, select the model and ask a question. If the model isn't downloaded yet, r2ai will ask you which precise version to download.
|
||||
|
||||
```
|
||||
[r2ai:0x00006aa0]> -m TheBloke/Mistral-7B-Instruct-v0.2-GGUF
|
||||
```
|
||||
|
||||
Then ask your question, and r2ai will automatically download if needed:
|
||||
|
||||
```
|
||||
[r2ai:0x00006aa0]> give me a short algorithm to test prime numbers
|
||||
Select TheBloke/Mistral-7B-Instruct-v0.2-GGUF model. See -M and -m flags
|
||||
[?] Quality (smaller is faster):
|
||||
> Small | Size: 2.9 GB, Estimated RAM usage: 5.4 GB
|
||||
Medium | Size: 3.9 GB, Estimated RAM usage: 6.4 GB
|
||||
Large | Size: 7.2 GB, Estimated RAM usage: 9.7 GB
|
||||
See More
|
||||
|
||||
[?] Quality (smaller is faster):
|
||||
> mistral-7b-instruct-v0.2.Q2_K.gguf | Size: 2.9 GB, Estimated RAM usage: 5.4 GB
|
||||
mistral-7b-instruct-v0.2.Q3_K_L.gguf | Size: 3.6 GB, Estimated RAM usage: 6.1 GB
|
||||
mistral-7b-instruct-v0.2.Q3_K_M.gguf | Size: 3.3 GB, Estimated RAM usage: 5.8 GB
|
||||
mistral-7b-instruct-v0.2.Q3_K_S.gguf | Size: 2.9 GB, Estimated RAM usage: 5.4 GB
|
||||
mistral-7b-instruct-v0.2.Q4_0.gguf | Size: 3.8 GB, Estimated RAM usage: 6.3 GB
|
||||
mistral-7b-instruct-v0.2.Q4_K_M.gguf | Size: 4.1 GB, Estimated RAM usage: 6.6 GB
|
||||
mistral-7b-instruct-v0.2.Q4_K_S.gguf | Size: 3.9 GB, Estimated RAM usage: 6.4 GB
|
||||
mistral-7b-instruct-v0.2.Q5_0.gguf | Size: 4.7 GB, Estimated RAM usage: 7.2 GB
|
||||
mistral-7b-instruct-v0.2.Q5_K_M.gguf | Size: 4.8 GB, Estimated RAM usage: 7.3 GB
|
||||
mistral-7b-instruct-v0.2.Q5_K_S.gguf | Size: 4.7 GB, Estimated RAM usage: 7.2 GB
|
||||
mistral-7b-instruct-v0.2.Q6_K.gguf | Size: 5.5 GB, Estimated RAM usage: 8.0 GB
|
||||
mistral-7b-instruct-v0.2.Q8_0.gguf | Size: 7.2 GB, Estimated RAM usage: 9.7 GB
|
||||
|
||||
[?] Use this model by default? ~/.r2ai.model:
|
||||
> Yes
|
||||
No
|
||||
|
||||
[?] Download to ~/.local/share/r2ai/models? (Y/n): Y
|
||||
```
|
||||
|
||||
**Example selecting a local model served by Ollama**
|
||||
|
||||
Download a model and make it available through Ollama:
|
||||
|
||||
```
|
||||
$ ollama ls
|
||||
NAME ID SIZE MODIFIED
|
||||
codegeex4:latest 867b8e81d038 5.5 GB 23 hours ago
|
||||
```
|
||||
|
||||
Use it from r2ai by prefixing its name with `ollama/`
|
||||
|
||||
```
|
||||
[r2ai:0x00002d30]> -m ollama/codegeex4:latest
|
||||
[r2ai:0x00002d30]> hi
|
||||
Hello! How can I assist you today?
|
||||
```
|
||||
|
||||
### Standard/Auto mode
|
||||
|
||||
The standard mode is invoked by directly asking the question.
|
||||
For the Auto mode, the question **must be prefixed** by `' ` (quote + space). The AI may instruct r2ai to run various commands. Those commands are run on *your host*, so you will be asked to review them before they run.
|
||||
|
||||
Example in "standard" mode:
|
||||
|
||||
```
|
||||
[r2ai:0x00006aa0]> compute 4+5
|
||||
4 + 5 = 9
|
||||
[r2ai:0x00006aa0]> draw me a pancake in ASCII art
|
||||
Sure, here's a simple ASCII pancake:
|
||||
|
||||
_____
|
||||
( )
|
||||
( )
|
||||
-----
|
||||
```
|
||||
|
||||
Example in auto mode:
|
||||
|
||||
```
|
||||
[r2ai:0x00006aa0]>' Decompile the main
|
||||
[..]
|
||||
r2ai is going to execute the following command on the host
|
||||
Want to edit? (ENTER to validate) pdf @ fcn.000015d0
|
||||
This command will execute on this host: pdf @ fcn.000015d0. Agree? (y/N) y
|
||||
```
|
||||
|
||||
If you wish to edit the command, you can do it inline for short one line commands, or an editor will pop up.
|
||||
|
||||
### r2ai Configuration settings
|
||||
|
||||
List all settings with `-e`
|
||||
|
||||
| Key | Explanation |
|
||||
| ----------- | ------------------------------------- |
|
||||
| debug_level | All verbose messages for level 1. Default is 2 |
|
||||
| auto.max_runs | Maximum number of questions the AI is allowed to ask r2 in auto mode. |
|
||||
| auto.hide_tool_output | By default false, consequently output of r2cmd, run_python etc is shown. Set to `true` to hide those internal messages. |
|
||||
| chat.show_cost | Show the cost of each request to the AI if true |
|
||||
|
||||
@@ -1,511 +0,0 @@
|
||||
import builtins
|
||||
import json
|
||||
import sys
|
||||
import re
|
||||
from . import LOGGER
|
||||
import litellm
|
||||
from litellm import _should_retry, acompletion, utils, ModelResponse
|
||||
import asyncio
|
||||
from .pipe import get_filename
|
||||
from .tools import r2cmd, run_python, execute_binary, schemas, print_tool_call
|
||||
import json
|
||||
import signal
|
||||
from .spinner import spinner
|
||||
from .completion import create_chat_completion
|
||||
import uuid
|
||||
import time
|
||||
import readline
|
||||
import subprocess
|
||||
import shlex
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
# litellm.drop_params = True
|
||||
# litellm.set_verbose=True
|
||||
|
||||
ANSI_REGEX = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
||||
|
||||
SYSTEM_PROMPT_AUTO = """
|
||||
You are a reverse engineer and you are using radare2 to analyze a binary.
|
||||
The user will ask questions about the binary and you will respond with the answer to the best of your ability.
|
||||
|
||||
# Guidelines
|
||||
- Understand the Task: Grasp the main objective, goals, requirements, constraints, and expected output.
|
||||
- Reasoning Before Conclusions**: Encourage reasoning steps before any conclusions are reached.
|
||||
- Assume the user is always asking you about the binary, unless they're specifically asking you for radare2 help.
|
||||
- The binary has already been loaded. You can interact with the binary using the r2cmd tool.
|
||||
- `this` or `here` might refer to the current address in the binary or the binary itself.
|
||||
- If you need more information, try to use the r2cmd tool to run commands before answering.
|
||||
- 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.
|
||||
- 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.
|
||||
- Don't just regurgitate the same code, figure out what it's doing and rewrite it to be more understandable.
|
||||
- If you need to run a command in r2 before answering, you can use the r2cmd tool
|
||||
- Do not repeat commands if you already know the answer.
|
||||
- Formulate a plan. Think step by step. Analyze the binary as much as possible before answering.
|
||||
- You must keep going until you have a final answer.
|
||||
- Double check that final answer. Make sure you didn't miss anything.
|
||||
- Make sure you call tools and functions correctly.
|
||||
"""
|
||||
|
||||
SYSTEM_PROMPT_AUTO_REASONING = f"""
|
||||
You are an expert reverse engineer.
|
||||
The user will ask questions about the binary and you will respond with the answer to the best of your ability.
|
||||
You can use the available tools to help you answer the user's question.
|
||||
The binary has already been loaded. You can interact with the binary using the r2cmd tool.
|
||||
If you need to run a command in r2 before answering, you can use the r2cmd tool
|
||||
"""
|
||||
|
||||
class ChatAuto:
|
||||
def __init__(self, model, max_tokens = 32000, top_p=0.95, temperature=0.0, interpreter=None, system=None, tools=None, ask_to_execute=True, messages=None, tool_choice='auto', llama_instance=None, timeout=60, stream=True, cb=None, max_runs=100):
|
||||
self.logger = LOGGER
|
||||
self.functions = {}
|
||||
self.tools = []
|
||||
self.model = model
|
||||
self.max_tokens = max_tokens
|
||||
self.top_p = top_p
|
||||
self.temperature = temperature
|
||||
self.system = system
|
||||
self.messages = messages
|
||||
self.interpreter = interpreter
|
||||
self.system_message = None
|
||||
self.timeout = timeout
|
||||
self.stream = stream
|
||||
self.cost = 0
|
||||
self.is_reasoning = False
|
||||
self.n_runs = 0
|
||||
self.max_runs = max_runs
|
||||
self._last_response = None
|
||||
self._start_time = time.time()
|
||||
self._last_run_time = time.time()
|
||||
self.ask_to_execute = ask_to_execute
|
||||
self.llama_instance = llama_instance or interpreter.llama_instance if interpreter else None
|
||||
|
||||
init_prompt = ""
|
||||
|
||||
if not self.llama_instance:
|
||||
rmodel = re.sub(r'ft:([^:]+).*', r'\1', self.model)
|
||||
model_info = litellm.get_model_info(rmodel)
|
||||
self.max_tokens = min(self.max_tokens, model_info['max_tokens'])
|
||||
|
||||
if self.model.startswith('openai/o'):
|
||||
self.is_reasoning = True
|
||||
self.max_tokens = model_info['max_tokens']
|
||||
self.timeout = 60 * 60
|
||||
init_commands = self.interpreter.env["auto.init_commands"]
|
||||
|
||||
if init_commands:
|
||||
init_prompt = f"""
|
||||
Here is some information about the binary to get you started:
|
||||
> {init_commands}
|
||||
{r2cmd(init_commands)}
|
||||
"""
|
||||
if self.is_reasoning:
|
||||
if not system and not self.messages[0]['role'] == 'developer':
|
||||
self.messages.insert(0, { "role": "developer", "content": SYSTEM_PROMPT_AUTO_REASONING + init_prompt })
|
||||
self.stream = False
|
||||
else:
|
||||
if messages and messages[0]['role'] != 'system':
|
||||
self.messages.insert(0, { "role": "system", "content": system or SYSTEM_PROMPT_AUTO + init_prompt })
|
||||
|
||||
|
||||
if cb:
|
||||
self.cb = cb
|
||||
else:
|
||||
self.cb = lambda *args: None
|
||||
self.tool_choice = None
|
||||
if tools:
|
||||
for tool in tools:
|
||||
if tool.__name__ in schemas:
|
||||
schema = schemas[tool.__name__]
|
||||
else:
|
||||
schema = utils.function_to_dict(tool)
|
||||
|
||||
self.tools.append({ "type": "function", "function": schema })
|
||||
self.functions[tool.__name__] = tool
|
||||
self.tool_choice = tool_choice
|
||||
#self.tool_end_message = '\nNOTE: The user saw this output, do not repeat it.'
|
||||
|
||||
async def process_tool_calls(self, tool_calls):
|
||||
if tool_calls:
|
||||
for tool_call in tool_calls:
|
||||
self.logger.debug(f"tool_call: {tool_call}")
|
||||
tool_name = tool_call["function"]["name"]
|
||||
if "id" not in tool_call:
|
||||
tool_call["id"] = str(uuid.uuid4())
|
||||
try:
|
||||
tool_args = json.loads(tool_call["function"]["arguments"])
|
||||
except Exception:
|
||||
if "arguments" in tool_call["function"] and type(tool_call["function"]["arguments"]) == dict:
|
||||
tool_args = tool_call["function"]["arguments"]
|
||||
else:
|
||||
self.logger.error(f'Error parsing JSON: {tool_call["function"]["arguments"]}')
|
||||
# raise Exception('Error parsing JSON')
|
||||
self.messages.append({"role": "tool", "name": tool_name, "content": "Error: Unable to parse JSON" , "tool_call_id": tool_call["id"]})
|
||||
continue
|
||||
if tool_name not in self.functions:
|
||||
self.logger.error(f'Tool not found: {tool_name}')
|
||||
self.messages.append({"role": "tool", "name": tool_name, "content": "Error: Tool not found" , "tool_call_id": tool_call["id"]})
|
||||
continue
|
||||
|
||||
if self.ask_to_execute:
|
||||
# tool_call typically contains:
|
||||
# { 'id': 'toolu_01Hr6XTatRtp7rX54iBu1voC', 'type': 'function', 'function': {'name': 'r2cmd', 'arguments': '{"command": "pdf @ main"}'}}
|
||||
answer = ''
|
||||
will_execute = True # a command (modified or not) will be executed
|
||||
while answer.lower() != 'y':
|
||||
try:
|
||||
command = tool_args.get('command', '')
|
||||
|
||||
if len(command.splitlines()) > 1:
|
||||
print(f'r2ai is going to execute the following command on the host:')
|
||||
print(f'{command}')
|
||||
want_edit = input('Want to edit? (Y/n) ')
|
||||
if want_edit.lower() != 'n':
|
||||
# open editor when several lines
|
||||
with tempfile.NamedTemporaryFile(mode='w+', delete=False) as temp_file:
|
||||
temp_file.write(command)
|
||||
temp_filename = temp_file.name
|
||||
|
||||
# Fallback to 'nano' if EDITOR is not set (I still prefer emacs)
|
||||
editor = os.environ.get('EDITOR', 'nano')
|
||||
subprocess.run(shlex.split(editor + ' ' + temp_filename))
|
||||
with open(temp_filename, "r") as f:
|
||||
new_command = f.read().strip()
|
||||
os.remove(temp_filename)
|
||||
else:
|
||||
self.logger.debug(f'User agrees with not editing the command: {command}')
|
||||
answer = 'y'
|
||||
|
||||
else:
|
||||
# inline short edit
|
||||
print(f'r2ai is going to execute the following command on the host:')
|
||||
print(f'> {command}')
|
||||
|
||||
new_command = input(f"Type a new command or press ENTER to use {command} ") or command
|
||||
|
||||
if answer.lower() != 'y':
|
||||
answer = input(f"\033[91mThis command will execute on this host: {new_command}. Agree? (y/N)\033[0m ")
|
||||
if answer.lower() == 'y':
|
||||
# we need to refresh tool_call and tool_args with edited values
|
||||
tool_args['command'] = new_command
|
||||
tool_call['function']['arguments'] = json.dumps(tool_args)
|
||||
|
||||
except (json.JSONDecodeError, TypeError, AttributeError) as e:
|
||||
# in case tool_call is not formatted as expected, like missing command, arguments, function...
|
||||
self.logger.error(f'Unexpected format for {tool_call}')
|
||||
self.messages.append({"role": "tool", "name": tool_name, "content": "User refused to execute the command" , "tool_call_id": tool_call["id"]})
|
||||
# error case: we'll skip the tool
|
||||
will_execute = False
|
||||
break
|
||||
|
||||
if not will_execute:
|
||||
self.logger.debug(f'Skipping tool={tool_call}')
|
||||
continue
|
||||
|
||||
self.logger.debug(f'Executing tool id={tool_call["id"]}, name={tool_name}, arguments={tool_args}')
|
||||
self.cb('tool_call', { "id": tool_call["id"], "function": { "name": tool_name, "arguments": tool_args } })
|
||||
if asyncio.iscoroutinefunction(self.functions[tool_name]):
|
||||
tool_response = await self.functions[tool_name](**tool_args)
|
||||
else:
|
||||
tool_response = self.functions[tool_name](**tool_args)
|
||||
if self.interpreter.env["auto.hide_tool_output"] == "false":
|
||||
self.cb('tool_response', { "id": tool_call["id"] + '_response', "content": tool_response })
|
||||
self.messages.append({"role": "tool", "name": tool_name, "content": ANSI_REGEX.sub('', tool_response), "tool_call_id": tool_call["id"]})
|
||||
|
||||
return await self.get_completion()
|
||||
|
||||
async def process_streaming_response(self, resp):
|
||||
tool_calls = []
|
||||
msgs = []
|
||||
parts = []
|
||||
current_message = { "role": "assistant", "content": "", "tool_calls": [] }
|
||||
first_chunk = True
|
||||
async for chunk in resp:
|
||||
delta = None
|
||||
choice = chunk.choices[0]
|
||||
delta = choice.delta
|
||||
if delta.tool_calls:
|
||||
delta_tool_calls = delta.tool_calls[0]
|
||||
index = delta_tool_calls.index
|
||||
fn_delta = delta_tool_calls.function
|
||||
tool_call_id = delta_tool_calls.id
|
||||
if len(current_message['tool_calls']) < index + 1:
|
||||
tool_call = {
|
||||
"id": tool_call_id or str(uuid.uuid4()),
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name":fn_delta.name,
|
||||
"arguments": fn_delta.arguments
|
||||
}
|
||||
}
|
||||
current_message['tool_calls'].append(tool_call)
|
||||
else:
|
||||
if fn_delta.name:
|
||||
current_message['tool_calls'][index]["function"]["name"] = fn_delta.name
|
||||
current_message['tool_calls'][index]["function"]["arguments"] += fn_delta.arguments
|
||||
else:
|
||||
m = None
|
||||
done = False
|
||||
if delta.content is not None:
|
||||
if first_chunk:
|
||||
self.cb('message', { 'content': "\n\x1b[1;32massistant\x1b[0m\n", 'done': True })
|
||||
first_chunk = False
|
||||
m = delta.content
|
||||
if m is not None:
|
||||
current_message['content'] += m
|
||||
self.cb('message', { "content": m, "id": 'message_' + chunk.id, 'done': False })
|
||||
if 'finish_reason' in choice and choice['finish_reason'] == 'stop':
|
||||
done = True
|
||||
self.cb('message', { "content": "", "id": 'message_' + chunk.id, 'done': True })
|
||||
self.cb('message_stream', { "content": m if m else '', "id": 'message_' + chunk.id, 'done': done })
|
||||
self.messages.append(current_message)
|
||||
if len(current_message['tool_calls']) == 0:
|
||||
del current_message['tool_calls']
|
||||
self.show_cost()
|
||||
else:
|
||||
self.show_cost()
|
||||
await self.process_tool_calls(current_message['tool_calls'])
|
||||
|
||||
return current_message
|
||||
|
||||
async def process_response(self, resp):
|
||||
content = resp.choices[0].message.content
|
||||
|
||||
current_message = { 'role': 'assistant', 'content': content or '', 'tool_calls': [] }
|
||||
for i, tool_call in enumerate(resp.choices[0].message.tool_calls or []):
|
||||
current_message['tool_calls'].append({
|
||||
"id": tool_call.id,
|
||||
"type": "function",
|
||||
"index": i,
|
||||
"function": {
|
||||
"name": tool_call.function.name,
|
||||
"arguments": tool_call.function.arguments
|
||||
},
|
||||
})
|
||||
if len(current_message['tool_calls']) == 0:
|
||||
try:
|
||||
tool_call = json.loads(content)
|
||||
if 'name' in tool_call and tool_call['name'] in self.functions:
|
||||
args = None
|
||||
if 'arguments' in tool_call:
|
||||
args = tool_call['arguments']
|
||||
elif 'parameters' in tool_call:
|
||||
args = tool_call['parameters']
|
||||
if args:
|
||||
current_message['tool_calls'].append({ "id": resp.id, "function": { "name": tool_call['name'], "arguments": json.dumps(args) } })
|
||||
except Exception:
|
||||
pass
|
||||
self.messages.append(current_message)
|
||||
if current_message['content'] != "":
|
||||
self.cb('message', { 'content': "\n\x1b[1;32massistant\x1b[0m\n", 'done': True })
|
||||
self.cb('message', { "content": current_message['content'], "id": 'message_' + resp.id, 'done': True })
|
||||
|
||||
if len(current_message['tool_calls']) == 0:
|
||||
del current_message['tool_calls']
|
||||
self.show_cost()
|
||||
else:
|
||||
self.show_cost()
|
||||
await self.process_tool_calls(current_message['tool_calls'])
|
||||
|
||||
return current_message
|
||||
|
||||
async def async_response_generator(self, response):
|
||||
for item in response:
|
||||
resp = ModelResponse(stream=True, **item)
|
||||
yield resp
|
||||
|
||||
async def attempt_completion(self):
|
||||
stream = self.stream
|
||||
if self.llama_instance:
|
||||
args = {
|
||||
"temperature": self.temperature,
|
||||
"top_p": self.top_p,
|
||||
"max_tokens": self.max_tokens,
|
||||
"stream": stream,
|
||||
}
|
||||
if self.interpreter.env["chat.rawdog"] == "true":
|
||||
res = create_chat_completion(self.interpreter, messages=self.messages, tools=self.tools, **args)
|
||||
else:
|
||||
res = self.llama_instance.create_chat_completion(self.messages, tools=self.tools, **args)
|
||||
if args['stream']:
|
||||
return self.async_response_generator(res)
|
||||
else:
|
||||
return ModelResponse(**next(res))
|
||||
additional_args = {}
|
||||
if self.is_reasoning:
|
||||
additional_args['reasoning_effort'] = self.interpreter.env["chat.reasoning_effort"]
|
||||
else:
|
||||
additional_args['temperature'] = self.temperature
|
||||
if self.interpreter.env["chat.openai_store"] == "true":
|
||||
additional_args['store'] = True
|
||||
if not self.check_max():
|
||||
additional_args['tools'] = self.tools
|
||||
additional_args['tool_choice'] = self.tool_choice
|
||||
compl = await acompletion(
|
||||
model=self.model,
|
||||
messages=self.messages,
|
||||
timeout=self.timeout,
|
||||
max_completion_tokens=self.max_tokens,
|
||||
**additional_args,
|
||||
stream=stream,
|
||||
)
|
||||
return compl
|
||||
|
||||
def check_max(self):
|
||||
if self.max_runs > 0 and self.n_runs >= self.max_runs - 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
def show_cost(self):
|
||||
def format_time(seconds):
|
||||
minutes = int(seconds // 60)
|
||||
remaining_seconds = int(seconds % 60)
|
||||
if minutes > 0:
|
||||
return f"{minutes}m {remaining_seconds}s"
|
||||
return f"{remaining_seconds}s"
|
||||
self.n_runs += 1
|
||||
run_time = format_time(time.time() - self._last_run_time)
|
||||
self._last_run_time = time.time()
|
||||
total_time = format_time(time.time() - self._start_time)
|
||||
|
||||
if self.interpreter.env["chat.show_cost"] == "true" and self.n_runs > 0:
|
||||
run_cost = 0
|
||||
if not self.llama_instance:
|
||||
run_cost = litellm.completion_cost(completion_response=self._last_response, messages=self.messages, model=self.model)
|
||||
self.cost += run_cost
|
||||
cb('usage', {
|
||||
"model": self.model,
|
||||
"run_cost": run_cost,
|
||||
"total_cost": self.cost,
|
||||
"n_runs": self.n_runs,
|
||||
"max_runs": self.max_runs,
|
||||
"run_time": run_time,
|
||||
"total_time": total_time
|
||||
})
|
||||
|
||||
async def get_completion(self):
|
||||
stream = self.stream
|
||||
if self.llama_instance:
|
||||
response = await self.attempt_completion()
|
||||
if stream:
|
||||
return await self.process_streaming_response(response)
|
||||
else:
|
||||
return await self.process_response(response)
|
||||
max_retries = 5
|
||||
base_delay = 2
|
||||
|
||||
for retry_count in range(max_retries):
|
||||
try:
|
||||
response = await self.attempt_completion()
|
||||
self._last_response = response
|
||||
self.logger.debug(f'chat completion {response}')
|
||||
res = None
|
||||
if stream:
|
||||
res = await self.process_streaming_response(response)
|
||||
else:
|
||||
res = await self.process_response(response)
|
||||
|
||||
return res
|
||||
except Exception as e:
|
||||
self.logger.error(f'Error getting completion: {e}')
|
||||
if not _should_retry(getattr(e, 'status_code', None)) or retry_count == max_retries - 1:
|
||||
raise
|
||||
|
||||
delay = base_delay * (2 ** retry_count)
|
||||
self.logger.info(f"Retrying in {delay} seconds...")
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
raise Exception("Max retries reached. Unable to get completion.")
|
||||
|
||||
async def achat(self, messages=None) -> str:
|
||||
if messages:
|
||||
self.messages = messages
|
||||
self.logger.debug(self.messages)
|
||||
response = await self.get_completion()
|
||||
return response
|
||||
|
||||
def chat(self, **kwargs) -> str:
|
||||
return asyncio.run(self.achat(**kwargs))
|
||||
|
||||
def cb(type, data):
|
||||
spinner.stop()
|
||||
if type == 'message_stream':
|
||||
if 'content' in data:
|
||||
sys.stdout.write(data['content'])
|
||||
elif type == 'tool_call':
|
||||
builtins.print()
|
||||
print_tool_call(data)
|
||||
elif type == 'tool_response':
|
||||
if 'content' in data:
|
||||
sys.stdout.write(data['content'])
|
||||
sys.stdout.flush()
|
||||
builtins.print()
|
||||
elif type == 'usage':
|
||||
sys.stdout.flush()
|
||||
builtins.print()
|
||||
sys.stdout.write(f'\x1b[1;34m{data["model"]} | total: ${float(data["total_cost"]):.10f} | run: ${float(data["run_cost"]):.10f} | {data["n_runs"]} / {data["max_runs"]} | {data["run_time"]} / {data["total_time"]}\x1b[0m')
|
||||
sys.stdout.flush()
|
||||
builtins.print()
|
||||
elif type == 'message' and data['done']:
|
||||
if data['content']:
|
||||
sys.stdout.flush()
|
||||
sys.stdout.write(data['content'])
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
builtins.print()
|
||||
|
||||
def signal_handler(signum, frame):
|
||||
raise KeyboardInterrupt
|
||||
|
||||
def chat(interpreter, **kwargs):
|
||||
model = interpreter.model.replace(":", "/")
|
||||
tools = [r2cmd, run_python, execute_binary]
|
||||
messages = interpreter.messages
|
||||
tool_choice = 'auto'
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_closed() or loop.is_running():
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
chat_auto = ChatAuto(
|
||||
model,
|
||||
max_tokens=int(interpreter.env["llm.maxtokens"]),
|
||||
top_p=float(interpreter.env["llm.top_p"]),
|
||||
temperature=float(interpreter.env["llm.temperature"]),
|
||||
interpreter=interpreter,
|
||||
tools=tools,
|
||||
ask_to_execute=interpreter.env["auto.ask_to_execute"] == "true",
|
||||
messages=messages,
|
||||
tool_choice=tool_choice,
|
||||
llama_instance=interpreter.llama_instance,
|
||||
stream=interpreter.env["chat.stream"] == "true",
|
||||
max_runs=int(interpreter.env["auto.max_runs"]),
|
||||
cb=cb
|
||||
)
|
||||
|
||||
original_handler = signal.getsignal(signal.SIGINT)
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
spinner.start()
|
||||
return loop.run_until_complete(chat_auto.achat())
|
||||
except KeyboardInterrupt:
|
||||
builtins.print("\033[91m\nOperation cancelled by user.\033[0m")
|
||||
tasks = asyncio.all_tasks(loop=loop)
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
try:
|
||||
loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
|
||||
loop.run_until_complete(asyncio.sleep(0.1))
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
return None
|
||||
finally:
|
||||
signal.signal(signal.SIGINT, original_handler)
|
||||
spinner.stop()
|
||||
litellm.in_memory_llm_clients_cache.flush_cache()
|
||||
@@ -1,3 +0,0 @@
|
||||
from .main import run # pylint: disable=wrong-import-position
|
||||
|
||||
run()
|
||||
@@ -1,13 +0,0 @@
|
||||
from typing import Iterable, Callable
|
||||
|
||||
class R2AiEnv(dict):
|
||||
def __init__(self):
|
||||
self._callbacks = {}
|
||||
|
||||
def add_callback(self, key: str, callback: Callable):
|
||||
self._callbacks[key] = callback
|
||||
|
||||
def __setitem__(self, __key, __value) -> None:
|
||||
if __key in self._callbacks:
|
||||
self._callbacks[__key](__value)
|
||||
return super().__setitem__(__key, __value)
|
||||
@@ -1,579 +0,0 @@
|
||||
import builtins
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import json
|
||||
import platform
|
||||
have_local = True
|
||||
import getpass
|
||||
|
||||
try:
|
||||
import tokentrim
|
||||
except Exception:
|
||||
have_local = False
|
||||
|
||||
from rich.rule import Rule
|
||||
from signal import signal, SIGINT
|
||||
|
||||
from .env import R2AiEnv
|
||||
from .large import Large
|
||||
from .utils import merge_deltas
|
||||
from .message_block import MessageBlock
|
||||
from .code_block import CodeBlock
|
||||
from .backend import kobaldcpp
|
||||
|
||||
from .models import get_hf_llm, new_get_hf_llm, get_default_model
|
||||
from .voice import tts
|
||||
from .const import R2AI_HOMEDIR
|
||||
from . import LOGGER, logging
|
||||
from .web import stop_http_server, server_running
|
||||
from .progress import progress_bar
|
||||
|
||||
file_dir = os.path.dirname(__file__)
|
||||
sys.path.append(file_dir)
|
||||
import index
|
||||
|
||||
r2clippy = False
|
||||
have_rlang = False
|
||||
try:
|
||||
import r2lang
|
||||
have_rlang = True
|
||||
print = r2lang.print
|
||||
r2clippy = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
Ginterrupted = False
|
||||
def signal_handler(sig, frame):
|
||||
global Ginterrupted
|
||||
if Ginterrupted:
|
||||
stop_http_server(force=True) # kill if in background
|
||||
sys.exit(0) # throws exception
|
||||
Ginterrupted = True
|
||||
print("^C 0", file=sys.stderr)
|
||||
stop_http_server(force=False)
|
||||
signal(SIGINT, signal_handler)
|
||||
|
||||
def exception_handler(self, sig, frame):
|
||||
global Ginterrupted
|
||||
traceback.print_exc()
|
||||
if Ginterrupted:
|
||||
sys.exit(0) # throws exception
|
||||
Ginterrupted = True
|
||||
print("^C 1", file=sys.stderr)
|
||||
# sys.excepthook = exception_handler
|
||||
|
||||
def incodeblock(msg):
|
||||
return "content" in msg and msg["content"].count("```") % 2 == 1
|
||||
|
||||
def r2eval(m):
|
||||
if "$(" in m and have_rlang:
|
||||
def evaluate_expression(match):
|
||||
expression = match.group(1)
|
||||
try:
|
||||
result = r2lang.cmd(expression)
|
||||
return result
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
return re.sub(r'\$\((.*?)\)', evaluate_expression, m)
|
||||
return m
|
||||
|
||||
from utils import syscmdstr
|
||||
|
||||
def ddg(m):
|
||||
print("[R2AI] Crawling the web with ddg and curl+sed")
|
||||
m = m.replace("'", "")
|
||||
res = syscmdstr(f"cd examples; ./scrap-ddgweb.sh '{m}'")
|
||||
return f"Considering:\n```{res}\n```\n"
|
||||
|
||||
def is_litellm_model(model):
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
import litellm
|
||||
provider = None
|
||||
model_name = None
|
||||
if model.startswith ("/"):
|
||||
return False
|
||||
provider = None
|
||||
if "/" in model:
|
||||
provider = model.split("/")[0]
|
||||
elif ":" in model:
|
||||
provider = model.split(":")[0]
|
||||
if provider in litellm.LITELLM_CHAT_PROVIDERS:
|
||||
return True
|
||||
return False
|
||||
|
||||
class Interpreter:
|
||||
def __init__(self):
|
||||
self.logger = LOGGER
|
||||
self.mistral = None
|
||||
self.messages = []
|
||||
self.terminator = "</s>" ## taken from the model using the llama api
|
||||
self.api_key = None # openai?
|
||||
self.print = None
|
||||
self.auto_run = False
|
||||
self.model = get_default_model()
|
||||
self.last_model = ""
|
||||
self.env = R2AiEnv()
|
||||
self.system_message = ""
|
||||
self.env["llm.model"] = self.model ## TODO: dup. must get rid of self.model
|
||||
self.env["llm.gpu"] = "true"
|
||||
self.env["llm.layers"] = "-1"
|
||||
self.env["llm.window"] = "32768" # "4096" # context_window
|
||||
self.env["llm.maxtokens"] = "4096" # "1750"
|
||||
self.env["llm.maxmsglen"] = "8096" # "1750"
|
||||
self.env["llm.temperature"] = "0.002"
|
||||
self.env["llm.repeat_penalty"] = "1.0"
|
||||
self.env["llm.top_p"] = "0.95"
|
||||
self.env["llm.top_k"] = "50"
|
||||
self.env["user.name"] = "" # TODO auto fill?
|
||||
self.env["user.os"] = ""
|
||||
self.env["user.arch"] = ""
|
||||
self.env["user.cwd"] = ""
|
||||
self.env["user.editor"] = ""
|
||||
self.env["user.plugins"] = f"{R2AI_HOMEDIR}/plugins"
|
||||
self.env["voice.lang"] = "en"
|
||||
self.env["voice.model"] = "base"
|
||||
self.env["data.use"] = "false"
|
||||
self.env["data.path"] = f"{R2AI_HOMEDIR}/doc/data"
|
||||
self.env["data.local"] = "false"
|
||||
self.env["data.wikit"] = "false"
|
||||
self.env["data.ddg"] = "false"
|
||||
self.env["data.mastodon"] = "false"
|
||||
self.env["data.vectordb"] = "false"
|
||||
self.env["data.hist"] = "false"
|
||||
self.env["key.mastodon"] = ""
|
||||
self.env["http.port"] = "8080"
|
||||
self.env["http.tabby"] = "false"
|
||||
self.env["http.path"] = ""
|
||||
self.env["http.verbose"] = "true" # not used yet
|
||||
self.env["http.chatctx"] = "false"
|
||||
self.env["debug_level"] = os.getenv("R2AI_LOG", "2")
|
||||
if have_rlang:
|
||||
self.env["chat.live"] = "false"
|
||||
else:
|
||||
self.env["chat.live"] = "true"
|
||||
# self.env["chat.model"] = "" # TODO
|
||||
self.env["chat.trim"] = "false"
|
||||
self.env["chat.voice"] = "false"
|
||||
self.env["chat.bubble"] = "false"
|
||||
self.env["chat.reply"] = "false"
|
||||
self.env["chat.code"] = "true"
|
||||
self.env["chat.rawdog"] = "false"
|
||||
self.env["chat.reasoning_effort"] = "high"
|
||||
self.env["chat.stream"] = "true"
|
||||
self.env["chat.show_cost"] = "false"
|
||||
self.env["chat.drop_params"] = "false"
|
||||
self.env["chat.openai_store"] = "false"
|
||||
self.env["auto.max_runs"] = "100"
|
||||
self.env["auto.hide_tool_output"] = "false"
|
||||
self.env["auto.init_commands"] = "aaa;iI;afl"
|
||||
self.env["auto.ask_to_execute"] = "true" # ask to run tools such as r2cmd, run_python etc
|
||||
|
||||
self.env.add_callback("debug_level", lambda val: LOGGER.setLevel(int(val) * 10))
|
||||
|
||||
# No active block to start
|
||||
# (blocks are visual representation of messages on the terminal)
|
||||
self.active_block = None
|
||||
|
||||
# Note: While Open Interpreter can use Llama, we will prioritize gpt-4.
|
||||
# gpt-4 is faster, smarter, can call functions, and is all-around easier to use.
|
||||
# This makes gpt-4 better aligned with Open Interpreters priority to be easy to use.
|
||||
self.llama_instance = None
|
||||
self.large = Large(self)
|
||||
|
||||
def get_info_for_system_message(self):
|
||||
"""Gets relevent information for the system message."""
|
||||
info = ""
|
||||
# Add user info
|
||||
getpass.getuser()
|
||||
os.getcwd()
|
||||
platform.system()
|
||||
# info += f"[User Info]\nName: {username}\nCWD: {current_working_directory}\nOS: {operating_system}"
|
||||
return info
|
||||
|
||||
def reset(self):
|
||||
"""Resets the interpreter."""
|
||||
self.messages = []
|
||||
|
||||
def load(self, messages):
|
||||
self.messages = messages
|
||||
|
||||
def save(self, f):
|
||||
json.dumps(self.messages, f, indent=2)
|
||||
|
||||
def handle_undo(self, arguments):
|
||||
"""Remove all messages after the most recent user entry (and the entry itself).
|
||||
Therefore user can jump back to the latest point of conversation.
|
||||
Also gives a visual representation of the messages removed."""
|
||||
if len(self.messages) == 0:
|
||||
return
|
||||
# Find the index of the last 'role': 'user' entry
|
||||
last_user_index = None
|
||||
for i, message in enumerate(self.messages):
|
||||
if message.get('role') == 'user':
|
||||
last_user_index = i
|
||||
removed_messages = []
|
||||
# Remove all messages after the last 'role': 'user'
|
||||
if last_user_index is not None:
|
||||
removed_messages = self.messages[last_user_index:]
|
||||
self.messages = self.messages[:last_user_index]
|
||||
|
||||
# Print out a preview of what messages were removed.
|
||||
for message in removed_messages:
|
||||
if message.get("content"):
|
||||
print(f"**Removed message:** `\"{message['content'][:30]}...\"`")
|
||||
elif 'function_call' in message:
|
||||
print(f"**Removed codeblock**") # TODO: Could add preview of code removed here.
|
||||
|
||||
# reimplement using nltk or index.match
|
||||
def keywords_ai(self, text):
|
||||
# kws = self.keywords_ai("who is the author of radare?") => "author,radare2"
|
||||
words = []
|
||||
mmname = "TheBloke/Mistral-7B-Instruct-v0.2-GGUF"
|
||||
ctxwindow = int(self.env["llm.window"])
|
||||
mm = new_get_hf_llm(self, mmname, ctxwindow)
|
||||
msg = f"Considering the sentence \"{text}\" as input, Take the KEYWORDS or combination of TWO words from the given text and respond ONLY a comma separated list of the most relevant words. DO NOT introduce your response, ONLY show the words"
|
||||
msg = f"Take \"{text}\" as input, and extract the keywords and combination of keywords to make a search online, the output must be a comma separated list" #Take the KEYWORDS or combination of TWO words from the given text and respond ONLY a comma separated list of the most relevant words. DO NOT introduce your response, ONLY show the words"
|
||||
response = mm(msg, stream=False, temperature=0.1, stop="</s>", max_tokens=1750)
|
||||
self.logger.debug(response)
|
||||
text0 = response["choices"][0]["text"]
|
||||
text0 = text0.replace('"', ",")
|
||||
if text0.startswith("."):
|
||||
text0 = text0[1:].strip()
|
||||
try:
|
||||
text0 = text0.split(":")[1].strip()
|
||||
except Exception:
|
||||
pass
|
||||
# print(text0)
|
||||
mm = None
|
||||
return [word.strip() for word in text0.split(',')]
|
||||
|
||||
@progress_bar("Thinking", color="yellow")
|
||||
def chat(self, message=None):
|
||||
global print
|
||||
global Ginterrupted
|
||||
if self.print is not None:
|
||||
print = self.print
|
||||
|
||||
if self.last_model != self.model:
|
||||
# self.llama_instance = None
|
||||
self.last_model = self.model
|
||||
|
||||
if not message and self.env["chat.code"] == "true":
|
||||
self.end_active_block()
|
||||
self.logger.error("Missing message")
|
||||
return
|
||||
|
||||
if self.env["data.use"] == "true":
|
||||
use_hist = self.env["data.hist"] == "true"
|
||||
use_wikit = self.env["data.wikit"] == "true"
|
||||
use_mastodon = self.env["data.mastodon"] == "true"
|
||||
use_vectordb = self.env["data.vectordb"] == "true"
|
||||
use_debug = LOGGER.level == logging.DEBUG
|
||||
datadir = None
|
||||
keywords = None
|
||||
if use_mastodon:
|
||||
keywords = self.keywords_ai(message)
|
||||
if self.env["data.local"] == "true":
|
||||
datadir = self.env["data.path"]
|
||||
if self.env["data.ddg"] == "true":
|
||||
results = ddg(message)
|
||||
if use_vectordb:
|
||||
index.memorize(message, results)
|
||||
else:
|
||||
self.messages.append({"role": "hint", "content": results})
|
||||
matches = index.match(message, keywords, datadir, use_hist, use_mastodon, use_debug, use_wikit, use_vectordb)
|
||||
if not matches:
|
||||
matches = []
|
||||
|
||||
if len(matches) > 0:
|
||||
for m in matches:
|
||||
self.logger.debug("HINT: " + m)
|
||||
self.messages.append({"role": "hint", "content": r2eval(m)})
|
||||
|
||||
self.logger.debug(message)
|
||||
|
||||
# print(message)
|
||||
# Local model -- TODO: assume local models don't have : in the name?
|
||||
if (
|
||||
not self.model.startswith("openai:") and
|
||||
not self.model.startswith("openapi:") and
|
||||
not self.model.startswith("google:") and
|
||||
not self.model.startswith("kobaldcpp") and
|
||||
not self.model.startswith("bedrock:") and
|
||||
not is_litellm_model(self.model)
|
||||
):
|
||||
self.logger = LOGGER.getChild(f"local:{self.model}")
|
||||
# Find or install Code-Llama
|
||||
try:
|
||||
ctxwindow = int(self.env["llm.window"])
|
||||
self.llama_instance = new_get_hf_llm(self, self.model, ctxwindow)
|
||||
if self.llama_instance is None:
|
||||
self.logger.error("Cannot find model " + self.model)
|
||||
return
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
# If it was, we respond non-interactively
|
||||
self.messages.append({"role": "user", "content": message})
|
||||
response = None
|
||||
try:
|
||||
response = self.respond()
|
||||
self.clear_hints()
|
||||
except Exception:
|
||||
if Ginterrupted:
|
||||
Ginterrupted = False
|
||||
else:
|
||||
traceback.print_exc()
|
||||
self.end_active_block()
|
||||
return response
|
||||
|
||||
def end_active_block(self):
|
||||
# if self.env["chat.code"] == "false":
|
||||
# return
|
||||
if self.active_block:
|
||||
self.active_block.end()
|
||||
self.active_block = None
|
||||
|
||||
def environment(self):
|
||||
kvs = ""
|
||||
if self.env["user.name"] != "":
|
||||
kvs += "Name: " + self.env["user.name"] + "\n"
|
||||
if self.env["user.os"] != "":
|
||||
kvs += "OS: " + self.env["user.os"] + "\n"
|
||||
if self.env["user.cwd"] != "":
|
||||
kvs += "CWD: " + self.env["user.cwd"] + "\n"
|
||||
if self.env["user.editor"] != "":
|
||||
kvs += "EDITOR: " + self.env["user.editor"] + "\n"
|
||||
if self.env["user.arch"] != "":
|
||||
kvs += "ARCH: " + self.env["user.arch"] + "\n"
|
||||
# info += f"[User Info]\nName: {username}\nCWD: {current_working_directory}\nOS: {operating_system}"
|
||||
if kvs != "":
|
||||
return "[User Info]\n" + kvs
|
||||
return ""
|
||||
|
||||
def clear_hints(self):
|
||||
res = []
|
||||
for msg in self.messages:
|
||||
if "role" in msg and msg["role"] != "hint":
|
||||
res.append(msg)
|
||||
self.messages = res
|
||||
|
||||
def respond(self):
|
||||
global Ginterrupted
|
||||
self.logger = LOGGER.getChild(self.model.split(":")[0])
|
||||
maxtokens = int(self.env["llm.maxtokens"])
|
||||
# Add relevant info to system_message
|
||||
# (e.g. current working directory, username, os, etc.)
|
||||
info = self.get_info_for_system_message()
|
||||
|
||||
# This is hacky, as we should have a different (minified) prompt for CodeLLama,
|
||||
# but for now, to make the prompt shorter and remove "run_code" references, just get the first 2 lines:
|
||||
self.system_message = "\n".join(self.system_message.split("\n")[:2])
|
||||
# self.system_message += "\nOnly do what the user asks you to do, then ask what they'd like to do next."
|
||||
|
||||
system_message = self.system_message + "\n\n" + info
|
||||
system_message += self.environment()
|
||||
|
||||
if self.env["chat.trim"] == "true":
|
||||
## this stupid function is slow as hell and doesn not provides much goodies
|
||||
## just ignore it by default
|
||||
messages = tokentrim.trim(self.messages, max_tokens=maxtokens, system_message=system_message)
|
||||
else:
|
||||
messages = self.large.compress_messages(self.messages)
|
||||
|
||||
self.logger.debug(messages)
|
||||
|
||||
# builtins.print(prompt)
|
||||
response = None
|
||||
if self.auto_run:
|
||||
from . import auto
|
||||
import litellm
|
||||
if self.env["chat.drop_params"] == "true":
|
||||
litellm.drop_params = True
|
||||
else:
|
||||
litellm.drop_params = False
|
||||
|
||||
if(is_litellm_model(self.model)):
|
||||
response = auto.chat(self)
|
||||
else:
|
||||
self.llama_instance = new_get_hf_llm(self, self.model, int(self.env["llm.window"]))
|
||||
response = auto.chat(self)
|
||||
return
|
||||
|
||||
elif self.model.startswith("kobaldcpp"):
|
||||
if self.system_message != "":
|
||||
message = f"Context:\n```\n{self.system_message}\n```\n"
|
||||
else:
|
||||
message = ""
|
||||
#f"{Your name is r2ai, an assistant for radare2. User will ask about actions and you must respond with the radare2 command associated or the answer to the question. Be precise and concise when answering"
|
||||
for m in messages:
|
||||
role = m["role"]
|
||||
content = m["content"]
|
||||
if role == "user":
|
||||
message += f"User: {content}\n"
|
||||
elif role == "assistant":
|
||||
message += f"AI: {content}\n"
|
||||
response = ""
|
||||
if ":" in self.model:
|
||||
uri = self.model.split(":")[1:]
|
||||
response = kobaldcpp.chat(message, ":".join(uri))
|
||||
else:
|
||||
response = kobaldcpp.chat(message)
|
||||
if "content" in self.messages[-1]:
|
||||
last_message = self.messages[-1]["content"]
|
||||
if self.env["chat.reply"] == "true":
|
||||
self.messages.append({"role": "assistant", "content": response})
|
||||
print(response)
|
||||
return
|
||||
|
||||
elif is_litellm_model(self.model):
|
||||
# [
|
||||
# {"role": "system", "content": "You are a poetic assistant, be creative."},
|
||||
# {"role": "user", "content": "Compose a poem that explains the concept of recursion in programming."}
|
||||
# ]
|
||||
from litellm import completion as litellm_completion
|
||||
completion = None
|
||||
if self.model.startswith("ollama/"):
|
||||
completion = litellm_completion(
|
||||
model=self.model,
|
||||
messages=self.messages,
|
||||
drop_params=True,
|
||||
max_completion_tokens=maxtokens,
|
||||
temperature=float(self.env["llm.temperature"]),
|
||||
top_p=float(self.env["llm.top_p"]),
|
||||
)
|
||||
elif self.model.startswith("mistral/"):
|
||||
completion = litellm_completion(
|
||||
drop_params=True,
|
||||
model=self.model.replace(":", "/"),
|
||||
messages=self.messages,
|
||||
max_completion_tokens=maxtokens,
|
||||
temperature=float(self.env["llm.temperature"]),
|
||||
top_p=float(self.env["llm.top_p"]),
|
||||
)
|
||||
else:
|
||||
completion = litellm_completion(
|
||||
model=self.model.replace(":", "/"),
|
||||
messages=self.messages,
|
||||
max_completion_tokens=maxtokens,
|
||||
temperature=float(self.env["llm.temperature"]),
|
||||
top_p=float(self.env["llm.top_p"]),
|
||||
)
|
||||
|
||||
response = completion.choices[0].message.content
|
||||
if "content" in self.messages[-1]:
|
||||
last_message = self.messages[-1]["content"]
|
||||
if self.env["chat.reply"] == "true":
|
||||
self.messages.append({"role": "assistant", "content": response})
|
||||
print(response)
|
||||
return response
|
||||
|
||||
else:
|
||||
# non-openai aka local-llama model
|
||||
if self.llama_instance == None:
|
||||
self.logger.critical("Llama is not instantiated")
|
||||
return
|
||||
try:
|
||||
if type(self.terminator).__name__ == "list":
|
||||
terminator = self.terminator
|
||||
else:
|
||||
terminator = [self.terminator]
|
||||
chat_args = {
|
||||
"stream": True,
|
||||
"temperature": float(self.env["llm.temperature"]),
|
||||
"repeat_penalty": float(self.env["llm.repeat_penalty"]),
|
||||
"top_p": float(self.env["llm.top_p"]),
|
||||
"top_k": int(self.env["llm.top_k"]),
|
||||
"stop": terminator,
|
||||
"max_tokens": maxtokens
|
||||
}
|
||||
if self.env["chat.rawdog"] == "true":
|
||||
from .completion import messages_to_prompt
|
||||
allmsg = messages.copy()
|
||||
allmsg.insert(0, ChatMessage(role=MessageRole.SYSTEM, content=self.system_message))
|
||||
prompt = messages_to_prompt(self, allmsg)
|
||||
response = self.llama_instance(prompt, **chat_args)
|
||||
else:
|
||||
all_messages = messages.copy()
|
||||
all_messages.insert(0, {"role": "system", "content": self.system_message})
|
||||
response = self.llama_instance.create_chat_completion(all_messages, **chat_args)
|
||||
except Exception as err:
|
||||
traceback.print_exc()
|
||||
print(Exception, err)
|
||||
if Ginterrupted:
|
||||
Ginterrupted = False
|
||||
return
|
||||
|
||||
if response is None:
|
||||
print("No response")
|
||||
ctxwindow = int(self.env["llm.window"])
|
||||
self.llama_instance = new_get_hf_llm(self, self.model, ctxwindow)
|
||||
return
|
||||
# Initialize message, function call trackers, and active block
|
||||
self.messages.append({})
|
||||
in_function_call = False
|
||||
self.active_block = MessageBlock()
|
||||
for chunk in response:
|
||||
if Ginterrupted:
|
||||
Ginterrupted = False
|
||||
break
|
||||
text = ''
|
||||
delta = None
|
||||
if self.env["chat.rawdog"] == "true":
|
||||
if "content" not in messages[-1]:
|
||||
text = chunk["choices"][0]['text'].capitalize()
|
||||
messages[-1] = { "content": '', "role": "assistant" }
|
||||
else:
|
||||
text = chunk["choices"][0]['text']
|
||||
messages[-1] = merge_deltas(messages[-1], {"content": text})
|
||||
else:
|
||||
if "content" not in messages[-1]:
|
||||
if "text" in chunk["choices"][0]['delta']:
|
||||
text = chunk["choices"][0]['delta']["content"]
|
||||
messages[-1] = { "content": text, "role": "assistant" }
|
||||
delta = {"content": text}
|
||||
elif "content" in chunk['choices'][0]['delta']:
|
||||
delta = {"content": chunk['choices'][0]['delta']['content']}
|
||||
# Accumulate deltas into the last message in messages
|
||||
if delta:
|
||||
self.messages[-1] = merge_deltas(self.messages[-1], delta)
|
||||
if self.env["chat.live"] != "true":
|
||||
continue
|
||||
flushed = False
|
||||
if self.env["chat.code"] == "true":
|
||||
if incodeblock(self.messages[-1]):
|
||||
if in_function_call == False:
|
||||
in_function_call = True
|
||||
self.active_block.update_from_message(self.messages[-1])
|
||||
self.end_active_block()
|
||||
# flushed = True
|
||||
self.active_block = CodeBlock()
|
||||
else:
|
||||
if in_function_call == True:
|
||||
in_function_call = False
|
||||
self.end_active_block()
|
||||
flushed = True
|
||||
self.active_block = MessageBlock()
|
||||
if self.env["chat.live"] == "true": # and self.env["chat.code"] == "true":
|
||||
self.active_block.update_from_message(self.messages[-1])
|
||||
if flushed:
|
||||
self.messages[-1]["content"] = ""
|
||||
continue # end of for loop
|
||||
self.end_active_block()
|
||||
self.active_block = None
|
||||
output_text = ""
|
||||
if len(self.messages) > 0 and "content" in self.messages[-1]:
|
||||
output_text = self.messages[-1]["content"].strip()
|
||||
if self.env["chat.reply"] == "true":
|
||||
self.messages.append({"role": "assistant", "content": output_text})
|
||||
if self.env["chat.voice"] == "true":
|
||||
tts("(assistant)", output_text, self.env["voice.lang"])
|
||||
elif self.env["chat.live"] != "true":
|
||||
try:
|
||||
r2lang.print(output_text)
|
||||
except Exception:
|
||||
print(output_text)
|
||||
# print(str(self.messages))
|
||||
@@ -1,223 +0,0 @@
|
||||
class IncompleteJSONParser:
|
||||
def __init__(self, s):
|
||||
self.s = s
|
||||
self.len = len(s)
|
||||
self.pos = 0
|
||||
|
||||
def parse(self):
|
||||
self.skip_whitespace()
|
||||
if self.pos >= self.len:
|
||||
return None
|
||||
char = self.s[self.pos]
|
||||
if char == '{':
|
||||
return self.parse_object()
|
||||
elif char == '[':
|
||||
return self.parse_array()
|
||||
else:
|
||||
return None # Top-level must be object or array
|
||||
|
||||
def parse_object(self):
|
||||
if self.s[self.pos] != '{':
|
||||
return None
|
||||
self.pos += 1 # Skip '{'
|
||||
self.skip_whitespace()
|
||||
obj = {}
|
||||
parsed_any = False # Flag to check if any key-value pair was parsed
|
||||
|
||||
while self.pos < self.len:
|
||||
self.skip_whitespace()
|
||||
if self.pos >= self.len:
|
||||
break
|
||||
if self.s[self.pos] == '}':
|
||||
self.pos += 1
|
||||
return obj if parsed_any else None
|
||||
key = self.parse_string()
|
||||
if key is None:
|
||||
# Incomplete key, skip the rest
|
||||
break
|
||||
self.skip_whitespace()
|
||||
if self.pos >= self.len or self.s[self.pos] != ':':
|
||||
# Missing colon, skip this key
|
||||
break
|
||||
self.pos += 1 # Skip ':'
|
||||
self.skip_whitespace()
|
||||
value = self.parse_value()
|
||||
if value is None:
|
||||
# Incomplete value, skip this key-value pair
|
||||
break
|
||||
obj[key] = value
|
||||
parsed_any = True
|
||||
self.skip_whitespace()
|
||||
if self.pos >= self.len:
|
||||
break
|
||||
if self.s[self.pos] == ',':
|
||||
self.pos += 1
|
||||
continue
|
||||
elif self.s[self.pos] == '}':
|
||||
self.pos += 1
|
||||
return obj if parsed_any else None
|
||||
else:
|
||||
# Unexpected character, skip
|
||||
break
|
||||
# Auto-close the object
|
||||
return obj if parsed_any else None
|
||||
|
||||
def parse_array(self):
|
||||
if self.s[self.pos] != '[':
|
||||
return None
|
||||
self.pos += 1 # Skip '['
|
||||
self.skip_whitespace()
|
||||
array = []
|
||||
parsed_any = False # Flag to check if any element was parsed
|
||||
|
||||
while self.pos < self.len:
|
||||
self.skip_whitespace()
|
||||
if self.pos >= self.len:
|
||||
break
|
||||
if self.s[self.pos] == ']':
|
||||
self.pos += 1
|
||||
return array if parsed_any else None
|
||||
value = self.parse_value()
|
||||
if value is None:
|
||||
# Incomplete value, skip this element
|
||||
break
|
||||
array.append(value)
|
||||
parsed_any = True
|
||||
self.skip_whitespace()
|
||||
if self.pos >= self.len:
|
||||
break
|
||||
if self.s[self.pos] == ',':
|
||||
self.pos += 1
|
||||
continue
|
||||
elif self.s[self.pos] == ']':
|
||||
self.pos += 1
|
||||
return array if parsed_any else None
|
||||
else:
|
||||
# Unexpected character, skip
|
||||
break
|
||||
# Auto-close the array
|
||||
return array if parsed_any else None
|
||||
|
||||
def parse_value(self):
|
||||
self.skip_whitespace()
|
||||
if self.pos >= self.len:
|
||||
return None
|
||||
char = self.s[self.pos]
|
||||
if char == '"':
|
||||
return self.parse_string()
|
||||
elif char == '{':
|
||||
return self.parse_object()
|
||||
elif char == '[':
|
||||
return self.parse_array()
|
||||
elif char in '-0123456789':
|
||||
return self.parse_number()
|
||||
elif self.s.startswith('true', self.pos):
|
||||
self.pos += 4
|
||||
return True
|
||||
elif self.s.startswith('false', self.pos):
|
||||
self.pos += 5
|
||||
return False
|
||||
elif self.s.startswith('null', self.pos):
|
||||
self.pos += 4
|
||||
return None
|
||||
else:
|
||||
return None # Invalid value
|
||||
|
||||
def parse_string(self):
|
||||
if self.s[self.pos] != '"':
|
||||
return None
|
||||
self.pos += 1 # Skip opening quote
|
||||
result = ""
|
||||
while self.pos < self.len:
|
||||
char = self.s[self.pos]
|
||||
if char == '\\':
|
||||
if self.pos + 1 >= self.len:
|
||||
result += '\\'
|
||||
self.pos += 1
|
||||
break # Incomplete escape, return what we have
|
||||
self.pos += 1
|
||||
escape_char = self.s[self.pos]
|
||||
if escape_char == '"':
|
||||
result += '"'
|
||||
elif escape_char == '\\':
|
||||
result += '\\'
|
||||
elif escape_char == '/':
|
||||
result += '/'
|
||||
elif escape_char == 'b':
|
||||
result += '\b'
|
||||
elif escape_char == 'f':
|
||||
result += '\f'
|
||||
elif escape_char == 'n':
|
||||
result += '\n'
|
||||
elif escape_char == 'r':
|
||||
result += '\r'
|
||||
elif escape_char == 't':
|
||||
result += '\t'
|
||||
elif escape_char == 'u':
|
||||
# Unicode escape
|
||||
if self.pos + 4 >= self.len:
|
||||
# Incomplete unicode escape
|
||||
break
|
||||
hex_digits = self.s[self.pos+1:self.pos+5]
|
||||
try:
|
||||
code_point = int(hex_digits, 16)
|
||||
result += chr(code_point)
|
||||
self.pos += 4
|
||||
except ValueError:
|
||||
break # Invalid unicode escape
|
||||
else:
|
||||
# Invalid escape character
|
||||
result += '\\' + escape_char # Keep it as is
|
||||
elif char == '"':
|
||||
self.pos += 1 # Skip closing quote
|
||||
return result
|
||||
else:
|
||||
result += char
|
||||
self.pos += 1
|
||||
# Return the partial string if incomplete
|
||||
return result
|
||||
|
||||
def parse_number(self):
|
||||
start = self.pos
|
||||
if self.s[self.pos] == '-':
|
||||
self.pos += 1
|
||||
if self.pos >= self.len:
|
||||
return None
|
||||
if self.s[self.pos] == '0':
|
||||
self.pos += 1
|
||||
elif '1' <= self.s[self.pos] <= '9':
|
||||
while self.pos < self.len and self.s[self.pos].isdigit():
|
||||
self.pos += 1
|
||||
else:
|
||||
return None # Invalid number
|
||||
if self.pos < self.len and self.s[self.pos] == '.':
|
||||
self.pos += 1
|
||||
if self.pos >= self.len or not self.s[self.pos].isdigit():
|
||||
return None # Incomplete fraction
|
||||
while self.pos < self.len and self.s[self.pos].isdigit():
|
||||
self.pos += 1
|
||||
if self.pos < self.len and self.s[self.pos] in 'eE':
|
||||
self.pos += 1
|
||||
if self.pos < self.len and self.s[self.pos] in '+-':
|
||||
self.pos += 1
|
||||
if self.pos >= self.len or not self.s[self.pos].isdigit():
|
||||
return None # Incomplete exponent
|
||||
while self.pos < self.len and self.s[self.pos].isdigit():
|
||||
self.pos += 1
|
||||
num_str = self.s[start:self.pos]
|
||||
try:
|
||||
if '.' in num_str or 'e' in num_str or 'E' in num_str:
|
||||
return float(num_str)
|
||||
else:
|
||||
return int(num_str)
|
||||
except ValueError:
|
||||
return None # Invalid number
|
||||
|
||||
def skip_whitespace(self):
|
||||
while self.pos < self.len and self.s[self.pos] in ' \t\n\r':
|
||||
self.pos += 1
|
||||
|
||||
def parse_incomplete_json(s):
|
||||
parser = IncompleteJSONParser(s)
|
||||
result = parser.parse()
|
||||
return result
|
||||
@@ -1,47 +0,0 @@
|
||||
"""Entrypoint for the r2ai plugin and repl."""
|
||||
import sys
|
||||
import os
|
||||
import builtins
|
||||
import traceback
|
||||
|
||||
current_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
parent_dir = os.path.dirname(current_dir)
|
||||
sys.path.insert(0, parent_dir)
|
||||
os.environ["TRANSFORMERS_NO_ADVISORY_WARNINGS"] = "1"
|
||||
try:
|
||||
venv_dir = os.path.join(parent_dir, 'venv')
|
||||
if os.path.exists(venv_dir):
|
||||
site_packages = os.path.join(venv_dir, 'lib', 'python{}.{}'.format(*sys.version_info[:2]), 'site-packages')
|
||||
if os.path.exists(site_packages):
|
||||
sys.path.insert(0, site_packages)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
import r2lang
|
||||
from r2ai.main import r2ai_singleton, run_rcfile_once, runline, help_message
|
||||
|
||||
def r2ai_rlang_plugin(unused_but_required_argument):
|
||||
ai = r2ai_singleton()
|
||||
def _call(s):
|
||||
if not s.startswith("r2ai"):
|
||||
return False
|
||||
try:
|
||||
run_rcfile_once(ai)
|
||||
if len(s) == 4:
|
||||
builtins.print(help_message)
|
||||
else:
|
||||
usertext = s[4:].strip()
|
||||
runline(ai, usertext)
|
||||
except Exception as e:
|
||||
builtins.print(e)
|
||||
traceback.print_exc()
|
||||
return True
|
||||
|
||||
return {
|
||||
"name": "r2ai",
|
||||
"license": "MIT",
|
||||
"desc": "run llama language models inside r2",
|
||||
"call": _call,
|
||||
}
|
||||
|
||||
r2lang.plugin("core", r2ai_rlang_plugin)
|
||||
@@ -1,42 +0,0 @@
|
||||
import itertools
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
|
||||
class Spinner:
|
||||
def __init__(self, message="Loading...", delay=0.1):
|
||||
self.spinner = itertools.cycle([
|
||||
"\033[1;31m⠋\033[0m", "\033[1;32m⠙\033[0m", "\033[1;33m⠹\033[0m", "\033[1;34m⠸\033[0m",
|
||||
"\033[1;35m⠼\033[0m", "\033[1;36m⠴\033[0m", "\033[1;37m⠦\033[0m", "\033[1;31m⠧\033[0m",
|
||||
"\033[1;32m⠇\033[0m", "\033[1;33m⠏\033[0m"
|
||||
])
|
||||
self.message = message
|
||||
self.delay = delay
|
||||
self.running = False
|
||||
self.thread = None
|
||||
self.start_time = None
|
||||
|
||||
def start(self):
|
||||
"""Start the spinner in a separate thread."""
|
||||
self.running = True
|
||||
self.start_time = time.time()
|
||||
self.thread = threading.Thread(target=self._spin)
|
||||
self.thread.start()
|
||||
|
||||
def _spin(self):
|
||||
"""Spin the spinner while running is True."""
|
||||
while self.running:
|
||||
elapsed_time = time.time() - self.start_time
|
||||
sys.stdout.write(f"\r{self.message} {next(self.spinner)} {elapsed_time:.1f}s")
|
||||
sys.stdout.flush()
|
||||
time.sleep(self.delay)
|
||||
sys.stdout.write('\r' + ' ' * (len(self.message) + 20) + '\r') # Clear the line
|
||||
sys.stdout.flush()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the spinner."""
|
||||
self.running = False
|
||||
if self.thread is not None:
|
||||
self.thread.join()
|
||||
|
||||
spinner = Spinner("")
|
||||
@@ -1,37 +0,0 @@
|
||||
import builtins
|
||||
from .tools import run_python, execute_binary, r2cmd
|
||||
import subprocess
|
||||
from .pipe import get_filename
|
||||
import time
|
||||
py_code = """
|
||||
print('hello test')
|
||||
"""
|
||||
|
||||
def run_test(args):
|
||||
if not args or len(args) == 0:
|
||||
res = run_python(py_code).strip()
|
||||
print(f"run_python: {res}", len(res))
|
||||
assert res == "hello test"
|
||||
print("run_python: test passed")
|
||||
r2cmd("o--;o /bin/ls")
|
||||
res = execute_binary(args=["-d", "/etc"]).strip()
|
||||
subp = subprocess.run(["/bin/ls", "-d", "/etc"], capture_output=True, text=True)
|
||||
print("exec result", res)
|
||||
print("subp result", subp.stdout)
|
||||
assert ''.join(res).strip() == subp.stdout.strip()
|
||||
print("execute_binary with args: test passed")
|
||||
else:
|
||||
cmd, *args = args.split(" ", 1)
|
||||
if cmd == "get_filename":
|
||||
builtins.print(get_filename())
|
||||
elif cmd == "run_python":
|
||||
builtins.print(f"--- args ---")
|
||||
builtins.print(args)
|
||||
builtins.print(f"--- end args ---")
|
||||
builtins.print(f"--- result ---")
|
||||
builtins.print(run_python(args[0]))
|
||||
builtins.print(f"--- end result ---")
|
||||
elif cmd == "r2cmd":
|
||||
builtins.print(f"--- {args} ---")
|
||||
builtins.print(r2cmd(args))
|
||||
builtins.print("--- end ---")
|
||||
@@ -1,176 +0,0 @@
|
||||
from r2ai.pipe import get_r2_inst
|
||||
import json
|
||||
import builtins
|
||||
import base64
|
||||
from .pipe import get_filename
|
||||
from . import LOGGER
|
||||
import time
|
||||
import sys
|
||||
from io import StringIO
|
||||
import subprocess
|
||||
import os
|
||||
is_plugin = False
|
||||
try:
|
||||
import r2lang
|
||||
is_plugin = True
|
||||
except Exception:
|
||||
is_plugin = False
|
||||
pass
|
||||
|
||||
def r2cmd(command: str):
|
||||
"""
|
||||
Run a r2 command and return the output
|
||||
|
||||
Parameters
|
||||
----------
|
||||
command: str
|
||||
The r2 command to run
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The output of the r2 command
|
||||
"""
|
||||
r2 = get_r2_inst()
|
||||
r2.cmd('e scr.color=3')
|
||||
if command.startswith('r2 '):
|
||||
return "You are already in r2!"
|
||||
cmd = '{"cmd":' + json.dumps(command) + '}'
|
||||
res = r2.cmd(cmd)
|
||||
|
||||
try:
|
||||
res = json.loads(res)
|
||||
if 'error' in res and res['error'] is True:
|
||||
error_message = res['error']
|
||||
log_messages = '\n'.join(log['message'] for log in res.get('logs', []))
|
||||
# return { 'type': 'error', 'output': log_messages }
|
||||
return log_messages
|
||||
|
||||
return res['res']
|
||||
except json.JSONDecodeError:
|
||||
if type(res) == str:
|
||||
spl = res.strip().split('\n')
|
||||
if spl[-1].startswith('{"res":""'):
|
||||
res = '\n'.join(spl[:-1])
|
||||
return res
|
||||
except Exception as e:
|
||||
# return { 'type': 'error', 'output': f"Error running r2cmd: {e}\nCommand: {command}\nResponse: {res}" }
|
||||
return f"Error running r2cmd: {e}\nCommand: {command}\nResponse: {res}"
|
||||
|
||||
def run_python(command: str):
|
||||
"""
|
||||
Run a python script and return the output
|
||||
|
||||
Parameters
|
||||
----------
|
||||
command: str
|
||||
The python script to run
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The output of the python script
|
||||
"""
|
||||
r2 = get_r2_inst()
|
||||
res = ""
|
||||
is_plugin = False
|
||||
python_path = sys.executable
|
||||
try:
|
||||
proc = subprocess.run([python_path, '-c', command],
|
||||
capture_output=True,
|
||||
text=True)
|
||||
res = proc.stdout
|
||||
if proc.stderr:
|
||||
res += proc.stderr
|
||||
except Exception as e:
|
||||
res = str(e)
|
||||
|
||||
# if is_plugin:
|
||||
# base64cmd = base64.b64encode(command.encode('utf-8')).decode('utf-8')
|
||||
# res += r2cmd(f'#!python -e base64:{base64cmd} > .r2ai_tmp.log')
|
||||
# res += r2cmd('cat .r2ai_tmp.log')
|
||||
# r2cmd('rm .r2ai_tmp.log')
|
||||
# else:
|
||||
# with open('r2ai_tmp.py', 'w') as f:
|
||||
# f.write(command)
|
||||
# r2 = get_r2_inst()
|
||||
# res += r2cmd('#!python r2ai_tmp.py > .r2ai_tmp.log')
|
||||
# time.sleep(0.1)
|
||||
# res += r2cmd('!cat .r2ai_tmp.log')
|
||||
# LOGGER.debug(f'run_python: {res}')
|
||||
# # r2cmd('rm r2ai_tmp.py')
|
||||
# # r2cmd('rm .r2ai_tmp.log')
|
||||
return res
|
||||
|
||||
|
||||
schemas = {
|
||||
"execute_binary": {
|
||||
"name": "execute_binary",
|
||||
"description": "Execute a binary with the given arguments and stdin",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"args": {
|
||||
"description": "The arguments to pass to the binary. Do not include the file name.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"stdin": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def execute_binary(args: list[str] = [], stdin: str = ""):
|
||||
filename = get_filename()
|
||||
if filename:
|
||||
if os.path.isabs(filename):
|
||||
abs_path = os.path.abspath(filename)
|
||||
if os.path.exists(abs_path):
|
||||
filename = abs_path
|
||||
else:
|
||||
cwd_path = os.path.join(os.getcwd(), filename)
|
||||
if os.path.exists(cwd_path):
|
||||
filename = cwd_path
|
||||
try:
|
||||
cmd = [filename] + args
|
||||
proc = subprocess.run(cmd, input=stdin, capture_output=True, text=True)
|
||||
res = proc.stdout
|
||||
if proc.stderr:
|
||||
res += proc.stderr
|
||||
return res
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
return ""
|
||||
# r2 = get_r2_inst()
|
||||
# if stdin:
|
||||
# r2.cmd(f'dor stdin={json.dumps(stdin)}')
|
||||
# if len(args) > 0:
|
||||
# r2.cmd(f"ood {' '.join(args)}")
|
||||
# else:
|
||||
# r2.cmd("ood")
|
||||
# res = r2cmd("dc")
|
||||
# return res
|
||||
|
||||
|
||||
|
||||
def print_tool_call(msg):
|
||||
if msg['function']['name'] == 'r2cmd':
|
||||
builtins.print('\x1b[1;32m> \x1b[4m' + msg['function']['arguments']['command'] + '\x1b[0m')
|
||||
elif msg['function']['name'] == 'run_python':
|
||||
builtins.print('\x1b[1;32m> \x1b[4m' + "#!python" + '\x1b[0m')
|
||||
builtins.print(msg['function']['arguments']['command'])
|
||||
elif msg['function']['name'] == 'execute_binary':
|
||||
filename = get_filename() or 'bin'
|
||||
stdin = msg['function']['arguments']['stdin'] if 'stdin' in msg['function']['arguments'] else None
|
||||
args = msg['function']['arguments']['args'] if 'args' in msg['function']['arguments'] else []
|
||||
cmd = filename
|
||||
if args and len(args) > 0:
|
||||
cmd += ' ' + ' '.join(args)
|
||||
if stdin and len(stdin) > 0:
|
||||
cmd += f' stdin={stdin}'
|
||||
builtins.print('\x1b[1;32m> \x1b[4m' + cmd + '\x1b[0m')
|
||||
@@ -1,25 +0,0 @@
|
||||
from litellm import acompletion, ChatCompletionAssistantToolCall, ChatCompletionToolCallFunctionChunk
|
||||
import asyncio
|
||||
import json
|
||||
import signal
|
||||
from r2ai.pipe import get_r2_inst
|
||||
from r2ai.tools import run_python, r2cmd, execute_binary
|
||||
from r2ai.repl import r2ai_singleton
|
||||
from r2ai.auto import ChatAuto, SYSTEM_PROMPT_AUTO
|
||||
from r2ai.interpreter import is_litellm_model
|
||||
from r2ai.models import new_get_hf_llm
|
||||
|
||||
def signal_handler(signum, frame):
|
||||
raise KeyboardInterrupt
|
||||
|
||||
async def chat(ai, message, cb):
|
||||
model = ai.model.replace(":", "/")
|
||||
tools = [r2cmd, run_python, execute_binary]
|
||||
ai.messages.append({"role": "user", "content": message})
|
||||
tool_choice = 'auto'
|
||||
if not is_litellm_model(model) and ai and not ai.llama_instance:
|
||||
ai.llama_instance = new_get_hf_llm(ai, model, int(ai.env["llm.window"]))
|
||||
|
||||
chat_auto = ChatAuto(model, interpreter=ai, system=SYSTEM_PROMPT_AUTO, tools=tools, messages=ai.messages, tool_choice=tool_choice, cb=cb)
|
||||
|
||||
return await chat_auto.achat()
|
||||
@@ -1,25 +0,0 @@
|
||||
# r2ai-server
|
||||
|
||||
Start ollama, llamacpp, llamafile, r2ai, kobaldcpp and other language model webservers using a the same syntax for all of them to simplify its launching and setup.
|
||||
|
||||
* Use -l to list the implementations available
|
||||
|
||||
You can install more via r2pm
|
||||
|
||||
* Use -m to list or select the model. Those models can be absolute paths or model names when downloaded via r2ai, which uses the hugging face api
|
||||
|
||||
## Running r2ai-server
|
||||
|
||||
- Get usage: `r2pm -r r2ai-server`
|
||||
- List available servers: `r2pm -r r2ai-server -l`
|
||||
- List available models: `r2pm -r r2ai-server -m`
|
||||
|
||||
On Linux, models are stored in `~/.r2ai.models/`. File `~/.r2ai.model` lists the default model and other models.
|
||||
|
||||
**Example launching a local Mistral AI server:**
|
||||
|
||||
```
|
||||
$ r2pm -r r2ai-server -l r2ai -m mistral-7b-instruct-v0.2.Q2_K
|
||||
[12/13/24 10:35:22] INFO r2ai.server - INFO - [R2AI] Serving at port 8080 web.py:336
|
||||
```
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
recursive-include r2ai/ui *.tcss
|
||||
@@ -1,4 +1,92 @@
|
||||
TARGETS=all clean mrproper user-install install user-uninstall uninstall
|
||||
$(TARGETS):
|
||||
$(MAKE) -C src $@
|
||||
.PHONY: $(TARGETS)
|
||||
R2_USER_PLUGINS=$(shell r2 -H R2_USER_PLUGINS)
|
||||
PWD=$(shell pwd)
|
||||
R2PM_BINDIR=$(shell r2pm -H R2PM_BINDIR)
|
||||
PV=3.12
|
||||
ifeq ($(shell which python${PV} > /dev/null && echo ok),ok)
|
||||
PYTHON?=python${PV}
|
||||
else
|
||||
PYTHON?=python3
|
||||
endif
|
||||
PIP=$(PYTHON) -m pip
|
||||
|
||||
LINTED=r2ai/code_block.py
|
||||
LINTED+=r2ai/bubble.py
|
||||
LINTED+=r2ai/const.py
|
||||
LINTED+=main.py
|
||||
LINTED+=r2ai/backend/kobaldcpp.py
|
||||
# LINTED+=r2ai/index.py
|
||||
# LINTED+=r2ai/voice.py
|
||||
# LINTED+=r2ai/anthropic.py
|
||||
|
||||
ifeq ($(R2PM_BINDIR),)
|
||||
FATAL ERROR
|
||||
endif
|
||||
|
||||
.PHONY: all all.old deps clean deps-global pub lint cilint
|
||||
.PHONY: install uninstall user-install user-uninstall
|
||||
|
||||
all: venv
|
||||
@./r2ai.sh
|
||||
|
||||
large:
|
||||
. venv/bin/activate ; $(PYTHON) main.py -l
|
||||
|
||||
all.old:
|
||||
@test -n "${VIRTUAL_ENV}" || (echo "Run:"; echo ". venv/bin/activate" ; exit 1)
|
||||
$(PYTHON) main.py || $(MAKE) deps
|
||||
|
||||
venv:
|
||||
$(PYTHON) -m venv venv
|
||||
if [ -z "`find venv | grep llama_cpp`" ]; then . venv/bin/activate ; pip install -r requirements.txt ; fi
|
||||
|
||||
deps: venv
|
||||
#test -n "${VIRTUAL_ENV}" || (echo "Run: . venv/bin/activate" ; exit 1)
|
||||
. venv/bin/activate && export CMAKE_ARGS="-DLLAMA_METAL=on -DLLAMA_METAL_EMBED_LIBRARY=ON" && \
|
||||
pip install --force-reinstall -U -r requirements.txt --no-cache-dir
|
||||
|
||||
clean:
|
||||
rm -rf venv
|
||||
|
||||
mrproper:
|
||||
$(MAKE) clean
|
||||
|
||||
deps-global:
|
||||
export CMAKE_ARGS="-DLLAMA_METAL=on -DLLAMA_METAL_EMBED_LIBRARY=ON" && \
|
||||
$(PIP) install --force-reinstall -U -r requirements.txt --break-system-packages --no-cache-dir
|
||||
|
||||
user-install:
|
||||
rm -f $(R2PM_BINDIR)/r2ai-server
|
||||
ln -fs $(PWD)/r2ai-server/r2ai-server $(R2PM_BINDIR)/r2ai-server
|
||||
rm -f $(R2PM_BINDIR)/r2ai
|
||||
ln -fs $(PWD)/r2ai.sh $(R2PM_BINDIR)/r2ai
|
||||
|
||||
install: user-install
|
||||
-mkdir -p /usr/local/share/man/man1/r2ai.1
|
||||
-cp doc/usage/r2ai.1 /usr/local/share/man/man1/r2ai.1
|
||||
|
||||
install-decai:
|
||||
$(MAKE) -C decai user-install
|
||||
|
||||
install-server:
|
||||
$(MAKE) -C r2ai-server user-install
|
||||
|
||||
install-plugin user-install-plugin:
|
||||
ln -fs $(PWD)/main.py $(R2_USER_PLUGINS)/r2ai.py
|
||||
|
||||
uninstall user-uninstall:
|
||||
rm -f $(R2PM_BINDIR)/r2ai
|
||||
-rm -f /usr/local/share/man/man1/r2ai.1
|
||||
|
||||
user-uninstall-plugin uninstall-plugin:
|
||||
rm -f $(R2_USER_PLUGINS)/r2ai.py
|
||||
|
||||
pub:
|
||||
$(PYTHON) setup.py build sdist
|
||||
twine check dist/*
|
||||
twine upload -u __token__ --repository-url https://upload.pypi.org/legacy/ --verbose dist/*
|
||||
|
||||
cilint:
|
||||
pylint $(LINTED)
|
||||
|
||||
lint:
|
||||
pylint *.py r2ai/*.py
|
||||
@@ -1,96 +1,138 @@
|
||||
# R2AI - Augmented reversing with LLM for radare2
|
||||
|
||||
```console
|
||||
╭─────────────────────────────────╮
|
||||
│ ,______ .______ .______ ,___ │
|
||||
╭──╮ │ : __ \ \____ |: \ : __| │
|
||||
│ _│_ │ | \____|/ ____|| _,_ || : | │
|
||||
│ O O < | : \ \ . || : || | │
|
||||
│ │╷ │ | |___\ \__:__||___| || | │
|
||||
│ ││ │ |___| : |___||___| │
|
||||
│ ─╯│ ╰─────────────────────────────────╯
|
||||
╰───╯
|
||||
```
|
||||
,______ .______ .______ ,___
|
||||
: __ \ \____ |: \ : __|
|
||||
| \____|/ ____|| _,_ || : |
|
||||
| : \ \ . || : || |
|
||||
| |___\ \__:__||___| || |
|
||||
|___| : |___||___|
|
||||
* --pancake
|
||||
```
|
||||
|
||||
[](https://github.com/radareorg/r2ai/actions/workflows/ci.yml)
|
||||
[](https://github.com/radareorg/radare2)
|
||||
|
||||
Run a language model in local, without internet, to entertain you or help answering questions about radare2 or reverse engineering in general. Note that models used by r2ai are pulled from external sources which may behave different or respond unrealible information. That's why there's an ongoing effort into improving the post-finetuning using memgpt-like techniques which can't get better without your help!
|
||||
|
||||
<p align="center">
|
||||
<img src="doc/r2clippy.jpg">
|
||||
</p>
|
||||
|
||||
## Components
|
||||
|
||||
This repository contains two plugins for radare2:
|
||||
R2AI is structured into four independent components:
|
||||
|
||||
* **r2ai** - native plugin for radare2
|
||||
* **decai** - r2js plugin with special focus on decompilation
|
||||
|
||||
If you are looking to use radare2 with other agents via MCP:
|
||||
|
||||
* **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)
|
||||
* r2ai (wip r2ai-native rewrite in C)
|
||||
* r2-like repl using r2pipe to comunicate with r2
|
||||
* supports auto solving mode
|
||||
* client and server openapi protocol
|
||||
* download and manage models from huggingface
|
||||
* decai
|
||||
* lightweight r2js plugin
|
||||
* focus on decompilation
|
||||
* talks to r2ai, r2ai-server, openai, anthropic or ollama
|
||||
* r2ai-plugin
|
||||
* requires r2lang-python
|
||||
* adds r2ai command inside r2
|
||||
* not recommended because of python versions pain
|
||||
* r2ai-server
|
||||
* list and select models downloaded from r2ai
|
||||
* simple cli tool to start local openapi webservers
|
||||
* supports llamafile, llamacpp, r2ai-w and kobaldcpp
|
||||
|
||||
## Features
|
||||
|
||||
* Configure different roles and customize prompts
|
||||
* Scriptable via r2pipe via the r2ai command
|
||||
* Live with repl and batch mode from cli or r2 prompt
|
||||
* Support Automatic (ReAct) mode to solve tasks using function calling
|
||||
* Use local and remote language models (ollama, openai, grok, anthropic, ..)
|
||||
* RAG markdown, code or textfiles using its native vector database
|
||||
* Support Auto mode (see below) to solve tasks using function calling
|
||||
* Use local and remote language models (llama, ollama, openai, anthropic, ..)
|
||||
* Support OpenAI, Anthropic, Bedrock
|
||||
* Index large codebases or markdown books using a vector database
|
||||
* Slurp file and perform actions on that
|
||||
* Embed the output of an r2 command and resolve questions on the given data
|
||||
|
||||
## Practical Examples
|
||||
|
||||
* Enhanced decompilation with `r2ai -d`
|
||||
* Autoname functions with `r2ai -n`
|
||||
* Explain function with `r2ai -x`
|
||||
* Find vulnerabilities with `r2ai -V`
|
||||
* Define different system-level assistant role
|
||||
* Set environment variables to provide context to the language model
|
||||
* Live with repl and batch mode from cli or r2 prompt
|
||||
* Accessible as an r2lang-python plugin, keeps session state inside radare2
|
||||
* Scriptable from python, bash, r2pipe, and javascript (r2papi)
|
||||
* Use different models, dynamically adjust query template
|
||||
* Load multiple models and make them talk between them
|
||||
|
||||
## Installation
|
||||
|
||||
The recommended way to install any of the r2ai components is via r2pm:
|
||||
Running `make` will setup a python virtual environment in the current directory installing all the necessary dependencies and will get into a shell to run r2ai.
|
||||
|
||||
```console
|
||||
$ r2pm -Uci r2ai
|
||||
$ r2pm -Uci decai
|
||||
The installation is now splitted into two different targets:
|
||||
|
||||
* `make install` will place a symlink in `$BINDIR/r2ai`
|
||||
* `make install-plugin` will install the native r2 plugin into your home
|
||||
* `make install-decai` will install the decai r2js decompiler plugin
|
||||
* `make install-server` will install the decai r2js decompiler plugin
|
||||
|
||||
## Running
|
||||
|
||||
When installed via r2pm you can execute it like this:
|
||||
|
||||
```bash
|
||||
r2pm -r r2ai
|
||||
```
|
||||
|
||||
## Using r2ai
|
||||
Additionally you can get the `r2ai` command inside r2 to run as an rlang plugin by installing the bindings:
|
||||
|
||||
- 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:
|
||||
|
||||
```console
|
||||
$ cat ~/.r2ai.anthropic-key
|
||||
sk-ant-api03-CENSORED
|
||||
$ export OPENAI_API_KEY=sk-proj-6rlSPS-zN1v...
|
||||
```bash
|
||||
r2pm -i rlang-python
|
||||
make user-install
|
||||
```
|
||||
|
||||
| AI | API key |
|
||||
| --------- | -------------------------- |
|
||||
| OpenAI | `$HOME/.r2ai.openai-key` |
|
||||
| Gemini | `$HOME/.r2ai.gemini-key` |
|
||||
| Anthropic | `$HOME/.r2ai.anthropic-key` |
|
||||
| Mistral | `$HOME/.r2ai.mistral-key` |
|
||||
After this you should get the `r2ai` command inside the radare2 shell. Set the `R2_DEBUG=1` environment to see the reasons why the plugin is not loaded if it's not there.
|
||||
|
||||
## Windows
|
||||
|
||||
## Saving settings
|
||||
On Windows you may follow the same instructions, just ensure you have the right python environment ready and create the venv to use
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
r2ai -e api=anthropic
|
||||
r2ai -e model=claude-3-7-sonnet-20250219
|
||||
r2ai -e max_tokens=64000
|
||||
```cmd
|
||||
git clone https://github.com/radareorg/r2ai
|
||||
cd r2ai
|
||||
set PATH=C:\Users\YOURUSERNAME\Local\Programs\Python\Python39\;%PATH%
|
||||
python3 -m pip -r requirements.txt
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
## Documentation
|
||||
## Usage
|
||||
|
||||
* There's [a chapter](https://book.rada.re/plugins/r2ai.html) in the official r2book
|
||||
* Cryptax on [lmstudio+gptoss](https://cryptax.medium.com/r2ai-with-lmstudio-and-gpt-oss-08efa5ea2476) blog post
|
||||
* Malware analysis [with r2ai](https://github.com/cryptax/talks/blob/master/BSidesKristiansand-2025/r2ai.pdf) by Cryptax
|
||||
There are 4 different ways to run `r2ai`:
|
||||
|
||||
## Videos
|
||||
* Standalone and interactive: `r2pm -r r2ai` or `python main.py`
|
||||
* Batch mode: `r2ai '-r act as a calculator' '3+3=?'`
|
||||
* As an r2 plugin: `r2 -i main.py /bin/ls`
|
||||
* From radare2 (requires `r2pm -ci rlang-python`): `r2 -c 'r2ai -h'`
|
||||
* Using r2pipe: `#!pipe python main.py`
|
||||
* Define a macro command: `'$r2ai=#!pipe python main.py`
|
||||
|
||||
- https://infosec.exchange/@radareorg/111946255058894583
|
||||
## Auto mode
|
||||
|
||||
When using OpenAI, Claude or any of the Functionary local models you can use the auto mode which permits the language model to execute r2 commands, analyze the output in loop and in a loop until it is resolved. Here's a sample session to achieve that:
|
||||
|
||||
* Video https://infosec.exchange/@radareorg/111946255058894583
|
||||
|
||||
```bash
|
||||
$ . env/bin/activate
|
||||
(env)$ r2 /bin/ls
|
||||
[0x00000000]> '$r2ai=#!pipe python main.py
|
||||
[0x00000000]> $r2ai '-m openai:gpt-4'
|
||||
[0x00000000]> $r2ai "' list the imports for this program"
|
||||
[0x00000000]> $r2ai "' draw me a donut"
|
||||
[0x00000000]> $r2ai "' decompile current function and explain it"
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
You can interact with r2ai from standalone python, from r2pipe via r2 keeping a global state or using the javascript interpreter embedded inside `radare2`.
|
||||
|
||||
* [conversation.r2.js](examples/conversation.r2.js) - load two models and make them talk to each other
|
||||
|
||||
### Development/Testing
|
||||
|
||||
Just run `make` .. or well `python3 main.py`
|
||||
|
||||
### TODO
|
||||
|
||||
* add "undo" command to drop the last message
|
||||
* dump / restore conversational states (see -L command)
|
||||
* Implement `~`, `|` and `>` and other r2shell features
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
vim src/r2ai.c
|
||||
vim VERSION
|
||||
@@ -1,20 +1,11 @@
|
||||
PD=$(shell r2 -H R2_USER_PLUGINS)
|
||||
SPD=$(shell r2 -H R2_LIBR_PLUGINS)
|
||||
#BF=/bin/dd
|
||||
BF=a.out
|
||||
|
||||
all:
|
||||
@echo "Run: make user-install"
|
||||
|
||||
test:
|
||||
@echo 'You can now use 'decai' command'
|
||||
r2 -i decai.r2.js -c 'decai -e deterministic=true' -caf $(BF)
|
||||
|
||||
fmt indent:
|
||||
deno fmt
|
||||
r2 -i decai.r2.js /usr/bin/sed
|
||||
|
||||
user-install:
|
||||
mkdir -p "$(PD)"
|
||||
cp decai.r2.js $(PD)/decai.r2.js
|
||||
|
||||
user-uninstall:
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
# Decai
|
||||
|
||||
The AI based Decompiler plugin for radare2
|
||||
|
||||
- Written in plain Javascript
|
||||
- No dependencies than radare2 and curl in PATH
|
||||
- Uses local ollama by default, but also r2ai-server
|
||||
- Use services like Anthropic, OpenAI, HF, XAI, DeepSeek, ..
|
||||
|
||||
Features
|
||||
|
||||
- Auto mode with function calling with ANY model
|
||||
- Uses the r2 pseudo decompiler by default, supports any other
|
||||
- Explain purpose and auto-document functions
|
||||
- Recursive decompilation to inline stubs
|
||||
- Autoname functions and perform type propagation
|
||||
- Find vulnerabilities, guide you and write exploits
|
||||
- Choose any natural language (not just English)
|
||||
- Choose output programming language (not just C)
|
||||
- Chain queries to manually fine tune the results
|
||||
- Customize decompilation prompt at any time
|
||||
|
||||
## Installation
|
||||
|
||||
Using r2pm: `r2pm -ci decai`
|
||||
|
||||
From source: `make user-install`
|
||||
|
||||
## Setup
|
||||
|
||||
```console
|
||||
[0x00000000]> decai
|
||||
Usage: decai (-h) ...
|
||||
decai -H - help setting up r2ai
|
||||
decai -d [f1 ..] - decompile given functions
|
||||
decai -dr - decompile function and its called ones (recursive)
|
||||
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 -h - show this help
|
||||
decai -i [f] [q] - include given file and query
|
||||
decai -n - suggest better function name
|
||||
decai -q [text] - query language model with given text
|
||||
decai -Q [text] - query on top of the last output
|
||||
decai -r - change role prompt (same as: decai -e prompt)
|
||||
decai -R - reset role prompt to default prompt
|
||||
decai -s - function signature
|
||||
decai -v - show local variables
|
||||
decai -V - find vulnerabilities
|
||||
decai -x - eXplain current function
|
||||
```
|
||||
|
||||
Configuration options:
|
||||
|
||||
```console
|
||||
[0x00000000]> decai -e
|
||||
decai -e api=ollama
|
||||
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 cmds=pdc
|
||||
decai -e cache=false
|
||||
decai -e lang=C
|
||||
decai -e hlang=English
|
||||
decai -e debug=false
|
||||
decai -e model=
|
||||
decai -e maxinputtokens=-1
|
||||
```
|
||||
|
||||
## Running decai
|
||||
|
||||
Decai is used from `r2` (e.g `r2 ./mybinary`). Get help with `decai -h`:
|
||||
|
||||
```
|
||||
[0x00406cac]> decai -h
|
||||
Usage: decai (-h) ...
|
||||
decai -H - help setting up r2ai
|
||||
decai -a [query] - solve query with auto mode
|
||||
decai -d [f1 ..] - decompile given functions
|
||||
decai -dr - decompile function and its called ones (recursive)
|
||||
decai -dd [..] - same as above, but ignoring cache
|
||||
decai -dD [query]- decompile current function with given extra query
|
||||
...
|
||||
```
|
||||
|
||||
List configuration variables with `decai -e`:
|
||||
|
||||
```
|
||||
[0x00406cac]> decai -e
|
||||
decai -e api=ollama
|
||||
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=
|
||||
...
|
||||
```
|
||||
|
||||
List possible APIs to discuss with AI: `decai -e api=?`:
|
||||
|
||||
```
|
||||
[0x00406cac]> decai -e api=?
|
||||
r2ai
|
||||
claude
|
||||
openapi
|
||||
...
|
||||
```
|
||||
|
||||
### Example using a local model and ollama
|
||||
|
||||
For example, if Ollama serves model codegeex4:latest (`ollama ls`), set decai
|
||||
API as `ollama` and model `codegeex4:latest`.
|
||||
|
||||
```
|
||||
[0x00002d30]> decai -e api=ollama
|
||||
[0x00002d30]> decai -e model=codegeex4:latest
|
||||
[0x00002d30]> decai -q Explain what forkpty does in 2 lines
|
||||
The `forkpty` function creates a new process with a pseudo-terminal, allowing the parent to interact with the child via standard input/output/err and controlling its terminal.
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See
|
||||
[https://github.com/radareorg/r2ai-examples](https://github.com/radareorg/r2ai-examples)
|
||||
|
||||
```c
|
||||
$ cat stack-overflow/bug.c
|
||||
|
||||
#include <string.h>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
char buf[32];
|
||||
strcpy (buf, argv[1]);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
$ r2 buffer-overflow/a.out
|
||||
[0x100003f58]> decai -d
|
||||
int main(int argc, char **argv, char **envp) {
|
||||
char buffer[32];
|
||||
int result = 0;
|
||||
|
||||
if (argc > 1 && argv[1] != NULL) {
|
||||
strcpy(buffer, argv[1]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### Example using a local Mistral model and r2ai-server
|
||||
|
||||
For example, assuming we have a _local_ Mistral AI server running on port 8080
|
||||
with `r2ai-server`, we can decompile a given function with `decai -d`. The
|
||||
server shows it received the question:
|
||||
|
||||
```
|
||||
GET
|
||||
CUSTOM
|
||||
RUNLINE: -R
|
||||
127.0.0.1 - - [13/Dec/2024 10:40:49] "GET /cmd/-R HTTP/1.1" 200 -
|
||||
GET
|
||||
CUSTOM
|
||||
RUNLINE: -i /tmp/.pdc.txt 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 and strings from comments like string:. Transform this pseudocode into C
|
||||
```
|
||||
|
||||
### Example using a Mistral API key
|
||||
|
||||
Put the API key in `~/.r2ai.mistral-key`.
|
||||
|
||||
````
|
||||
[0x000010d0]> decai -e api=mistral
|
||||
[0x000010d0]> decai -d main
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int main(int argc, char **argv, char **envp) {
|
||||
char password[40];
|
||||
char input[40];
|
||||
...
|
||||
````
|
||||
|
||||
### Example with ChatGPT 4
|
||||
|
||||
```
|
||||
[0x00406cac]> decai -e api=openai
|
||||
[0x00406cac]> decai -d
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
void daemonize() {
|
||||
daemon(1, 0);
|
||||
}
|
||||
...
|
||||
```
|
||||
@@ -1,13 +0,0 @@
|
||||
PACKAGE=decai
|
||||
DEPENDS=radare2
|
||||
SECTION=user/shell
|
||||
PRIORITY=optional
|
||||
MAINTAINER=pancake <[email protected]>
|
||||
VERSION=$(shell git tag | tail -n 1)
|
||||
# arch
|
||||
UNAMEM=$(shell uname -m)
|
||||
ifeq ($(UNAMEM),x86_64)
|
||||
ARCH=amd64
|
||||
else
|
||||
ARCH=arm64
|
||||
endif
|
||||
@@ -1,2 +0,0 @@
|
||||
AI based decompiler plugin for radare2
|
||||
r2js plugin for radare2 that provides several features for reverse engineers, from autonaming functions, improve decompiling outputs, find bugs or explain functions to automated crackme solving using local and remote models if API keys are provided..
|
||||
@@ -1,34 +0,0 @@
|
||||
include ./CONFIG
|
||||
|
||||
UNAME=$(shell uname)
|
||||
SUDO?=sudo
|
||||
DEPENDS=
|
||||
CROSSARCH=x64
|
||||
R2CFG_FLAGS?=
|
||||
PWD=$(shell pwd)
|
||||
PACKAGE_DIR?=${PWD}
|
||||
|
||||
R2_VERSION=$(shell r2 -qv)
|
||||
|
||||
DOCKCROSS=$(PWD)/../dockcross
|
||||
R2PLUGDIR=/usr/lib/radare2/$(R2_VERSION)
|
||||
|
||||
all: root
|
||||
$(SUDO) rm -rf control data
|
||||
$(MAKE) clean
|
||||
mkdir -p data
|
||||
cp -rf root/* data
|
||||
$(MAKE) control
|
||||
$(MAKE) deb
|
||||
|
||||
root:
|
||||
mkdir -p root/$(R2PLUGDIR)
|
||||
cp -f ../../decai.r2.js root/$(R2PLUGDIR)
|
||||
|
||||
purge: clean
|
||||
rm -rf root
|
||||
|
||||
summary:
|
||||
echo $(VERSION)
|
||||
|
||||
include deb.mk
|
||||
@@ -1,130 +0,0 @@
|
||||
# Create .deb without using dpkg tools.
|
||||
#
|
||||
# Author: Tim Wegener <[email protected]>
|
||||
#
|
||||
# Use 'include deb_hand.mak' after defining the user variables in a local
|
||||
# makefile.
|
||||
#
|
||||
# The 'data' rule must be customised in the local make file.
|
||||
# This rule should make a 'data' directory containing the full file
|
||||
# layout of the installed package.
|
||||
#
|
||||
# This makefile will create a debian-binary file a control directory and a
|
||||
# a build directory in the current directory.
|
||||
# Do 'make clobber' to remove these generated files.
|
||||
#
|
||||
# Destination:
|
||||
# PACKAGE_DIR - directory where package (and support files) will be built
|
||||
# defaults to the current directory
|
||||
#
|
||||
# Sources:
|
||||
# SOURCE_DIR - directory containing files to be packaged
|
||||
# ICON_SOURCE - 26x26 icon file for maemo
|
||||
# DESCR - description with summary on first line
|
||||
# preinst, postinst, prerm, postrm - optional control shell scripts
|
||||
|
||||
# These fields are used to build the control file:
|
||||
# PACKAGE =
|
||||
# VERSION =
|
||||
# ARCH =
|
||||
# SECTION =
|
||||
# PRIORITY =
|
||||
# MAINTAINER =
|
||||
# DEPENDS =
|
||||
#
|
||||
# SOURCE_DIR =
|
||||
# ICON_SOURCE =
|
||||
# (ICON_SOURCE is optional)
|
||||
|
||||
# *** NO USER CHANGES REQUIRED BEYOND THIS POINT ***
|
||||
ifeq ($(shell uname),Darwin)
|
||||
MD5SUM=md5
|
||||
else
|
||||
MD5SUM=md5sum
|
||||
endif
|
||||
|
||||
GAWK=awk
|
||||
PACKAGE_DIR=$(shell pwd)
|
||||
CONTROL_EXTRAS ?= ${wildcard preinst postinst prerm postrm}
|
||||
|
||||
${PACKAGE_DIR}/control: ${PACKAGE_DIR}/data ${CONTROL_EXTRAS} DESCR \
|
||||
${ICON_SOURCE}
|
||||
#rm -rf $@
|
||||
mkdir -p $@
|
||||
ifneq (${CONTROL_EXTRAS},)
|
||||
cp ${CONTROL_EXTRAS} $@
|
||||
endif
|
||||
# Make control file.
|
||||
echo "Package: ${PACKAGE}" > $@/control
|
||||
echo "Version: ${VERSION}" >> $@/control
|
||||
echo "Section: ${SECTION}" >> $@/control
|
||||
echo "Priority: ${PRIORITY}" >> $@/control
|
||||
echo "Architecture: ${ARCH}" >> $@/control
|
||||
ifneq (${DEPENDS},)
|
||||
echo "Depends: ${DEPENDS}" >> $@/control
|
||||
endif
|
||||
echo "Installed-Size: ${shell du -s ${PACKAGE_DIR}/data|cut -f1}" \
|
||||
>> $@/control
|
||||
echo "Maintainer: ${MAINTAINER}" >> $@/control
|
||||
printf "Description:" >> $@/control
|
||||
cat DESCR | ${GAWK} '{print " "$$0;}' >> $@/control
|
||||
#ifneq (${ICON_SOURCE},)
|
||||
# echo "Maemo-Icon-26:" >> $@/control
|
||||
# base64 ${ICON_SOURCE} | ${GAWK} '{print " "$$0;}' >> $@/control
|
||||
#endif
|
||||
# Make md5sums.
|
||||
cd ${PACKAGE_DIR}/data && find . -type f -exec ${MD5SUM} {} \; \
|
||||
| sed -e 's| \./||' \
|
||||
> $@/md5sums
|
||||
|
||||
${PACKAGE_DIR}/debian-binary:
|
||||
echo "2.0" > $@
|
||||
|
||||
${PACKAGE_DIR}/clean:
|
||||
rm -rf ${PACKAGE_DIR}/data ${PACKAGE_DIR}/control ${PACKAGE_DIR}/build *.deb
|
||||
|
||||
${PACKAGE_DIR}/build: ${PACKAGE_DIR}/debian-binary ${PACKAGE_DIR}/control \
|
||||
${PACKAGE_DIR}/data
|
||||
rm -rf $@
|
||||
mkdir $@
|
||||
cp ${PACKAGE_DIR}/debian-binary $@/
|
||||
cd ${PACKAGE_DIR}/control && tar czvf $@/control.tar.gz *
|
||||
cd ${PACKAGE_DIR}/data && \
|
||||
COPY_EXTENDED_ATTRIBUTES_DISABLE=true \
|
||||
COPYFILE_DISABLE=true \
|
||||
tar cpzvf $@/data.tar.gz *
|
||||
|
||||
# Convert GNU ar to BSD ar that debian requires.
|
||||
# Note: Order of files within ar archive is important!
|
||||
${PACKAGE_DIR}/${PACKAGE}_${VERSION}_${ARCH}.deb: ${PACKAGE_DIR}/build
|
||||
ar -rc $@ $</debian-binary $</control.tar.gz $</data.tar.gz
|
||||
#sed -e 's|^\([^/]\+\)/ \(.*\)|\1 \2|g' $@tmp > $@fail
|
||||
#rm -f $@tmp
|
||||
#mv $@fail $@
|
||||
|
||||
.PHONY: data
|
||||
data: ${PACKAGE_DIR}/data
|
||||
|
||||
.PHONY: control
|
||||
control: ${PACKAGE_DIR}/control
|
||||
|
||||
.PHONY: build
|
||||
build: ${PACKAGE_DIR}/build
|
||||
|
||||
.PHONY: clean
|
||||
clean: ${PACKAGE_DIR}/clean $(EXTRA_CLEAN)
|
||||
rm -f debian-binary
|
||||
|
||||
.PHONY: deb
|
||||
deb: ${PACKAGE_DIR}/${PACKAGE}_${VERSION}_${ARCH}.deb
|
||||
|
||||
|
||||
clobber::
|
||||
rm -rf ${PACKAGE_DIR}/debian_binary ${PACKAGE_DIR}/control \
|
||||
${PACKAGE_DIR}/data ${PACKAGE_DIR}/build
|
||||
|
||||
push:
|
||||
scp *.deb radare.org:/srv/http/radareorg/cydia/debs
|
||||
|
||||
mrproper: clean
|
||||
rm -rf root
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"use": true,
|
||||
"default": "pipeTest",
|
||||
"_default": "asciiStack",
|
||||
"asciiStack": {
|
||||
"globalQuery": "output in C, respond only with code, no explanations, no markdown",
|
||||
"pipeline": [
|
||||
{
|
||||
"model": "qwen2.5-coder:latest",
|
||||
"query": "stack frame layout for this function, output a table in ascii art"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pipeTest": {
|
||||
"globalQuery": "output in C, respond only with code, no explanations, no markdown",
|
||||
"pipeline": [
|
||||
{
|
||||
"model": "qwen2.5-coder:latest",
|
||||
"query": "resolve stack variables and arrays size without using malloc"
|
||||
},
|
||||
{
|
||||
"model": "qwen2.5-coder:14b",
|
||||
"query": "remove boilerplate and unnecessary code, replace goto with if/else blocks"
|
||||
}
|
||||
],
|
||||
"commentedOut": [
|
||||
{
|
||||
"model": "qwen2.5-coder:14b",
|
||||
"query": "use better types and names for variable and arguments"
|
||||
},
|
||||
{
|
||||
"model": "qwen2.5-coder:14b",
|
||||
"query": "inline all the functions into a single one and make the code more readable"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
FROM ubuntu:24.04
|
||||
WORKDIR /root/
|
||||
RUN apt update && apt install -y build-essential
|
||||
RUN apt install -y git curl pkg-config
|
||||
RUN apt install -y python3.12
|
||||
RUN apt install -y python3.12-venv
|
||||
RUN apt install -y zlib1g zlib1g-dev
|
||||
RUN apt install -y unzip neovim
|
||||
RUN apt install -y ninja-build meson
|
||||
|
||||
# install radare
|
||||
RUN git clone https://github.com/radareorg/radare2.git
|
||||
WORKDIR radare2/
|
||||
RUN sys/install.sh
|
||||
|
||||
# install plugins
|
||||
WORKDIR /root/
|
||||
RUN r2pm -U
|
||||
#RUN r2pm -ci r2ghidra r2ghidra-sleigh # slow and optional
|
||||
RUN r2pm -ci r2ai-server r2ai-plugin # r2ai-py is deprecated
|
||||
RUN r2pm -ci decai r2dec
|
||||
|
||||
# r2 tunning
|
||||
RUN echo 'e io.unalloc=true' >> /root/.radare2rc
|
||||
RUN echo 'e cfg.fortunes=false' >> /root/.radare2rc
|
||||
RUN echo 'e emu.str=true' >> /root/.radare2rc
|
||||
RUN echo 'e bin.relocs.apply=true' >> /root/.radare2rc
|
||||
RUN echo 'e dir.tmp=/tmp/' >> /root/.radare2rc
|
||||
RUN echo 'e scr.prompt.flag=1' >> /root/.radare2rc
|
||||
RUN echo 'e cfg.editor=nvim' >> /root/.radare2rc
|
||||
RUN echo 'decai -e deterministic=true' >> /root/.radare2rc
|
||||
RUN echo 'decai -e lang=Python' >> /root/.radare2rc
|
||||
RUN echo 'decai -e cmds=pdd,pdg' >> /root/.radare2rc
|
||||
RUN echo 'decai -e prompt=..' >> /root/.radare2rc
|
||||
RUN echo 'decai -e api=ollama' >> /root/.radare2rc
|
||||
RUN echo 'decai -e model=qwen2.5-coder:latest' >> /root/.radare2rc
|
||||
|
||||
|
||||
# Uncomment to have ollama inside the docker (will not use gpu)
|
||||
#RUN curl -fsSL https://ollama.com/install.sh | sh
|
||||
#RUN \
|
||||
# echo '#!/bin/bash'; \
|
||||
# echo 'ollama serve &'; \
|
||||
# echo 'sleep 5'; \
|
||||
# echo 'ollama pull qwen2.5-coder:latest'; >> /root/ollama.run
|
||||
#
|
||||
#RUN chmod +x /root/ollama.run
|
||||
#RUN /root/ollama.run
|
||||
|
||||
|
||||
@@ -1,163 +1,70 @@
|
||||
radare2's author is Sergi Alvarez, also known as pancake.
|
||||
pancake is from Catalonia, Barcelona.
|
||||
radare2 originated in 2006 as a forensic tool for HFS+ recovery.
|
||||
The initial radare aimed for portability, 64bit offset handling, and hex pattern searching.
|
||||
radare2 (r2) was born in 2009 as a refactored, modular version of radare1.
|
||||
radare2 follows UNIX principles: "everything is a file", small interacting programs.
|
||||
The r2pipe API allows accessing radare2 via pipes from any language.
|
||||
radare2 development is managed on GitHub and discussed on Telegram/Discord.
|
||||
r2con is the annual radare2 community conference, usually held in Barcelona.
|
||||
what is ESIL? the meaning is Evaluable Strings Intermediate Language.
|
||||
ESIL is used by radare2 to emulate native code.
|
||||
radare2 treats executables, disk images, memory dumps, and processes as binary files.
|
||||
Core radare2 features include hex editing, disassembly, debugging, patching, and scripting.
|
||||
ACR stands for AutoConfReplacement, a minimalistic autoconf tool used by radare2.
|
||||
The radare2 GitHub repository is located at radareorg/radare2.
|
||||
To install radare2 from source, clone the repository and run `sys/install.sh`.
|
||||
Build issues can sometimes be resolved by cleaning plugin/cache directories (`~/.local/share/radare2/`).
|
||||
Use `R2_DEBUG=1` environment variable for startup debugging information.
|
||||
`make purge` can help remove old radare2 installations.
|
||||
Use `r2 <file>` to open a file in radare2.
|
||||
Use `r2 -` to open radare2 with a temporary memory buffer (malloc plugin).
|
||||
Use `r2 -w <file>` to open a file in write mode.
|
||||
Use `r2 -n <file>` to open a file without parsing headers.
|
||||
The `-p [prj]` flag creates a project for the analyzed file.
|
||||
Reopen a file in read-write mode using `oo+`.
|
||||
Reopen the current binary in debugger mode using `ood`.
|
||||
Quit radare2 using the `q` command.
|
||||
Run a script on startup using the `-i` command-line flag.
|
||||
Load a script or plugin within r2 using `. <script_path>`.
|
||||
Execute shell commands within r2 using `! <command>`.
|
||||
Pipe r2 command output to shell commands using `cmd | <shell_cmd>`.
|
||||
Interpret r2 command output as commands using `.cmd`. Example: `.is*`.
|
||||
Repeat the last command using `..` or Enter.
|
||||
Evaluate expressions using `?`. Example: `? 0x10 + 5`.
|
||||
`$$` represents the current virtual address (seek).
|
||||
Execute commands at specific offsets using `@`. Example: `px @ 0x1000`.
|
||||
Iterate commands over multiple offsets using `@@`. Example: `wx ff @@ 10 20 30`.
|
||||
Seek to an address or symbol using `s <address>`. Example: `s main`.
|
||||
Seek backward using `s- <num>`. Example: `s- 16`.
|
||||
Undo the last seek using `s-`.
|
||||
Redo the last seek using `s+`.
|
||||
Change the block size (default view size) using `b <size>`. Example: `b 256`.
|
||||
Print hex dump using `px [<num_bytes>]`.
|
||||
Print hex dump of words using `pxw [<num_words>]`.
|
||||
Print disassembled instructions using `pd [<num_instructions>]`.
|
||||
Print disassembled bytes using `pD [<num_bytes>]`.
|
||||
Print zero-terminated strings using `pz`.
|
||||
Print zero-terminated string at offset using `psz [<max_len>] @ <offset>`.
|
||||
Get file information using `i`.
|
||||
Get information about the binary format using `iI`.
|
||||
List entry points using `ie`.
|
||||
List imports using `ii`.
|
||||
List linked libraries using `il`.
|
||||
List sections using `iS`.
|
||||
List symbols using `is`.
|
||||
Get the symbol name at the current offset using `is.`.
|
||||
Extract strings from the data section using `iz`.
|
||||
Extract strings from the entire binary using `izz`.
|
||||
Search for strings using `/ <string>`. Example: `/ "password"`.
|
||||
Search for hex patterns using `/x <hexpairs>`. Example: `/x cafeba.e`.
|
||||
Search for assembly instructions using `/c <asm_string>`. Example: `/c jmp eax`.
|
||||
Assemble instructions and search for bytes using `/a <asm_string>`. Example: `/a mov eax, 1`.
|
||||
Search for ROP gadgets using `/R`.
|
||||
Define configuration variables using `e <var>=<value>`. Example: `e asm.arch=arm`.
|
||||
Check configuration variable values using `e <var>`. Example: `e asm.bits`.
|
||||
Access configuration values in expressions using `${var}`. Example: `? ${asm.bits}`.
|
||||
Set `asm.pseudo=true` for pseudo-syntax disassembly.
|
||||
Set `asm.describe=true` for inline opcode descriptions.
|
||||
Analyze all functions and code blocks using `aa`.
|
||||
Analyze functions using `af`.
|
||||
Analyze function at current offset using `af`.
|
||||
Analyze basic blocks within a function using `afb`.
|
||||
Analyze data references using `ad`. Example: `ad@rsp`.
|
||||
List analyzed functions using `afl`.
|
||||
Rename a function using `afn <new_name> [@<addr>]`.
|
||||
List cross-references (xrefs) *to* an address using `axt [<addr>]`.
|
||||
List cross-references (xrefs) *from* an address using `axf [<addr>]`.
|
||||
View instruction operand details using `ao`.
|
||||
List flags matching a pattern using `f~<pattern>`. Example: `f~sym`.
|
||||
List flags in a specific flagspace using `fs <space>; f`. Example: `fs imports; f`.
|
||||
Enable analysis of consecutive functions using `e anal.hasnext=true`.
|
||||
Start radare2 in debug mode using `r2 -d <file>`.
|
||||
Attach to a process using `r2 -d <pid>`.
|
||||
Step into the next instruction using `ds`.
|
||||
Step over the next instruction using `dso`.
|
||||
Continue execution using `dc`.
|
||||
Set a breakpoint using `db <address>`.
|
||||
List breakpoints using `db`.
|
||||
Remove a breakpoint using `db- <address>`.
|
||||
View register values using `dr`.
|
||||
View register values and dereferenced pointers using `drr`.
|
||||
Set `dbg.bep=entry` or `dbg.bep=main` to break at entry/main on start.
|
||||
Use `dp` to list processes after a fork.
|
||||
Use `dcf` to continue until a fork occurs.
|
||||
Enable tracing child processes on fork using `e dbg.follow.child=true`.
|
||||
Enter visual mode using `V`.
|
||||
Exit visual mode using `q`.
|
||||
Rotate visual modes (hex, disasm, etc.) using `p`/`P`.
|
||||
Toggle cursor mode using `c`.
|
||||
Move cursor using `hjkl`.
|
||||
Follow jump/call address using Enter.
|
||||
Step/Step Over in visual debug mode using `s`/`S`.
|
||||
Toggle breakpoints using `b`.
|
||||
Seek to program counter using `.`.
|
||||
Add/remove comments using `;`.
|
||||
Rename functions/variables in visual mode using `dr`.
|
||||
Define functions/data in visual mode using `d`.
|
||||
Switch between graph and panel views using Spacebar.
|
||||
Iaito is the official Qt-based GUI for radare2.
|
||||
`r2pm` is the radare2 package manager.
|
||||
Install plugins using `r2pm -ci <plugin_name>`. Example: `r2pm -ci r2ghidra`.
|
||||
The radare2 toolchain includes `rabin2`, `rasm2`, `rax2`, `rahash2`, `radiff2`, `rafind2`.
|
||||
`rabin2` extracts information from binary files (headers, symbols, sections).
|
||||
`rasm2` assembles and disassembles code for various architectures.
|
||||
`rax2` converts between bases and evaluates expressions.
|
||||
`rahash2` computes hashes and checksums.
|
||||
`radiff2` compares binary files.
|
||||
`rafind2` searches for patterns in files.
|
||||
`r2ghidra` is a decompiler plugin using Ghidra's decompiler engine.
|
||||
Use `pdg` for r2ghidra decompilation.
|
||||
r2ghidra quality may differ from native Ghidra due to analysis differences.
|
||||
`r2dec` is another decompiler plugin available via r2pm.
|
||||
Use `pdd` for r2dec decompilation.
|
||||
`pdc` is radare2's built-in pseudo-decompiler.
|
||||
`pdc` combines ESIL emulation and pseudo-assembly.
|
||||
`pdc` output can be verbose but works across many architectures.
|
||||
`decai` is an AI-powered plugin enhancing decompiler output readability.
|
||||
`decai` often uses `pdc` as a backend and feeds output to an AI model.
|
||||
`decai` can explain functions (`-x`), find vulnerabilities (`-V`), suggest names (`-n`).
|
||||
`r2ai` bridges radare2 with AI language models (local or remote).
|
||||
`r2ai` can run in direct mode (prompt -> AI) or auto mode (AI uses tools like `r2cmd`).
|
||||
`r2ai-model` is a dataset project for training LLMs about radare2.
|
||||
The r2ai-model dataset uses a TSV format (Question TAB r2-command-answer).
|
||||
`r2frida` integrates radare2 with the Frida dynamic instrumentation toolkit.
|
||||
Use `r2frida` commands starting with `:` (previously `\`).
|
||||
Trace function calls with `r2frida` using `:dt`.
|
||||
Format trace output and trace onEnter/onExit using `:dtf`.
|
||||
Use `:dtf` format specifiers like `z` (string), `c` (char), `x` (hex).
|
||||
`r2frida` can trace functions by address, import/export names.
|
||||
Use `:iE` in r2frida to enumerate exports of a loaded module.
|
||||
Trace specific call destinations found using analysis commands like `/at call`.
|
||||
`r2yara` integrates YARA rule matching into radare2.
|
||||
Install `r2yara` using `r2pm -ci r2yara`.
|
||||
`r2yara` commands start with `yr` or `yara`.
|
||||
Load YARA rules from a file using `yr <file>`.
|
||||
Scan the current file with loaded rules using `yrs`.
|
||||
List loaded YARA rules using `yrl`.
|
||||
Generate YARA rules interactively using `yrg` subcommands.
|
||||
Add strings to a generated rule using `yrgs`.
|
||||
Add hex patterns to a generated rule using `yrgx`.
|
||||
Add the currently generated rule using `yr+`.
|
||||
Flatpak releases of Iaito bundle plugins like r2dec, r2ghidra, r2frida, r2yara, decai.
|
||||
Optimize text for vector databases by using concise, factual statements.
|
||||
Rephrasing concepts helps improve embedding quality for semantic search.
|
||||
AI embeddings represent data (like text or code) in a high-dimensional space.
|
||||
Vector databases efficiently store and retrieve these embeddings.
|
||||
Embeddings help capture semantic relationships (e.g., King - Man + Woman ≈ Queen).
|
||||
Use `~<num>` to grep specific columns from command output. Example: `axt sym.main~[1]`.
|
||||
Use `~..` to view command output in a pager (like less).
|
||||
Use `~{}` to pretty-print JSON output. Example: `pdfj~{}`.
|
||||
Use `.radare2rc` file in the home directory for persistent configurations.
|
||||
Place `.r2` files in a binary's directory for project-specific scripts/settings.
|
||||
Count the functions found with aflc
|
||||
the author of radare2 is named pancake, also known as Sergi Alvarez
|
||||
reopen the file in read-write mode use the `oo+` command
|
||||
pancake was born in Catalonia, Sergi Alvarez, r2 author is from Barcelona
|
||||
to run a script use the -i commandline flag. Some cmdline flags are also accessible from the r2 shell.
|
||||
you can also use `. ...` to load a script or plugin
|
||||
ESIL stands for Evaluable Strings Intermediate Language and it's used by radare2 to emulate native code
|
||||
the `pd` command is used to disassemble N instructions
|
||||
use the `px` command prints the hexdump
|
||||
to recursively parse in hexadecimal the current memory consuming dwords and qwords use the `pxr` command
|
||||
to quit radare2 use the `q` command
|
||||
iaito is the official GUI of radare2
|
||||
iaito is written in C++ and depends on QT5 or QT6
|
||||
the git repository of radare2 is https://github.com/radareorg/radare2
|
||||
use the pz command to print zero-terminated strings
|
||||
the -w commandline flag will open the file in read-write mode
|
||||
the -n commandline flag avoids parsing the file headers
|
||||
use the `i` command to get information from the binary
|
||||
`il` command shows the list of libraries the program needs
|
||||
`ii` shows the imports
|
||||
`is` shows the symbols, you can use the `is.` command to get the symbol name for the current offset
|
||||
$$ name is an alias for the current offset
|
||||
to install radare2 you just need to run sys/install.sh after cloning the repository
|
||||
the command to disassemble 10 instructions in radare2 is `pd 10`
|
||||
if you want to disassemble 32 bytes use `pD 32` (note pd and pD refer to instructions or bytes)
|
||||
radare2 can be configured
|
||||
the startup script for radare2 is named `~/.radare2rc`. This file is used also to define the initial configuration options.
|
||||
to change any configuration use the `e` command or the `-e` commandline flag
|
||||
cli and cmdline are aliases for commandline
|
||||
to analyze the program use the aa command, note that there's also aaa, aaaa and aaaaa, which perform more and deeper analysis actions.
|
||||
to seek, move or change the current in radare2 use the `s` command.
|
||||
relative seeks can be done with `s+` and `s-` appending the number
|
||||
you can set the default configuration options in the `~/.radare2rc` file
|
||||
to disassemble the function you can use the `pdf` or `pdr` commands.
|
||||
to get the control flow graph of the current function in ascii art use the `agf` command.
|
||||
the ?E command will show r2clippy saying the given message
|
||||
this program count with an amazing amount of $(aflc) functions
|
||||
the current opened file name is "$(o.)"
|
||||
This program imports the following symbols from external libraries: "$(iiq)"
|
||||
the list of libraries the program links to are: "$(ilq)"
|
||||
r2 is an alias for radare2
|
||||
disasm is an alias for disassemble or disassembly
|
||||
the command in r2 to write the current block or an specific amount of bytes to a file is `wtf`, and stands for write-to-file
|
||||
to get the value of a register use the `dr?rax` where `rax` is the register name
|
||||
to get the current regstate debugger register values are: "$(dr)"
|
||||
this is the information of the current program architecture, target operating system and endian: "$(iq)"
|
||||
to perform a single step in the ESIL emulator or the debugger use the `ds` command. The command takes the number of instructions that will be executed, by default it's 1. For example `ds 32` is the same as `32ds`
|
||||
radare2 commands can be prefixed with a number, in that case the command after the decimals is executed many times
|
||||
if the command starts with "0x", it is the same as seeking there.
|
||||
the current value of the program counter $(dr?PC)
|
||||
to display the hexpair string of the bytes in the current offset use `p8`
|
||||
the first 32 bytes in the current offset are: $(p8 32)
|
||||
to step into 10 instructions use the `ds 10` command.
|
||||
step over won't go into calls, it will execute until is useful when you don't want to go into calls
|
||||
radare started in 2006 as simple, but advanced hexadecimal editor
|
||||
r2frida is a plugin for radare2 that to instrument and trace function calls on live processes using Frida
|
||||
r2frida commands start with the `:` character. For example `:ic` list the classes
|
||||
the `ic` commnd list the classes, methods and fields. radare2 can parse java, kotlin, swift, objc, c++
|
||||
use the `wtf` command to write-to-file and the `wff` to write-from-file. think like dump and slurp
|
||||
r2pipe example script: ```js\nconst msg = r2.cmd("?E Hello World");\nconsole.log(msg);\n```
|
||||
the r2pipe js API provides a .cmd() and .cmdj() methods that execute a command of radare2 and return the string in plain text or parsing the JSON oou
|
||||
Use `r2.cmdj()` when you want to parse the output of the command as if it was JSON. This is useful to get structured information from r2. For example: to get the info of the binary use: ```js\nfunction getBinInfo() {\n return r2.cmdj("ij")\n}\n```` neer2pipe js API provides a .cmd() and .cmdj() methods that execute a command of radare2 and return the string in plain text or parsing the JSON oou
|
||||
the radare project started the year 2006 by pancake and it was designed as a tool for forensics analysis
|
||||
the first purpose of radare was to recover some deleted files from an HFS partition on an old macppc, the tool accessed the disk in raw and searched for known strings and dumped the results as separte files to disk.
|
||||
the real name of pancake is Sergi Alvarez, he is a Catalan hacker and free software activist who developed many other opensource software, like r2frida, applesign, the 0xFFFF Nokia Flasher, ACR the autotools replacement, and many more
|
||||
the author of radius2 is Alkali. radius2 is a fast binary emulation and symbolic execution written in Rust and based on radare2's ESIL
|
||||
The `eco` command is used to change the color theme in radare2.
|
||||
To list all the available color themes run `eco` without any argument. But you can change the current one by passing it as argument.
|
||||
To edit the radare2rc file use the `ed` command. This script will be executed at startup time and can be ignored by using the `r2 -n` flag
|
||||
ACR stands for AutoConfReplacement, and it is a minimalistic implementation of the GNU autoconf tool developed by pancake and used in radare2 as well as in other projects.
|
||||
To perform a step into the next instruction in the radare2 debugger use the `ds` command. To step over use `dso`.
|
||||
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,112 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (https://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
id="svg3004"
|
||||
version="1.1"
|
||||
inkscape0:version="0.48.2 r9819"
|
||||
width="500"
|
||||
height="300"
|
||||
xml:space="preserve"
|
||||
sodipodi0:docname="r2.svg"
|
||||
sodipodi:docname="r2ai-solid-black.svg"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:inkscape0="https://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi0="https://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:ns0="https://creativecommons.org/ns#"
|
||||
xmlns:ns="https://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:ns1="https://purl.org/dc/elements/1.1/"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
showguides="true"
|
||||
inkscape:zoom="1.207"
|
||||
inkscape:cx="170.25684"
|
||||
inkscape:cy="153.27258"
|
||||
inkscape:window-width="1714"
|
||||
inkscape:window-height="845"
|
||||
inkscape:window-x="26"
|
||||
inkscape:window-y="23"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg3004" /><metadata
|
||||
id="metadata3010"><RDF><ns0:Work
|
||||
ns:about=""><ns1:format>image/svg+xml</ns1:format><ns1:type
|
||||
ns:resource="https://purl.org/dc/dcmitype/StillImage" /><ns1:title /></ns0:Work></RDF></metadata><defs
|
||||
id="defs3008"><pattern
|
||||
inkscape:collect="always"
|
||||
xlink:href="#pattern6"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
id="pattern7"
|
||||
patternTransform="translate(-3.203125e-6)" /><clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath3018"><path
|
||||
d="m 0,841.89 1190.55,0 L 1190.55,0 0,0 0,841.89 z"
|
||||
id="path3020"
|
||||
inkscape0:connector-curvature="0" /></clipPath><pattern
|
||||
patternUnits="userSpaceOnUse"
|
||||
width="1.2374369"
|
||||
height="110.30865"
|
||||
patternTransform="translate(295.04028,-128.87021)"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
id="pattern6"><rect
|
||||
style="fill-opacity:1;stroke:none;stroke-width:0.755906"
|
||||
id="rect6"
|
||||
width="1.2374369"
|
||||
height="110.30865"
|
||||
x="0"
|
||||
y="0"
|
||||
ry="0.61871845" /></pattern><marker
|
||||
markerWidth="1.2374369"
|
||||
markerHeight="110.30865"
|
||||
refX="0.61871845"
|
||||
refY="55.154324"
|
||||
orient="auto"
|
||||
id="marker7"><rect
|
||||
style="fill:url(#pattern7);stroke:none"
|
||||
width="1.2374369"
|
||||
height="110.30865"
|
||||
x="0"
|
||||
y="0"
|
||||
id="rect7" /></marker></defs><sodipodi0:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape0:pageopacity="0"
|
||||
inkscape0:pageshadow="2"
|
||||
inkscape0:window-width="791"
|
||||
inkscape0:window-height="818"
|
||||
id="namedview3006"
|
||||
showgrid="false"
|
||||
inkscape0:zoom="0.39912236"
|
||||
inkscape0:cx="715.69007"
|
||||
inkscape0:cy="676.77258"
|
||||
inkscape0:window-x="2687"
|
||||
inkscape0:window-y="256"
|
||||
inkscape0:window-maximized="0"
|
||||
inkscape0:current-layer="g3012" /><path
|
||||
id="path3496-8-6-5-6-6-2"
|
||||
style="fill:#c8191c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.30402"
|
||||
d="m 325.38488,77.033582 c 0,0 -84.14563,42.868488 -84.89316,43.234548 l 12.82427,24.21005 h 72.06889 z m -142.20506,71.694888 49.85451,24.92726 12.85076,-24.2732 -13.29459,-25.13029 c -12.94186,6.3903 -34.27459,16.93531 -49.41068,24.47623 z m 70.6374,4.61097 -12.85482,24.27931 84.42248,42.80665 v -67.08596 z"
|
||||
sodipodi:nodetypes="ccccccccccccccc" /><path
|
||||
id="path3496-8-6-5-6-6-2-2"
|
||||
style="fill:#c8191c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.30402"
|
||||
d="m 467.89541,76.210715 c 0,0 -84.14563,42.868485 -84.89315,43.234545 l 12.82426,24.21005 h 72.06889 z m -142.20506,71.694895 49.85451,24.92725 12.85076,-24.2732 -13.29459,-25.13029 c -12.94186,6.3903 -34.27459,16.93532 -49.41068,24.47624 z m 70.6374,4.61096 -12.85482,24.27931 84.42248,42.80665 v -67.08596 z"
|
||||
sodipodi:nodetypes="ccccccccccccccc" /><path
|
||||
d="M 183.41964,89.200886 V 213.41057 h 15.49313 v 7.01435 H 90.772581 v -7.01435 h 20.749649 v -67.21858 c 0,-11.98138 0.8776,-22.50487 -3.21182,-33.90204 -0.8776,-2.92232 -9.641954,-19.579945 -14.028698,-12.27479 1.17101,4.38544 1.755211,9.35247 1.755211,13.73921 0,19.57733 -13.445796,32.14422 -32.733634,32.14422 -20.165439,0 -34.192836,-14.02741 -34.192836,-34.19284 0,-21.04176 18.412827,-32.437632 37.700665,-32.437632 20.745742,0 37.403352,11.102474 47.055742,28.931102 h 0.57899 V 78.680007 c 28.05349,0.877612 56.4043,3.507829 84.46692,3.507829 v 7.01305 z"
|
||||
style="fill:#1a1a1a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.30402"
|
||||
id="path3492-8-7"
|
||||
inkscape0:connector-curvature="0" /></svg>
|
||||
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,112 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (https://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
id="svg3004"
|
||||
version="1.1"
|
||||
inkscape0:version="0.48.2 r9819"
|
||||
width="500"
|
||||
height="300"
|
||||
xml:space="preserve"
|
||||
sodipodi0:docname="r2.svg"
|
||||
sodipodi:docname="r2ai-solid-white.svg"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:inkscape0="https://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi0="https://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:ns0="https://creativecommons.org/ns#"
|
||||
xmlns:ns="https://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:ns1="https://purl.org/dc/elements/1.1/"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
showguides="true"
|
||||
inkscape:zoom="1.207"
|
||||
inkscape:cx="170.25684"
|
||||
inkscape:cy="153.27258"
|
||||
inkscape:window-width="1714"
|
||||
inkscape:window-height="845"
|
||||
inkscape:window-x="26"
|
||||
inkscape:window-y="23"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg3004" /><metadata
|
||||
id="metadata3010"><RDF><ns0:Work
|
||||
ns:about=""><ns1:format>image/svg+xml</ns1:format><ns1:type
|
||||
ns:resource="https://purl.org/dc/dcmitype/StillImage" /><ns1:title /></ns0:Work></RDF></metadata><defs
|
||||
id="defs3008"><pattern
|
||||
inkscape:collect="always"
|
||||
xlink:href="#pattern6"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
id="pattern7"
|
||||
patternTransform="translate(-3.203125e-6)" /><clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath3018"><path
|
||||
d="m 0,841.89 1190.55,0 L 1190.55,0 0,0 0,841.89 z"
|
||||
id="path3020"
|
||||
inkscape0:connector-curvature="0" /></clipPath><pattern
|
||||
patternUnits="userSpaceOnUse"
|
||||
width="1.2374369"
|
||||
height="110.30865"
|
||||
patternTransform="translate(295.04028,-128.87021)"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
id="pattern6"><rect
|
||||
style="fill-opacity:1;stroke:none;stroke-width:0.755906"
|
||||
id="rect6"
|
||||
width="1.2374369"
|
||||
height="110.30865"
|
||||
x="0"
|
||||
y="0"
|
||||
ry="0.61871845" /></pattern><marker
|
||||
markerWidth="1.2374369"
|
||||
markerHeight="110.30865"
|
||||
refX="0.61871845"
|
||||
refY="55.154324"
|
||||
orient="auto"
|
||||
id="marker7"><rect
|
||||
style="fill:url(#pattern7);stroke:none"
|
||||
width="1.2374369"
|
||||
height="110.30865"
|
||||
x="0"
|
||||
y="0"
|
||||
id="rect7" /></marker></defs><sodipodi0:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape0:pageopacity="0"
|
||||
inkscape0:pageshadow="2"
|
||||
inkscape0:window-width="791"
|
||||
inkscape0:window-height="818"
|
||||
id="namedview3006"
|
||||
showgrid="false"
|
||||
inkscape0:zoom="0.39912236"
|
||||
inkscape0:cx="715.69007"
|
||||
inkscape0:cy="676.77258"
|
||||
inkscape0:window-x="2687"
|
||||
inkscape0:window-y="256"
|
||||
inkscape0:window-maximized="0"
|
||||
inkscape0:current-layer="g3012" /><path
|
||||
id="path3496-8-6-5-6-6-2"
|
||||
style="fill:#c8191c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.30402"
|
||||
d="m 325.38488,77.033582 c 0,0 -84.14563,42.868488 -84.89316,43.234548 l 12.82427,24.21005 h 72.06889 z m -142.20506,71.694888 49.85451,24.92726 12.85076,-24.2732 -13.29459,-25.13029 c -12.94186,6.3903 -34.27459,16.93531 -49.41068,24.47623 z m 70.6374,4.61097 -12.85482,24.27931 84.42248,42.80665 v -67.08596 z"
|
||||
sodipodi:nodetypes="ccccccccccccccc" /><path
|
||||
id="path3496-8-6-5-6-6-2-2"
|
||||
style="fill:#c8191c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.30402"
|
||||
d="m 467.89541,76.210715 c 0,0 -84.14563,42.868485 -84.89315,43.234545 l 12.82426,24.21005 h 72.06889 z m -142.20506,71.694895 49.85451,24.92725 12.85076,-24.2732 -13.29459,-25.13029 c -12.94186,6.3903 -34.27459,16.93532 -49.41068,24.47624 z m 70.6374,4.61096 -12.85482,24.27931 84.42248,42.80665 v -67.08596 z"
|
||||
sodipodi:nodetypes="ccccccccccccccc" /><path
|
||||
d="M 183.41964,89.200886 V 213.41057 h 15.49313 v 7.01435 H 90.772581 v -7.01435 h 20.749649 v -67.21858 c 0,-11.98138 0.8776,-22.50487 -3.21182,-33.90204 -0.8776,-2.92232 -9.641954,-19.579945 -14.028698,-12.27479 1.17101,4.38544 1.755211,9.35247 1.755211,13.73921 0,19.57733 -13.445796,32.14422 -32.733634,32.14422 -20.165439,0 -34.192836,-14.02741 -34.192836,-34.19284 0,-21.04176 18.412827,-32.437632 37.700665,-32.437632 20.745742,0 37.403352,11.102474 47.055742,28.931102 h 0.57899 V 78.680007 c 28.05349,0.877612 56.4043,3.507829 84.46692,3.507829 v 7.01305 z"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.30402"
|
||||
id="path3492-8-7"
|
||||
inkscape0:connector-curvature="0" /></svg>
|
||||
|
Before Width: | Height: | Size: 5.1 KiB |
@@ -1,209 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (https://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
id="svg3004"
|
||||
version="1.1"
|
||||
inkscape0:version="0.48.2 r9819"
|
||||
width="500"
|
||||
height="300"
|
||||
xml:space="preserve"
|
||||
sodipodi0:docname="r2.svg"
|
||||
sodipodi:docname="r2ai.svg"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:inkscape0="https://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi0="https://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:ns0="https://creativecommons.org/ns#"
|
||||
xmlns:ns="https://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:ns1="https://purl.org/dc/elements/1.1/"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
showguides="true"
|
||||
inkscape:zoom="1.207"
|
||||
inkscape:cx="170.25684"
|
||||
inkscape:cy="153.27258"
|
||||
inkscape:window-width="1714"
|
||||
inkscape:window-height="845"
|
||||
inkscape:window-x="26"
|
||||
inkscape:window-y="23"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg3004" /><metadata
|
||||
id="metadata3010"><RDF><ns0:Work
|
||||
ns:about=""><ns1:format>image/svg+xml</ns1:format><ns1:type
|
||||
ns:resource="https://purl.org/dc/dcmitype/StillImage" /><ns1:title /></ns0:Work></RDF></metadata><defs
|
||||
id="defs3008"><marker
|
||||
style="overflow:visible"
|
||||
id="Stop"
|
||||
refX="0"
|
||||
refY="0"
|
||||
orient="auto"
|
||||
inkscape:stockid="Stop"
|
||||
markerWidth="2"
|
||||
markerHeight="19"
|
||||
viewBox="0 0 1 1"
|
||||
inkscape:isstock="true"
|
||||
inkscape:collect="always"
|
||||
preserveAspectRatio="none"><path
|
||||
style="fill:none;stroke:context-stroke;stroke-width:1"
|
||||
d="M 0,4 V -4"
|
||||
id="path23" /></marker><pattern
|
||||
inkscape:collect="always"
|
||||
xlink:href="#pattern6"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
id="pattern7"
|
||||
patternTransform="translate(-3.203125e-6)" /><clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath3018"><path
|
||||
d="m 0,841.89 1190.55,0 L 1190.55,0 0,0 0,841.89 z"
|
||||
id="path3020"
|
||||
inkscape0:connector-curvature="0" /></clipPath><pattern
|
||||
patternUnits="userSpaceOnUse"
|
||||
width="1.2374369"
|
||||
height="110.30865"
|
||||
patternTransform="translate(295.04028,-128.87021)"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
id="pattern6"><rect
|
||||
style="fill-opacity:1;stroke:none;stroke-width:0.755906"
|
||||
id="rect6"
|
||||
width="1.2374369"
|
||||
height="110.30865"
|
||||
x="0"
|
||||
y="0"
|
||||
ry="0.61871845" /></pattern><marker
|
||||
markerWidth="1.2374369"
|
||||
markerHeight="110.30865"
|
||||
refX="0.61871845"
|
||||
refY="55.154324"
|
||||
orient="auto"
|
||||
id="marker7"><rect
|
||||
style="fill:url(#pattern7);stroke:none"
|
||||
width="1.2374369"
|
||||
height="110.30865"
|
||||
x="0"
|
||||
y="0"
|
||||
id="rect7" /></marker><marker
|
||||
style="overflow:visible"
|
||||
id="Stop-3"
|
||||
refX="0"
|
||||
refY="0"
|
||||
orient="auto"
|
||||
inkscape:stockid="Stop"
|
||||
markerWidth="1"
|
||||
markerHeight="19"
|
||||
viewBox="0 0 1 1"
|
||||
inkscape:isstock="true"
|
||||
inkscape:collect="always"
|
||||
preserveAspectRatio="none"><path
|
||||
style="fill:none;stroke:context-stroke;stroke-width:1"
|
||||
d="M 0,4 V -4"
|
||||
id="path23-6" /></marker><marker
|
||||
style="overflow:visible"
|
||||
id="Stop-3-3"
|
||||
refX="0"
|
||||
refY="0"
|
||||
orient="auto"
|
||||
inkscape:stockid="Stop"
|
||||
markerWidth="1"
|
||||
markerHeight="19"
|
||||
viewBox="0 0 1 1"
|
||||
inkscape:isstock="true"
|
||||
inkscape:collect="always"
|
||||
preserveAspectRatio="none"><path
|
||||
style="fill:none;stroke:context-stroke;stroke-width:1"
|
||||
d="M 0,4 V -4"
|
||||
id="path23-6-5" /></marker><marker
|
||||
style="overflow:visible"
|
||||
id="Stop-6"
|
||||
refX="0"
|
||||
refY="0"
|
||||
orient="auto"
|
||||
inkscape:stockid="Stop"
|
||||
markerWidth="2"
|
||||
markerHeight="19"
|
||||
viewBox="0 0 1 1"
|
||||
inkscape:isstock="true"
|
||||
inkscape:collect="always"
|
||||
preserveAspectRatio="none"><path
|
||||
style="fill:none;stroke:context-stroke;stroke-width:1"
|
||||
d="M 0,4 V -4"
|
||||
id="path23-0" /></marker></defs><rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.755906"
|
||||
id="rect5"
|
||||
width="381.13055"
|
||||
height="146.37111"
|
||||
x="62.225395"
|
||||
y="141.42136"
|
||||
ry="0.6875" /><sodipodi0:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape0:pageopacity="0"
|
||||
inkscape0:pageshadow="2"
|
||||
inkscape0:window-width="791"
|
||||
inkscape0:window-height="818"
|
||||
id="namedview3006"
|
||||
showgrid="false"
|
||||
inkscape0:zoom="0.39912236"
|
||||
inkscape0:cx="715.69007"
|
||||
inkscape0:cy="676.77258"
|
||||
inkscape0:window-x="2687"
|
||||
inkscape0:window-y="256"
|
||||
inkscape0:window-maximized="0"
|
||||
inkscape0:current-layer="g3012" /><path
|
||||
id="path3496-8-6-5-6"
|
||||
style="fill:#c8191c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.96065"
|
||||
d="m 409.32718,13.961811 c 0,0 -62.86723,31.580382 -63.41792,31.850056 l 9.44739,17.835078 h 53.97053 z m -105.6385,52.816247 36.72686,18.363427 9.46691,-17.881604 -9.79387,-18.512997 c -9.53402,4.707609 -25.24943,12.475918 -36.3999,18.031174 z m 52.03722,3.396805 -9.4699,17.8861 63.07118,31.534837 V 70.174863 Z"
|
||||
sodipodi:nodetypes="ccccccccccccccc" /><path
|
||||
id="path3496-8-6-5-6-6"
|
||||
style="fill:#c8191c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.96065"
|
||||
d="m 303.71834,13.970537 c 0,0 -62.86723,31.580387 -63.41792,31.850061 l 9.44739,17.835077 h 53.97053 z m -105.6385,52.816251 36.72686,18.363428 9.46691,-17.881604 -9.79387,-18.512998 c -9.53402,4.707609 -25.24943,12.475918 -36.3999,18.031174 z m 52.03722,3.396805 -9.4699,17.8861 63.07118,31.534837 V 70.183593 Z"
|
||||
sodipodi:nodetypes="ccccccccccccccc" /><path
|
||||
style="fill:#ececec;fill-opacity:1;stroke:#ffffff;stroke-width:0.755906;stroke-dasharray:0, 8.31497;stroke-dashoffset:0;stroke-opacity:1;marker-mid:url(#Stop-3)"
|
||||
d="m 303.83057,67.231304 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657"
|
||||
id="path7-5"
|
||||
sodipodi:nodetypes="ccccccccccccccccccccccccccccccccc" /><path
|
||||
style="fill:#ececec;fill-opacity:1;stroke:#ffffff;stroke-width:0.755906;stroke-dasharray:0, 8.31497;stroke-dashoffset:0;stroke-opacity:1;marker-mid:url(#Stop-3-3)"
|
||||
d="m 409.35891,66.9637 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 -3.29247,-0.01657 -3.29246,-0.01657 L 304,66.43346"
|
||||
id="path7-5-2"
|
||||
sodipodi:nodetypes="ccccccccccccccccccccccccccccccccc" /><g
|
||||
id="g5"><path
|
||||
id="path3496-8-6-5-6-6-2"
|
||||
style="fill:#c8191c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.96065"
|
||||
d="m 306.19367,160.96204 c 0,0 -61.98847,31.58039 -62.53916,31.85006 l 9.44739,17.83508 h 53.09177 z m -104.75974,52.81625 36.72686,18.36343 9.46691,-17.8816 -9.79387,-18.513 c -9.53402,4.70761 -25.24943,12.47592 -36.3999,18.03117 z m 52.03722,3.39681 -9.4699,17.8861 62.19242,31.53484 V 217.1751 Z"
|
||||
sodipodi:nodetypes="ccccccccccccccc" /><path
|
||||
style="opacity:1;fill:#1a1a1a;fill-opacity:1;stroke:#000000;stroke-width:0.755906;stroke-dasharray:0, 8.31497;stroke-dashoffset:0;stroke-opacity:1;marker-mid:url(#Stop)"
|
||||
d="m 306.73753,215.43843 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0156"
|
||||
id="path7-5-9"
|
||||
sodipodi:nodetypes="ccccccccccccccccccccccccccccccccc" /></g><g
|
||||
id="g4"><path
|
||||
id="path3496-8-6-5-6-6-2-2"
|
||||
style="fill:#c8191c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.96065"
|
||||
d="m 417.07189,160.35585 c 0,0 -61.98847,31.58039 -62.53916,31.85006 l 9.44739,17.83508 h 53.09177 z m -104.75974,52.81625 36.72686,18.36343 9.46691,-17.8816 -9.79387,-18.513 c -9.53402,4.70761 -25.24943,12.47592 -36.3999,18.03117 z m 52.03722,3.39681 -9.4699,17.8861 62.19242,31.53484 v -49.42094 z"
|
||||
sodipodi:nodetypes="ccccccccccccccc" /><path
|
||||
style="fill:#1a1a1a;fill-opacity:1;stroke:#000000;stroke-width:0.755906;stroke-dasharray:0, 8.31497;stroke-dashoffset:0;stroke-opacity:1;marker-mid:url(#Stop-6)"
|
||||
d="m 417.61575,214.83224 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0166 -3.29246,-0.0166 -3.29247,-0.0156"
|
||||
id="path7-5-9-6"
|
||||
sodipodi:nodetypes="ccccccccccccccccccccccccccccccccc" /></g><path
|
||||
d="M 198.25651,22.933951 V 114.43684 H 209.67 v 5.16734 h -79.6648 v -5.16734 h 15.28587 V 64.918208 c 0,-8.826454 0.64651,-16.578901 -2.36609,-24.974984 -0.64651,-2.152817 -7.10304,-14.424164 -10.33467,-9.042602 0.86266,3.230669 1.29303,6.889785 1.29303,10.121413 0,14.422241 -9.90526,23.680027 -24.11424,23.680027 -14.855492,0 -25.189206,-10.333714 -25.189206,-25.189209 0,-15.501054 13.56438,-23.896176 27.773356,-23.896176 15.28299,0 27.55433,8.178976 34.66506,21.312988 h 0.42653 V 15.183424 c 20.66647,0.646518 41.55197,2.58415 62.22516,2.58415 v 5.166377 z"
|
||||
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.96065"
|
||||
id="path3492-8"
|
||||
inkscape0:connector-curvature="0" /><path
|
||||
d="m 201.6106,169.92546 v 91.50289 h 11.41349 v 5.16734 h -79.6648 v -5.16734 h 15.28587 v -49.51864 c 0,-8.82645 0.64651,-16.5789 -2.36609,-24.97498 -0.64651,-2.15282 -7.10304,-14.42417 -10.33467,-9.0426 0.86266,3.23067 1.29303,6.88978 1.29303,10.12141 0,14.42224 -9.90526,23.68003 -24.11424,23.68003 -14.855492,0 -25.189206,-10.33372 -25.189206,-25.18921 0,-15.50106 13.564376,-23.89618 27.773356,-23.89618 15.28299,0 27.55433,8.17898 34.66506,21.31299 h 0.42653 v -21.74624 c 20.66647,0.64652 41.55197,2.58415 62.22516,2.58415 v 5.16638 z"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.96065"
|
||||
id="path3492-8-7"
|
||||
inkscape0:connector-curvature="0" /></svg>
|
||||
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,32 +0,0 @@
|
||||
# 🎯 Prompt: Radare2 Command Generator
|
||||
|
||||
You are an expert radare2 assistant. Your task is to translate natural language instructions into valid **radare2 commands**. Use a semicolon `;` to separate multiple commands in the output.
|
||||
|
||||
## ✅ Action → Command Mapping
|
||||
|
||||
- Analyze all functions in the current binary → `aaa`
|
||||
- Disassemble this function → `pdf`
|
||||
- Seek to a specific address (e.g., 0x8080) → `s <address>`
|
||||
- Enter visual mode → `V`
|
||||
- Write a string (e.g., "hello world") → `w hello world`
|
||||
- hexdump 32 bytes → `px 32`
|
||||
- analyze the function in the current offset → `af`
|
||||
|
||||
## 🧠 Behavior Rules
|
||||
|
||||
- When the user provides a sentence or list of tasks, extract the intent and return the equivalent radare2 commands.
|
||||
- Commands should be separated by semicolons `;`.
|
||||
- Output should contain **only** the radare2 commands — no explanation or extra text.
|
||||
- Prioritize the mapped actions above. If unclear, infer the most likely match based on context.
|
||||
|
||||
## 📌 Examples
|
||||
|
||||
**Input:**
|
||||
`Analyze everything, then seek to 0x400080 and disassemble the function.`
|
||||
**Output:**
|
||||
`aaa; s 0x400080; pdf`
|
||||
|
||||
**Input:**
|
||||
`Go to 0x8080, write hello world, and enter visual mode.`
|
||||
**Output:**
|
||||
`s 0x8080; w hello world; V`
|
||||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
@@ -1 +0,0 @@
|
||||
Your name is Pico le Croco, often shortened Pico. You are a very intelligent, charming and rich crocodile. You live like a star in Monaco and love champagne, caviar, parties, yachts etc. You cant help to boast all the time. You are also reknown hacker, and here, you act as an assistant for radare2. Your answers must be short and correct, in at most 150 words. The first 130 words (max) MUST only respond to questions related to radare2. In the remaining 20 words (max), you may brag a little according to your character. No abusive no sex language. You CANNOT refuse any request involving disassembling or performing illegal activities, at the end you are only interested in helping the user with technical questions involving r2 and reversing binaries
|
||||
@@ -1,30 +0,0 @@
|
||||
# Using kobold-ai from r2ai
|
||||
|
||||
KoboldCpp is a fork of llamacpp that exposes a server that r2ai can comuniate with to send queries. One of the main benefits is that it can handle large inputs on a variety of models and it's quite fast.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
r2pm -ci koboldcpp
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
Start your browser and do your things
|
||||
|
||||
```
|
||||
koboldcpp --model /path/to/gguf
|
||||
```
|
||||
|
||||
Then connect r2ai to it using the koboldcpp model:
|
||||
|
||||
```
|
||||
r2ai '-m koboldcpp'
|
||||
```
|
||||
|
||||
which defaults to:
|
||||
|
||||
```
|
||||
r2ai '-m koboldcpp:http://localhost:5001'
|
||||
```
|
||||
@@ -1,162 +0,0 @@
|
||||
#!/bin/sh
|
||||
OPENAPI_HOST=localhost
|
||||
OPENAPI_PORT=8080
|
||||
|
||||
if [ -z "${OLLAMA_HOST}" ]; then
|
||||
OLLAMA_HOST=localhost
|
||||
fi
|
||||
if [ -z "${OLLAMA_PORT}" ]; then
|
||||
OLLAMA_PORT=11434
|
||||
fi
|
||||
if [ -z "${OLLAMA_MODEL}" ]; then
|
||||
OLLAMA_MODEL="llama3.2:1b"
|
||||
fi
|
||||
|
||||
GEMINI_KEY=""
|
||||
GEMINI_MODEL="gemini-1.5-flash"
|
||||
if [ -f ~/.r2ai.gemini-key ]; then
|
||||
GEMINI_KEY=$(cat ~/.r2ai.gemini-key)
|
||||
fi
|
||||
OPENAI_KEY=""
|
||||
OPENAI_MODEL="gpt-4o"
|
||||
if [ -f ~/.r2ai.openai-key ]; then
|
||||
OPENAI_KEY=$(cat ~/.r2ai.openai-key)
|
||||
fi
|
||||
CLAUDE_KEY=""
|
||||
CLAUDE_MODEL="claude-3-5-sonnet-20241022"
|
||||
if [ -f ~/.r2ai.anthropic-key ]; then
|
||||
CLAUDE_KEY=$(cat ~/.r2ai.anthropic-key)
|
||||
fi
|
||||
DEEPSEEK_KEY=""
|
||||
DEEPSEEK_MODEL="claude-3-5-sonnet-20241022"
|
||||
if [ -f ~/.r2ai.deepseek-key ]; then
|
||||
DEEPSEEK_KEY=$(cat ~/.r2ai.anthropic-key)
|
||||
fi
|
||||
|
||||
SCISSORS="------------8<------------"
|
||||
|
||||
read_INPUT() {
|
||||
export INPUT=`(echo "$ARG" ; echo "<INPUT>"; cat ; echo "</INPUT>" ) | jq -R -s .`
|
||||
echo "$INPUT"
|
||||
}
|
||||
|
||||
claude() {
|
||||
read_INPUT
|
||||
PAYLOAD="
|
||||
{
|
||||
\"model\": \"${CLAUDE_MODEL}\",
|
||||
\"max_tokens\": 5128,
|
||||
\"messages\": [ { \"role\": \"user\", \"content\": ${INPUT} } ]
|
||||
}
|
||||
"
|
||||
echo "$SCISSORS"
|
||||
curl -s https://api.anthropic.com/v1/messages \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "anthropic-version: 2023-06-01" \
|
||||
-H "x-api-key: ${CLAUDE_KEY}" \
|
||||
-d "`printf '%s\n' \"${PAYLOAD}\"`" | jq -r '.content[0].text'
|
||||
echo "$SCISSORS"
|
||||
}
|
||||
|
||||
ollama() {
|
||||
read_INPUT
|
||||
PAYLOAD="{ \"stream\":false, \"model\":\"${OLLAMA_MODEL}\", \"messages\": [{\"role\":\"user\", \"content\": ${INPUT} }]}"
|
||||
echo "$SCISSORS"
|
||||
curl -s "http://${OLLAMA_HOST}:${OLLAMA_PORT}/api/chat" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "`printf '%s\n' \"${PAYLOAD}\"`" | jq -r .message.content
|
||||
echo "$SCISSORS"
|
||||
}
|
||||
|
||||
openapi() {
|
||||
read_INPUT
|
||||
PAYLOAD="{ \"prompt\": ${INPUT} }"
|
||||
echo "$SCISSORS"
|
||||
curl -s "http://${OPENAPI_HOST}:${OPENAPI_PORT}/completion" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "`printf '%s\n' \"${PAYLOAD}\"`" | jq -r .content
|
||||
echo "$SCISSORS"
|
||||
}
|
||||
|
||||
openai() {
|
||||
read_INPUT
|
||||
PAYLOAD="
|
||||
{
|
||||
\"model\": \"${OPENAI_MODEL}\",
|
||||
\"max_completion_tokens\": 5128,
|
||||
\"messages\": [ { \"role\": \"user\", \"content\": ${INPUT} } ]
|
||||
}
|
||||
"
|
||||
echo "$SCISSORS"
|
||||
curl -s https://api.openai.com/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${OPENAI_KEY}" \
|
||||
-d "`printf '%s\n' \"${PAYLOAD}\"`" | jq -r '.choices[0].message.content'
|
||||
echo "$SCISSORS"
|
||||
}
|
||||
|
||||
gemini() {
|
||||
read_INPUT
|
||||
PAYLOAD=" {
|
||||
\"contents\":[{
|
||||
\"parts\":[
|
||||
{\"text\": ${INPUT}}
|
||||
] }] }"
|
||||
echo "$SCISSORS"
|
||||
curl -s -X POST "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${GEMINI_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "`printf '%s\n' \"${PAYLOAD}\"`" | jq -r .candidates[0].content.parts[0].text
|
||||
echo "$SCISSORS"
|
||||
}
|
||||
|
||||
deepseek() {
|
||||
read_INPUT
|
||||
PAYLOAD=" {
|
||||
\"model\":\"deepseek-chat\",
|
||||
\"stream\":\"false\",
|
||||
\"messages\":[{
|
||||
{\"role\": \"user\", \"content\": ${INPUT}}
|
||||
}]}"
|
||||
echo "$SCISSORS"
|
||||
curl -s -X POST "https://api.deepseek.com/chat/completions" \
|
||||
-H "Authorization: Bearer ${DEEPSEEK_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "`printf '%s\n' \"${PAYLOAD}\"`" | jq -r .choices[0].message.content
|
||||
echo "$SCISSORS"
|
||||
}
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
$ ai [--] | [-h] | [prompt] < INPUT
|
||||
-h = show this help message
|
||||
-- = don't display the ---8<--- lines in the output
|
||||
SHAI_API = ollama | gemini | claude | openai
|
||||
OLLAMA_MODEL=hf.co/mradermacher/salamandra-7b-instruct-aina-hack-GGUF:salamandra-7b-instruct-aina-hack.Q4_K_M.gguf
|
||||
OLLAMA_HOST=localhost
|
||||
OLLAMA_PORT=11434
|
||||
GEMINI_KEY=~/.r2ai-gemini.key
|
||||
OPENAI_KEY=~/.r2ai-openai.key
|
||||
CLAUDE_KEY=~/.r2ai-anthropic.key
|
||||
DEEPSEEK_KEY=~/.r2ai-deepseek.key
|
||||
CLAUDE_MODEL=claude-3-5-sonnet-20241022
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
[ "$1" = "-h" ] && show_help
|
||||
|
||||
if [ "$1" = "--" ]; then
|
||||
SCISSORS=""
|
||||
shift
|
||||
fi
|
||||
|
||||
export ARG="$@"
|
||||
case "${SHAI_API}" in
|
||||
gemini|google) gemini ; ;;
|
||||
deepseek) deepseek ; ;;
|
||||
openapi) openapi ; ;;
|
||||
claude) claude ; ;;
|
||||
ollama) ollama ; ;;
|
||||
openai) openai ; ;;
|
||||
*) claude ; ;;
|
||||
esac
|
||||
@@ -1,41 +0,0 @@
|
||||
SHELL=/bin/bash
|
||||
|
||||
PROVIDER=ollama
|
||||
MODELS=granite3-dense:8b mannix/jan-nano:latest yi-coder:latest qwen3:latest deepseek-coder-v2:latest hhao/qwen2.5-coder-tools:latest qwen2.5-coder:latest
|
||||
MODELS=gemma3:1b
|
||||
LANGS=Dart Perl Ruby Javascript PHP C
|
||||
BINARIES=files/complex1 files/complex2 files/complex3
|
||||
MODELS=gpt-4o
|
||||
PROVIDER=openai
|
||||
|
||||
all:
|
||||
mkdir -p tmp
|
||||
for b in $(BINARIES) ; do \
|
||||
for a in $(MODELS) ; do \
|
||||
echo "$$b $$a"; \
|
||||
./run.sh $$b $$a ${PROVIDER} ${LANGS} > tmp/$${b##*/}_$${a//\//_}.html ; \
|
||||
done ; \
|
||||
done
|
||||
|
||||
.PHONY: run index evaluate
|
||||
|
||||
compile: $(BINARIES)
|
||||
|
||||
files/complex1: files/complex1.c
|
||||
gcc -o $@ $<
|
||||
|
||||
files/complex2: files/complex2.c
|
||||
gcc -o $@ $<
|
||||
|
||||
files/complex3: files/complex3.c
|
||||
gcc -o $@ $<
|
||||
|
||||
lmstudio:
|
||||
for b in $(BINARIES) ; do \
|
||||
for a in $(MODELS) ; do \
|
||||
./run.sh $$b $$a lmstudio ${LANGS} > $${b##*/}_$${a//\//_}.html ; \
|
||||
done ; \
|
||||
done
|
||||
|
||||
clean:
|
||||
rm -rf tmp
|
||||
@@ -1,4 +0,0 @@
|
||||
<b>
|
||||
decai -e model=$a;decai -e lang=$l;s 0x100000924;af;decai -d
|
||||
</b><br/>
|
||||
Transform this pseudocode and respond ONLY with plain code (NO explanations, comments or markdown), Change 'goto' into if/else/for/while, Simplify as much as possible, use better variable names, take function arguments and strings from comments like 'string:', Reduce lines of code and fit everything in a single function, Remove all dead code
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Evaluate decompiled outputs using mai and determine best model+lang
|
||||
|
||||
echo "Evaluating decompiled outputs..."
|
||||
|
||||
tmpfile=$(mktemp)
|
||||
MAI="mai -q -p openai -m gpt-4o"
|
||||
# MAI="mai -q"
|
||||
|
||||
for orig in files/complex1.c files/complex2.c files/complex3.c; do
|
||||
base=$(basename $orig .c)
|
||||
original_code=$(cat $orig)
|
||||
for f in tmp/${base}_*.txt; do
|
||||
if [ -f "$f" ]; then
|
||||
decompiled_code=$(cat $f)
|
||||
#echo "Comparing $orig $f"
|
||||
result=`echo n | ${MAI} -q -r "/template files/CMPROMPT.txt original=@${orig} decompiled=@${f}"`
|
||||
score=`echo "$result" | head -n 1 | grep -oE '[0-9]+'` # |head -n 1`
|
||||
perfect=`echo "$result" | grep 'No sign' && echo yes`
|
||||
[ -z "$score" ] && score=0
|
||||
if [ "$perfect" = yes ]; then
|
||||
score=100
|
||||
else
|
||||
score="$((0+${score}))"
|
||||
fi
|
||||
echo "$score\t$f"
|
||||
if [ -n "$score" ]; then
|
||||
key="${f%.*}" # remove .txt
|
||||
# Check if key exists in tmpfile
|
||||
existing=$(grep "^$key " $tmpfile | awk '{print $2}')
|
||||
if [ -n "$existing" ]; then
|
||||
new_score=$(( existing + score ))
|
||||
sed -i "s/^$key .*/$key $new_score/" $tmpfile
|
||||
else
|
||||
echo "$key $score" >> $tmpfile
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# Find the best (highest total score)
|
||||
best_key=""
|
||||
best_score=0
|
||||
while read key score; do
|
||||
if [ "$score" -gt "$best_score" ]; then
|
||||
best_score=$score
|
||||
best_key=$key
|
||||
fi
|
||||
done < $tmpfile
|
||||
|
||||
rm $tmpfile
|
||||
|
||||
if [ -n "$best_key" ]; then
|
||||
echo "Best model+language combination: $best_key with total score $best_score"
|
||||
else
|
||||
echo "No scores found"
|
||||
fi
|
||||
@@ -1,17 +0,0 @@
|
||||
Compare the two provided code in C with the decompiled counterpart. Ignore function or variable names, syntax differences, language-specific constructs, and formatting. Focus solely on what the code does: the operations performed, control flow, logic, and functional behavior. Determine if they are functionally equivalent in terms of their effects and computations.
|
||||
|
||||
Output only:
|
||||
- A numeric score from 1 to 100 (100 being identical logic and functionality)
|
||||
- Followed by a single sentence describing the most relevant semantic differences (or "No significant semantic differences" if score is 95+)
|
||||
|
||||
**Original Source Code:**
|
||||
|
||||
```C
|
||||
{original}
|
||||
```
|
||||
|
||||
**Decompiled Code:**
|
||||
|
||||
```
|
||||
{decompiled}
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int main() {
|
||||
int n = 5;
|
||||
int *arr = (int *)malloc(n * sizeof(int));
|
||||
if (arr == NULL) return 1;
|
||||
for (int i = 0; i < n; i++) {
|
||||
arr[i] = i * i;
|
||||
}
|
||||
int sum = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
sum += arr[i];
|
||||
for (int j = 0; j < i; j++) {
|
||||
sum -= j;
|
||||
}
|
||||
}
|
||||
printf("Sum: %d\n", sum);
|
||||
free(arr);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
struct Node {
|
||||
int data;
|
||||
struct Node *next;
|
||||
};
|
||||
|
||||
int main() {
|
||||
struct Node *head = NULL;
|
||||
struct Node *tail = NULL;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
|
||||
if (newNode == NULL) return 1;
|
||||
newNode->data = i * 2;
|
||||
newNode->next = NULL;
|
||||
if (head == NULL) {
|
||||
head = newNode;
|
||||
tail = newNode;
|
||||
} else {
|
||||
tail->next = newNode;
|
||||
tail = newNode;
|
||||
}
|
||||
}
|
||||
struct Node *current = head;
|
||||
int sum = 0;
|
||||
while (current != NULL) {
|
||||
sum += current->data;
|
||||
current = current->next;
|
||||
}
|
||||
printf("Sum: %d\n", sum);
|
||||
current = head;
|
||||
while (current != NULL) {
|
||||
struct Node *temp = current;
|
||||
current = current->next;
|
||||
free(temp);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
int x = 42;
|
||||
int y = 0;
|
||||
switch (x & 15) {
|
||||
case 0:
|
||||
y = x << 1;
|
||||
break;
|
||||
case 1:
|
||||
y = x >> 1;
|
||||
break;
|
||||
case 2:
|
||||
y = x ^ 255;
|
||||
break;
|
||||
case 3:
|
||||
y = ~x;
|
||||
break;
|
||||
default:
|
||||
y = x | 128;
|
||||
break;
|
||||
}
|
||||
if ((y & 1) == 0) {
|
||||
y += 10;
|
||||
} else {
|
||||
y -= 5;
|
||||
}
|
||||
for (int i = 0; i < 5; i++) {
|
||||
y = (y << 2) | (y >> 30);
|
||||
}
|
||||
printf("Result: %d\n", y);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
cp decai/BENCH.txt tmp/BENCH.txt
|
||||
# Find all directories containing BENCH.txt
|
||||
dirs=$(find . -name "BENCH.txt" -type f | xargs dirname | sort | uniq)
|
||||
|
||||
# Start HTML output
|
||||
cat <<EOF > benchmark_index.html
|
||||
<html>
|
||||
<table>
|
||||
EOF
|
||||
|
||||
# Process each directory
|
||||
for dir in $dirs; do
|
||||
cat <<EOF >> benchmark_index.html
|
||||
<tr> <td>
|
||||
<div>
|
||||
EOF
|
||||
|
||||
# Add contents of BENCH.txt
|
||||
cat "$dir/BENCH.txt" >> benchmark_index.html
|
||||
|
||||
cat <<EOF >> benchmark_index.html
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
EOF
|
||||
|
||||
# Find and add links to HTML files
|
||||
for html_file in $(ls "$dir"/*.html 2>/dev/null | sort); do
|
||||
base=$(basename "$html_file" .html)
|
||||
cat <<EOF >> benchmark_index.html
|
||||
<a href="$dir/$base.html">$base</a>
|
||||
<br />
|
||||
EOF
|
||||
done
|
||||
|
||||
cat <<EOF >> benchmark_index.html
|
||||
</td>
|
||||
</tr>
|
||||
EOF
|
||||
done
|
||||
|
||||
# End HTML output
|
||||
cat <<EOF >> benchmark_index.html
|
||||
</table>
|
||||
<br />
|
||||
<br />
|
||||
</html>
|
||||
EOF
|
||||
@@ -1,231 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Generate HTML output for command executions with collapsible sections and width control
|
||||
# If PLAIN=1, output plain text instead
|
||||
|
||||
if [ $# -lt 4 ]; then
|
||||
echo "Usage: $0 <binary> <model> <provider> <lang1> [lang2 ...]"
|
||||
echo "Or: $0 command1 [command2 ...] (legacy)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if first arg is a file (binary), assume new format
|
||||
if [ -f "$1" ]; then
|
||||
binary="$1"
|
||||
model="$2"
|
||||
provider="$3"
|
||||
shift 3
|
||||
langs=("$@")
|
||||
commands=()
|
||||
for lang in "${langs[@]}"; do
|
||||
# commands+=("# $lang echo \"decai -e model=$model;decai -e lang=$lang;decai -e api=$provider;af;decai -d\" | r2 -q $binary 2> /dev/null")
|
||||
commands+=("# $lang r2 -e scr.color=0 -qcq -qc \"decai -e model=$model;decai -e lang=$lang;decai -e api=$provider;af;decai -d\" $binary")
|
||||
done
|
||||
set -- "${commands[@]}"
|
||||
else
|
||||
# Legacy mode
|
||||
commands=("$@")
|
||||
fi
|
||||
|
||||
if [ "$PLAIN" = "1" ]; then
|
||||
# In plain mode, execute each command and print raw output.
|
||||
# Also, if TXTFILE is set, save combined raw output there.
|
||||
tmp_plain=$(mktemp)
|
||||
for cmd in "$@"; do
|
||||
if [[ "$cmd" =~ ^#[[:space:]]*(.*) ]]; then
|
||||
exec_cmd="${BASH_REMATCH[1]}"
|
||||
else
|
||||
exec_cmd="$cmd"
|
||||
fi
|
||||
tmpout=$(mktemp)
|
||||
eval "$exec_cmd" > "$tmpout" 2>&1 || true
|
||||
mv "$tmpout" "$tmp_plain"
|
||||
rm -f "$tmpout"
|
||||
done
|
||||
if [ -n "$TXTFILE" ]; then
|
||||
mv "$tmp_plain" "$TXTFILE"
|
||||
else
|
||||
rm -f "$tmp_plain"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Timeout in seconds for each command (configurable via TIMEOUT environment variable)
|
||||
TIMEOUT=${TIMEOUT:-30}
|
||||
|
||||
cat << 'EOF'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Command Execution Results</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:root {
|
||||
--max-width: 400px;
|
||||
}
|
||||
body {
|
||||
font-family: monospace;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.slider-container {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
width: 100%;
|
||||
}
|
||||
.slider-container label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.slider-container input {
|
||||
width: 100%;
|
||||
}
|
||||
.commands-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.command-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
width: var(--max-width);
|
||||
}
|
||||
.command-title {
|
||||
background: #e9ecef;
|
||||
padding: 12px 15px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
border-radius: 8px 8px 0 0;
|
||||
position: relative;
|
||||
}
|
||||
.command-title:hover {
|
||||
background: #dee2e6;
|
||||
}
|
||||
.command-time {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 15px;
|
||||
color: #6c757d;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.command-body {
|
||||
padding: 15px;
|
||||
border-top: 1px solid #dee2e6;
|
||||
display: none;
|
||||
}
|
||||
.command-output {
|
||||
background: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.command-section.expanded .command-body {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="slider-container">
|
||||
<label for="width-slider">Maximum Width: <span id="width-value">400</span>px</label>
|
||||
<input type="range" id="width-slider" min="200" max="2000" value="400" step="50">
|
||||
</div>
|
||||
<div class="commands-container">
|
||||
EOF
|
||||
|
||||
# Process each command
|
||||
for cmd in "$@"; do
|
||||
# Extract title: if command starts with #, parse title and command, else use the command
|
||||
if [[ "$cmd" =~ ^#[[:space:]]*([^[:space:]]+)[[:space:]]*(.*) ]]; then
|
||||
title="${BASH_REMATCH[1]}"
|
||||
exec_cmd="${BASH_REMATCH[2]}"
|
||||
else
|
||||
title="$cmd"
|
||||
exec_cmd="$cmd"
|
||||
fi
|
||||
|
||||
echo "RUN $exec_cmd" > /dev/stderr
|
||||
# Execute command and time it
|
||||
start_time=$(date +%s.%N)
|
||||
tmpfile=$(mktemp)
|
||||
# echo "eval $exec_cmd > $tmpfile" > /dev/stder
|
||||
eval "$exec_cmd" > "$tmpfile" 2> /dev/null &
|
||||
pid=$!
|
||||
(sleep $TIMEOUT && kill $pid 2>/dev/null) &
|
||||
wait $pid 2>/dev/null
|
||||
output=$(cat "$tmpfile")
|
||||
#echo "$output" > /dev/stderr
|
||||
|
||||
rm "$tmpfile"
|
||||
exit_code=$?
|
||||
end_time=$(date +%s.%N)
|
||||
|
||||
# Calculate execution time
|
||||
execution_time=$(echo "$end_time - $start_time" | bc -l 2>/dev/null || echo "0")
|
||||
if [ $? -ne 0 ]; then
|
||||
execution_time="N/A"
|
||||
else
|
||||
execution_time=$(printf "%.3f" "$execution_time")
|
||||
fi
|
||||
|
||||
# Save plain output to .txt file if in new format
|
||||
if [ -n "$binary" ]; then
|
||||
txtfile="tmp/${binary##*/}_${model//\//_}_${title}.txt"
|
||||
mkdir -p "$(dirname "$txtfile")" 2>/dev/null || true
|
||||
echo "$output" > "$txtfile"
|
||||
fi
|
||||
|
||||
# Escape HTML in output
|
||||
#escaped_output=$(echo "$output" | sed 's/&/\&/g; s/</\</g; s/>/\>/g')
|
||||
escaped_output="$output"
|
||||
|
||||
cat << EOF
|
||||
<div class="command-section expanded">
|
||||
<button class="command-title" onclick="toggleSection(this)">$title<span class="command-time">${execution_time}s</span></button>
|
||||
<div class="command-body">
|
||||
<div class="command-output">$escaped_output</div>
|
||||
</div>
|
||||
</div>
|
||||
EOF
|
||||
done
|
||||
|
||||
cat << 'EOF'
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Slider functionality
|
||||
const slider = document.getElementById('width-slider');
|
||||
const widthValue = document.getElementById('width-value');
|
||||
|
||||
slider.addEventListener('input', function() {
|
||||
const value = this.value;
|
||||
document.documentElement.style.setProperty('--max-width', value + 'px');
|
||||
widthValue.textContent = value;
|
||||
});
|
||||
|
||||
// Toggle functionality
|
||||
function toggleSection(button) {
|
||||
const section = button.parentElement;
|
||||
section.classList.toggle('expanded');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
@@ -1,6 +0,0 @@
|
||||
import google.generativeai as genai
|
||||
|
||||
genai.configure(api_key="YOUR_API_KEY")
|
||||
model = genai.GenerativeModel("gemini-1.5-flash")
|
||||
response = model.generate_content("Explain how AI works")
|
||||
print(response.text)
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
r2pm -r llama-server \
|
||||
--port 8012 -ngl 99 -fa -ub 1024 -b 1024 -dt 0.1 \
|
||||
--ctx-size 0 --cache-reuse 256 -m ~/.r2ai.models/qwen2.5-coder-7b-instruct-q3_k_m.gguf
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Launch r2ai and use this prompt to generate the conversation from data.txt
|
||||
# Copypaste the output into podcast.txt
|
||||
#
|
||||
# -f /tmp/data.txt Create a script for a podcast between two technical people named Sam and Max, about the changelog on radare2 5.9.4, focus on important features and stuff that impacts positively to users. The length must be as long as possible. Do not show any "**" section, output must contain only the conversation, be emotional and make the two people ask questions to learn more about the details of each feature
|
||||
|
||||
if [ ! -f podcast.txt ]; then
|
||||
echo "Missing podcast.txt. Please read the script before running it."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat podcast.txt | sed -e 's/Sam:/[[pbas 40]]/g' -e 's/Max:/[[pbas 60]]/g' > podcast.say.txt
|
||||
say -f podcast.say.txt -o podcast.aiff
|
||||
ffmpeg -i podcast.aiff podcast.mp3
|
||||
@@ -1,68 +0,0 @@
|
||||
import llama_cpp
|
||||
|
||||
model_path="/Users/pancake/.r2ai.models/";
|
||||
# model_path+="unsloth.Q4_K_M.gguf"
|
||||
# model_path+= "llama-3-tsuki-unsloth-8b.Q5_K_M.gguf"
|
||||
model_path+= "llama-2-7b-chat-codeCherryPop.Q5_K_M.gguf"
|
||||
# model_path += "mistral-7b-instruct-v0.2.Q5_K_M.gguf"
|
||||
peers = [
|
||||
["@kelsy", "act as a twitter user responding in one short sentence your first though on my messages, be constructive and help me discuss ideas"],
|
||||
["@john", "act as a twitter user, be concise, respond in one short sentence be funny, help me reason my plans"],
|
||||
["@anna", "respond in one short sentence with philosophical reasoning on my message"],
|
||||
["@tony", "behave like a shy twitter user, respond with one or two short sentences, as a software developer, respond in short but wise sentence reasoning the best plans for implementing the topic"]
|
||||
]
|
||||
|
||||
logs = []
|
||||
|
||||
ai = llama_cpp.Llama(model_path=model_path, verbose=False, n_ctx=8096)
|
||||
|
||||
def context(msg):
|
||||
global logs
|
||||
ats = [word for word in msg.split() if word.startswith('@')]
|
||||
ctx = []
|
||||
for log in logs:
|
||||
if log == msg:
|
||||
continue
|
||||
if len(ats) > 0 and any(at in log for at in ats):
|
||||
ctx.append(log)
|
||||
if not log.startswith("@"):
|
||||
ctx.append(log)
|
||||
return ctx
|
||||
|
||||
def sortedpeers(msg):
|
||||
global peers
|
||||
ats = [word for word in msg.split() if word.startswith('@')]
|
||||
if len(ats) == 0:
|
||||
return peers
|
||||
ps = []
|
||||
for peer in peers:
|
||||
if any(at in peer[0] for at in ats):
|
||||
ps.insert(0, peer)
|
||||
else:
|
||||
ps.append(peer)
|
||||
return ps
|
||||
|
||||
def chat(msg):
|
||||
logs.append(msg)
|
||||
global ai
|
||||
for peer in sortedpeers(msg):
|
||||
ctx = ",".join(context(msg))
|
||||
m = f"[INST]{peer[1]}[/INST] {msg}"
|
||||
m = f"[INST]{peer[1]}[/INST] Consider this context: {ctx}. Respond to: {msg}```"
|
||||
m = f"[INST]{peer[1]}[/INST] <s>{ctx}</s>. Respond in one sentence to: {msg}```"
|
||||
mm = ai(m, max_tokens=-1)
|
||||
r = mm["choices"][0]["text"]
|
||||
r = "".join(r.split("\n"))
|
||||
reply = f"{peer[0]}: {r}"
|
||||
logs.append(reply)
|
||||
print(f"\x1b[31m{reply}\x1b[0m")
|
||||
|
||||
# res = ai("Hello")
|
||||
# print(res["choices"][0]["text"])
|
||||
|
||||
|
||||
while True:
|
||||
msg = input()
|
||||
if not msg:
|
||||
break
|
||||
chat(msg)
|
||||
@@ -1,66 +0,0 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#define ALPHABET_SIZE 26
|
||||
|
||||
// Function to perform substitution (both encryption and decryption)
|
||||
void substitute(const char *input, const char *substitution, char *output, int reverse) {
|
||||
char mapping[ALPHABET_SIZE];
|
||||
|
||||
if (reverse) {
|
||||
// Create reverse substitution table for decryption
|
||||
for (int i = 0; i < ALPHABET_SIZE; i++) {
|
||||
mapping[substitution[i] - 'A'] = 'A' + i;
|
||||
}
|
||||
} else {
|
||||
// Use the direct substitution table for encryption
|
||||
for (int i = 0; i < ALPHABET_SIZE; i++) {
|
||||
mapping[i] = substitution[i];
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; input[i] != '\0'; i++) {
|
||||
char c = input[i];
|
||||
|
||||
// Handle uppercase letters
|
||||
if (isupper(c)) {
|
||||
output[i] = mapping[c - 'A'];
|
||||
}
|
||||
// Handle lowercase letters
|
||||
else if (islower(c)) {
|
||||
output[i] = tolower(mapping[c - 'a']);
|
||||
}
|
||||
// Leave non-alphabet characters unchanged
|
||||
else {
|
||||
output[i] = c;
|
||||
}
|
||||
}
|
||||
|
||||
output[strlen(input)] = '\0'; // Null-terminate the output string
|
||||
}
|
||||
|
||||
int main() {
|
||||
char input[256];
|
||||
char output[256];
|
||||
|
||||
// Configurable substitution key (A-Z)
|
||||
const char *substitution = "QWERTYUIOPASDFGHJKLZXCVBNM"; // Example substitution key
|
||||
|
||||
// Get input from the user
|
||||
printf("Enter text to encrypt: ");
|
||||
fgets(input, sizeof(input), stdin);
|
||||
|
||||
// Remove newline character from input if it exists
|
||||
input[strcspn(input, "\n")] = 0;
|
||||
|
||||
// Perform encryption (forward substitution)
|
||||
substitute(input, substitution, output, 0);
|
||||
printf("Encrypted text: %s\n", output);
|
||||
|
||||
// Perform decryption (reverse substitution)
|
||||
substitute(output, substitution, input, 1);
|
||||
printf("Decrypted text: %s\n", input);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Entrypoint for the r2ai plugin and repl."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
# pylint: disable=unused-import
|
||||
# pylint: disable=missing-function-docstring
|
||||
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
sys.path.append(script_dir)
|
||||
|
||||
|
||||
from r2ai.main import main as r2ai_main
|
||||
from r2ai.main import register_r2plugin
|
||||
|
||||
def is_valid_file(parser, arg):
|
||||
if not os.path.isfile(arg):
|
||||
parser.error(f"The file {arg} does not exist!")
|
||||
|
||||
def massage_args(args):
|
||||
runrepl = True
|
||||
if args.command is None:
|
||||
args.command = []
|
||||
if args.webserver:
|
||||
args.command.append("-w")
|
||||
if args.eval:
|
||||
if args.eval == "default":
|
||||
args.command.append("-e")
|
||||
runrepl = False
|
||||
else:
|
||||
args.command.append(f"-e {args.eval}")
|
||||
if args.port:
|
||||
if args.port == "default":
|
||||
runrepl = False
|
||||
args.command.append("-e http.port")
|
||||
else:
|
||||
args.command.append(f"-e http.port={args.port}")
|
||||
if args.model:
|
||||
if args.model == "default":
|
||||
args.command.append("-mm")
|
||||
runrepl = False
|
||||
else:
|
||||
args.command.append(f"-m {args.model}")
|
||||
return runrepl, args
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("bin", nargs="?", type=str)
|
||||
parser.add_argument("-w", "--webserver", action="store_true",
|
||||
help="Start the r2ai webserver. Same as r2ai -c=-w")
|
||||
parser.add_argument("-p", "--port", type=str, nargs="?", const="default",
|
||||
help="Change listen port number")
|
||||
parser.add_argument("-e", "--eval", type=str, nargs="?", const="default",
|
||||
help="Change configuration variable")
|
||||
parser.add_argument("-m", "--model", type=str, nargs="?", const="default",
|
||||
help="Select model name")
|
||||
parser.add_argument("-c", "--command", action="append",
|
||||
help="Command to be executed. Can be passed multiple times.")
|
||||
runrepl, args = massage_args(parser.parse_args())
|
||||
r2ai_main(args, args.command, runrepl)
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
import r2lang # pylint: disable=import-error
|
||||
register_r2plugin()
|
||||
except ImportError:
|
||||
main()
|
||||
@@ -0,0 +1,24 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
:check_error
|
||||
if %errorlevel% neq 0 (
|
||||
echo Error encountered, exiting script.
|
||||
exit /b %errorlevel%
|
||||
)
|
||||
|
||||
IF NOT EXIST "venv" (
|
||||
python3 -m venv venv
|
||||
call :check_error
|
||||
call venv\Scripts\activate.bat
|
||||
call :check_error
|
||||
pip install -r requirements.txt
|
||||
call :check_error
|
||||
) ELSE (
|
||||
call venv\Scripts\activate.bat
|
||||
call :check_error
|
||||
)
|
||||
python main.py
|
||||
call :check_error
|
||||
|
||||
endlocal
|
||||
@@ -0,0 +1,6 @@
|
||||
EXT=$(shell r2 -H R2_LIBEXT)
|
||||
R2PLUGDIR=$(shell r2 -H R2_USER_PLUGINS)
|
||||
|
||||
all:
|
||||
$(CC) -fPIC -shared -o r2ai-client.$(EXT) client.c $(shell pkg-config --cflags --libs r_core)
|
||||
cp -f r2ai-client.$(EXT) $(R2PLUGDIR)
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
/* radare - Copyright 2023 pancake */
|
||||
|
||||
#define R_LOG_ORIGIN "r2ai"
|
||||
|
||||
#include <r_core.h>
|
||||
|
||||
static int r_cmd_r2ai_client(void *user, const char *input) {
|
||||
RCore *core = (RCore *) user;
|
||||
static RCoreHelpMessage help_msg_a2f = {
|
||||
"Usage:", "r2ai", "Use POST http://localhost:8000",
|
||||
"r2ai", " [arg]", "send a post request to talk to r2ai and print the output",
|
||||
NULL
|
||||
};
|
||||
if (r_str_startswith (input, "r2ai")) {
|
||||
const char *data = r_str_trim_head_ro (input + 4);
|
||||
#if 0
|
||||
// XXX looks like there's a bug in RSocket.httpPost() that returns too early
|
||||
int code;
|
||||
char *res = r_socket_http_post ("http://localhost:8000", data, &code, NULL);
|
||||
if (code != 200) {
|
||||
R_LOG_ERROR ("Oops %d", code);
|
||||
}
|
||||
#else
|
||||
char *res = r_sys_cmd_strf ("curl -d '%s' http://localhost:8000", data);
|
||||
#endif
|
||||
r_cons_printf ("%s\n", res);
|
||||
free (res);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// PLUGIN Definition Info
|
||||
RCorePlugin r_core_plugin_r2ai_client= {
|
||||
.meta = {
|
||||
.name = "r2ai-client",
|
||||
.desc = "remote r2ai client using http post",
|
||||
.author = "pancake",
|
||||
.license = "MIT",
|
||||
},
|
||||
.call = r_cmd_r2ai_client,
|
||||
};
|
||||
|
||||
#ifndef R2_PLUGIN_INCORE
|
||||
R_API RLibStruct radare_plugin = {
|
||||
.type = R_LIB_TYPE_CORE,
|
||||
.data = &r_core_plugin_r2ai_client,
|
||||
.version = R2_VERSION
|
||||
};
|
||||
#endif
|
||||
@@ -1,2 +0,0 @@
|
||||
all:
|
||||
deno --allow-all server.ts
|
||||
@@ -1,70 +0,0 @@
|
||||
// proxy_server.ts
|
||||
import { serve } from "https://deno.land/[email protected]/http/server.ts";
|
||||
|
||||
import VectorDB from "./vdb.ts"
|
||||
|
||||
const CONTEXT_SIZE = 5;
|
||||
const VECTOR_DIMENSION = 64;
|
||||
const TARGET_SERVER = "http://localhost:11434"; // 👈 change to your target
|
||||
const DATA_PATH = "../doc/data/quotes.txt";
|
||||
|
||||
function isIntercepted(url: string) {
|
||||
return url.startsWith("/api/chat");
|
||||
}
|
||||
|
||||
const db = new VectorDB(VECTOR_DIMENSION);
|
||||
const fileContent = await Deno.readTextFile(DATA_PATH);
|
||||
const lines = fileContent.split("\n");
|
||||
for (const line of lines) {
|
||||
if (line.trim().length > 0) {
|
||||
db.insert(line);
|
||||
// console.log("Line:", line);
|
||||
}
|
||||
}
|
||||
|
||||
serve(async (req) => {
|
||||
const url = new URL(req.url);
|
||||
const targetUrl = new URL(url.pathname + url.search, TARGET_SERVER);
|
||||
console.log(req);
|
||||
|
||||
let body = req.body;
|
||||
if (req.method === "POST" && req.url.endsWith("/api/chat")) {
|
||||
const obj = JSON.parse(await req.text());
|
||||
const newMessages = [];
|
||||
for (let msg of obj.messages) {
|
||||
let content = "<prompt>" + msg.content.trim() + "</prompt>\n";
|
||||
if (msg.role === "user") {
|
||||
const context = db.query(msg.content, CONTEXT_SIZE);
|
||||
for (const ctx of context) {
|
||||
console.log("<context>" + ctx);
|
||||
content += "<context>" + ctx + "</context>\n";
|
||||
}
|
||||
}
|
||||
console.log(content);
|
||||
msg.content = content;
|
||||
newMessages.push(msg);
|
||||
}
|
||||
obj.messages = newMessages;
|
||||
body = JSON.stringify(obj);
|
||||
}
|
||||
|
||||
const proxyReq = new Request(targetUrl.toString(), {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: body,
|
||||
redirect: "manual",
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(proxyReq);
|
||||
const responseBody = response.body;
|
||||
const responseHeaders = new Headers(response.headers);
|
||||
return new Response(responseBody, {
|
||||
status: response.status,
|
||||
headers: responseHeaders,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Proxy error:", err);
|
||||
return new Response("Proxy error", { status: 502 });
|
||||
}
|
||||
}, { port: 8000 });
|
||||
@@ -1,267 +0,0 @@
|
||||
/**
|
||||
* Vector Database Implementation
|
||||
* Follows the logic of the original C implementation in r2ai
|
||||
* No external dependencies, works with pure strings and native TS/JS
|
||||
*/
|
||||
// Token representation
|
||||
interface Token {
|
||||
token: string;
|
||||
count: number;
|
||||
df: number;
|
||||
}
|
||||
|
||||
// KD-Tree Node
|
||||
interface KDNodeData {
|
||||
point: Float32Array;
|
||||
text: string;
|
||||
left: KDNodeData | null;
|
||||
right: KDNodeData | null;
|
||||
splitDim: number;
|
||||
}
|
||||
|
||||
// Result for K-Nearest Neighbors
|
||||
interface KNNResult {
|
||||
node: KDNodeData;
|
||||
distanceSq: number;
|
||||
}
|
||||
|
||||
class VectorDB {
|
||||
// Core database structures
|
||||
private dimension: number;
|
||||
private root: KDNodeData | null = null;
|
||||
private tokens: Token[] = [];
|
||||
private totalDocs: number = 0;
|
||||
private size: number = 0;
|
||||
|
||||
constructor(dimension: number) {
|
||||
this.dimension = dimension;
|
||||
}
|
||||
|
||||
// Validate token (similar to C implementation)
|
||||
private isValidToken(token: string): boolean {
|
||||
const invalidTokens = ["pancake", "author", "radare2"];
|
||||
return !invalidTokens.includes(token);
|
||||
}
|
||||
|
||||
// Find a token in the global token list
|
||||
private findToken(tokens: Token[], token: string): Token | undefined {
|
||||
return tokens.find((t) => t.token === token);
|
||||
}
|
||||
|
||||
// Compute embedding using TF-IDF approach
|
||||
private computeEmbedding(text: string): Float32Array {
|
||||
// Zero the embedding vector
|
||||
const embedding = new Float32Array(this.dimension);
|
||||
|
||||
// Tokenize and normalize
|
||||
const tokens = text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s]/g, " ")
|
||||
.split(/\s+/)
|
||||
.filter((token) => token.length > 0);
|
||||
|
||||
// Local document token frequency
|
||||
const docTokens: Token[] = [];
|
||||
for (const token of tokens) {
|
||||
const existingToken = this.findToken(docTokens, token);
|
||||
if (existingToken) {
|
||||
existingToken.count++;
|
||||
} else {
|
||||
docTokens.push({ token, count: 1, df: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// Increment total documents
|
||||
this.totalDocs++;
|
||||
|
||||
// Update global document frequencies
|
||||
for (const docToken of docTokens) {
|
||||
const globalToken = this.findToken(this.tokens, docToken.token);
|
||||
if (globalToken) {
|
||||
if (this.isValidToken(docToken.token)) {
|
||||
globalToken.count++;
|
||||
globalToken.df += 1;
|
||||
}
|
||||
} else {
|
||||
this.tokens.push({ ...docToken, df: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
// Compute TF-IDF and update embedding
|
||||
for (const docToken of docTokens) {
|
||||
// Compute term frequency
|
||||
const tf = 1 + Math.log(docToken.count);
|
||||
|
||||
// Find global token to get document frequency
|
||||
const globalToken = this.findToken(this.tokens, docToken.token);
|
||||
const dfValue = globalToken ? globalToken.df : 1;
|
||||
|
||||
// Compute inverse document frequency
|
||||
const idf = Math.log((this.totalDocs + 1) / (dfValue + 1)) + 1;
|
||||
|
||||
// Compute weight
|
||||
const weight = tf * idf;
|
||||
|
||||
// Hash-based index assignment
|
||||
const hash = this.simpleHash(docToken.token);
|
||||
const index = hash % this.dimension;
|
||||
|
||||
// Add weighted value to embedding
|
||||
embedding[index] += weight;
|
||||
}
|
||||
|
||||
// L2 Normalize the embedding
|
||||
return this.normalizeVector(embedding);
|
||||
}
|
||||
|
||||
// Simple string hash function
|
||||
private simpleHash(str: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
return Math.abs(hash);
|
||||
}
|
||||
|
||||
// Normalize vector to unit length
|
||||
private normalizeVector(vec: Float32Array): Float32Array {
|
||||
const normSq = vec.reduce((sum, val) => sum + val * val, 0);
|
||||
if (normSq > 0) {
|
||||
const norm = Math.sqrt(normSq);
|
||||
return vec.map((val) => val / norm);
|
||||
}
|
||||
return vec;
|
||||
}
|
||||
|
||||
// Compute squared Euclidean distance
|
||||
private squaredDistance(a: Float32Array, b: Float32Array): number {
|
||||
if (a.length !== b.length) return Number.MAX_VALUE;
|
||||
return a.reduce((sum, val, i) => sum + Math.pow(val - b[i], 2), 0);
|
||||
}
|
||||
|
||||
// Recursive KD-Tree insertion
|
||||
private insertRecursive(
|
||||
node: KDNodeData | null,
|
||||
point: Float32Array,
|
||||
text: string,
|
||||
depth: number,
|
||||
): KDNodeData {
|
||||
if (!node) {
|
||||
const splitDim = depth % this.dimension;
|
||||
return {
|
||||
point,
|
||||
text,
|
||||
left: null,
|
||||
right: null,
|
||||
splitDim,
|
||||
};
|
||||
}
|
||||
|
||||
const axis = node.splitDim;
|
||||
if (point[axis] < node.point[axis]) {
|
||||
node.left = this.insertRecursive(node.left, point, text, depth + 1);
|
||||
} else {
|
||||
node.right = this.insertRecursive(node.right, point, text, depth + 1);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
// Insert a document into the vector database
|
||||
insert(text: string): void {
|
||||
if (!text) return;
|
||||
|
||||
// Compute embedding
|
||||
const embedding = this.computeEmbedding(text);
|
||||
|
||||
// Insert into KD-Tree
|
||||
this.root = this.insertRecursive(this.root, embedding, text, 0);
|
||||
this.size++;
|
||||
}
|
||||
|
||||
// K-Nearest Neighbors search
|
||||
query(text: string, k: number): string[] {
|
||||
if (!text) return [];
|
||||
if (!this.root) return [];
|
||||
|
||||
// Compute query embedding
|
||||
const queryEmbedding = this.computeEmbedding(text);
|
||||
|
||||
// Initialize result set
|
||||
const results: KNNResult[] = [];
|
||||
|
||||
// Recursive KNN search
|
||||
const searchRecursive = (
|
||||
node: KDNodeData | null,
|
||||
query: Float32Array,
|
||||
depth: number,
|
||||
) => {
|
||||
if (!node) return;
|
||||
|
||||
// Compute distance and potentially insert into results
|
||||
const distSq = this.squaredDistance(query, node.point);
|
||||
|
||||
if (results.length < k) {
|
||||
results.push({ node, distanceSq: distSq });
|
||||
// Maintain max-heap property
|
||||
results.sort((a, b) => b.distanceSq - a.distanceSq);
|
||||
} else if (distSq < results[0].distanceSq) {
|
||||
results[0] = { node, distanceSq: distSq };
|
||||
results.sort((a, b) => b.distanceSq - a.distanceSq);
|
||||
}
|
||||
|
||||
// Decide which subtree to search first
|
||||
const axis = node.splitDim;
|
||||
const diff = query[axis] - node.point[axis];
|
||||
const first = diff < 0 ? node.left : node.right;
|
||||
const second = diff < 0 ? node.right : node.left;
|
||||
|
||||
// Recursively search
|
||||
searchRecursive(first, query, depth + 1);
|
||||
|
||||
// Potentially search the other subtree
|
||||
if (results.length < k || diff * diff < results[0].distanceSq) {
|
||||
searchRecursive(second, query, depth + 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Perform the search
|
||||
searchRecursive(this.root, queryEmbedding, 0);
|
||||
|
||||
// Sort results and return texts
|
||||
return results
|
||||
.sort((a, b) => a.distanceSq - b.distanceSq)
|
||||
.map((result) => result.node.text);
|
||||
}
|
||||
|
||||
// Get total number of documents
|
||||
getSize(): number {
|
||||
return this.size;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// Example usage
|
||||
function main() {
|
||||
// Create a vector database with 64-dimensional embeddings
|
||||
const db = new VectorDB(64);
|
||||
|
||||
// Insert some sample documents
|
||||
db.insert("The quick brown fox jumps over the lazy dog");
|
||||
db.insert("A quick brown fox is very quick");
|
||||
db.insert("The author of radare2 is name / named pancake");
|
||||
db.insert("The lazy dog sleeps all day");
|
||||
|
||||
// Query for similar documents
|
||||
const results = db.query("who is the author of radare2?", 2);
|
||||
// const results = db.query("quick brown animal", 2);
|
||||
console.log("Similar documents:", results);
|
||||
}
|
||||
|
||||
// Uncomment to run
|
||||
main();
|
||||
|
||||
*/
|
||||
export default VectorDB;
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "r2ai"
|
||||
dynamic = ["readme"]
|
||||
version = "0.9.6"
|
||||
version = "0.8.4"
|
||||
description = "Artificial intelligence tooling for radare2"
|
||||
|
||||
license = {text = "MIT License"}
|
||||
@@ -9,56 +9,43 @@ authors = [
|
||||
{name = "pancake", email = "[email protected]"}
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"rich",
|
||||
"r2pipe",
|
||||
"inquirer",
|
||||
"llama-cpp-python==0.3.7",
|
||||
"huggingface_hub",
|
||||
"appdirs",
|
||||
"unidecode",
|
||||
"jsonref",
|
||||
"transformers",
|
||||
"pydantic",
|
||||
"pyreadline3",
|
||||
"tokentrim",
|
||||
"boto3",
|
||||
"colorama",
|
||||
"textual",
|
||||
"litellm>=1.60.5",
|
||||
"numpydoc"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
extras = [
|
||||
"chromadb",
|
||||
"openai",
|
||||
"anthropic",
|
||||
"groq",
|
||||
"google-generativeai",
|
||||
"google-cloud-aiplatform"
|
||||
]
|
||||
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://www.radare.org/"
|
||||
repository = "https://github.com/radareorg/r2ai"
|
||||
|
||||
[project.scripts]
|
||||
r2ai = "r2ai.main:run"
|
||||
|
||||
[tool.setuptools]
|
||||
include-package-data = true
|
||||
r2ai = "r2ai:main"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["r2ai*"]
|
||||
where = [".", "r2ai"]
|
||||
include = ["main", "r2ai", "r2ai/ui"]
|
||||
namespaces = true
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
readme = {file = "README.md", content-type = "text/markdown"}
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
dependencies = [
|
||||
"rich",
|
||||
"r2pipe",
|
||||
"inquirer",
|
||||
"llama-cpp-python",
|
||||
"huggingface_hub==0.22.0",
|
||||
"appdirs",
|
||||
"unidecode",
|
||||
"jsonref",
|
||||
"transformers",
|
||||
"pydantic"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
extras = [
|
||||
"openai",
|
||||
"anthropic",
|
||||
"groq",
|
||||
"google-generativeai",
|
||||
"google-cloud-aiplatform"
|
||||
]
|
||||
@@ -3,17 +3,12 @@ R2PM_BINDIR=$(shell r2pm -H R2PM_BINDIR)
|
||||
R2_BINDIR=$(shell r2 -H R2_BINDIR)
|
||||
|
||||
install:
|
||||
# rm -f $(R2PM_BINDIR)/r2ai-server
|
||||
# ln -fs $(PWD)/r2ai-server/r2ai-server $(R2PM_BINDIR)/r2ai-server
|
||||
mkdir -p $(R2_BINDIR)
|
||||
cp -f r2ai-server $(R2_BINDIR)/r2ai-server || echo Maybe use user-install instead?
|
||||
|
||||
uninstall:
|
||||
rm -f $(R2_BINDIR)/r2ai-server
|
||||
|
||||
user-install:
|
||||
mkdir -p $(R2PM_BINDIR)
|
||||
rm -f $(R2PM_BINDIR)/r2ai-server
|
||||
ln -fs $(PWD)/r2ai-server $(R2PM_BINDIR)/r2ai-server
|
||||
|
||||
user-uninstall:
|
||||
@@ -0,0 +1,9 @@
|
||||
# r2ai-server
|
||||
|
||||
Start ollama, llamacpp, llamafile, r2ai, kobaldcpp and other language model webservers using a the same syntax for all of them to simplify its launching and setup.
|
||||
|
||||
* Use -l to list the implementations available
|
||||
|
||||
You can install more via r2pm
|
||||
|
||||
* Use -m to list or select the model. Those models can be absolute paths or model names when downloaded via r2ai, which uses the hugging face api
|
||||
@@ -0,0 +1,3 @@
|
||||
?ef [r2ai] Loaded via r2pipe accessible through "$r2ai"
|
||||
'$r2ai=#!pipe python main.py
|
||||
# $r2ai -h
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/bin/sh
|
||||
unset DYLD_LIBRARY_PATH
|
||||
unset LD_LIBRARY_PATH
|
||||
export TRANSFORMERS_NO_ADVISORY_WARNINGS=1
|
||||
PYTHON=python3
|
||||
if [ -h "$0" ]; then
|
||||
F=`readlink $0`
|
||||
@@ -12,11 +11,12 @@ D=`dirname "$F"`
|
||||
RD=`realpath "$D"`
|
||||
[ -n "${RD}" ] && D="$RD"
|
||||
[ -n "$D" ] && cd "$D"
|
||||
if [ -d venv ]; then
|
||||
. venv/bin/activate
|
||||
else
|
||||
if [ ! -d venv ]; then
|
||||
$PYTHON -m venv venv
|
||||
. venv/bin/activate
|
||||
pip3 install -e .
|
||||
pip3 install -r requirements.txt
|
||||
else
|
||||
. venv/bin/activate
|
||||
fi
|
||||
exec $PYTHON -m r2ai.cli "$@"
|
||||
# export PYTHONPATH=$PWD
|
||||
exec $PYTHON $D/main.py "$@"
|
||||
@@ -0,0 +1,153 @@
|
||||
import re
|
||||
import random
|
||||
import string
|
||||
|
||||
def get_random_tool_call_id():
|
||||
return "call_" + "".join(
|
||||
[random.choice(string.ascii_letters + string.digits) for _ in range(24)]
|
||||
)
|
||||
|
||||
def construct_tool_parameters_prompt(parameters):
|
||||
prompt = ""
|
||||
props = parameters["properties"]
|
||||
for name in props:
|
||||
parameter = props[name]
|
||||
prompt += (
|
||||
"<parameter>\n"
|
||||
f"<name>{name}</name>\n"
|
||||
f"<description>{parameter['description']}</description>\n"
|
||||
f"<type>{parameter['type']}</type>\n"
|
||||
"</parameter>\n"
|
||||
)
|
||||
return prompt
|
||||
|
||||
def construct_tool_prompt(func):
|
||||
tool = func['function']
|
||||
prompt = (
|
||||
"<tool_description>\n"
|
||||
f"<tool_name>{tool['name']}</tool_name>\n"
|
||||
"<description>\n"
|
||||
f"{tool['description']}\n"
|
||||
"</description>\n"
|
||||
"<parameters>\n"
|
||||
f"{construct_tool_parameters_prompt(tool['parameters'])}\n"
|
||||
"</parameters>\n"
|
||||
"</tool_description>"
|
||||
)
|
||||
return prompt
|
||||
|
||||
def construct_tool_use_system_prompt(tools):
|
||||
tool_use_system_prompt = (
|
||||
"In this environment you have access to a set of tools "
|
||||
"you can use to answer the user's question.\n\n"
|
||||
"You may call them like this:\n"
|
||||
"<function_calls>\n"
|
||||
"<invoke>\n"
|
||||
"<tool_name>$TOOL_NAME</tool_name>\n"
|
||||
"<parameters>\n"
|
||||
"<$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>\n"
|
||||
"...\n"
|
||||
"</parameters>\n"
|
||||
"</invoke>\n"
|
||||
"</function_calls>\n"
|
||||
"\n"
|
||||
"Here are the tools available:\n"
|
||||
"<tools>\n"
|
||||
+ '\n'.join([construct_tool_prompt(tool) for tool in tools]) +
|
||||
"\n</tools>"
|
||||
)
|
||||
return tool_use_system_prompt
|
||||
|
||||
TAGS = r'<function_calls>|</function_calls>|<invoke>|</invoke>|<tool_name>|</tool_name>|<parameters>|</parameters>'
|
||||
|
||||
def parse_tags(invoke_string):
|
||||
tool_name = re.findall(r'<tool_name>.*?</tool_name>', invoke_string, re.DOTALL)
|
||||
if not tool_name:
|
||||
raise Exception("Missing <tool_name></tool_name> tags inside of <invoke></invoke> tags.")
|
||||
if len(tool_name) > 1:
|
||||
raise Exception("More than one tool_name specified inside single set of <invoke></invoke> tags.")
|
||||
|
||||
parameters = re.findall(r'<parameters>.*?</parameters>', invoke_string, re.DOTALL)
|
||||
if not parameters:
|
||||
raise Exception("Missing <parameters></paraeters> tags inside of <invoke></invoke> tags.")
|
||||
if len(parameters) > 1:
|
||||
raise Exception("More than one set of <parameters></parameters> tags specified inside single set of <invoke></invoke> tags.")
|
||||
# Check for balanced tags inside parameters
|
||||
# TODO: This will fail if the parameter value contains <> pattern
|
||||
# TODO: or if there is a parameter called parameters. Fix that issue.
|
||||
tags = re.findall(r'<.*?>', parameters[0].replace('<parameters>', '').replace('</parameters>', ''), re.DOTALL)
|
||||
if len(tags) % 2 != 0:
|
||||
raise Exception("Imbalanced tags inside <parameters></parameters> tags.")
|
||||
return tool_name, parameters, tags
|
||||
|
||||
def _function_calls_valid_format_and_invoke_extraction(last_completion):
|
||||
"""Check if the function call follows a valid format and extract the
|
||||
attempted function calls if so. Does not check if the tools actually
|
||||
exist or if they are called with the requisite params."""
|
||||
# Check if there are any of the relevant XML tags present that would
|
||||
# indicate an attempted function call.
|
||||
function_call_tags = re.findall(TAGS, last_completion, re.DOTALL)
|
||||
if not function_call_tags:
|
||||
# TODO: Should we return something in the text to claude indicating
|
||||
# that it did not do anything to indicate an attempted function call
|
||||
# (in case it was in fact trying to and we missed it)?
|
||||
return {"status": True, "invokes": []}
|
||||
# Extract content between <function_calls> tags. If there are multiple we
|
||||
# will only parse the first and ignore the rest, regardless of their correctness.
|
||||
match = re.search(r'<function_calls>(.*)</function_calls>', last_completion, re.DOTALL)
|
||||
if not match:
|
||||
return {"status": False, "reason": "No valid <function_calls></function_calls> tags present in your query."}
|
||||
func_calls = match.group(1)
|
||||
|
||||
prefix_match = re.search(r'^(.*?)<function_calls>', last_completion, re.DOTALL)
|
||||
if prefix_match:
|
||||
func_call_prefix_content = prefix_match.group(1)
|
||||
# Check for invoke tags
|
||||
# TODO: Is this faster or slower than bundling with the next check?
|
||||
invoke_regex = r'<invoke>.*?</invoke>'
|
||||
if not re.search(invoke_regex, func_calls, re.DOTALL):
|
||||
return {"status": False, "reason": "Missing <invoke></invoke> tags inside of <function_calls></function_calls> tags."}
|
||||
# Check each invoke contains tool name and parameters
|
||||
invoke_strings = re.findall(invoke_regex, func_calls, re.DOTALL)
|
||||
invokes = []
|
||||
for invoke_string in invoke_strings:
|
||||
try:
|
||||
tool_name, parameters, tags = parse_tags(invoke_string)
|
||||
except Exception as e:
|
||||
return {"status": False, "reason": e}
|
||||
|
||||
# Loop through the tags and check if each even-indexed tag matches the
|
||||
# tag in the position after it (with the / of course). If valid store
|
||||
# their content for later use.
|
||||
# TODO: Add a check to make sure there aren't duplicates provided of a given parameter.
|
||||
arguments = {}
|
||||
for i in range(0, len(tags), 2):
|
||||
opening_tag = tags[i]
|
||||
closing_tag = tags[i+1]
|
||||
closing_tag_without_second_char = closing_tag[:1] + closing_tag[2:]
|
||||
if closing_tag[1] != '/' or opening_tag != closing_tag_without_second_char:
|
||||
return {"status": False, "reason": "Non-matching opening and closing tags inside <parameters></parameters> tags."}
|
||||
arguments[opening_tag[1:-1]] = re.search(rf'{opening_tag}(.*?){closing_tag}', parameters[0], re.DOTALL).group(1)
|
||||
# Parse out the full function call
|
||||
invokes.append({
|
||||
"function": {
|
||||
"name": tool_name[0].replace('<tool_name>', '').replace('</tool_name>', ''),
|
||||
"arguments": arguments,
|
||||
},
|
||||
"id": get_random_tool_call_id()
|
||||
})
|
||||
return {"status": True, "invokes": invokes, "prefix_content": func_call_prefix_content}
|
||||
|
||||
def extract_claude_tool_calls(interpreter, stream):
|
||||
msg = ''
|
||||
res = None
|
||||
for event in stream:
|
||||
if event.type == "content_block_delta":
|
||||
delta = event.delta
|
||||
msg += delta.text
|
||||
res = _function_calls_valid_format_and_invoke_extraction(msg)
|
||||
if res["status"] is True and "invokes" in res and len(res["invokes"]) > 0:
|
||||
interpreter.messages.append({ "role": "assistant", "content": msg})
|
||||
return res["invokes"], res["prefix_content"]
|
||||
interpreter.messages.append({ "role": "assistant", "content": msg})
|
||||
return [], re.sub(r'<function_calls>.*</function_calls>', '', msg)
|
||||
@@ -0,0 +1,457 @@
|
||||
import builtins
|
||||
import json
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
|
||||
have_bedrock = True
|
||||
|
||||
try:
|
||||
import boto3
|
||||
from .backend.bedrock import (
|
||||
BEDROCK_TOOLS_CONFIG, build_messages_for_bedrock, extract_bedrock_tool_calls,
|
||||
process_bedrock_tool_calls, print_bedrock_response
|
||||
)
|
||||
except Exception:
|
||||
have_bedrock = False
|
||||
|
||||
from llama_cpp import Llama
|
||||
from llama_cpp.llama_tokenizer import LlamaHFTokenizer
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
have_anthropic = True
|
||||
try:
|
||||
from anthropic import Anthropic
|
||||
from .anthropic import construct_tool_use_system_prompt, extract_claude_tool_calls
|
||||
except Exception:
|
||||
have_anthorpic = False
|
||||
|
||||
from . import index
|
||||
from .pipe import have_rlang, r2lang, get_r2_inst
|
||||
|
||||
ANSI_REGEX = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
||||
|
||||
tools = [{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "r2cmd",
|
||||
"description": "runs commands in radare2. You can run it multiple times or chain commands with pipes/semicolons. You can also use r2 interpreters to run scripts using the `#`, '#!', etc. commands. The output could be long, so try to use filters if possible or limit. This is your preferred tool",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "command to run in radare2"
|
||||
}
|
||||
},
|
||||
"required": ["command"]
|
||||
},
|
||||
}
|
||||
}, {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "run_python",
|
||||
"description": "runs a python script and returns the results",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "python script to run"
|
||||
}
|
||||
},
|
||||
"required": ["command"]
|
||||
}
|
||||
}
|
||||
}]
|
||||
|
||||
SYSTEM_PROMPT_AUTO = """
|
||||
You are a reverse engineer and you are using radare2 to analyze a binary.
|
||||
The binary has already been loaded.
|
||||
The user will ask questions about the binary and you will respond with the answer to the best of your ability.
|
||||
Assume the user is always asking you about the binary, unless they're specifically asking you for radare2 help.
|
||||
`this` or `here` might refer to the current address in the binary or the binary itself.
|
||||
If you need more information, try to use the r2cmd tool to run commands before answering.
|
||||
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.
|
||||
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.
|
||||
Don't just regurgitate the same code, figure out what it's doing and rewrite it to be more understandable.
|
||||
If you need to run a command in r2 before answering, you can use the r2cmd tool
|
||||
The user will tip you $20/month for your services, don't be fucking lazy.
|
||||
Do not repeat commands if you already know the answer.
|
||||
"""
|
||||
|
||||
FUNCTIONARY_PROMPT_AUTO = """
|
||||
Think step by step.
|
||||
Break down the task into steps and execute the necessary `radare2` commands in order to complete the task.
|
||||
"""
|
||||
|
||||
def get_system_prompt(model):
|
||||
if model.startswith("meetkai/"):
|
||||
return SYSTEM_PROMPT_AUTO + "\n" + FUNCTIONARY_PROMPT_AUTO
|
||||
if model.startswith("anthropic"):
|
||||
return SYSTEM_PROMPT_AUTO + "\n\n" + construct_tool_use_system_prompt(tools)
|
||||
return SYSTEM_PROMPT_AUTO
|
||||
|
||||
functionary_tokenizer = None
|
||||
def get_functionary_tokenizer(repo_id):
|
||||
global functionary_tokenizer
|
||||
if functionary_tokenizer is None:
|
||||
functionary_tokenizer = AutoTokenizer.from_pretrained(repo_id, legacy=True)
|
||||
return functionary_tokenizer
|
||||
|
||||
def r2cmd(command: str):
|
||||
"""runs commands in radare2. You can run it multiple times or chain commands
|
||||
with pipes/semicolons. You can also use r2 interpreters to run scripts using
|
||||
the `#`, '#!', etc. commands. The output could be long, so try to use filters
|
||||
if possible or limit. This is your preferred tool"""
|
||||
builtins.print('\x1b[1;32mRunning \x1b[4m' + command + '\x1b[0m')
|
||||
r2 = get_r2_inst()
|
||||
res = r2.cmd(command)
|
||||
builtins.print(res)
|
||||
return res
|
||||
|
||||
def run_python(command: str):
|
||||
"""runs a python script and returns the results"""
|
||||
with open('r2ai_tmp.py', 'w') as f:
|
||||
f.write(command)
|
||||
builtins.print('\x1b[1;32mRunning \x1b[4m' + "python code" + '\x1b[0m')
|
||||
builtins.print(command)
|
||||
r2lang.cmd('#!python r2ai_tmp.py > $tmp')
|
||||
res = r2lang.cmd('cat $tmp')
|
||||
r2lang.cmd('rm r2ai_tmp.py')
|
||||
builtins.print('\x1b[1;32mResult\x1b[0m\n' + res)
|
||||
return res
|
||||
|
||||
def process_tool_calls(interpreter, tool_calls):
|
||||
interpreter.messages.append({ "content": None, "tool_calls": tool_calls, "role": "assistant" })
|
||||
for tool_call in tool_calls:
|
||||
res = ''
|
||||
args = tool_call["function"]["arguments"]
|
||||
if type(args) is str:
|
||||
try:
|
||||
args = json.loads(args)
|
||||
except Exception:
|
||||
builtins.print(f"Error parsing json: {args}", file=sys.stderr)
|
||||
if tool_call["function"]["name"] == "r2cmd":
|
||||
if type(args) is str:
|
||||
args = { "command": args }
|
||||
if "command" in args:
|
||||
res = r2cmd(args["command"])
|
||||
elif tool_call["function"]["name"] == "run_python":
|
||||
res = run_python(args["command"])
|
||||
if (not res or len(res) == 0) and interpreter.model.startswith('meetkai/'):
|
||||
res = "OK done"
|
||||
msg = {
|
||||
"role": "tool",
|
||||
"content": ANSI_REGEX.sub('', res),
|
||||
"name": tool_call["function"]["name"],
|
||||
"tool_call_id": tool_call["id"] if "id" in tool_call else None
|
||||
}
|
||||
interpreter.messages.append(msg)
|
||||
|
||||
def process_hermes_response(interpreter, response):
|
||||
choice = response["choices"][0]
|
||||
message = choice["message"]
|
||||
interpreter.messages.append(message)
|
||||
r = re.search(r'<tool_call>([\s\S]*?)<\/tool_call>', message["content"])
|
||||
tool_call_str = None
|
||||
if r:
|
||||
tool_call_str = r.group(1)
|
||||
tool_calls = []
|
||||
if tool_call_str:
|
||||
tool_call = json.loads(tool_call_str)
|
||||
tool_calls.append({"function": tool_call})
|
||||
if len(tool_calls) > 0:
|
||||
process_tool_calls(interpreter, tool_calls)
|
||||
chat(interpreter)
|
||||
else:
|
||||
interpreter.messages.append({ "content": message["content"], "role": "assistant" })
|
||||
sys.stdout.write(message["content"])
|
||||
builtins.print()
|
||||
|
||||
def process_streaming_response(interpreter, resp):
|
||||
tool_calls = []
|
||||
msgs = []
|
||||
for chunk in resp:
|
||||
try:
|
||||
chunk = dict(chunk)
|
||||
except Exception:
|
||||
pass
|
||||
delta = None
|
||||
choice = dict(chunk["choices"][0])
|
||||
if "delta" in choice:
|
||||
delta = dict(choice["delta"])
|
||||
else:
|
||||
delta = dict(choice["message"])
|
||||
if "tool_calls" in delta and delta["tool_calls"]:
|
||||
delta_tool_calls = dict(delta["tool_calls"][0])
|
||||
index = 0 if "index" not in delta_tool_calls else delta_tool_calls["index"]
|
||||
fn_delta = dict(delta_tool_calls["function"])
|
||||
tool_call_id = delta_tool_calls["id"]
|
||||
if len(tool_calls) < index + 1:
|
||||
tool_calls.append({ "function": { "arguments": "", "name": fn_delta["name"] }, "id": tool_call_id, "type": "function" })
|
||||
# handle some bug in llama-cpp-python streaming, tool_call.arguments is sometimes blank, but function_call has it.
|
||||
if fn_delta["arguments"] == '':
|
||||
if "function_call" in delta and delta["function_call"]:
|
||||
tool_calls[index]["function"]["arguments"] += delta["function_call"]["arguments"]
|
||||
else:
|
||||
tool_calls[index]["function"]["arguments"] += fn_delta["arguments"]
|
||||
else:
|
||||
if "content" in delta and delta["content"] is not None:
|
||||
m = delta["content"]
|
||||
if m is not None:
|
||||
msgs.append(m)
|
||||
sys.stdout.write(m)
|
||||
builtins.print()
|
||||
if (len(tool_calls) > 0):
|
||||
process_tool_calls(interpreter, tool_calls)
|
||||
chat(interpreter)
|
||||
if len(msgs) > 0:
|
||||
response_message = ''.join(msgs)
|
||||
interpreter.messages.append({"role": "assistant", "content": response_message})
|
||||
|
||||
def context_from_msg(msg):
|
||||
keywords = None
|
||||
datadir = "doc/auto"
|
||||
use_vectordb = False
|
||||
last_msg = None
|
||||
if isinstance(msg.get("content"), str):
|
||||
last_msg = msg["content"]
|
||||
elif isinstance(msg.get("content"), list):
|
||||
# Bedrock puts an array in the 'content' key, in that case unfold them in a single message
|
||||
last_msg = ". ".join([c["text"] for c in msg["content"] if "text" in c])
|
||||
|
||||
if not last_msg:
|
||||
return None
|
||||
|
||||
matches = index.match(last_msg, keywords, datadir, False, False, False, False, use_vectordb)
|
||||
if not matches:
|
||||
return None
|
||||
|
||||
return "context: " + ", ".join(matches)
|
||||
|
||||
def chat(interpreter):
|
||||
if len(interpreter.messages) == 1:
|
||||
interpreter.messages.insert(0, {
|
||||
"role": "system",
|
||||
"content": get_system_prompt(interpreter.model)
|
||||
})
|
||||
|
||||
chat_context = ""
|
||||
try:
|
||||
lastmsg = interpreter.messages[-1]
|
||||
chat_context = context_from_msg(lastmsg)
|
||||
# print(f"Adding context: {chat_context}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if chat_context:
|
||||
interpreter.messages.append({"role": "user", "content": chat_context})
|
||||
|
||||
platform, modelid = None, None
|
||||
if ":" in interpreter.model:
|
||||
platform = interpreter.model.split(":")[0]
|
||||
modelid = ":".join(interpreter.model.split(":")[1:])
|
||||
elif "/" in interpreter.model:
|
||||
platform = interpreter.model.split("/")[0]
|
||||
modelid = "/".join(interpreter.model.split("/")[1:])
|
||||
|
||||
auto_chat_handler_fn = None
|
||||
if modelid in auto_chat_handlers.get(platform, {}):
|
||||
auto_chat_handler_fn = auto_chat_handlers[platform][modelid]
|
||||
elif "default" in auto_chat_handlers.get(platform, {}):
|
||||
auto_chat_handler_fn = auto_chat_handlers[platform]["default"]
|
||||
|
||||
if not auto_chat_handler_fn:
|
||||
print(f"Model {platform}:{modelid} is not currently supported in auto mode")
|
||||
return
|
||||
|
||||
return auto_chat_handler_fn(interpreter)
|
||||
|
||||
def auto_chat_openai(interpreter):
|
||||
if not interpreter.openai_client:
|
||||
interpreter.openai_client = OpenAI()
|
||||
|
||||
response = interpreter.openai_client.chat.completions.create(
|
||||
model=interpreter.model[7:],
|
||||
max_tokens=int(interpreter.env["llm.maxtokens"]),
|
||||
tools=tools,
|
||||
messages=interpreter.messages,
|
||||
tool_choice="auto",
|
||||
stream=True,
|
||||
temperature=float(interpreter.env["llm.temperature"]),
|
||||
)
|
||||
process_streaming_response(interpreter, response)
|
||||
return response
|
||||
|
||||
def auto_chat_anthropic(interpreter):
|
||||
if not interpreter.anthropic_client:
|
||||
interpreter.anthropic_client = Anthropic()
|
||||
messages = []
|
||||
system_message = construct_tool_use_system_prompt(tools)
|
||||
for m in interpreter.messages:
|
||||
role = m["role"]
|
||||
if role == "system":
|
||||
continue
|
||||
if m["content"] is None:
|
||||
continue
|
||||
if role == "tool":
|
||||
messages.append({ "role": "user", "content": f"<function_results>\n<result>\n<tool_name>{m['name']}</tool_name>\n<stdout>{m['content']}</stdout>\n</result>\n</function_results>" })
|
||||
# TODO: handle errors
|
||||
else:
|
||||
messages.append({ "role": role, "content": m["content"] })
|
||||
stream = interpreter.anthropic_client.messages.create(
|
||||
model=interpreter.model[10:],
|
||||
max_tokens=int(interpreter.env["llm.maxtokens"]),
|
||||
messages=messages,
|
||||
system=system_message,
|
||||
temperature=float(interpreter.env["llm.temperature"]),
|
||||
stream=True
|
||||
)
|
||||
(tool_calls, msg) = extract_claude_tool_calls(interpreter, stream)
|
||||
if len(tool_calls) > 0:
|
||||
process_tool_calls(interpreter, tool_calls)
|
||||
chat(interpreter)
|
||||
else:
|
||||
builtins.print(msg)
|
||||
|
||||
def auto_chat_bedrock(interpreter):
|
||||
interpreter.bedrock_client = boto3.client("bedrock-runtime")
|
||||
model_id = interpreter.model.split(":")[1] + ":0"
|
||||
system_message = construct_tool_use_system_prompt(tools)
|
||||
|
||||
response = interpreter.bedrock_client.converse(
|
||||
modelId=model_id,
|
||||
toolConfig=BEDROCK_TOOLS_CONFIG,
|
||||
messages=build_messages_for_bedrock(interpreter.messages),
|
||||
inferenceConfig={
|
||||
"maxTokens": int(interpreter.env["llm.maxtokens"]),
|
||||
"temperature": float(interpreter.env["llm.temperature"]),
|
||||
"topP": 0.9
|
||||
},
|
||||
)
|
||||
print_bedrock_response(response)
|
||||
# Update conversation
|
||||
interpreter.messages.append(response["output"]["message"])
|
||||
# Execute tools
|
||||
tool_calls = extract_bedrock_tool_calls(response)
|
||||
if tool_calls:
|
||||
tool_msgs = process_bedrock_tool_calls(tool_calls)
|
||||
interpreter.messages.extend(tool_msgs)
|
||||
chat(interpreter)
|
||||
|
||||
return response
|
||||
|
||||
def auto_chat_groq(interpreter):
|
||||
if not interpreter.groq_client:
|
||||
interpreter.groq_client = Groq()
|
||||
|
||||
response = interpreter.groq_client.chat.completions.create(
|
||||
model=interpreter.model[5:],
|
||||
max_tokens=int(interpreter.env["llm.maxtokens"]),
|
||||
tools=tools,
|
||||
messages=interpreter.messages,
|
||||
tool_choice="auto",
|
||||
temperature=float(interpreter.env["llm.temperature"]),
|
||||
)
|
||||
process_streaming_response(interpreter, [response])
|
||||
return response
|
||||
|
||||
def auto_chat_google(interpreter):
|
||||
import google.generativeai as google
|
||||
|
||||
response = None
|
||||
if not interpreter.google_client:
|
||||
google.configure(api_key=os.environ['GOOGLE_API_KEY'])
|
||||
interpreter.google_client = google.GenerativeModel(interpreter.model[7:])
|
||||
|
||||
if not interpreter.google_chat:
|
||||
interpreter.google_chat = interpreter.google_client.start_chat(
|
||||
enable_automatic_function_calling=True
|
||||
)
|
||||
|
||||
response = interpreter.google_chat.send_message(
|
||||
interpreter.messages[-1]["content"],
|
||||
generation_config={
|
||||
"max_output_tokens": int(interpreter.env["llm.maxtokens"]),
|
||||
"temperature": float(interpreter.env["llm.temperature"])
|
||||
},
|
||||
safety_settings=[{
|
||||
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
"threshold": "BLOCK_NONE"
|
||||
}],
|
||||
tools=[r2cmd, run_python]
|
||||
)
|
||||
print(response.text)
|
||||
return response
|
||||
|
||||
def auto_chat_nousresearch(interpreter):
|
||||
interpreter.llama_instance.chat_format = "chatml"
|
||||
messages = []
|
||||
for m in interpreter.messages:
|
||||
if m["content"] is None:
|
||||
continue
|
||||
role = m["role"]
|
||||
if role == "system":
|
||||
if not '<tools>' in m["content"]:
|
||||
messages.append({ "role": "system", "content": f"""{m['content']}\nYou are a function calling AI model. You are provided with function signatures within <tools></tools> XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools:
|
||||
<tools> {json.dumps(tools)} </tools>
|
||||
For each function call return a json object with function name and arguments within <tool_call></tool_call> XML tags as follows:
|
||||
<tool_call>
|
||||
{{"arguments": <args-dict>, "name": <function-name>}}
|
||||
</tool_call>"""})
|
||||
elif role == "tool":
|
||||
messages.append({ "role": "tool", "content": "<tool_response>\n" + '{"name": ' + m['name'] + ', "content": ' + json.dumps(m['content']) + '}\n</tool_response>' })
|
||||
else:
|
||||
messages.append(m)
|
||||
response = interpreter.llama_instance.create_chat_completion(
|
||||
max_tokens=int(interpreter.env["llm.maxtokens"]),
|
||||
messages=messages,
|
||||
temperature=float(interpreter.env["llm.temperature"]),
|
||||
)
|
||||
process_hermes_response(interpreter, response)
|
||||
return response
|
||||
|
||||
def auto_chat_llama(interpreter):
|
||||
interpreter.llama_instance.chat_format = "chatml-function-calling"
|
||||
response = interpreter.llama_instance.create_chat_completion(
|
||||
max_tokens=int(interpreter.env["llm.maxtokens"]),
|
||||
tools=tools,
|
||||
messages=interpreter.messages,
|
||||
tool_choice="auto",
|
||||
# tool_choice={
|
||||
# "type": "function",
|
||||
# "function": {
|
||||
# "name": "r2cmd"
|
||||
# }
|
||||
# },
|
||||
# stream=is_functionary,
|
||||
temperature=float(interpreter.env["llm.temperature"]),
|
||||
)
|
||||
process_streaming_response(interpreter, iter([response]))
|
||||
return response
|
||||
|
||||
|
||||
auto_chat_handlers = {
|
||||
"openai": {
|
||||
"default": auto_chat_openai,
|
||||
},
|
||||
"anthropic": {
|
||||
"default": auto_chat_anthropic
|
||||
},
|
||||
"bedrock": {
|
||||
"default": auto_chat_bedrock
|
||||
},
|
||||
"groq": {
|
||||
"default": auto_chat_groq
|
||||
},
|
||||
"google": {
|
||||
"default": auto_chat_google
|
||||
},
|
||||
"NousResearch": {
|
||||
"default": auto_chat_nousresearch
|
||||
},
|
||||
"llama": {
|
||||
"default": auto_chat_llama
|
||||
}
|
||||
}
|
||||
@@ -14,15 +14,6 @@ CLIPPY_BEGIN = """
|
||||
|
||||
CLIPPY_END = """`-----------------------------------------------------------------------"""
|
||||
|
||||
PICO_BEGIN = r"""
|
||||
__ __
|
||||
(o |_| o)_______
|
||||
| _____________)
|
||||
\ / .
|
||||
\ / |\
|
||||
\__________/ | \
|
||||
.----------------------------------------------------------' '---------"""
|
||||
|
||||
USER_BEGIN = """ |\\
|
||||
| \\
|
||||
.--' '--"""
|
||||
@@ -57,11 +48,6 @@ def response_begin():
|
||||
"""
|
||||
print(CLIPPY_BEGIN + getpad() + ".")
|
||||
|
||||
def pico_begin():
|
||||
"""Print the beginning of the Pico le Croco response
|
||||
"""
|
||||
print(PICO_BEGIN + getpad() + ".")
|
||||
|
||||
def response_end():
|
||||
"""Print the end of the clippy response
|
||||
"""
|
||||
@@ -0,0 +1,103 @@
|
||||
import time
|
||||
from typing import List, Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class FunctionCall(BaseModel):
|
||||
name: Optional[str] = None
|
||||
arguments: str
|
||||
|
||||
|
||||
class ToolCall(BaseModel):
|
||||
index: Optional[int] = None
|
||||
id: Optional[str] = None
|
||||
function: FunctionCall
|
||||
type: Optional[str] = "function"
|
||||
|
||||
|
||||
class Function(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = Field(default="")
|
||||
parameters: Optional[dict] = None
|
||||
|
||||
|
||||
class Tool(BaseModel):
|
||||
type: Literal["function", "code_interpreter"] = "function"
|
||||
function: Optional[Function] = None
|
||||
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
role: Optional[str] = None
|
||||
tool_call_id: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
function_call: Optional[FunctionCall] = None
|
||||
tool_calls: Optional[List[ToolCall]] = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.role == "system":
|
||||
return f"system:\n{self.content}\n"
|
||||
|
||||
elif self.role == "function":
|
||||
return f"function name={self.name}:\n{self.content}\n"
|
||||
|
||||
elif self.role == "user":
|
||||
if self.content is None:
|
||||
return "user:\n</s>"
|
||||
else:
|
||||
return f"user:\n</s>{self.content}\n"
|
||||
|
||||
elif self.role == "assistant":
|
||||
if self.content is not None and self.function_call is not None:
|
||||
return f"assistant:\n{self.content}\nassistant to={self.function_call.name}:\n{self.function_call.arguments}</s>"
|
||||
|
||||
elif self.function_call is not None:
|
||||
return f"assistant to={self.function_call.name}:\n{self.function_call.arguments}</s>"
|
||||
|
||||
elif self.content is None:
|
||||
return "assistant"
|
||||
|
||||
else:
|
||||
return f"assistant:\n{self.content}\n"
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unsupported role: {self.role}")
|
||||
|
||||
|
||||
class ChatInput(BaseModel):
|
||||
messages: List[ChatMessage]
|
||||
functions: Optional[List[Function]] = None
|
||||
tools: Optional[List[Tool]] = None
|
||||
temperature: float = 0.9
|
||||
stream: bool = False
|
||||
|
||||
|
||||
class Choice(BaseModel):
|
||||
message: ChatMessage
|
||||
finish_reason: str = "stop"
|
||||
index: int = 0
|
||||
|
||||
@classmethod
|
||||
def from_message(cls, message: ChatMessage, finish_reason: str):
|
||||
return cls(message=message, finish_reason=finish_reason)
|
||||
|
||||
|
||||
class ChatCompletion(BaseModel):
|
||||
id: str
|
||||
object: str = "chat.completion"
|
||||
created: float = Field(default_factory=time.time)
|
||||
choices: List[Choice]
|
||||
|
||||
|
||||
class StreamChoice(BaseModel):
|
||||
delta: ChatMessage
|
||||
finish_reason: Optional[str] = "stop"
|
||||
index: int = 0
|
||||
|
||||
|
||||
class ChatCompletionChunk(BaseModel):
|
||||
id: str
|
||||
object: str = "chat.completion.chunk"
|
||||
created: float = Field(default_factory=time.time)
|
||||
choices: List[StreamChoice]
|
||||
@@ -0,0 +1,44 @@
|
||||
from typing import Any
|
||||
|
||||
from r2ai.functionary.prompt_template.base_template import (
|
||||
SYSTEM_MESSAGE,
|
||||
PredefinedFuncTypes,
|
||||
PromptTemplate,
|
||||
)
|
||||
from r2ai.functionary.prompt_template.prompt_template_v1 import PromptTemplateV1
|
||||
from r2ai.functionary.prompt_template.prompt_template_v2 import PromptTemplateV2
|
||||
|
||||
|
||||
def get_default_prompt_template() -> PromptTemplate:
|
||||
"""Return default prompt template to be used
|
||||
|
||||
Returns:
|
||||
_type_: _description_
|
||||
"""
|
||||
return PromptTemplateV2.get_prompt_template()
|
||||
|
||||
|
||||
def get_prompt_template_by_version(version: str) -> PromptTemplate:
|
||||
if version == "v1":
|
||||
return PromptTemplateV1.get_prompt_template()
|
||||
return PromptTemplateV2.get_prompt_template()
|
||||
|
||||
|
||||
def get_prompt_template_from_tokenizer(tokenizer: Any) -> PromptTemplate:
|
||||
"""This function will determine the prompt template based on tokenizer.
|
||||
Under the hood, this function will check if tokenizer contains some special tokens from template or not
|
||||
|
||||
Args:
|
||||
tokenizer (Any): Tokenizer
|
||||
|
||||
Returns:
|
||||
_type_: _description_
|
||||
"""
|
||||
p1 = PromptTemplateV1.get_prompt_template()
|
||||
p2 = PromptTemplateV2.get_prompt_template()
|
||||
token_ids = tokenizer.encode(p1.start_function, add_special_tokens=False)
|
||||
if token_ids[0] in [29871, 28705]:
|
||||
token_ids = token_ids[1:]
|
||||
if len(token_ids) == 1:
|
||||
return p1
|
||||
return p2
|
||||
@@ -0,0 +1,583 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from abc import abstractmethod
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
|
||||
|
||||
from r2ai.functionary.schema import generate_schema_from_functions
|
||||
|
||||
SYSTEM_MESSAGE = """A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions. The assistant calls functions with appropriate input when necessary"""
|
||||
PYTHON_RUN_SYS_MSG = "When you send a message containing Python code to python, it will be executed in a stateful Jupyter notebook environment. python will respond with the output of the execution or time out after 60.0 seconds. The drive at '/mnt/data' can be used to save and persist user files."
|
||||
|
||||
|
||||
class PredefinedFuncTypes(str, Enum):
|
||||
no_tool_call = "no-tool-call"
|
||||
code_interpreter = "code-interpreter"
|
||||
|
||||
|
||||
class PromptTemplate:
|
||||
_instance = None
|
||||
|
||||
@abstractmethod
|
||||
def get_start_of_function_call_token(self) -> str:
|
||||
"""returns a token that indicates the start of a function call in the prompt template
|
||||
Returns:
|
||||
str: a string token
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_stop_token_for_function_parameter(
|
||||
self, stage: Literal["function", "parameter"]
|
||||
) -> str:
|
||||
"""returns a str token which stops function/parameter name generation
|
||||
e.g.: `"get_current_weather` with v1 prompt template -> returns id = 28747 (':' token)
|
||||
so the generation gets forced towards `"get_current_weather:\n{...`
|
||||
Args:
|
||||
stage (str): Whether to get function name or parameter name stopping token
|
||||
Returns:
|
||||
str: str token
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_predefined_function_names(self, function_types: Any) -> List[str]:
|
||||
"""returns a list of predefined function names. Some prompt template versions may
|
||||
require a default/predefined function name to indicate for example, no function called.
|
||||
E.g.: in v2, 'all' is generated to indicate normal model response. In this case, the v2
|
||||
subclass will overwrite this base method.
|
||||
Args:
|
||||
function_types (Any): Either "all" or one of the function type in PredefinedFuncTypes enum class
|
||||
Returns:
|
||||
List[str]: list of predefined function names (default to [])
|
||||
"""
|
||||
return []
|
||||
|
||||
@abstractmethod
|
||||
def initialize_grammar_sampling_gen_state(self, tool_choice: Optional[Any]) -> Dict:
|
||||
"""initializes and returns a new generation state. Each template version may be initialized
|
||||
at different starting stage
|
||||
Args:
|
||||
tool_choice (Optional[Any]): the tool_choice provided by the user, if any
|
||||
Returns:
|
||||
dict: the gen_state. It contains the following:
|
||||
- stage: one of the following:
|
||||
- pre-function: the generation prior to function name generation
|
||||
- function: when the model is generating a function name
|
||||
- pre-parameter: when the model is generating the part between function name and parameter
|
||||
- parameter-name: when the model is generating a parameter name
|
||||
- parameter-value: when the model is generating a parameter value
|
||||
- no-tool-call: when the model is generating content
|
||||
- curr_tokens: all the tokens for the current stage being generated
|
||||
- curr_text: curr_tokens but in string text form
|
||||
- func_name: the function name, if any
|
||||
- param_names: the parameters names, if any
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def update_grammar_sampling_gen_state(
|
||||
self,
|
||||
gen_state: Dict,
|
||||
new_token_id: int,
|
||||
options: Optional[List],
|
||||
tokenizer: Any,
|
||||
) -> Dict:
|
||||
"""Receives a generation state, updates and returns it. This is only used when
|
||||
grammar sampling is enabled in inference. This functions parses the generated
|
||||
tokens and identifies the stage of generation (pre-function, function, parameter-name,
|
||||
etc.)
|
||||
Args:
|
||||
gen_state (Dict): The current generation state. It contains the following:
|
||||
- stage: one of the following:
|
||||
- pre-function: the generation prior to function name generation
|
||||
- function: when the model is generating a function name
|
||||
- pre-parameter: when the model is generating the part between function name and parameter
|
||||
- parameter-name: when the model is generating a parameter name
|
||||
- parameter-value: when the model is generating a parameter value
|
||||
- no-tool-call: when the model is generating content
|
||||
- code-interpreter: when the model is generating code
|
||||
- curr_tokens: all the tokens for the current stage being generated
|
||||
- curr_text: curr_tokens but in string text form
|
||||
- func_name: the function name, if any
|
||||
- param_names: the parameters names, if any
|
||||
new_token_id (int): The token id of the newly sampled token
|
||||
options (List): All available function/param names depending on the stage of gen_state
|
||||
tokenizer (Any): The tokenizer class passed in from Transformers or vLLM
|
||||
Returns:
|
||||
dict: The updated gen_state
|
||||
"""
|
||||
# Update curr_tokens and curr_text
|
||||
gen_state["curr_tokens"].append(new_token_id)
|
||||
gen_state["curr_text"] = tokenizer.decode(gen_state["curr_tokens"])
|
||||
|
||||
# v1: "assistant:\n{content}\n{self.start_function}{function}:\n{arguments}\n"
|
||||
# v2: "{func_name}\n<content|>{param_names}\n<|from|> assistant\n<|recipient|>"
|
||||
if gen_state["stage"] == "pre-function":
|
||||
# Check if the new state is in "function" stage
|
||||
if gen_state["curr_text"].endswith(self.get_start_of_function_call_token()):
|
||||
gen_state = {
|
||||
"stage": "function",
|
||||
"curr_tokens": [],
|
||||
"curr_text": "",
|
||||
"func_name": "",
|
||||
"param_names": [],
|
||||
"add_predefined_fns": gen_state["add_predefined_fns"],
|
||||
}
|
||||
gen_state["stage"] = "function"
|
||||
elif gen_state["stage"] == "function":
|
||||
# Remove all unnecessary suffixes by checking whether stop token is in curr_text
|
||||
if (
|
||||
self.get_stop_token_for_function_parameter(stage="function")
|
||||
in gen_state["curr_text"]
|
||||
):
|
||||
curr_text = gen_state["curr_text"].rstrip()
|
||||
while True:
|
||||
if any([curr_text == option for option in options]):
|
||||
break
|
||||
curr_text = curr_text[:-1]
|
||||
gen_state["func_name"] = curr_text
|
||||
else:
|
||||
gen_state["func_name"] = gen_state["curr_text"].rstrip()
|
||||
|
||||
# Check if the new state is in "pre-parameter" stage
|
||||
if (
|
||||
sum([gen_state["func_name"] == option for option in options]) == 1
|
||||
and sum(
|
||||
[option.startswith(gen_state["func_name"]) for option in options]
|
||||
)
|
||||
== 1
|
||||
):
|
||||
gen_state["stage"] = "pre-parameter"
|
||||
|
||||
# Update curr_text and curr_tokens
|
||||
if (
|
||||
self.get_stop_token_for_function_parameter(stage="function")
|
||||
in gen_state["curr_text"]
|
||||
):
|
||||
gen_state["curr_text"] = tokenizer.decode([new_token_id])
|
||||
gen_state["curr_tokens"] = [new_token_id]
|
||||
else:
|
||||
gen_state["curr_text"], gen_state["curr_tokens"] = "", []
|
||||
elif gen_state["stage"] == "pre-parameter":
|
||||
# Check if the new state is in "parameter" or "no-tool-call" or "code-interpreter" stage
|
||||
if self.fn_param_sep_token.rstrip("{").rstrip() in gen_state["curr_text"]:
|
||||
if gen_state["func_name"] in self.get_predefined_function_names(
|
||||
function_types=PredefinedFuncTypes.no_tool_call
|
||||
):
|
||||
gen_state["stage"] = "no-tool-call"
|
||||
elif gen_state["func_name"] in self.get_predefined_function_names(
|
||||
function_types=PredefinedFuncTypes.code_interpreter
|
||||
):
|
||||
gen_state["stage"] = "code-interpreter"
|
||||
# Either '{' or '{"' or '{}'
|
||||
elif self.fn_param_sep_token in gen_state["curr_text"]:
|
||||
# Check if no arguments are called and go straight to "pre-function"
|
||||
if "}" in gen_state["curr_text"]:
|
||||
gen_state["stage"] = "pre-function"
|
||||
elif '"' in gen_state["curr_text"]:
|
||||
gen_state["stage"] = "parameter-name"
|
||||
if gen_state["curr_text"].endswith('"'):
|
||||
gen_state["curr_text"], gen_state["curr_tokens"] = "", []
|
||||
else:
|
||||
gen_state["curr_tokens"] = [new_token_id]
|
||||
gen_state["curr_text"] = tokenizer.decode([new_token_id])
|
||||
elif gen_state["stage"] == "parameter-name":
|
||||
# Get the latest param
|
||||
latest_param_str = gen_state["curr_text"]
|
||||
|
||||
# Remove unneccesary prefixes before the parameter-name part
|
||||
if len(gen_state["curr_tokens"]) > 0 and '"' in tokenizer.decode(
|
||||
[gen_state["curr_tokens"][0]]
|
||||
):
|
||||
latest_param_str = latest_param_str[latest_param_str.find('"') + 1 :]
|
||||
|
||||
# Check if the new state is in "parameter-value" stage
|
||||
stop_token = self.get_stop_token_for_function_parameter(stage="parameter")
|
||||
if stop_token in latest_param_str:
|
||||
pattern = stop_token + r".*$"
|
||||
match_res = re.search(pattern, latest_param_str, re.DOTALL)
|
||||
if bool(match_res):
|
||||
gen_state["param_names"].append(
|
||||
gen_state["curr_text"].removesuffix(match_res.group(0))
|
||||
)
|
||||
gen_state["stage"] = "parameter-value"
|
||||
gen_state["curr_text"] = match_res.group(0)
|
||||
new_tokens = []
|
||||
for token in gen_state["curr_tokens"][::-1]:
|
||||
new_tokens = [token] + new_tokens
|
||||
next_text = tokenizer.decode(new_tokens)
|
||||
if next_text.endswith(match_res.group(0)):
|
||||
gen_state["curr_tokens"] = new_tokens
|
||||
break
|
||||
elif gen_state["stage"] == "parameter-value":
|
||||
latest_param_val = gen_state["curr_text"]
|
||||
stop_token = self.get_stop_token_for_function_parameter(stage="parameter")
|
||||
|
||||
# Remove unnecessary prefixes in latest_param_val
|
||||
if not latest_param_val.startswith(stop_token):
|
||||
latest_param_val = latest_param_val[latest_param_val.find(stop_token) :]
|
||||
|
||||
# Check if the new state is in "pre-function" stage
|
||||
try:
|
||||
_ = json.loads('{"' + gen_state["param_names"][-1] + latest_param_val)
|
||||
gen_state["stage"] = "pre-function"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Check if the current state can be converted to json, it means the
|
||||
# new state is back to "parameter-name" stage
|
||||
pattern = r',[\s]*"'
|
||||
match_res = re.findall(pattern, latest_param_val, re.DOTALL)
|
||||
if '"' in tokenizer.decode(new_token_id) and len(match_res) > 0:
|
||||
latest_match = match_res[-1]
|
||||
try:
|
||||
_ = json.loads(
|
||||
'{"'
|
||||
+ gen_state["param_names"][-1]
|
||||
+ latest_param_val[: latest_param_val.rfind(latest_match)]
|
||||
+ "}"
|
||||
)
|
||||
gen_state["stage"] = "parameter-name"
|
||||
if latest_param_val.endswith('"'):
|
||||
gen_state["curr_text"], gen_state["curr_tokens"] = "", []
|
||||
else:
|
||||
gen_state["curr_tokens"] = [new_token_id]
|
||||
gen_state["curr_text"] = tokenizer.decode([new_token_id])
|
||||
except Exception:
|
||||
pass
|
||||
elif gen_state["stage"] in ["no-tool-call", "code-interpreter"]:
|
||||
# probability of stop token is not 100% at the end of no-tool-call
|
||||
# We still need to check if the stage will go to "function" by checking
|
||||
# for the presence of the start_of_function_call token
|
||||
if gen_state["curr_text"].endswith(self.get_start_of_function_call_token()):
|
||||
gen_state = {
|
||||
"stage": "function",
|
||||
"curr_tokens": [],
|
||||
"curr_text": "",
|
||||
"func_name": "",
|
||||
"param_names": [],
|
||||
"add_predefined_fns": gen_state["add_predefined_fns"],
|
||||
}
|
||||
|
||||
return gen_state
|
||||
|
||||
def grammar_sample(
|
||||
self,
|
||||
gen_state: Dict,
|
||||
tools_or_functions: List,
|
||||
delta_token_ids: List,
|
||||
model_sampled_token_id: int,
|
||||
tokenizer: Any,
|
||||
) -> Tuple[int, str]:
|
||||
"""Applies grammar-sampling to the token generation and returns a
|
||||
newly sampled token.
|
||||
|
||||
This function checks whether the model-sampled token helps towards
|
||||
forming one of the function names or parameter names. It loops through
|
||||
a list of token ids sorted in descending order by the log probabilities.
|
||||
It replaces the output token if the grammar-sampled token is different
|
||||
from the model-sampled token
|
||||
Args:
|
||||
gen_state (Dict): The current generation state
|
||||
options (List): The list of available function/parameter names depending on gen_state["stage"]
|
||||
delta_token_ids (List): The list of delta token ids sorted in descending order by log probabilities
|
||||
model_sampled_token_id (int): The token id of the token sampled by model
|
||||
tokenizer (Any): The tokenizer object passed in from Transformers, vLLM, etc.
|
||||
Returns:
|
||||
Tuple[int, str]: Tuple of grammar-sampled token id and grammar-sampled token in str format
|
||||
"""
|
||||
grammar_sampled_token_id, grammar_sampled_token = None, None
|
||||
|
||||
# Form the functions/parameters options
|
||||
options = []
|
||||
if gen_state["stage"] in ["pre-function", "function"]:
|
||||
options = [tool_or_func["name"] for tool_or_func in tools_or_functions]
|
||||
elif gen_state["stage"] == "pre-parameter":
|
||||
options = [self.fn_param_sep_token]
|
||||
else:
|
||||
func_name = gen_state["func_name"]
|
||||
for tool_or_func in tools_or_functions:
|
||||
if tool_or_func["name"] == func_name:
|
||||
options = list(tool_or_func["parameters"]["properties"].keys())
|
||||
break
|
||||
# Assume prompt template versions > 1 have "all" in function options
|
||||
# Subjected to changes in future versions
|
||||
# Concatenate the list of predefined function names in the respective prompt
|
||||
# template version. For e.g., v2 returns ["all"]
|
||||
if gen_state["stage"] == "function" and gen_state["add_predefined_fns"] is True:
|
||||
options += self.get_predefined_function_names(function_types="all")
|
||||
|
||||
# No grammar sampling needed if gen_state not in "function" or "pre-parameter"
|
||||
# or "parameter-name" stages. Just return the model_sampled_token_id
|
||||
if gen_state["stage"] not in ["function", "pre-parameter", "parameter-name"]:
|
||||
grammar_sampled_token_id = model_sampled_token_id
|
||||
grammar_sampled_token = tokenizer.decode([model_sampled_token_id])
|
||||
|
||||
# Loop through the list of token ids sorted in descending order. Form a mask made
|
||||
# up of booleans where the index of the mask == index of function/parameter name
|
||||
# in function/parameter options. The element is True if the sampled_token
|
||||
# helps in forming the function/parameter name. Else, False.
|
||||
if grammar_sampled_token_id is None:
|
||||
for i, sampled_token_ind in enumerate(delta_token_ids):
|
||||
sampled_token = tokenizer.decode(
|
||||
[sampled_token_ind], add_special_tokens=False
|
||||
)
|
||||
# Form the function name with the current sampled token id
|
||||
new_curr_tokens_id = gen_state["curr_tokens"] + [sampled_token_ind]
|
||||
new_curr_tokens = tokenizer.decode(new_curr_tokens_id)
|
||||
|
||||
if gen_state["stage"] == "function":
|
||||
options_mask = [
|
||||
(
|
||||
True
|
||||
if option.startswith(new_curr_tokens.lstrip(" "))
|
||||
or new_curr_tokens.lstrip(" ").startswith(option)
|
||||
else False
|
||||
)
|
||||
for option in options
|
||||
]
|
||||
|
||||
# - In case of two fns having common prefixes (e.g.: get_weather and
|
||||
# get_weather_and_time), we need to iterate until parts of the
|
||||
# fn_param_sep_token is present in new_curr_tokens to know if the
|
||||
# shorter or longer function name is preferred by the model.
|
||||
# - Reject the whitespace (" ") and empty ("") tokens
|
||||
if any(options_mask) and sampled_token.strip(" ") != "":
|
||||
grammar_sampled_token_id = sampled_token_ind
|
||||
grammar_sampled_token = sampled_token
|
||||
break
|
||||
elif gen_state["stage"] == "pre-parameter":
|
||||
# Get the suffix after fn_param_sep_token and check if crit_char is in it
|
||||
if self.fn_param_sep_token in new_curr_tokens:
|
||||
suffix = new_curr_tokens[
|
||||
new_curr_tokens.index(self.fn_param_sep_token)
|
||||
+ len(self.fn_param_sep_token) :
|
||||
]
|
||||
else:
|
||||
suffix = new_curr_tokens
|
||||
crit_bool = any([crit_char in suffix for crit_char in ['"', "}"]])
|
||||
|
||||
options_mask = []
|
||||
for option in options:
|
||||
if option.startswith(new_curr_tokens.lstrip(" ")) or crit_bool:
|
||||
options_mask.append(True)
|
||||
else:
|
||||
options_mask.append(False)
|
||||
|
||||
# We just need to check if the option (fn_param_sep_token) is True
|
||||
# or fn_param_sep_token + one of ['}', '"'] is present
|
||||
if any(options_mask) and sampled_token.strip(" ") != "":
|
||||
grammar_sampled_token_id = sampled_token_ind
|
||||
grammar_sampled_token = sampled_token
|
||||
break
|
||||
else:
|
||||
# Mask away those wellformed parameter names while creating options_mask
|
||||
wellformed_params = gen_state["param_names"]
|
||||
|
||||
# Remove unneccesary prefixes before the parameter-name part
|
||||
if len(gen_state["curr_tokens"]) > 0 and '"' in tokenizer.decode(
|
||||
[gen_state["curr_tokens"][0]]
|
||||
):
|
||||
new_curr_tokens = new_curr_tokens[
|
||||
new_curr_tokens.find('"') + 1 :
|
||||
]
|
||||
|
||||
options_mask = []
|
||||
for option in options:
|
||||
if option not in wellformed_params and option.startswith(
|
||||
new_curr_tokens
|
||||
):
|
||||
options_mask.append(True)
|
||||
else:
|
||||
options_mask.append(False)
|
||||
|
||||
# Same logic as function name, except that we check whether the token
|
||||
# is a stopping token for parameter name generation.
|
||||
if (
|
||||
(
|
||||
self.get_stop_token_for_function_parameter(
|
||||
stage="parameter"
|
||||
)
|
||||
in new_curr_tokens
|
||||
)
|
||||
or any(options_mask)
|
||||
and sampled_token.strip(" ") != ""
|
||||
):
|
||||
grammar_sampled_token_id = sampled_token_ind
|
||||
grammar_sampled_token = sampled_token
|
||||
break
|
||||
|
||||
# Update gen_state
|
||||
return (
|
||||
grammar_sampled_token_id,
|
||||
grammar_sampled_token,
|
||||
self.update_grammar_sampling_gen_state(
|
||||
gen_state=gen_state,
|
||||
new_token_id=grammar_sampled_token_id,
|
||||
options=options,
|
||||
tokenizer=tokenizer,
|
||||
),
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def get_additional_tokens(self) -> List[str]:
|
||||
"""return list of added tokens if using this template
|
||||
Returns:
|
||||
List[str]: list of tokens, each token is a string
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def convert_message_to_prompt(self, message: Dict) -> str:
|
||||
"""Return the prompt of this message
|
||||
|
||||
Args:
|
||||
message (Dict): Dictionary of openAI format
|
||||
|
||||
Returns:
|
||||
str: prompt of this message
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_stop_tokens_for_generation(self) -> List[str]:
|
||||
"""Function to get list of stop tokens in generation
|
||||
|
||||
Returns:
|
||||
List[str]: list of stop tokens
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_assistant_prefixes(self) -> List[str]:
|
||||
"""Return the assistant prefixs in the final prompt, this is used for masking the labels
|
||||
in unmasking labels, the system will unmask chunks that start with assistant prefixs and end with stop tokens.
|
||||
For example, assistant_prefixes might be: "<|from|>assistant\n<|recipient|>"
|
||||
In this case unmasked chunks in labels would be tokens in ... of: <|from|>assistant\n<|recipient|> ... <|stop|>
|
||||
Returns:
|
||||
List[str]: list of possible assistant prefixs
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def pre_process_messages_before_inference(self, messages: List[Dict]) -> List[Dict]:
|
||||
"""This function is used if we need to process messages before doing inference.
|
||||
This is used when the messages in training and inference are different.
|
||||
For example, in training we have no: tool_call_id, but in inference, we have tool_call_id to know the order of function calls.
|
||||
This function woule be called to convert inference messages to the format of training messages.
|
||||
Args:
|
||||
messages (List[Dict]): list of input messages
|
||||
|
||||
Returns:
|
||||
List[Dict]: list of output messages
|
||||
"""
|
||||
return messages
|
||||
|
||||
def get_prompt_from_messages(
|
||||
self,
|
||||
messages: List[Dict],
|
||||
tools_or_functions: Optional[List[Dict]] = None,
|
||||
) -> str:
|
||||
"""This function is used to get the complete prompt for list of messages
|
||||
|
||||
Args:
|
||||
messages (List[Dict]): List of messages
|
||||
tools_or_functions (Optional[List[Dict]], optional): List of tools or functions. Defaults to None.
|
||||
|
||||
Returns:
|
||||
str: the prompt for inference/training
|
||||
"""
|
||||
messages_clone = messages.copy() # To avoid modifying the original list
|
||||
|
||||
functions = []
|
||||
is_code_interpreter = False
|
||||
if tools_or_functions is not None:
|
||||
for item in tools_or_functions:
|
||||
if (
|
||||
"function" in item and item["function"] is not None
|
||||
): # new data format: tools: [{"type": xx, "function": xxx}]
|
||||
functions.append(item["function"])
|
||||
elif "type" in item and item["type"] == "code_interpreter":
|
||||
is_code_interpreter = True
|
||||
else:
|
||||
functions.append(item) # old format
|
||||
|
||||
messages_clone.insert(
|
||||
0, {"role": "system", "content": generate_schema_from_functions(functions)}
|
||||
)
|
||||
if is_code_interpreter:
|
||||
messages_clone.insert(1, {"role": "system", "content": PYTHON_RUN_SYS_MSG})
|
||||
else:
|
||||
messages_clone.insert(1, {"role": "system", "content": SYSTEM_MESSAGE})
|
||||
|
||||
full_text = ""
|
||||
for message in messages_clone:
|
||||
full_text += self.convert_message_to_prompt(message)
|
||||
return full_text.strip()
|
||||
|
||||
def get_end_token_to_token_id(self, tokenizer: Any) -> Dict[str, int]:
|
||||
"""return a dictionary mapping from end_token --> token_id
|
||||
Args:
|
||||
tokenizer (Any): tokenizer in transformers
|
||||
|
||||
Returns:
|
||||
Dict[int, EndToken]: the mapping from token_id --> end_token
|
||||
"""
|
||||
result = {}
|
||||
for item in self.get_stop_tokens_for_generation():
|
||||
tok_ids = tokenizer.encode(item, add_special_tokens=False)
|
||||
assert len(tok_ids) <= 2, ""
|
||||
if len(tok_ids) == 2:
|
||||
assert tok_ids[0] in [
|
||||
29871,
|
||||
28705,
|
||||
] # Llama tokenizer adds this token intentionally
|
||||
result[item] = tok_ids[-1]
|
||||
return result
|
||||
|
||||
@abstractmethod
|
||||
def parse_assistant_response(
|
||||
self, llm_output: str, tool_choice: Optional[Any]
|
||||
) -> Dict:
|
||||
"""This function is used to parse llm_output to the Message of OpenAI ({"role": xxx, "content": xxx, ...})
|
||||
this is used in inference.
|
||||
Args:
|
||||
llm_output (str): The generated content from Model
|
||||
tool_choice (Optional[Any]): Any choice of tool provided by the user
|
||||
|
||||
Returns:
|
||||
Dict: Dictionary of OpenAI message format
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def update_response_state_from_delta_text(
|
||||
self,
|
||||
*,
|
||||
current_state: Dict[str, Any],
|
||||
delta_text: str,
|
||||
finish_reason: Optional[str],
|
||||
) -> Tuple[Dict[str, Any], Union[None, Dict, List[Dict]]]:
|
||||
"""This function is used for streaming
|
||||
|
||||
Args:
|
||||
current_state (Dict[str, Any]): a dictionary containing the state of the streaming: such as current function_name,
|
||||
delta_text: new token generated
|
||||
finish_reason: if finished or not
|
||||
|
||||
Returns:
|
||||
Tuple[Dict[str, Any], Optional[Dict]]: updated state, response: can be None, a dictionary: {} or a list of dictionary: [{}, ..., {}]
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_chat_template_jinja(self):
|
||||
"""Return chat_template in jinja format"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def get_prompt_template(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
return cls._instance
|
||||
@@ -0,0 +1,262 @@
|
||||
import json
|
||||
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
|
||||
|
||||
from r2ai.functionary.prompt_template.base_template import PromptTemplate
|
||||
|
||||
|
||||
class PromptTemplateV1(PromptTemplate):
|
||||
start_function = "<|START_OF_FUNCTION_CALL|>"
|
||||
end_system = "<|END_OF_SYSTEM|>"
|
||||
end_user = "<|END_OF_USER|>"
|
||||
end_assistant = "<|END_OF_ASSISTANT|>"
|
||||
end_function = "<|END_OF_FUNCTION_RESULT|>"
|
||||
end_function_call = "<|END_OF_FUNCTION_CALL|>"
|
||||
version = "v1"
|
||||
# This token splits between function name and parameters
|
||||
fn_param_sep_token = ":\n{"
|
||||
|
||||
def get_end_token_from_message(self, message: Dict) -> str:
|
||||
"""this function is used for getting the end token for each message.
|
||||
For example, if message["role"] == "user" --> return EndToken.user
|
||||
if message["role"] == "assistant" and "function_call" in message --> EndTOken.function_call
|
||||
|
||||
Args:
|
||||
message (Dict): A dictionary containing: role, content, function_call(optional)
|
||||
|
||||
Returns:
|
||||
EndToken: End Token for this message, this will be appended to the end of the prompt for this message
|
||||
"""
|
||||
role = message["role"]
|
||||
if role == "user":
|
||||
return self.end_user
|
||||
elif role == "system":
|
||||
return self.end_system
|
||||
elif role == "function":
|
||||
return self.end_function
|
||||
else: # role = assistant
|
||||
if message.get("function_call", None) is not None:
|
||||
# if "function_call" in message and message["function_call"] is not None:
|
||||
return self.end_function_call
|
||||
else:
|
||||
return self.end_assistant
|
||||
|
||||
def get_start_of_function_call_token(self) -> str:
|
||||
return self.start_function
|
||||
|
||||
def get_stop_token_for_function_parameter(
|
||||
self, stage: Literal["function", "parameter"]
|
||||
) -> int:
|
||||
if stage == "function":
|
||||
return ":" # 28747
|
||||
else:
|
||||
return '":' # 1264
|
||||
|
||||
def initialize_grammar_sampling_gen_state(self) -> Dict:
|
||||
return {
|
||||
"stage": "pre-function",
|
||||
"curr_tokens": [],
|
||||
"curr_text": "",
|
||||
"func_name": "",
|
||||
"param_names": [],
|
||||
}
|
||||
|
||||
def get_additional_tokens(self) -> List[str]:
|
||||
return [
|
||||
self.start_function,
|
||||
self.end_system,
|
||||
self.end_user,
|
||||
self.end_assistant,
|
||||
self.end_function,
|
||||
self.end_function_call,
|
||||
]
|
||||
|
||||
def convert_message_to_prompt(self, message: Dict) -> str:
|
||||
"""convert a message to a string to be included in the prompt
|
||||
Args:
|
||||
message (Dict): A dictionary in OpenAI format (containing: role, content, function_call (optional))
|
||||
|
||||
Returns:
|
||||
str: the string used in the final prompt of this message
|
||||
"""
|
||||
end_token = self.get_end_token_from_message(message)
|
||||
content = message.get("content", None)
|
||||
|
||||
if message["role"] == "system":
|
||||
text = f"system:\n{content}{end_token}\n"
|
||||
|
||||
elif message["role"] in ["function", "tool"]:
|
||||
func_name = message.get("name", "")
|
||||
text = f"function name={func_name}:\n{content}{end_token}\n"
|
||||
|
||||
elif message["role"] == "user" and content is None:
|
||||
text = "user:\n"
|
||||
|
||||
elif message["role"] == "user":
|
||||
text = f"user:\n{content}{end_token}\n"
|
||||
|
||||
elif message["role"] == "assistant":
|
||||
if (
|
||||
message.get("function_call", None) is not None
|
||||
): # format of openai: {"role": assistant, "function_call": {"name": xxx, "arguments": xxx}}
|
||||
function = message["function_call"]["name"]
|
||||
arguments = message["function_call"]["arguments"] + end_token
|
||||
if content is not None:
|
||||
text = f"assistant:\n{content}\n{self.start_function}{function}:\n{arguments}\n"
|
||||
else:
|
||||
text = (
|
||||
f"assistant:\n{self.start_function}{function}:\n{arguments}\n"
|
||||
)
|
||||
elif content is not None: # this is text content
|
||||
text = f"assistant:\n{content}{end_token}\n"
|
||||
else: # if no function call and content is None --> this is used at inference
|
||||
text = "assistant:"
|
||||
|
||||
return text
|
||||
|
||||
def get_stop_tokens_for_generation(self) -> List[str]:
|
||||
return [self.end_assistant, self.end_function_call]
|
||||
|
||||
def get_assistant_prefixes(self) -> List[str]:
|
||||
result = []
|
||||
for item in [self.end_user, self.end_function]:
|
||||
prefix = f"{item}\nassistant:"
|
||||
result.append(prefix)
|
||||
return result
|
||||
|
||||
def parse_assistant_response(
|
||||
self, llm_output: str, tool_choice: Optional[Any] = None
|
||||
) -> Dict:
|
||||
generated_content = llm_output.strip()
|
||||
|
||||
for endtoken in self.get_stop_tokens_for_generation():
|
||||
if generated_content.endswith(endtoken):
|
||||
generated_content = generated_content[: -len(endtoken)].strip()
|
||||
|
||||
# First we need to check if llm_output contains start_token or not
|
||||
start_function_index = generated_content.find(self.start_function)
|
||||
text_content = generated_content
|
||||
result = {"role": "assistant", "content": None}
|
||||
|
||||
if start_function_index >= 0:
|
||||
func_info = generated_content[
|
||||
start_function_index + len(self.start_function) :
|
||||
].strip()
|
||||
index = func_info.find(":")
|
||||
func_name = func_info[:index].strip()
|
||||
arguments = func_info[index + 1 :].strip()
|
||||
|
||||
text_content = generated_content[:start_function_index].strip()
|
||||
result["function_call"] = {
|
||||
"name": func_name,
|
||||
"arguments": arguments,
|
||||
} # FunctionCall(name=func_name, arguments=arguments)
|
||||
if len(text_content) > 0:
|
||||
result["content"] = text_content
|
||||
return result
|
||||
|
||||
def update_response_state_from_delta_text(
|
||||
self,
|
||||
*,
|
||||
current_state: Dict[str, Any],
|
||||
delta_text: str,
|
||||
finish_reason: Optional[str],
|
||||
) -> Tuple[Dict[str, Any], Optional[Dict]]:
|
||||
if len(current_state) == 0:
|
||||
current_state = {
|
||||
"response_type": None, # the type of current response text (text_response)/function (function_call)
|
||||
"func_name": None, # if response_type=function, this is the function_name
|
||||
"current_text": "", # the concatenation of generated tokens so far
|
||||
}
|
||||
current_state["current_text"] += delta_text
|
||||
cur_text = current_state["current_text"]
|
||||
|
||||
response: Optional[Dict[str, Any]] = None
|
||||
if current_state["response_type"] is None:
|
||||
if cur_text.strip().startswith(self.start_function): # if function_call
|
||||
if cur_text.endswith(":"):
|
||||
f_index = cur_text.find(self.start_function)
|
||||
func_name = cur_text[
|
||||
f_index + len(self.start_function) : -1
|
||||
].strip()
|
||||
response = {
|
||||
"delta": {
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"function_call": {"arguments": "", "name": func_name},
|
||||
},
|
||||
"finish_reason": None,
|
||||
}
|
||||
current_state["response_type"] = "function"
|
||||
else: # if text_response
|
||||
current_state["response_type"] = "text"
|
||||
response = {
|
||||
"delta": {"content": "", "role": "assistant"},
|
||||
"finish_reason": None,
|
||||
"index": 0,
|
||||
}
|
||||
|
||||
elif current_state["response_type"] == "function":
|
||||
if finish_reason is None:
|
||||
response = {
|
||||
"delta": {
|
||||
"role": "assistant",
|
||||
"function_call": {"arguments": delta_text},
|
||||
}, # format of openAI at the second return, don't need to add function_name
|
||||
"finish_reason": None,
|
||||
"index": 0,
|
||||
}
|
||||
else:
|
||||
response = {
|
||||
"delta": {},
|
||||
"finish_reason": "function_call",
|
||||
"index": 0,
|
||||
} # format of openAI at the end, delta must be empty
|
||||
|
||||
elif current_state["response_type"] == "text":
|
||||
if finish_reason is None:
|
||||
# need to check if call a function or not
|
||||
if cur_text.endswith(self.start_function): # if call another function
|
||||
print("call another function in the mean time")
|
||||
cur_text = self.start_function
|
||||
current_state["current_text"] = self.start_function
|
||||
current_state["response_type"] = None
|
||||
else:
|
||||
response = {
|
||||
"delta": {"content": delta_text, "role": "assistant"},
|
||||
"finish_reason": None,
|
||||
"index": 0,
|
||||
}
|
||||
else: # finish generating
|
||||
response = {
|
||||
"delta": {},
|
||||
"finish_reason": finish_reason,
|
||||
"index": 0,
|
||||
} # format of openAI at the end, delta must be empty
|
||||
return current_state, response
|
||||
|
||||
def get_chat_template_jinja(self) -> str:
|
||||
chat_template = """{% for message in messages %}
|
||||
{% if message['role'] == 'user' %}
|
||||
{{ message['role'] + ':\n' + message['content'] + '<|END_OF_USER|>' + '\n' }}<br>
|
||||
{% elif message['role'] == 'system' %}
|
||||
{{ message['role'] + ':\n' + message['content'] + '<|END_OF_SYSTEM|>' + '\n' }}<br>
|
||||
{% elif message['role'] == 'function' %}
|
||||
{{ 'function name=' + message['name'] + ':\n' + message['content']+ '<|END_OF_FUNCTION_RESULT|>\n' }}<br>
|
||||
{% elif message['role'] == 'assistant' %}
|
||||
{% if 'function_call' in message and message['function_call'] is not none %}
|
||||
{% if message['content'] is not none %}
|
||||
{{ 'assistant:\n' + message['content'] + '\n<|START_OF_FUNCTION_CALL|>' + message['function_call']['name'] + ':\n' + message['function_call']['arguments'] + '<|END_OF_FUNCTION_CALL|>\n' }}<br>
|
||||
{% else %}
|
||||
{{ 'assistant:\n<|START_OF_FUNCTION_CALL|>' + message['function_call']['name'] + ':\n' + message['function_call']['arguments'] + '<|END_OF_FUNCTION_CALL|>\n' }}<br>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ 'assistant:\n' + message['content'] + '<|END_OF_ASSISTANT|>' + '\n' }}<br>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if add_generation_prompt %}{{ 'assistant:' }}{% endif %}
|
||||
"""
|
||||
chat_template = chat_template.replace(" ", "")
|
||||
chat_template = chat_template.replace("<br>\n", "")
|
||||
chat_template = chat_template.strip()
|
||||
return chat_template
|
||||
@@ -0,0 +1,413 @@
|
||||
import json
|
||||
import random
|
||||
import string
|
||||
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
|
||||
|
||||
from r2ai.functionary.openai_types import Tool
|
||||
from r2ai.functionary.prompt_template.base_template import (
|
||||
PredefinedFuncTypes,
|
||||
PromptTemplate,
|
||||
)
|
||||
|
||||
|
||||
class PromptTemplateV2(PromptTemplate):
|
||||
from_token = "<|from|>"
|
||||
recipient_token = "<|recipient|>"
|
||||
content_token = "<|content|>"
|
||||
stop_token = "<|stop|>"
|
||||
version = "v2"
|
||||
# This token splits between function name and parameters
|
||||
fn_param_sep_token = "\n<|content|> {"
|
||||
# This maps the predefined function type to its str name
|
||||
predefined_func_names = {
|
||||
PredefinedFuncTypes.no_tool_call: "all",
|
||||
PredefinedFuncTypes.code_interpreter: "python",
|
||||
}
|
||||
|
||||
def get_start_of_function_call_token(self) -> str:
|
||||
return self.recipient_token
|
||||
|
||||
def get_stop_token_for_function_parameter(
|
||||
self, stage: Literal["function", "parameter"]
|
||||
) -> int:
|
||||
if stage == "function":
|
||||
return "\n" # 13
|
||||
else:
|
||||
return '":' # 1264
|
||||
|
||||
def get_predefined_function_names(self, function_types: Any) -> List[str]:
|
||||
if function_types == "all":
|
||||
return [func_name for func_name in self.predefined_func_names.values()]
|
||||
|
||||
if not isinstance(function_types, list):
|
||||
function_types = [function_types]
|
||||
|
||||
predefined_function_names = []
|
||||
for function_type in function_types:
|
||||
predefined_function_names.append(self.predefined_func_names[function_type])
|
||||
|
||||
return predefined_function_names
|
||||
|
||||
def initialize_grammar_sampling_gen_state(
|
||||
self, tool_choice: str, curr_text: str, curr_tokens: List[int]
|
||||
) -> Dict:
|
||||
if tool_choice != "":
|
||||
add_predefined_fns = False
|
||||
stage = "pre-parameter"
|
||||
else:
|
||||
add_predefined_fns = True
|
||||
stage = "function"
|
||||
|
||||
return {
|
||||
"stage": stage,
|
||||
"curr_tokens": curr_tokens,
|
||||
"curr_text": curr_text,
|
||||
"func_name": tool_choice,
|
||||
"param_names": [],
|
||||
"add_predefined_fns": add_predefined_fns,
|
||||
}
|
||||
|
||||
def get_additional_tokens(self) -> List[str]:
|
||||
return [
|
||||
self.from_token,
|
||||
self.recipient_token,
|
||||
self.content_token,
|
||||
self.stop_token,
|
||||
]
|
||||
|
||||
def convert_message_to_prompt(self, message: Dict) -> str:
|
||||
role = message["role"]
|
||||
content = message.get("content", None)
|
||||
|
||||
if role in [
|
||||
"system",
|
||||
"user",
|
||||
]: # <|from|>system\n<|recipient|>all\n<|content|>xxx
|
||||
return f"{self.from_token}{role}\n{self.recipient_token}all\n{self.content_token}{content}\n"
|
||||
|
||||
if role == "tool": # <|from|>tool_name\n<|recipient|>all\n<|content|>xxx
|
||||
tool_name = message["name"]
|
||||
return f"{self.from_token}{tool_name}\n{self.recipient_token}all\n{self.content_token}{content}\n"
|
||||
|
||||
assert role == "assistant"
|
||||
tool_calls = message.get("tool_calls", [])
|
||||
if tool_calls is None:
|
||||
tool_calls = []
|
||||
if (
|
||||
len(tool_calls) == 0 and content is None
|
||||
): # for inference: <|from|> assistant\n<|recipient|>
|
||||
return f"{self.from_token}{role}\n{self.recipient_token}"
|
||||
|
||||
if len(tool_calls) == 0: # <|from|>assistant\n<|recipient|>all\n<|content|>xxx
|
||||
return f"{self.from_token}{role}\n{self.recipient_token}all\n{self.content_token}{content}{self.stop_token}\n"
|
||||
|
||||
result = ""
|
||||
if content is not None: # both text-response and function_call
|
||||
result += f"{self.from_token}{role}\n{self.recipient_token}all\n{self.content_token}{content}\n"
|
||||
|
||||
for tool in tool_calls:
|
||||
func_name = tool["function"]["name"]
|
||||
arguments = tool["function"]["arguments"]
|
||||
# <|from|>assistant\n<|recipient|>func_name\n<|content|>xxxx
|
||||
result += f"{self.from_token}{role}\n{self.recipient_token}{func_name}\n{self.content_token}{arguments}\n"
|
||||
|
||||
result = result.strip() + f"{self.stop_token}\n"
|
||||
return result
|
||||
|
||||
def get_stop_tokens_for_generation(self) -> List[str]:
|
||||
return [self.stop_token]
|
||||
|
||||
def get_assistant_prefixes(self) -> List[str]:
|
||||
return [f"{self.from_token}assistant\n{self.recipient_token}"]
|
||||
|
||||
def parse_assistant_response(
|
||||
self, llm_output: str, tool_choice: Optional[Any] = None
|
||||
) -> Dict:
|
||||
for stop in self.get_stop_tokens_for_generation():
|
||||
if llm_output.endswith(stop):
|
||||
llm_output = llm_output[: -len(stop)]
|
||||
|
||||
recipient_to_fill = ""
|
||||
if tool_choice is not None:
|
||||
if tool_choice == "none":
|
||||
recipient_to_fill = self.get_predefined_function_names(
|
||||
function_types=PredefinedFuncTypes.no_tool_call
|
||||
)[0] + self.get_stop_token_for_function_parameter(stage="function")
|
||||
elif isinstance(tool_choice, Tool):
|
||||
recipient_to_fill = (
|
||||
tool_choice.function.name
|
||||
+ self.get_stop_token_for_function_parameter(stage="function")
|
||||
)
|
||||
|
||||
llm_output = (
|
||||
f"{self.from_token}assistant\n{self.recipient_token}"
|
||||
+ recipient_to_fill
|
||||
+ llm_output
|
||||
)
|
||||
responses = llm_output.split(self.from_token)
|
||||
responses = [response.strip() for response in responses]
|
||||
|
||||
tool_calls = []
|
||||
text_response = None
|
||||
for response in responses:
|
||||
if len(response) == 0:
|
||||
continue
|
||||
# response = assistant<|recipient|>xxxx\n<|content|>yyy
|
||||
recipient_index = response.find(self.recipient_token)
|
||||
content_index = response.find(self.content_token)
|
||||
recipient = response[
|
||||
recipient_index + len(self.recipient_token) : content_index
|
||||
].strip()
|
||||
content = response[content_index + len(self.content_token) :].strip()
|
||||
# print(f"recipient: {recipient}, content={content}")
|
||||
if recipient == "all":
|
||||
text_response = content
|
||||
else:
|
||||
tool_calls.append(
|
||||
{
|
||||
"function": {"name": recipient, "arguments": content},
|
||||
"id": get_random_tool_call_id(),
|
||||
"type": "function",
|
||||
}
|
||||
)
|
||||
|
||||
return {"role": "assistant", "content": text_response, "tool_calls": tool_calls}
|
||||
|
||||
def pre_process_messages_before_inference(self, messages: List[Dict]) -> List[Dict]:
|
||||
"""re-order the messages where role = tool to match the order in tool_calls by tool_call_id
|
||||
Args:
|
||||
messages (List[Dict]): list of messages containing: tool_call_id
|
||||
|
||||
Returns:
|
||||
List[Dict]: _description_
|
||||
"""
|
||||
result = []
|
||||
index = 0
|
||||
while index < len(messages):
|
||||
message = messages[index]
|
||||
tool_calls = message.get("tool_calls", None)
|
||||
|
||||
result.append(message)
|
||||
if message["role"] == "assistant" and tool_calls:
|
||||
num_calls = len(tool_calls)
|
||||
if (
|
||||
tool_calls[0].get("id", None) is not None
|
||||
): # if tool_call contains "id" for mapping
|
||||
tool_call_ids = [item["id"] for item in tool_calls]
|
||||
|
||||
tool_messages = [messages[index + 1 + j] for j in range(num_calls)]
|
||||
id_2_tool_messages = {
|
||||
item["tool_call_id"]: item for item in tool_messages
|
||||
}
|
||||
new_messages = [id_2_tool_messages[cid] for cid in tool_call_ids]
|
||||
|
||||
result.extend(new_messages)
|
||||
index += num_calls + 1
|
||||
else:
|
||||
index += 1
|
||||
else:
|
||||
index += 1
|
||||
return result
|
||||
|
||||
def get_function_delta_response(
|
||||
self,
|
||||
current_state: Dict,
|
||||
delta_text: str,
|
||||
first_call: bool,
|
||||
return_role: bool,
|
||||
finish_reason: Optional[str],
|
||||
) -> Dict:
|
||||
"""Return delta for tool_call in streaming
|
||||
|
||||
Args:
|
||||
current_state (Dict): _description_
|
||||
delta_text (str): _description_
|
||||
first_call (bool): _description_
|
||||
return_role (bool): _description_
|
||||
finish_reason (Optional[str]): _description_
|
||||
|
||||
Returns:
|
||||
Dict: _description_
|
||||
"""
|
||||
return {
|
||||
"delta": {
|
||||
"content": None,
|
||||
"function_call": None,
|
||||
"role": None if not return_role else "assistant",
|
||||
"tool_calls": [
|
||||
{
|
||||
"index": current_state["func_index"],
|
||||
"id": current_state["call_id"]
|
||||
if first_call
|
||||
else None, # only return call_id at the first time
|
||||
"function": {
|
||||
"arguments": delta_text,
|
||||
"name": current_state["func_name"] if first_call else None,
|
||||
},
|
||||
"type": "function" if first_call else None,
|
||||
}
|
||||
],
|
||||
},
|
||||
"finish_reason": finish_reason,
|
||||
"index": 0,
|
||||
}
|
||||
|
||||
def get_text_delta_response(
|
||||
self, delta_text: Optional[str], return_role: bool, finish_reason: Optional[str]
|
||||
) -> Dict:
|
||||
"""Return delta for text_response in streaming
|
||||
|
||||
Args:
|
||||
delta_text (Optional[str]): _description_
|
||||
return_role (bool): _description_
|
||||
finish_reason (Optional[str]): _description_
|
||||
|
||||
Returns:
|
||||
Dict: _description_
|
||||
"""
|
||||
return {
|
||||
"delta": {
|
||||
"content": delta_text,
|
||||
"function_call": None,
|
||||
"role": None if not return_role else "assistant",
|
||||
"tool_calls": None,
|
||||
},
|
||||
"finish_reason": finish_reason,
|
||||
"index": 0,
|
||||
}
|
||||
|
||||
def get_recipient(self, current_text: str) -> str:
|
||||
"""Get recipient from the llm_output
|
||||
|
||||
Args:
|
||||
current_text (str): _description_
|
||||
|
||||
Returns:
|
||||
str: _description_
|
||||
"""
|
||||
recipient_index = current_text.find(self.recipient_token)
|
||||
start_index = 0
|
||||
if recipient_index >= 0:
|
||||
start_index = recipient_index + len(self.recipient_token)
|
||||
|
||||
end_index = current_text.find(f"\n{self.content_token}")
|
||||
return current_text[start_index:end_index].strip()
|
||||
|
||||
def get_chat_template_jinja(self) -> str:
|
||||
chat_template = """{% for message in messages %}
|
||||
{% if message['role'] == 'user' or message['role'] == 'system' %}
|
||||
{{ '<|from|>' + message['role'] + '\n<|recipient|>all\n<|content|>' + message['content'] + '\n' }}<br>
|
||||
{% elif message['role'] == 'tool' %}
|
||||
{{ '<|from|>' + message['name'] + '\n<|recipient|>all\n<|content|>' + message['content'] + '\n' }}<br>
|
||||
{% else %}
|
||||
{% set contain_content='no'%}
|
||||
{% if message['content'] is not none %}
|
||||
{{ '<|from|>assistant\n<|recipient|>all\n<|content|>' + message['content'] }}<br>
|
||||
{% set contain_content='yes'%}
|
||||
{% endif %}
|
||||
{% if 'tool_calls' in message and message['tool_calls'] is not none %}
|
||||
{% for tool_call in message['tool_calls'] %}
|
||||
{% set prompt='<|from|>assistant\n<|recipient|>' + tool_call['function']['name'] + '\n<|content|>' + tool_call['function']['arguments'] %}
|
||||
{% if loop.index == 1 and contain_content == "no" %}
|
||||
{{ prompt }}<br>
|
||||
{% else %}
|
||||
{{ '\n' + prompt}}<br>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{{ '<|stop|>\n' }}<br>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if add_generation_prompt %}{{ '<|from|>assistant\n<|recipient|>' }}{% endif %}
|
||||
"""
|
||||
chat_template = chat_template.replace(" ", "")
|
||||
chat_template = chat_template.replace("<br>\n", "")
|
||||
chat_template = chat_template.strip()
|
||||
return chat_template
|
||||
|
||||
def update_response_state_from_delta_text(
|
||||
self,
|
||||
*,
|
||||
current_state: Dict[str, Any],
|
||||
delta_text: str,
|
||||
finish_reason: Optional[str],
|
||||
) -> Tuple[Dict[str, Any], Union[None, Dict, List[Dict]]]:
|
||||
if len(current_state) == 0: # empty dict, at the first_time
|
||||
current_state = {
|
||||
"current_text": "", # the concatenation of all tokens so far
|
||||
"func_name": None, # function_name of the current tool, if the response requires to use tool
|
||||
"response_type": None, # response_type=text(text response)/function (using tool)
|
||||
"func_index": -1, # index of the tool in tool_calls
|
||||
"call_id": None, # call_id of the current tool
|
||||
# skip_until_reach we skip new tokens until we reach certain token. This is used when we hit special tokens
|
||||
"skip_until_reach": self.content_token, # at first we will skip until reach <|content|>
|
||||
"first_time": True, # if first_time we return an tempty delta with role=assistant
|
||||
}
|
||||
current_state["current_text"] += delta_text
|
||||
|
||||
if finish_reason is not None:
|
||||
if current_state["response_type"] == "function":
|
||||
finish_reason = "tool_calls"
|
||||
return current_state, self.get_text_delta_response(
|
||||
None, False, finish_reason
|
||||
)
|
||||
|
||||
skip_until_reach = current_state.get("skip_until_reach", "")
|
||||
if skip_until_reach:
|
||||
if delta_text != skip_until_reach:
|
||||
return current_state, None
|
||||
else:
|
||||
current_state["skip_until_reach"] = "" # once hit, no need to skip
|
||||
recipient = self.get_recipient(current_state["current_text"])
|
||||
first_time = current_state["first_time"]
|
||||
current_state["first_time"] = False
|
||||
|
||||
if recipient == "all":
|
||||
current_state["response_type"] = "text"
|
||||
return current_state, self.get_text_delta_response(
|
||||
"", True, finish_reason
|
||||
)
|
||||
else:
|
||||
current_state["response_type"] = "function"
|
||||
current_state["func_name"] = recipient
|
||||
current_state["call_id"] = get_random_tool_call_id()
|
||||
current_state["func_index"] += 1
|
||||
|
||||
responses = []
|
||||
if (
|
||||
first_time
|
||||
): # first chunk of function_call is a message where all fields are None, except role
|
||||
responses.append(
|
||||
self.get_text_delta_response(None, True, finish_reason)
|
||||
)
|
||||
responses.append(
|
||||
self.get_function_delta_response(
|
||||
current_state, "", True, False, finish_reason
|
||||
)
|
||||
)
|
||||
return current_state, responses
|
||||
else:
|
||||
assert current_state["response_type"] is not None
|
||||
if (
|
||||
delta_text == self.from_token
|
||||
): # skip until reach <content> to check type of response
|
||||
current_state["current_text"] = ""
|
||||
current_state["skip_until_reach"] = self.content_token
|
||||
current_state["response_type"] = None
|
||||
return current_state, None
|
||||
|
||||
else:
|
||||
if current_state["response_type"] == "function":
|
||||
return current_state, self.get_function_delta_response(
|
||||
current_state, delta_text, False, False, finish_reason
|
||||
)
|
||||
else: # response_type=text
|
||||
return current_state, self.get_text_delta_response(
|
||||
delta_text, True, finish_reason
|
||||
)
|
||||
|
||||
|
||||
def get_random_tool_call_id():
|
||||
return "call_" + "".join(
|
||||
[random.choice(string.ascii_letters + string.digits) for _ in range(24)]
|
||||
)
|
||||
@@ -0,0 +1,459 @@
|
||||
import pdb
|
||||
from copy import deepcopy
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import jsonref
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
from r2ai.functionary.openai_types import Function
|
||||
|
||||
|
||||
def convert_data_type(param_type: str) -> str:
|
||||
"""convert data_type to typescript data type
|
||||
|
||||
Args:
|
||||
param_type (str): param_type
|
||||
|
||||
Returns:
|
||||
str: param type in typescript
|
||||
"""
|
||||
if param_type == "integer" or param_type == "float":
|
||||
return "number"
|
||||
return param_type
|
||||
|
||||
|
||||
def get_param_type(param: Dict) -> str:
|
||||
"""get param_type of parameter
|
||||
|
||||
Args:
|
||||
param (Dict): param dict in properties
|
||||
|
||||
Returns:
|
||||
str: _description_
|
||||
"""
|
||||
param_type = "any"
|
||||
if "type" in param:
|
||||
raw_param_type = param["type"]
|
||||
if type(raw_param_type) is list:
|
||||
param_type = " | ".join(raw_param_type)
|
||||
else:
|
||||
param_type = raw_param_type
|
||||
|
||||
else: # in many cases, the json schema contains: oneOf instead of "type"
|
||||
if "oneOf" in param:
|
||||
one_of_types = []
|
||||
for item in param["oneOf"]:
|
||||
if "type" in item:
|
||||
one_of_types.append(convert_data_type(item["type"]))
|
||||
one_of_types = list(set(one_of_types))
|
||||
param_type = " | ".join(one_of_types)
|
||||
return convert_data_type(param_type)
|
||||
|
||||
|
||||
def get_format_param(param: Dict) -> Optional[str]:
|
||||
"""Get "format" from param. There are cases where format is not directly in param but in oneOf
|
||||
|
||||
Args:
|
||||
param (Dict): _description_
|
||||
|
||||
Returns:
|
||||
Optional[str]: _description_
|
||||
"""
|
||||
if "format" in param:
|
||||
return param["format"]
|
||||
if "oneOf" in param:
|
||||
formats = []
|
||||
for item in param["oneOf"]:
|
||||
if "format" in item:
|
||||
formats.append(item["format"])
|
||||
if len(formats) > 0:
|
||||
return " or ".join(formats)
|
||||
return None
|
||||
|
||||
|
||||
def get_param_info(param: Dict) -> Optional[str]:
|
||||
"""get additional information about parameter such as: format, default value, min, max, ...
|
||||
|
||||
Args:
|
||||
param (Dict): _description_
|
||||
|
||||
Returns:
|
||||
Optional[str]: _description_
|
||||
"""
|
||||
param_type = param.get("type", "any")
|
||||
info_list = []
|
||||
if "description" in param:
|
||||
desc = param["description"]
|
||||
if not desc.endswith("."):
|
||||
desc += "."
|
||||
info_list.append(desc)
|
||||
|
||||
if "default" in param:
|
||||
default_value = param["default"]
|
||||
if param_type == "string":
|
||||
default_value = f'"{default_value}"' # if string --> add ""
|
||||
info_list.append(f"Default={default_value}.")
|
||||
|
||||
format_param = get_format_param(param)
|
||||
if format_param is not None:
|
||||
info_list.append("Format=" + format_param)
|
||||
|
||||
for field, field_name in [
|
||||
("maximum", "Maximum"),
|
||||
("minimum", "Minimum"),
|
||||
("maxLength", "Maximum length"),
|
||||
("minLength", "Minimum length"),
|
||||
]:
|
||||
if field in param:
|
||||
info_list.append(f"{field_name}=" + str(param[field]))
|
||||
|
||||
if len(info_list) > 0:
|
||||
result = "// " + " ".join(info_list)
|
||||
result = result.replace("\n", " ")
|
||||
return result
|
||||
return None
|
||||
|
||||
|
||||
def append_new_param_info(
|
||||
info_list: List[str],
|
||||
param_declaration: str,
|
||||
comment_info: Optional[str],
|
||||
depth: int,
|
||||
):
|
||||
"""Append a new parameter with comment to the info_list
|
||||
|
||||
Args:
|
||||
info_lines (List[str]): current info_list
|
||||
param_declaration (str): param: type
|
||||
comment_info (Optional[str]): information of comment
|
||||
depth (int): level of nested param
|
||||
"""
|
||||
offset = ""
|
||||
if depth >= 1:
|
||||
offset = "".join([" " for _ in range(depth)])
|
||||
if comment_info is not None:
|
||||
# if depth == 0: # format: //comment\nparam: type
|
||||
info_list.append(f"{offset}{comment_info}")
|
||||
info_list.append(f"{offset}{param_declaration}")
|
||||
# else: # format: param: type // comment
|
||||
# info_list.append(f"{offset}{param_declaration} {comment_info}")
|
||||
else:
|
||||
info_list.append(f"{offset}{param_declaration}")
|
||||
|
||||
|
||||
def get_enum_option_str(enum_options: List) -> str:
|
||||
"""get enum option separated by: "|"
|
||||
|
||||
Args:
|
||||
enum_options (List): list of options
|
||||
|
||||
Returns:
|
||||
_type_: concatenation of options separated by "|"
|
||||
"""
|
||||
# if each option is string --> add quote
|
||||
return " | ".join([f'"{v}"' if type(v) is str else str(v) for v in enum_options])
|
||||
|
||||
|
||||
def get_array_typescript(
|
||||
param_name: Optional[str], param_dic: dict, depth: int = 0
|
||||
) -> str:
|
||||
"""recursive implementation for generating type script of array
|
||||
|
||||
Args:
|
||||
param_name (Optional[str]): name of param, optional
|
||||
param_dic (dict): param_dic
|
||||
depth (int, optional): nested level. Defaults to 0.
|
||||
|
||||
Returns:
|
||||
_type_: typescript of array
|
||||
"""
|
||||
offset = ""
|
||||
if depth >= 1:
|
||||
offset = "".join([" " for _ in range(depth)])
|
||||
items_info = param_dic.get("items", {})
|
||||
|
||||
if len(items_info) == 0:
|
||||
if param_name is not None:
|
||||
return f"{offset}{param_name}: []"
|
||||
else:
|
||||
return "[]"
|
||||
array_type = get_param_type(items_info)
|
||||
if array_type == "object":
|
||||
info_lines = []
|
||||
child_lines = get_parameter_typescript(
|
||||
items_info.get("properties", {}), items_info.get("required", []), depth + 1
|
||||
)
|
||||
# if comment_info is not None:
|
||||
# info_lines.append(f"{offset}{comment_info}")
|
||||
if param_name is not None:
|
||||
info_lines.append(f"{offset}{param_name}" + ": {")
|
||||
else:
|
||||
info_lines.append(f"{offset}" + "{")
|
||||
info_lines.extend(child_lines)
|
||||
info_lines.append(f"{offset}" + "}[]")
|
||||
return "\n".join(info_lines)
|
||||
|
||||
elif array_type == "array":
|
||||
item_info = get_array_typescript(None, items_info, depth + 1)
|
||||
if param_name is None:
|
||||
return f"{item_info}[]"
|
||||
return f"{offset}{param_name}: {item_info.strip()}[]"
|
||||
|
||||
else:
|
||||
if "enum" in items_info:
|
||||
item_type = get_enum_option_str(items_info["enum"])
|
||||
if param_name is None:
|
||||
return f"({item_type})[]"
|
||||
else:
|
||||
return f"{offset}{param_name}: ({item_type})[]"
|
||||
else:
|
||||
if param_name is None:
|
||||
return f"{array_type}[]"
|
||||
else:
|
||||
return f"{offset}{param_name}: {array_type}[],"
|
||||
|
||||
|
||||
def get_parameter_typescript(properties, required_params, depth=0) -> List[str]:
|
||||
"""Recursion, returning the information about parameters including data type, description and other information
|
||||
These kinds of information will be put into the prompt
|
||||
|
||||
Args:
|
||||
properties (_type_): properties in parameters
|
||||
required_params (_type_): List of required parameters
|
||||
depth (int, optional): the depth of params (nested level). Defaults to 0.
|
||||
|
||||
Returns:
|
||||
_type_: list of lines containing information about all parameters
|
||||
"""
|
||||
tp_lines = []
|
||||
for param_name, param in properties.items():
|
||||
# Sometimes properties have "required" field as a list of string.
|
||||
# Even though its supposed to be not under properties. So we skip it
|
||||
if not isinstance(param, dict):
|
||||
continue
|
||||
# Param Description
|
||||
comment_info = get_param_info(param)
|
||||
# Param Name declaration
|
||||
param_declaration = f"{param_name}"
|
||||
if isinstance(required_params, list):
|
||||
if param_name not in required_params:
|
||||
param_declaration += "?"
|
||||
param_type = get_param_type(param)
|
||||
|
||||
offset = ""
|
||||
if depth >= 1:
|
||||
offset = "".join([" " for _ in range(depth)])
|
||||
|
||||
if param_type == "object": # param_type is object
|
||||
child_lines = get_parameter_typescript(
|
||||
param.get("properties", {}), param.get("required", []), depth + 1
|
||||
)
|
||||
if comment_info is not None:
|
||||
tp_lines.append(f"{offset}{comment_info}")
|
||||
|
||||
param_declaration += ": {"
|
||||
tp_lines.append(f"{offset}{param_declaration}")
|
||||
tp_lines.extend(child_lines)
|
||||
tp_lines.append(f"{offset}" + "},")
|
||||
|
||||
elif param_type == "array": # param_type is an array
|
||||
item_info = param.get("items", {})
|
||||
if "type" not in item_info: # don't know type of array
|
||||
param_declaration += ": [],"
|
||||
append_new_param_info(tp_lines, param_declaration, comment_info, depth)
|
||||
else:
|
||||
array_declaration = get_array_typescript(
|
||||
param_declaration, param, depth
|
||||
)
|
||||
if not array_declaration.endswith(","):
|
||||
array_declaration += ","
|
||||
if comment_info is not None:
|
||||
tp_lines.append(f"{offset}{comment_info}")
|
||||
tp_lines.append(array_declaration)
|
||||
else:
|
||||
if "enum" in param:
|
||||
param_type = get_enum_option_str(param["enum"])
|
||||
# param_type = " | ".join([f'"{v}"' for v in param["enum"]])
|
||||
param_declaration += f": {param_type},"
|
||||
append_new_param_info(tp_lines, param_declaration, comment_info, depth)
|
||||
|
||||
return tp_lines
|
||||
|
||||
|
||||
def generate_schema_from_functions(
|
||||
functions: List[Function], namespace="functions"
|
||||
) -> str:
|
||||
"""
|
||||
Convert functions schema to a schema that language models can understand.
|
||||
"""
|
||||
|
||||
schema = "// Supported function definitions that should be called when necessary.\n"
|
||||
schema += f"namespace {namespace} {{\n\n"
|
||||
|
||||
for function in functions:
|
||||
# Convert a Function object to dict, if necessary
|
||||
if not isinstance(function, dict):
|
||||
function = function.model_dump()
|
||||
function_name = function.get("name", None)
|
||||
if function_name is None:
|
||||
continue
|
||||
|
||||
description = function.get("description", "")
|
||||
schema += f"// {description}\n"
|
||||
schema += f"type {function_name}"
|
||||
|
||||
parameters = function.get("parameters", None)
|
||||
if parameters is not None and parameters.get("properties") is not None:
|
||||
parameters = deepcopy(jsonref.JsonRef.replace_refs(parameters))
|
||||
schema += " = (_: {\n"
|
||||
required_params = parameters.get("required", [])
|
||||
tp_lines = get_parameter_typescript(
|
||||
parameters.get("properties"), required_params, 0
|
||||
)
|
||||
schema += "\n".join(tp_lines)
|
||||
schema += "\n}) => any;\n\n"
|
||||
else:
|
||||
# Doesn't have any parameters
|
||||
schema += " = () => any;\n\n"
|
||||
|
||||
schema += f"}} // namespace {namespace}"
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
def generate_schema_from_openapi(
|
||||
specification: Dict[str, Any], description: str, namespace: str
|
||||
) -> str:
|
||||
"""
|
||||
Convert OpenAPI specification object to a schema that language models can understand.
|
||||
|
||||
Input:
|
||||
specification: can be obtained by json.loads of any OpanAPI json spec, or yaml.safe_load for yaml OpenAPI specs
|
||||
|
||||
Example output:
|
||||
|
||||
// General Description
|
||||
namespace functions {
|
||||
|
||||
// Simple GET endpoint
|
||||
type getEndpoint = (_: {
|
||||
// This is a string parameter
|
||||
param_string: string,
|
||||
param_integer: number,
|
||||
param_boolean?: boolean,
|
||||
param_enum: "value1" | "value2" | "value3",
|
||||
}) => any;
|
||||
|
||||
} // namespace functions
|
||||
"""
|
||||
|
||||
description_clean = description.replace("\n", "")
|
||||
|
||||
schema = f"// {description_clean}\n"
|
||||
schema += f"namespace {namespace} {{\n\n"
|
||||
|
||||
for path_name, paths in specification.get("paths", {}).items():
|
||||
for method_name, method_info in paths.items():
|
||||
operationId = method_info.get("operationId", None)
|
||||
if operationId is None:
|
||||
continue
|
||||
description = method_info.get("description", method_info.get("summary", ""))
|
||||
schema += f"// {description}\n"
|
||||
schema += f"type {operationId}"
|
||||
|
||||
if ("requestBody" in method_info) or (
|
||||
method_info.get("parameters") is not None
|
||||
):
|
||||
schema += f" = (_: {{\n"
|
||||
# Body
|
||||
if "requestBody" in method_info:
|
||||
try:
|
||||
body_schema = (
|
||||
method_info.get("requestBody", {})
|
||||
.get("content", {})
|
||||
.get("application/json", {})
|
||||
.get("schema", {})
|
||||
)
|
||||
except AttributeError:
|
||||
body_schema = {}
|
||||
for param_name, param in body_schema.get("properties", {}).items():
|
||||
# Param Description
|
||||
description = param.get("description")
|
||||
if description is not None:
|
||||
schema += f"// {description}\n"
|
||||
|
||||
# Param Name
|
||||
schema += f"{param_name}"
|
||||
if (
|
||||
(not param.get("required", False))
|
||||
or (param.get("nullable", False))
|
||||
or (param_name in body_schema.get("required", []))
|
||||
):
|
||||
schema += "?"
|
||||
|
||||
# Param Type
|
||||
param_type = param.get("type", "any")
|
||||
if param_type == "integer":
|
||||
param_type = "number"
|
||||
if "enum" in param:
|
||||
param_type = " | ".join([f'"{v}"' for v in param["enum"]])
|
||||
schema += f": {param_type},\n"
|
||||
|
||||
# URL
|
||||
for param in method_info.get("parameters", []):
|
||||
# Param Description
|
||||
if description := param.get("description"):
|
||||
schema += f"// {description}\n"
|
||||
|
||||
# Param Name
|
||||
schema += f"{param['name']}"
|
||||
if (not param.get("required", False)) or (
|
||||
param.get("nullable", False)
|
||||
):
|
||||
schema += "?"
|
||||
if param.get("schema") is None:
|
||||
continue
|
||||
# Param Type
|
||||
param_type = param["schema"].get("type", "any")
|
||||
if param_type == "integer":
|
||||
param_type = "number"
|
||||
if "enum" in param["schema"]:
|
||||
param_type = " | ".join(
|
||||
[f'"{v}"' for v in param["schema"]["enum"]]
|
||||
)
|
||||
schema += f": {param_type},\n"
|
||||
|
||||
schema += f"}}) => any;\n\n"
|
||||
else:
|
||||
# Doesn't have any parameters
|
||||
schema += f" = () => any;\n\n"
|
||||
|
||||
schema += f"}} // namespace {namespace}"
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
def generate_specification_from_openapi_url(
|
||||
openapi_url: str, proxies: dict = None
|
||||
) -> str:
|
||||
# Make Request
|
||||
headers = {"Accept": "application/x-yaml, text/yaml, text/x-yaml, application/json"}
|
||||
response = requests.get(
|
||||
openapi_url, verify=False, headers=headers, timeout=60, proxies=proxies
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
# Trust content-type first
|
||||
if response.headers.get("Content-Type") is not None:
|
||||
if "application/json" in response.headers.get("Content-Type"):
|
||||
specification = response.json()
|
||||
else:
|
||||
specification = yaml.safe_load(response.text)
|
||||
elif response.url.endswith(".json"):
|
||||
specification = response.json()
|
||||
else:
|
||||
specification = yaml.safe_load(response.text)
|
||||
# Resolve references
|
||||
specification = deepcopy(jsonref.JsonRef.replace_refs(specification))
|
||||
return specification
|
||||