mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-08-14 08:52:06 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9725fbb6a | ||
|
|
23f04264f9 | ||
|
|
8b59eb87e0 | ||
|
|
2e68e227b7 | ||
|
|
fc98614437 | ||
|
|
b1c5aba6fd | ||
|
|
6240c59ca3 | ||
|
|
9f3c7fd086 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "Git Clones",
|
||||
"message": "148,419",
|
||||
"message": "159,322",
|
||||
"color": "green",
|
||||
"namedLogo": "git"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"total_clones": 148419,
|
||||
"last_updated": "2026-07-11T07:51:15Z",
|
||||
"total_clones": 159322,
|
||||
"last_updated": "2026-07-16T08:08:14Z",
|
||||
"daily": {
|
||||
"2026-03-27": 2189,
|
||||
"2026-03-28": 1874,
|
||||
@@ -107,6 +107,11 @@
|
||||
"2026-07-07": 905,
|
||||
"2026-07-08": 1171,
|
||||
"2026-07-09": 1857,
|
||||
"2026-07-10": 1181
|
||||
"2026-07-10": 1181,
|
||||
"2026-07-11": 2185,
|
||||
"2026-07-12": 1917,
|
||||
"2026-07-13": 2102,
|
||||
"2026-07-14": 2337,
|
||||
"2026-07-15": 2362
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -55,5 +55,5 @@ See how the OpenJarvis community saves money, energy, and compute by running AI
|
||||
<div id="leaderboard-pagination" class="lb-pagination"></div>
|
||||
|
||||
<p style="font-size:12px;opacity:0.6;margin-top:12px">
|
||||
*Dollar savings estimated vs. Claude Opus 4.6 API pricing ($5/1M input, $25/1M output tokens). Assumes local open-source models produce roughly the same number of tokens per request as cloud models.
|
||||
*Dollar savings estimated vs. Claude Fable 5 API pricing ($10/1M input, $50/1M output tokens). Assumes local open-source models produce roughly the same number of tokens per request as cloud models.
|
||||
</p>
|
||||
|
||||
@@ -29,8 +29,8 @@ interface TelemetryStats {
|
||||
}
|
||||
|
||||
const CLOUD_PRICING = [
|
||||
{ name: 'GPT-5.3', input: 2.00, output: 10.00, primary: true },
|
||||
{ name: 'Claude Opus 4.6', input: 5.00, output: 25.00, primary: false },
|
||||
{ name: 'GPT-5.6 Sol', input: 5.00, output: 30.00, primary: true },
|
||||
{ name: 'Claude Fable 5', input: 10.00, output: 50.00, primary: false },
|
||||
{ name: 'Gemini 3.1 Pro', input: 2.00, output: 12.00, primary: false },
|
||||
];
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { DollarSign, TrendingDown, Cloud, HardDrive } from 'lucide-react';
|
||||
import { useAppStore } from '../../lib/store';
|
||||
|
||||
const CLOUD_PRICING = [
|
||||
{ name: 'GPT-5.3', input: 2.00, output: 10.00 },
|
||||
{ name: 'Claude Opus 4.6', input: 5.00, output: 25.00 },
|
||||
{ name: 'GPT-5.6 Sol', input: 5.00, output: 30.00 },
|
||||
{ name: 'Claude Fable 5', input: 10.00, output: 50.00 },
|
||||
{ name: 'Gemini 3.1 Pro', input: 2.00, output: 12.00 },
|
||||
];
|
||||
|
||||
|
||||
@@ -222,8 +222,8 @@ const styles: Record<string, React.CSSProperties> = {
|
||||
};
|
||||
|
||||
const PROVIDER_COLORS: Record<string, string> = {
|
||||
'gpt-5.3': colors.green,
|
||||
'claude-opus-4.6': colors.yellow,
|
||||
'gpt-5.6-sol': colors.green,
|
||||
'claude-fable-5': colors.yellow,
|
||||
'gemini-3.1-pro': colors.accent,
|
||||
};
|
||||
|
||||
|
||||
@@ -3740,8 +3740,8 @@ export function AgentsPage() {
|
||||
const paramsB = paramMatch ? parseFloat(paramMatch[1]) : 9;
|
||||
const flops = 2 * paramsB * 1e9 * (inTok + outTok);
|
||||
const providers = [
|
||||
{ label: 'GPT-5.3', inPer1M: 2.0, outPer1M: 10.0 },
|
||||
{ label: 'Claude Opus 4.6', inPer1M: 5.0, outPer1M: 25.0 },
|
||||
{ label: 'GPT-5.6 Sol', inPer1M: 5.0, outPer1M: 30.0 },
|
||||
{ label: 'Claude Fable 5', inPer1M: 10.0, outPer1M: 50.0 },
|
||||
{ label: 'Gemini 3.1 Pro', inPer1M: 2.0, outPer1M: 12.0 },
|
||||
];
|
||||
const energyWh = (inTok + outTok) / 1000 * 0.4;
|
||||
|
||||
@@ -215,8 +215,8 @@ COMPARISON_HTML = """\
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>OpenJarvis (Local)</th>
|
||||
<th>GPT-5.3</th>
|
||||
<th>Claude Opus 4.6</th>
|
||||
<th>GPT-5.6 Sol</th>
|
||||
<th>Claude Fable 5</th>
|
||||
<th>Gemini 3.1 Pro</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -261,11 +261,11 @@ COMPARISON_HTML = """\
|
||||
<div class="cc-value">$0.00/mo</div>
|
||||
</div>
|
||||
<div class="calc-card cloud">
|
||||
<div class="cc-label">GPT-5.3</div>
|
||||
<div class="cc-label">GPT-5.6 Sol</div>
|
||||
<div class="cc-value" id="calc-gpt">--</div>
|
||||
</div>
|
||||
<div class="calc-card cloud">
|
||||
<div class="cc-label">Claude Opus 4.6</div>
|
||||
<div class="cc-label">Claude Fable 5</div>
|
||||
<div class="cc-value" id="calc-claude">--</div>
|
||||
</div>
|
||||
<div class="calc-card cloud">
|
||||
@@ -292,13 +292,13 @@ COMPARISON_HTML = """\
|
||||
<script>
|
||||
// Embedded data -- avoids API calls, keeps the page static and fast.
|
||||
const CLOUD_PRICING = {
|
||||
"gpt-5.3": {
|
||||
input_per_1m: 2.00, output_per_1m: 10.00,
|
||||
label: "GPT-5.3"
|
||||
"gpt-5.6-sol": {
|
||||
input_per_1m: 5.00, output_per_1m: 30.00,
|
||||
label: "GPT-5.6 Sol"
|
||||
},
|
||||
"claude-opus-4.6": {
|
||||
input_per_1m: 5.00, output_per_1m: 25.00,
|
||||
label: "Claude Opus 4.6"
|
||||
"claude-fable-5": {
|
||||
input_per_1m: 10.00, output_per_1m: 50.00,
|
||||
label: "Claude Fable 5"
|
||||
},
|
||||
"gemini-3.1-pro": {
|
||||
input_per_1m: 2.00, output_per_1m: 12.00,
|
||||
@@ -376,8 +376,8 @@ function updateTable() {
|
||||
const sc = SCENARIOS[activeScenario];
|
||||
const i = sc.avg_input_tokens, o = sc.avg_output_tokens;
|
||||
const c = sc.calls_per_month;
|
||||
const gpt = calcMonthlyCost(c, i, o, 'gpt-5.3');
|
||||
const claude = calcMonthlyCost(c, i, o, 'claude-opus-4.6');
|
||||
const gpt = calcMonthlyCost(c, i, o, 'gpt-5.6-sol');
|
||||
const claude = calcMonthlyCost(c, i, o, 'claude-fable-5');
|
||||
const gemini = calcMonthlyCost(c, i, o, 'gemini-3.1-pro');
|
||||
|
||||
document.getElementById('t-gpt-m').textContent = fmtDollar(gpt);
|
||||
@@ -410,8 +410,8 @@ function updateCalc() {
|
||||
const avgOut = tpc - avgIn;
|
||||
const callsPerMonth = cpd * 30;
|
||||
|
||||
const gpt = calcMonthlyCost(callsPerMonth, avgIn, avgOut, 'gpt-5.3');
|
||||
const claude = calcMonthlyCost(callsPerMonth, avgIn, avgOut, 'claude-opus-4.6');
|
||||
const gpt = calcMonthlyCost(callsPerMonth, avgIn, avgOut, 'gpt-5.6-sol');
|
||||
const claude = calcMonthlyCost(callsPerMonth, avgIn, avgOut, 'claude-fable-5');
|
||||
const gemini = calcMonthlyCost(callsPerMonth, avgIn, avgOut, 'gemini-3.1-pro');
|
||||
|
||||
document.getElementById('calc-gpt').textContent = fmtDollar(gpt) + '/mo';
|
||||
|
||||
@@ -184,7 +184,7 @@ DASHBOARD_HTML = """\
|
||||
<div class="providers">
|
||||
<div class="provider-card openai">
|
||||
<div class="pname">OpenAI</div>
|
||||
<div class="pmodel">GPT-5.3 — $2.00 / $10.00 per 1M tokens</div>
|
||||
<div class="pmodel">GPT-5.6 Sol — $5.00 / $30.00 per 1M tokens</div>
|
||||
<div class="savings-amount" id="save-openai">$0.00</div>
|
||||
<div class="breakdown">
|
||||
<div class="item">
|
||||
@@ -199,7 +199,7 @@ DASHBOARD_HTML = """\
|
||||
</div>
|
||||
<div class="provider-card anthropic">
|
||||
<div class="pname">Anthropic</div>
|
||||
<div class="pmodel">Claude Opus 4.6 — $5.00 / $25.00 per 1M tokens</div>
|
||||
<div class="pmodel">Claude Fable 5 — $10.00 / $50.00 per 1M tokens</div>
|
||||
<div class="savings-amount" id="save-anthropic">$0.00</div>
|
||||
<div class="breakdown">
|
||||
<div class="item">
|
||||
@@ -281,12 +281,12 @@ DASHBOARD_HTML = """\
|
||||
<div class="providers-heading">Energy & Compute Avoided</div>
|
||||
<div class="metrics-row">
|
||||
<div class="metric-card">
|
||||
<div class="mheading">Energy Saved (vs GPT-5.3)</div>
|
||||
<div class="mheading">Energy Saved (vs GPT-5.6 Sol)</div>
|
||||
<div class="mvalue green" id="energy-joules">0 <span class="munit">J</span></div>
|
||||
<div class="msub" id="energy-kwh">0 kWh of cloud datacenter energy avoided</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="mheading">FLOPs Avoided (vs GPT-5.3)</div>
|
||||
<div class="mheading">FLOPs Avoided (vs GPT-5.6 Sol)</div>
|
||||
<div class="mvalue purple" id="flops-val">0 <span class="munit">FLOP</span></div>
|
||||
<div class="msub" id="flops-sub">cloud compute operations not needed</div>
|
||||
</div>
|
||||
@@ -354,8 +354,8 @@ async function refresh() {
|
||||
providerMap[p.provider] = p;
|
||||
});
|
||||
|
||||
// OpenAI / GPT-5.3
|
||||
const oa = providerMap['gpt-5.3'] || {};
|
||||
// OpenAI / GPT-5.6 Sol
|
||||
const oa = providerMap['gpt-5.6-sol'] || {};
|
||||
document.getElementById('save-openai')
|
||||
.textContent = fmtDollar(oa.total_cost || 0);
|
||||
document.getElementById('save-openai-in')
|
||||
@@ -363,8 +363,8 @@ async function refresh() {
|
||||
document.getElementById('save-openai-out')
|
||||
.textContent = fmtDollar(oa.output_cost || 0);
|
||||
|
||||
// Anthropic / Claude Opus 4.6
|
||||
const an = providerMap['claude-opus-4.6'] || {};
|
||||
// Anthropic / Claude Fable 5
|
||||
const an = providerMap['claude-fable-5'] || {};
|
||||
document.getElementById('save-anthropic')
|
||||
.textContent = fmtDollar(an.total_cost || 0);
|
||||
document.getElementById('save-anthropic-in')
|
||||
@@ -384,13 +384,13 @@ async function refresh() {
|
||||
// Monthly projections
|
||||
const proj = d.monthly_projection || {};
|
||||
document.getElementById('proj-openai')
|
||||
.textContent = fmtDollar(proj['gpt-5.3'] || 0);
|
||||
.textContent = fmtDollar(proj['gpt-5.6-sol'] || 0);
|
||||
document.getElementById('proj-anthropic')
|
||||
.textContent = fmtDollar(proj['claude-opus-4.6'] || 0);
|
||||
.textContent = fmtDollar(proj['claude-fable-5'] || 0);
|
||||
document.getElementById('proj-google')
|
||||
.textContent = fmtDollar(proj['gemini-3.1-pro'] || 0);
|
||||
|
||||
// Energy / FLOPs (use GPT-5.3 as reference)
|
||||
// Energy / FLOPs (use GPT-5.6 Sol as reference)
|
||||
const ej = oa.energy_joules || 0;
|
||||
const eWh = oa.energy_wh || 0;
|
||||
const fl = oa.flops || 0;
|
||||
|
||||
@@ -23,19 +23,19 @@ from openjarvis.core.types import TOKEN_COUNTING_VERSION # noqa: E402,F401
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
CLOUD_PRICING: Dict[str, Dict[str, float]] = {
|
||||
"gpt-5.3": {
|
||||
"input_per_1m": 2.00,
|
||||
"output_per_1m": 10.00,
|
||||
"label": "GPT-5.3",
|
||||
"gpt-5.6-sol": {
|
||||
"input_per_1m": 5.00,
|
||||
"output_per_1m": 30.00,
|
||||
"label": "GPT-5.6 Sol",
|
||||
"provider": "OpenAI",
|
||||
"params_b": 200.0,
|
||||
"energy_wh_per_1k_tokens": 0.4,
|
||||
"flops_per_token": 3.0e12,
|
||||
},
|
||||
"claude-opus-4.6": {
|
||||
"input_per_1m": 5.00,
|
||||
"output_per_1m": 25.00,
|
||||
"label": "Claude Opus 4.6",
|
||||
"claude-fable-5": {
|
||||
"input_per_1m": 10.00,
|
||||
"output_per_1m": 50.00,
|
||||
"label": "Claude Fable 5",
|
||||
"provider": "Anthropic",
|
||||
"params_b": 137.0,
|
||||
"energy_wh_per_1k_tokens": 0.5,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from typing import List, Optional
|
||||
|
||||
@@ -105,11 +106,15 @@ class FasterWhisperBackend(SpeechBackend):
|
||||
try:
|
||||
model = self._ensure_model()
|
||||
|
||||
# Write audio to a temp file (faster-whisper needs a file path)
|
||||
# Write audio to a temp file (faster-whisper needs a file path).
|
||||
# delete=False + manual unlink: on Windows an open
|
||||
# NamedTemporaryFile holds an exclusive handle, so PyAV's reopen
|
||||
# of tmp.name inside model.transcribe() fails with EACCES.
|
||||
suffix = f".{format}" if not format.startswith(".") else format
|
||||
with tempfile.NamedTemporaryFile(suffix=suffix, delete=True) as tmp:
|
||||
tmp.write(audio)
|
||||
tmp.flush()
|
||||
tmp = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
|
||||
try:
|
||||
with tmp:
|
||||
tmp.write(audio)
|
||||
|
||||
kwargs = {}
|
||||
if language:
|
||||
@@ -117,6 +122,15 @@ class FasterWhisperBackend(SpeechBackend):
|
||||
|
||||
segments_iter, info = model.transcribe(tmp.name, **kwargs)
|
||||
segments_list = list(segments_iter)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(tmp.name)
|
||||
except OSError as unlink_exc:
|
||||
logger.debug(
|
||||
"Could not remove temp audio file %s: %s",
|
||||
tmp.name,
|
||||
unlink_exc,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._last_error = str(exc)
|
||||
raise
|
||||
|
||||
@@ -6,6 +6,7 @@ and filtering operations that BM25 search cannot handle.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sqlite3
|
||||
from typing import Any, Optional
|
||||
|
||||
@@ -16,10 +17,25 @@ from openjarvis.tools._stubs import BaseTool, ToolSpec
|
||||
|
||||
_MAX_ROWS = 50
|
||||
|
||||
# Write keywords are matched on word boundaries (mirroring db_query.py) so that
|
||||
# a read-only SELECT is not rejected just because a column/alias/literal happens
|
||||
# to contain one as a substring (e.g. "deleted_at", "created_at").
|
||||
_FORBIDDEN_RE = re.compile(
|
||||
r"\b(DROP|DELETE|INSERT|UPDATE|ALTER|CREATE|TRUNCATE|ATTACH)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
# String literals are stripped before the keyword scan so that data mentioning
|
||||
# a write keyword (e.g. WHERE content LIKE '%delete%') is not rejected. A write
|
||||
# "hidden" in a literal still cannot execute: the query must start with SELECT
|
||||
# and sqlite3 refuses multi-statement strings.
|
||||
_STRING_LITERAL_RE = re.compile(r"'[^']*'")
|
||||
|
||||
_SCHEMA_DESCRIPTION = (
|
||||
"Table: knowledge_chunks\n"
|
||||
"Columns: id, content, source, doc_type, doc_id, title, author, "
|
||||
"participants, timestamp, thread_id, url, metadata, chunk_index"
|
||||
"participants, timestamp, thread_id, url, metadata, chunk_index, "
|
||||
"created_at, deleted_at (NULL for active rows)"
|
||||
)
|
||||
|
||||
|
||||
@@ -84,21 +100,20 @@ class KnowledgeSQLTool(BaseTool):
|
||||
success=False,
|
||||
)
|
||||
|
||||
_FORBIDDEN = ("DROP", "DELETE", "INSERT", "UPDATE", "ALTER", "CREATE", "ATTACH")
|
||||
for forbidden in _FORBIDDEN:
|
||||
if forbidden in normalized:
|
||||
return ToolResult(
|
||||
tool_name="knowledge_sql",
|
||||
content=(
|
||||
f"Query contains forbidden keyword: {forbidden}."
|
||||
" Only SELECT queries allowed."
|
||||
),
|
||||
success=False,
|
||||
)
|
||||
forbidden = _FORBIDDEN_RE.search(_STRING_LITERAL_RE.sub("''", query))
|
||||
if forbidden:
|
||||
return ToolResult(
|
||||
tool_name="knowledge_sql",
|
||||
content=(
|
||||
f"Query contains forbidden keyword: {forbidden.group(1).upper()}."
|
||||
" Only SELECT queries allowed."
|
||||
),
|
||||
success=False,
|
||||
)
|
||||
|
||||
try:
|
||||
rows = self._store._conn.execute(query).fetchmany(_MAX_ROWS)
|
||||
except sqlite3.OperationalError as exc:
|
||||
except sqlite3.Error as exc:
|
||||
return ToolResult(
|
||||
tool_name="knowledge_sql",
|
||||
content=f"SQL error: {exc}",
|
||||
|
||||
@@ -347,7 +347,7 @@ class TestCostCalculator:
|
||||
calls_per_month=1000,
|
||||
avg_input_tokens=500,
|
||||
avg_output_tokens=200,
|
||||
provider_key="gpt-5.3",
|
||||
provider_key="gpt-5.6-sol",
|
||||
)
|
||||
assert est.monthly_cost > 0
|
||||
assert est.annual_cost == est.monthly_cost * 12
|
||||
|
||||
@@ -53,6 +53,69 @@ def test_faster_whisper_transcribe():
|
||||
assert result.duration_seconds == 1.5
|
||||
|
||||
|
||||
def test_faster_whisper_transcribe_temp_file_reopenable_and_removed():
|
||||
"""The temp file must be closed before the model reads it, and gone after.
|
||||
|
||||
On Windows, an open NamedTemporaryFile holds an exclusive handle, so
|
||||
PyAV's reopen of the path inside model.transcribe() fails with EACCES
|
||||
unless the file is closed first. Opening the path inside the mocked
|
||||
transcribe reproduces that failure mode on Windows.
|
||||
"""
|
||||
import os
|
||||
|
||||
mock_info = MagicMock()
|
||||
mock_info.language = "en"
|
||||
mock_info.language_probability = 0.95
|
||||
mock_info.duration = 1.5
|
||||
|
||||
seen = {}
|
||||
|
||||
def fake_transcribe(path, **kwargs):
|
||||
seen["path"] = path
|
||||
with open(path, "rb") as fh:
|
||||
seen["content"] = fh.read()
|
||||
return iter(()), mock_info
|
||||
|
||||
mock_model = MagicMock()
|
||||
mock_model.transcribe.side_effect = fake_transcribe
|
||||
|
||||
with patch(
|
||||
"openjarvis.speech.faster_whisper.WhisperModel",
|
||||
return_value=mock_model,
|
||||
):
|
||||
backend = FasterWhisperBackend(model_size="base", device="cpu")
|
||||
backend.transcribe(b"fake audio bytes")
|
||||
|
||||
assert seen["content"] == b"fake audio bytes"
|
||||
assert not os.path.exists(seen["path"])
|
||||
|
||||
|
||||
def test_faster_whisper_transcribe_removes_temp_file_on_error():
|
||||
"""The temp file is cleaned up even when transcription fails."""
|
||||
import os
|
||||
|
||||
seen = {}
|
||||
|
||||
def fake_transcribe(path, **kwargs):
|
||||
seen["path"] = path
|
||||
raise RuntimeError("decode failed")
|
||||
|
||||
mock_model = MagicMock()
|
||||
mock_model.transcribe.side_effect = fake_transcribe
|
||||
|
||||
with patch(
|
||||
"openjarvis.speech.faster_whisper.WhisperModel",
|
||||
return_value=mock_model,
|
||||
):
|
||||
backend = FasterWhisperBackend(model_size="base", device="cpu")
|
||||
with pytest.raises(RuntimeError, match="decode failed"):
|
||||
backend.transcribe(b"fake audio bytes")
|
||||
|
||||
assert "path" in seen
|
||||
assert not os.path.exists(seen["path"])
|
||||
assert "decode failed" in (backend.last_error() or "")
|
||||
|
||||
|
||||
def test_faster_whisper_falls_back_from_unsupported_float16():
|
||||
mock_model = MagicMock()
|
||||
|
||||
|
||||
@@ -64,6 +64,41 @@ def test_rejects_drop(store: KnowledgeStore) -> None:
|
||||
assert not result.success
|
||||
|
||||
|
||||
def test_allows_select_with_keyword_substring(store: KnowledgeStore) -> None:
|
||||
"""A read-only SELECT must not be rejected because a column/alias merely
|
||||
contains a write keyword as a substring (e.g. 'created' -> CREATE)."""
|
||||
from openjarvis.tools.knowledge_sql import KnowledgeSQLTool
|
||||
|
||||
tool = KnowledgeSQLTool(store=store)
|
||||
result = tool.execute(
|
||||
query="SELECT author AS created_author FROM knowledge_chunks"
|
||||
)
|
||||
assert result.success, result.content
|
||||
assert "Alice" in result.content
|
||||
|
||||
|
||||
def test_allows_keyword_inside_string_literal(store: KnowledgeStore) -> None:
|
||||
"""A write keyword appearing only inside a string literal must not be
|
||||
treated as a forbidden statement."""
|
||||
from openjarvis.tools.knowledge_sql import KnowledgeSQLTool
|
||||
|
||||
tool = KnowledgeSQLTool(store=store)
|
||||
result = tool.execute(
|
||||
query="SELECT content FROM knowledge_chunks WHERE content LIKE '%delete%'"
|
||||
)
|
||||
assert result.success, result.content
|
||||
|
||||
|
||||
def test_rejects_multi_statement(store: KnowledgeStore) -> None:
|
||||
"""Multi-statement strings fail with a ToolResult, not an exception."""
|
||||
from openjarvis.tools.knowledge_sql import KnowledgeSQLTool
|
||||
|
||||
tool = KnowledgeSQLTool(store=store)
|
||||
result = tool.execute(query="SELECT 1; VACUUM")
|
||||
assert not result.success
|
||||
assert "error" in result.content.lower()
|
||||
|
||||
|
||||
def test_handles_bad_sql(store: KnowledgeStore) -> None:
|
||||
from openjarvis.tools.knowledge_sql import KnowledgeSQLTool
|
||||
|
||||
|
||||
Reference in New Issue
Block a user