needle-tr 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- needle_tr/__init__.py +21 -0
- needle_tr/benchmark.py +565 -0
- needle_tr/cli.py +151 -0
- needle_tr/dataset_generator.py +302 -0
- needle_tr/export_ollama.py +90 -0
- needle_tr/tools.py +75 -0
- needle_tr/trainer.py +159 -0
- needle_tr-0.1.0.dist-info/METADATA +149 -0
- needle_tr-0.1.0.dist-info/RECORD +11 -0
- needle_tr-0.1.0.dist-info/WHEEL +4 -0
- needle_tr-0.1.0.dist-info/entry_points.txt +4 -0
needle_tr/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import needle
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from .tools import ACTIVE_TOOLS, calculate, get_weather, send_message, set_alarm
|
|
5
|
+
|
|
6
|
+
agent = needle.Needle(
|
|
7
|
+
tools=ACTIVE_TOOLS,
|
|
8
|
+
stateless=True,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
print("Hava durumu:", agent.run("Lagos'ta hava nasıl?")["results"])
|
|
14
|
+
print("Mesaj gönderme:", agent.run("Ahmet'e 'Toplantı başladı' mesajı gönder")["results"])
|
|
15
|
+
print("Alarm kurma:", agent.run("Saat 07:30'a alarm kur")["results"])
|
|
16
|
+
print("Hesaplama:", agent.run("25 * 4 hesapla")["results"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
main()
|
|
21
|
+
|
needle_tr/benchmark.py
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
"""Needle-TR Benchmark ve Başarım Ölçüm Sistemi.
|
|
2
|
+
|
|
3
|
+
Bu modül:
|
|
4
|
+
1. Modelin veya ajanın (Cactus Needle / Ollama) Türkçe araç çağırma doğruluğunu ve gecikmesini test eder.
|
|
5
|
+
2. Doğruluk (Tool & Argüman Eşleşmesi), geçerli JSON oranı ve gecikme (ms) metriklerini hesaplar.
|
|
6
|
+
3. Otomatik olarak yüksek çözünürlüklü ve estetik bir SVG performans grafiği üretir.
|
|
7
|
+
4. Sonuçları ve grafiği doğrudan README.md dosyasına işler.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import time
|
|
15
|
+
import urllib.error
|
|
16
|
+
import urllib.request
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
DEFAULT_BENCHMARK_DATA = [
|
|
24
|
+
# calculate
|
|
25
|
+
{
|
|
26
|
+
"query": "25 * 4 hesapla",
|
|
27
|
+
"expected_tool": "calculate",
|
|
28
|
+
"expected_args": {"expression": "25 * 4"},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"query": "15 ile 48'i topla",
|
|
32
|
+
"expected_tool": "calculate",
|
|
33
|
+
"expected_args": {"expression": "15 + 48"},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"query": "120 bölü 6 kaç eder?",
|
|
37
|
+
"expected_tool": "calculate",
|
|
38
|
+
"expected_args": {"expression": "120 / 6"},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"query": "7'nin karesini al",
|
|
42
|
+
"expected_tool": "calculate",
|
|
43
|
+
"expected_args": {"expression": "7 ** 2"},
|
|
44
|
+
},
|
|
45
|
+
# get_weather
|
|
46
|
+
{
|
|
47
|
+
"query": "İstanbul'da hava durumu nasıl?",
|
|
48
|
+
"expected_tool": "get_weather",
|
|
49
|
+
"expected_args": {"city": "İstanbul"},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"query": "Ankara için hava durumunu göster",
|
|
53
|
+
"expected_tool": "get_weather",
|
|
54
|
+
"expected_args": {"city": "Ankara"},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"query": "İzmir hava durumu nedir?",
|
|
58
|
+
"expected_tool": "get_weather",
|
|
59
|
+
"expected_args": {"city": "İzmir"},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"query": "Lagos'ta hava nasıl?",
|
|
63
|
+
"expected_tool": "get_weather",
|
|
64
|
+
"expected_args": {"city": "Lagos"},
|
|
65
|
+
},
|
|
66
|
+
# send_message
|
|
67
|
+
{
|
|
68
|
+
"query": "Ahmet'e 'Toplantı başladı' mesajı gönder",
|
|
69
|
+
"expected_tool": "send_message",
|
|
70
|
+
"expected_args": {"recipient": "Ahmet", "message": "Toplantı başladı"},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"query": "Mehmet'e 'Yarın buluşuyoruz' diye mesaj ilet",
|
|
74
|
+
"expected_tool": "send_message",
|
|
75
|
+
"expected_args": {"recipient": "Mehmet", "message": "Yarın buluşuyoruz"},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"query": "Ayşe'ye 'Dosyayı gönderdim' mesajı at",
|
|
79
|
+
"expected_tool": "send_message",
|
|
80
|
+
"expected_args": {"recipient": "Ayşe", "message": "Dosyayı gönderdim"},
|
|
81
|
+
},
|
|
82
|
+
# set_alarm
|
|
83
|
+
{
|
|
84
|
+
"query": "Saat 07:30'a alarm kur",
|
|
85
|
+
"expected_tool": "set_alarm",
|
|
86
|
+
"expected_args": {"time": "07:30"},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"query": "14:15'e Toplantı alarmı kur",
|
|
90
|
+
"expected_tool": "set_alarm",
|
|
91
|
+
"expected_args": {"time": "14:15", "label": "Toplantı"},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"query": "Sabah 08:00'e Uyanış alarmı ayarla",
|
|
95
|
+
"expected_tool": "set_alarm",
|
|
96
|
+
"expected_args": {"time": "08:00", "label": "Uyanış"},
|
|
97
|
+
},
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class TestCaseResult:
|
|
103
|
+
query: str
|
|
104
|
+
expected_tool: str
|
|
105
|
+
expected_args: Dict[str, Any]
|
|
106
|
+
predicted_tool: Optional[str]
|
|
107
|
+
predicted_args: Dict[str, Any]
|
|
108
|
+
tool_correct: bool
|
|
109
|
+
args_correct: bool
|
|
110
|
+
valid_format: bool
|
|
111
|
+
latency_ms: float
|
|
112
|
+
raw_response: Any = None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class BenchmarkSummary:
|
|
117
|
+
target_name: str
|
|
118
|
+
total_tests: int
|
|
119
|
+
tool_correct_count: int
|
|
120
|
+
tool_accuracy: float
|
|
121
|
+
args_correct_count: int
|
|
122
|
+
args_accuracy: float
|
|
123
|
+
valid_format_count: int
|
|
124
|
+
valid_format_rate: float
|
|
125
|
+
avg_latency_ms: float
|
|
126
|
+
min_latency_ms: float
|
|
127
|
+
max_latency_ms: float
|
|
128
|
+
per_tool_stats: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
129
|
+
timestamp: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
|
130
|
+
details: List[TestCaseResult] = field(default_factory=list)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def load_test_cases(dataset_path: Optional[Path] = None, use_builtin_suite: bool = False) -> List[Dict[str, Any]]:
|
|
134
|
+
"""Test veri setini dosyadan (JSONL) veya yerleşik şablondan yükler."""
|
|
135
|
+
if use_builtin_suite:
|
|
136
|
+
return DEFAULT_BENCHMARK_DATA
|
|
137
|
+
|
|
138
|
+
if dataset_path and dataset_path.exists():
|
|
139
|
+
cases = []
|
|
140
|
+
with open(dataset_path, "r", encoding="utf-8") as f:
|
|
141
|
+
for line in f:
|
|
142
|
+
line = line.strip()
|
|
143
|
+
if not line:
|
|
144
|
+
continue
|
|
145
|
+
try:
|
|
146
|
+
obj = json.loads(line)
|
|
147
|
+
if "messages" in obj:
|
|
148
|
+
msgs = obj["messages"]
|
|
149
|
+
user_msg = next((m["content"] for m in msgs if m.get("role") == "user"), "")
|
|
150
|
+
asst_msg = next((m["content"] for m in msgs if m.get("role") == "assistant"), "")
|
|
151
|
+
|
|
152
|
+
# JSON bloğunu ayıkla
|
|
153
|
+
json_match = re.search(r"```json\s*(.*?)\s*```", asst_msg, re.DOTALL)
|
|
154
|
+
json_str = json_match.group(1) if json_match else asst_msg.strip()
|
|
155
|
+
parsed = json.loads(json_str)
|
|
156
|
+
|
|
157
|
+
cases.append({
|
|
158
|
+
"query": user_msg,
|
|
159
|
+
"expected_tool": parsed.get("name", ""),
|
|
160
|
+
"expected_args": parsed.get("arguments", {}),
|
|
161
|
+
})
|
|
162
|
+
elif "query" in obj and "tool" in obj:
|
|
163
|
+
cases.append({
|
|
164
|
+
"query": obj["query"],
|
|
165
|
+
"expected_tool": obj["tool"],
|
|
166
|
+
"expected_args": obj.get("arguments", {}),
|
|
167
|
+
})
|
|
168
|
+
except Exception:
|
|
169
|
+
continue
|
|
170
|
+
if cases:
|
|
171
|
+
return cases
|
|
172
|
+
|
|
173
|
+
return DEFAULT_BENCHMARK_DATA
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _parse_agent_response(res: Dict[str, Any]) -> Tuple[Optional[str], Dict[str, Any], bool]:
|
|
177
|
+
"""Cactus Needle agent yanıtından çağrılan aracı ve argümanları çıkarır."""
|
|
178
|
+
if not isinstance(res, dict):
|
|
179
|
+
return None, {}, False
|
|
180
|
+
|
|
181
|
+
# 1. results listesinden çıkarım
|
|
182
|
+
results = res.get("results", [])
|
|
183
|
+
if results and isinstance(results, list):
|
|
184
|
+
first_res = results[0]
|
|
185
|
+
if isinstance(first_res, dict):
|
|
186
|
+
if "result" in first_res or "expression" in first_res:
|
|
187
|
+
return "calculate", {"expression": str(first_res.get("expression", ""))}, True
|
|
188
|
+
if "temp_c" in first_res or "sky" in first_res:
|
|
189
|
+
return "get_weather", {"city": str(first_res.get("city", ""))}, True
|
|
190
|
+
if "status" in first_res and "recipient" in first_res:
|
|
191
|
+
return "send_message", {
|
|
192
|
+
"recipient": str(first_res.get("recipient", "")),
|
|
193
|
+
"message": str(first_res.get("message", "")),
|
|
194
|
+
}, True
|
|
195
|
+
if "status" in first_res and ("time" in first_res or "label" in first_res):
|
|
196
|
+
args = {"time": str(first_res.get("time", ""))}
|
|
197
|
+
if "label" in first_res:
|
|
198
|
+
args["label"] = str(first_res.get("label", ""))
|
|
199
|
+
return "set_alarm", args, True
|
|
200
|
+
|
|
201
|
+
# 2. function_calls listesinden çıkarım
|
|
202
|
+
calls = res.get("function_calls", [])
|
|
203
|
+
if calls and isinstance(calls, list):
|
|
204
|
+
c = calls[0]
|
|
205
|
+
if isinstance(c, dict):
|
|
206
|
+
return c.get("name"), c.get("arguments", {}), True
|
|
207
|
+
|
|
208
|
+
# 3. suppressed_calls listesinden çıkarım
|
|
209
|
+
suppressed = res.get("suppressed_calls", [])
|
|
210
|
+
if suppressed and isinstance(suppressed, list):
|
|
211
|
+
c = suppressed[0]
|
|
212
|
+
if isinstance(c, dict):
|
|
213
|
+
return c.get("name"), c.get("arguments", {}), True
|
|
214
|
+
|
|
215
|
+
# Araç çağrısı bulunamadı
|
|
216
|
+
return None, {}, bool(res.get("success", False))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _query_ollama(
|
|
220
|
+
query: str,
|
|
221
|
+
model: str = "needle-tr",
|
|
222
|
+
host: str = "http://localhost:11434",
|
|
223
|
+
timeout: int = 15,
|
|
224
|
+
) -> Tuple[Optional[str], Dict[str, Any], bool, float]:
|
|
225
|
+
"""Ollama API'sine istek gönderip dönen yanıtı parse eder."""
|
|
226
|
+
url = f"{host.rstrip('/')}/api/chat"
|
|
227
|
+
payload = {
|
|
228
|
+
"model": model,
|
|
229
|
+
"messages": [{"role": "user", "content": query}],
|
|
230
|
+
"stream": False,
|
|
231
|
+
"options": {"temperature": 0.1},
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
req = urllib.request.Request(
|
|
235
|
+
url,
|
|
236
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
237
|
+
headers={"Content-Type": "application/json"},
|
|
238
|
+
method="POST",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
t0 = time.perf_counter()
|
|
242
|
+
try:
|
|
243
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
244
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
245
|
+
latency_ms = (time.perf_counter() - t0) * 1000
|
|
246
|
+
content = data.get("message", {}).get("content", "").strip()
|
|
247
|
+
|
|
248
|
+
# Markdown JSON ayıkla
|
|
249
|
+
match = re.search(r"```(?:json)?\s*(.*?)\s*```", content, re.DOTALL)
|
|
250
|
+
raw_json = match.group(1).strip() if match else content
|
|
251
|
+
|
|
252
|
+
parsed = json.loads(raw_json)
|
|
253
|
+
pred_tool = parsed.get("name")
|
|
254
|
+
pred_args = parsed.get("arguments", {})
|
|
255
|
+
return pred_tool, pred_args, True, latency_ms
|
|
256
|
+
except Exception:
|
|
257
|
+
latency_ms = (time.perf_counter() - t0) * 1000
|
|
258
|
+
return None, {}, False, latency_ms
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _args_match(expected: Dict[str, Any], predicted: Dict[str, Any]) -> bool:
|
|
262
|
+
"""Argümanların anlamsal/içeriksel eşleşmesini kontrol eder."""
|
|
263
|
+
if not expected and not predicted:
|
|
264
|
+
return True
|
|
265
|
+
for k, exp_val in expected.items():
|
|
266
|
+
if k not in predicted:
|
|
267
|
+
return False
|
|
268
|
+
pred_val = predicted[k]
|
|
269
|
+
# Küçük harf ve boşluk normalizasyonu
|
|
270
|
+
if isinstance(exp_val, str) and isinstance(pred_val, str):
|
|
271
|
+
if exp_val.strip().lower() != pred_val.strip().lower():
|
|
272
|
+
# Kısmi eşleşme kontrolü (örn. "İstanbul için" vs "İstanbul")
|
|
273
|
+
if exp_val.strip().lower() not in pred_val.strip().lower() and pred_val.strip().lower() not in exp_val.strip().lower():
|
|
274
|
+
return False
|
|
275
|
+
elif exp_val != pred_val:
|
|
276
|
+
return False
|
|
277
|
+
return True
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def run_benchmark(
|
|
281
|
+
target: str = "agent",
|
|
282
|
+
dataset_path: Optional[Path] = None,
|
|
283
|
+
use_builtin_suite: bool = False,
|
|
284
|
+
ollama_model: str = "needle-tr",
|
|
285
|
+
ollama_host: str = "http://localhost:11434",
|
|
286
|
+
) -> BenchmarkSummary:
|
|
287
|
+
"""Belirtilen hedef üzerinde benchmark çalıştırır ve özet metrikleri döner."""
|
|
288
|
+
test_cases = load_test_cases(dataset_path, use_builtin_suite=use_builtin_suite)
|
|
289
|
+
|
|
290
|
+
# Hedef model/ajan hazırlığı
|
|
291
|
+
agent_instance = None
|
|
292
|
+
if target == "agent":
|
|
293
|
+
from . import agent as default_agent
|
|
294
|
+
agent_instance = default_agent
|
|
295
|
+
target_display = "Cactus Needle (On-Device Agent)"
|
|
296
|
+
else:
|
|
297
|
+
target_display = f"Ollama ({ollama_model})"
|
|
298
|
+
|
|
299
|
+
results: List[TestCaseResult] = []
|
|
300
|
+
per_tool: Dict[str, Dict[str, Any]] = {}
|
|
301
|
+
|
|
302
|
+
for case in test_cases:
|
|
303
|
+
query = case["query"]
|
|
304
|
+
expected_tool = case["expected_tool"]
|
|
305
|
+
expected_args = case.get("expected_args", {})
|
|
306
|
+
|
|
307
|
+
if expected_tool not in per_tool:
|
|
308
|
+
per_tool[expected_tool] = {
|
|
309
|
+
"total": 0,
|
|
310
|
+
"tool_correct": 0,
|
|
311
|
+
"args_correct": 0,
|
|
312
|
+
"latencies": [],
|
|
313
|
+
}
|
|
314
|
+
per_tool[expected_tool]["total"] += 1
|
|
315
|
+
|
|
316
|
+
if target == "agent":
|
|
317
|
+
t0 = time.perf_counter()
|
|
318
|
+
try:
|
|
319
|
+
res = agent_instance.run(query)
|
|
320
|
+
latency_ms = (time.perf_counter() - t0) * 1000
|
|
321
|
+
pred_tool, pred_args, valid = _parse_agent_response(res)
|
|
322
|
+
except Exception as e:
|
|
323
|
+
latency_ms = (time.perf_counter() - t0) * 1000
|
|
324
|
+
pred_tool, pred_args, valid, res = None, {}, False, str(e)
|
|
325
|
+
else:
|
|
326
|
+
pred_tool, pred_args, valid, latency_ms = _query_ollama(
|
|
327
|
+
query=query, model=ollama_model, host=ollama_host
|
|
328
|
+
)
|
|
329
|
+
res = None
|
|
330
|
+
|
|
331
|
+
tool_ok = bool(pred_tool and pred_tool.lower() == expected_tool.lower())
|
|
332
|
+
args_ok = tool_ok and _args_match(expected_args, pred_args)
|
|
333
|
+
|
|
334
|
+
if tool_ok:
|
|
335
|
+
per_tool[expected_tool]["tool_correct"] += 1
|
|
336
|
+
if args_ok:
|
|
337
|
+
per_tool[expected_tool]["args_correct"] += 1
|
|
338
|
+
per_tool[expected_tool]["latencies"].append(latency_ms)
|
|
339
|
+
|
|
340
|
+
results.append(
|
|
341
|
+
TestCaseResult(
|
|
342
|
+
query=query,
|
|
343
|
+
expected_tool=expected_tool,
|
|
344
|
+
expected_args=expected_args,
|
|
345
|
+
predicted_tool=pred_tool,
|
|
346
|
+
predicted_args=pred_args,
|
|
347
|
+
tool_correct=tool_ok,
|
|
348
|
+
args_correct=args_ok,
|
|
349
|
+
valid_format=valid,
|
|
350
|
+
latency_ms=latency_ms,
|
|
351
|
+
raw_response=res,
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# Genel metrikler
|
|
356
|
+
total = len(results)
|
|
357
|
+
tool_correct_cnt = sum(1 for r in results if r.tool_correct)
|
|
358
|
+
args_correct_cnt = sum(1 for r in results if r.args_correct)
|
|
359
|
+
valid_format_cnt = sum(1 for r in results if r.valid_format)
|
|
360
|
+
latencies = [r.latency_ms for r in results]
|
|
361
|
+
|
|
362
|
+
per_tool_final: Dict[str, Dict[str, Any]] = {}
|
|
363
|
+
for t_name, data in per_tool.items():
|
|
364
|
+
t_total = data["total"]
|
|
365
|
+
t_latencies = data["latencies"]
|
|
366
|
+
per_tool_final[t_name] = {
|
|
367
|
+
"total": t_total,
|
|
368
|
+
"tool_correct": data["tool_correct"],
|
|
369
|
+
"tool_accuracy": (data["tool_correct"] / t_total * 100) if t_total else 0.0,
|
|
370
|
+
"args_correct": data["args_correct"],
|
|
371
|
+
"args_accuracy": (data["args_correct"] / t_total * 100) if t_total else 0.0,
|
|
372
|
+
"avg_latency_ms": sum(t_latencies) / len(t_latencies) if t_latencies else 0.0,
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return BenchmarkSummary(
|
|
376
|
+
target_name=target_display,
|
|
377
|
+
total_tests=total,
|
|
378
|
+
tool_correct_count=tool_correct_cnt,
|
|
379
|
+
tool_accuracy=(tool_correct_cnt / total * 100) if total else 0.0,
|
|
380
|
+
args_correct_count=args_correct_cnt,
|
|
381
|
+
args_accuracy=(args_correct_cnt / total * 100) if total else 0.0,
|
|
382
|
+
valid_format_count=valid_format_cnt,
|
|
383
|
+
valid_format_rate=(valid_format_cnt / total * 100) if total else 0.0,
|
|
384
|
+
avg_latency_ms=sum(latencies) / len(latencies) if latencies else 0.0,
|
|
385
|
+
min_latency_ms=min(latencies) if latencies else 0.0,
|
|
386
|
+
max_latency_ms=max(latencies) if latencies else 0.0,
|
|
387
|
+
per_tool_stats=per_tool_final,
|
|
388
|
+
details=results,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def generate_benchmark_svg(summary: BenchmarkSummary, output_path: Path) -> Path:
|
|
393
|
+
"""Modern, şık ve karanlık temalı vektörel SVG performans kartı oluşturur."""
|
|
394
|
+
width = 820
|
|
395
|
+
tool_count = len(summary.per_tool_stats)
|
|
396
|
+
height = 420 + max(0, (tool_count - 4) * 45)
|
|
397
|
+
|
|
398
|
+
svg_lines = [
|
|
399
|
+
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}" width="100%" height="100%">',
|
|
400
|
+
" <defs>",
|
|
401
|
+
" <!-- Arka plan ve kart gradyanları -->",
|
|
402
|
+
' <linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">',
|
|
403
|
+
' <stop offset="0%" stop-color="#0f172a" />',
|
|
404
|
+
' <stop offset="100%" stop-color="#1e293b" />',
|
|
405
|
+
" </linearGradient>",
|
|
406
|
+
' <linearGradient id="cardGrad" x1="0%" y1="0%" x2="0%" y2="100%">',
|
|
407
|
+
' <stop offset="0%" stop-color="#1e293b" stop-opacity="0.8" />',
|
|
408
|
+
' <stop offset="100%" stop-color="#0f172a" stop-opacity="0.9" />',
|
|
409
|
+
" </linearGradient>",
|
|
410
|
+
' <linearGradient id="barGradCyan" x1="0%" y1="0%" x2="100%" y2="0%">',
|
|
411
|
+
' <stop offset="0%" stop-color="#38bdf8" />',
|
|
412
|
+
' <stop offset="100%" stop-color="#818cf8" />',
|
|
413
|
+
" </linearGradient>",
|
|
414
|
+
' <linearGradient id="barGradGreen" x1="0%" y1="0%" x2="100%" y2="0%">',
|
|
415
|
+
' <stop offset="0%" stop-color="#34d399" />',
|
|
416
|
+
' <stop offset="100%" stop-color="#10b981" />',
|
|
417
|
+
" </linearGradient>",
|
|
418
|
+
' <filter id="shadow" x="-5%" y="-5%" width="110%" height="110%">',
|
|
419
|
+
' <feDropShadow dx="0" dy="4" stdDeviation="6" flood-color="#000000" flood-opacity="0.4" />',
|
|
420
|
+
" </filter>",
|
|
421
|
+
" </defs>",
|
|
422
|
+
"",
|
|
423
|
+
' <style>',
|
|
424
|
+
' .title { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 20px; font-weight: 700; fill: #f8fafc; }',
|
|
425
|
+
' .subtitle { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 13px; fill: #94a3b8; }',
|
|
426
|
+
' .kpi-title { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; fill: #94a3b8; font-weight: 600; }',
|
|
427
|
+
' .kpi-value { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 24px; font-weight: 800; }',
|
|
428
|
+
' .kpi-sub { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 11px; fill: #64748b; }',
|
|
429
|
+
' .section-title { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px; font-weight: 600; fill: #cbd5e1; }',
|
|
430
|
+
' .tool-label { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace; font-size: 13px; fill: #e2e8f0; }',
|
|
431
|
+
' .tool-stat { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 12px; fill: #94a3b8; }',
|
|
432
|
+
" </style>",
|
|
433
|
+
"",
|
|
434
|
+
f' <!-- Arka Plan -->',
|
|
435
|
+
f' <rect width="{width}" height="{height}" rx="16" fill="url(#bgGrad)" stroke="#334155" stroke-width="1" />',
|
|
436
|
+
"",
|
|
437
|
+
" <!-- Başlık ve Meta -->",
|
|
438
|
+
' <text x="32" y="44" class="title">⚡ Needle-TR Benchmark Raporu</text>',
|
|
439
|
+
f' <text x="32" y="68" class="subtitle">Hedef: <tspan fill="#38bdf8" font-weight="600">{summary.target_name}</tspan> | Test Sayısı: {summary.total_tests} | Tarih: {summary.timestamp}</text>',
|
|
440
|
+
"",
|
|
441
|
+
" <!-- KPI Kartları -->",
|
|
442
|
+
]
|
|
443
|
+
|
|
444
|
+
kpis = [
|
|
445
|
+
("Araç Doğruluğu", f"%{summary.tool_accuracy:.1f}", f"{summary.tool_correct_count}/{summary.total_tests} başarılı", "#38bdf8"),
|
|
446
|
+
("Argüman Doğruluğu", f"%{summary.args_accuracy:.1f}", f"{summary.args_correct_count}/{summary.total_tests} eşleşme", "#818cf8"),
|
|
447
|
+
("Geçerli Çağrı Oranı", f"%{summary.valid_format_rate:.1f}", f"{summary.valid_format_count}/{summary.total_tests} parse edildi", "#34d399"),
|
|
448
|
+
("Ortalama Gecikme", f"{summary.avg_latency_ms:.1f} ms", f"min: {summary.min_latency_ms:.1f} / max: {summary.max_latency_ms:.1f}", "#f472b6"),
|
|
449
|
+
]
|
|
450
|
+
|
|
451
|
+
card_w = 175
|
|
452
|
+
card_h = 86
|
|
453
|
+
card_gap = 16
|
|
454
|
+
start_x = 32
|
|
455
|
+
start_y = 96
|
|
456
|
+
|
|
457
|
+
for i, (ktitle, kval, ksub, color) in enumerate(kpis):
|
|
458
|
+
cx = start_x + i * (card_w + card_gap)
|
|
459
|
+
svg_lines.extend([
|
|
460
|
+
f' <g transform="translate({cx}, {start_y})" filter="url(#shadow)">',
|
|
461
|
+
f' <rect width="{card_w}" height="{card_h}" rx="12" fill="url(#cardGrad)" stroke="#334155" stroke-width="1" />',
|
|
462
|
+
f' <text x="14" y="24" class="kpi-title">{ktitle}</text>',
|
|
463
|
+
f' <text x="14" y="54" class="kpi-value" fill="{color}">{kval}</text>',
|
|
464
|
+
f' <text x="14" y="72" class="kpi-sub">{ksub}</text>',
|
|
465
|
+
" </g>",
|
|
466
|
+
])
|
|
467
|
+
|
|
468
|
+
# Araç Başarı Dağılımı Çubuk Grafiği
|
|
469
|
+
bar_y_start = 226
|
|
470
|
+
svg_lines.extend([
|
|
471
|
+
"",
|
|
472
|
+
f' <!-- Araç Bazında Başarım Dağılımı -->',
|
|
473
|
+
f' <text x="32" y="{bar_y_start}" class="section-title">📊 Araç Bazında Doğruluk Dağılımı</text>',
|
|
474
|
+
f' <rect x="32" y="{bar_y_start + 12}" width="{width - 64}" height="{height - bar_y_start - 36}" rx="12" fill="url(#cardGrad)" stroke="#334155" stroke-width="1" filter="url(#shadow)" />',
|
|
475
|
+
])
|
|
476
|
+
|
|
477
|
+
item_y = bar_y_start + 44
|
|
478
|
+
max_bar_width = 380
|
|
479
|
+
|
|
480
|
+
for tool_name, stats in summary.per_tool_stats.items():
|
|
481
|
+
t_acc = stats["tool_accuracy"]
|
|
482
|
+
a_acc = stats["args_accuracy"]
|
|
483
|
+
bar_len = int((t_acc / 100.0) * max_bar_width)
|
|
484
|
+
|
|
485
|
+
svg_lines.extend([
|
|
486
|
+
f' <!-- Tool: {tool_name} -->',
|
|
487
|
+
f' <text x="52" y="{item_y + 14}" class="tool-label">{tool_name}()</text>',
|
|
488
|
+
f' <!-- Background bar -->',
|
|
489
|
+
f' <rect x="220" y="{item_y}" width="{max_bar_width}" height="18" rx="9" fill="#1e293b" stroke="#334155" stroke-width="1" />',
|
|
490
|
+
f' <!-- Fill bar -->',
|
|
491
|
+
f' <rect x="220" y="{item_y}" width="{max(bar_len, 6)}" height="18" rx="9" fill="url(#barGradCyan)" />',
|
|
492
|
+
f' <!-- Percent & Details -->',
|
|
493
|
+
f' <text x="{220 + max_bar_width + 16}" y="{item_y + 14}" class="tool-stat"><tspan fill="#38bdf8" font-weight="700">%{t_acc:.1f}</tspan> (Araç) | <tspan fill="#818cf8">%{a_acc:.1f}</tspan> (Arg) | <tspan fill="#94a3b8">{stats["avg_latency_ms"]:.1f}ms</tspan></text>',
|
|
494
|
+
])
|
|
495
|
+
item_y += 38
|
|
496
|
+
|
|
497
|
+
svg_lines.append("</svg>")
|
|
498
|
+
|
|
499
|
+
content = "\n".join(svg_lines)
|
|
500
|
+
output_path.write_text(content, encoding="utf-8")
|
|
501
|
+
return output_path
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def update_readme_benchmark(summary: BenchmarkSummary, svg_relative_path: str = "benchmark_results.svg", readme_path: Path = Path("README.md")) -> None:
|
|
505
|
+
"""README.md dosyasındaki benchmark tablosunu ve görsel referansını günceller."""
|
|
506
|
+
if not readme_path.exists():
|
|
507
|
+
return
|
|
508
|
+
|
|
509
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
510
|
+
|
|
511
|
+
# Tablo satırları oluştur
|
|
512
|
+
table_rows = []
|
|
513
|
+
for tool_name, stats in summary.per_tool_stats.items():
|
|
514
|
+
table_rows.append(
|
|
515
|
+
f"| `{tool_name}` | %{stats['tool_accuracy']:.1f} | %{stats['args_accuracy']:.1f} | {stats['total']} | {stats['avg_latency_ms']:.1f} ms |"
|
|
516
|
+
)
|
|
517
|
+
rows_str = "\n".join(table_rows)
|
|
518
|
+
|
|
519
|
+
benchmark_section = f"""## 📊 Benchmark ve Başarım Sonuçları
|
|
520
|
+
|
|
521
|
+
`needle-tr` yerleşik benchmark aracı ile ölçülen son başarım sonuçları:
|
|
522
|
+
|
|
523
|
+

|
|
524
|
+
|
|
525
|
+
### 📈 Özet Performans Metrikleri
|
|
526
|
+
|
|
527
|
+
| Metrik | Değer | Açıklama |
|
|
528
|
+
| :--- | :--- | :--- |
|
|
529
|
+
| **Test Edilen Hedef** | `{summary.target_name}` | Test edilen ortam (Ajan / Model) |
|
|
530
|
+
| **Toplam Test Sayısı** | **{summary.total_tests}** | Doğrulama sorgusu sayısı |
|
|
531
|
+
| **Araç Seçim Doğruluğu** | **%{summary.tool_accuracy:.1f}** ({summary.tool_correct_count}/{summary.total_tests}) | Doğru fonksiyonun seçilme oranı |
|
|
532
|
+
| **Argüman Doğruluğu** | **%{summary.args_accuracy:.1f}** ({summary.args_correct_count}/{summary.total_tests}) | Parametrelerin eksiksiz eşleşme oranı |
|
|
533
|
+
| **Geçerli Yanıt / JSON** | **%{summary.valid_format_rate:.1f}** ({summary.valid_format_count}/{summary.total_tests}) | Bozulma olmadan parse edilen çağrılar |
|
|
534
|
+
| **Ortalama Gecikme** | **{summary.avg_latency_ms:.1f} ms** | Min: {summary.min_latency_ms:.1f} ms, Max: {summary.max_latency_ms:.1f} ms |
|
|
535
|
+
| **Son Güncelleme** | `{summary.timestamp}` | Otomatik benchmark çalıştırma zamanı |
|
|
536
|
+
|
|
537
|
+
### 🔍 Araç Bazında Detay Dağılımı
|
|
538
|
+
|
|
539
|
+
| Araç (Tool) | Araç Doğruluğu | Argüman Eşleşmesi | Test Sayısı | Ortalama Gecikme |
|
|
540
|
+
| :--- | :---: | :---: | :---: | :---: |
|
|
541
|
+
{rows_str}
|
|
542
|
+
|
|
543
|
+
Benchmark testlerini kendiniz çalıştırmak ve grafiği güncellemek için:
|
|
544
|
+
|
|
545
|
+
```bash
|
|
546
|
+
# Cactus Needle (On-device) Ajanı için
|
|
547
|
+
uv run needle-tr benchmark --target agent
|
|
548
|
+
|
|
549
|
+
# Ollama Modeli için
|
|
550
|
+
uv run needle-tr benchmark --target ollama --ollama-model needle-tr
|
|
551
|
+
```
|
|
552
|
+
"""
|
|
553
|
+
|
|
554
|
+
pattern = r"## 📊 Benchmark ve Başarım Sonuçları[\s\S]*?(?=\n## |\Z)"
|
|
555
|
+
if re.search(pattern, content):
|
|
556
|
+
new_content = re.sub(pattern, benchmark_section.strip() + "\n", content)
|
|
557
|
+
else:
|
|
558
|
+
# Ajan kullanımından hemen sonrasına veya referansların öncesine ekle
|
|
559
|
+
ref_pattern = r"(## 🙏 Referanslar & Teşekkürler)"
|
|
560
|
+
if re.search(ref_pattern, content):
|
|
561
|
+
new_content = re.sub(ref_pattern, benchmark_section.strip() + "\n\n---\n\n\\1", content)
|
|
562
|
+
else:
|
|
563
|
+
new_content = content.rstrip() + "\n\n---\n\n" + benchmark_section.strip() + "\n"
|
|
564
|
+
|
|
565
|
+
readme_path.write_text(new_content, encoding="utf-8")
|