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 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
+ ![Needle-TR Benchmark Sonuçları]({svg_relative_path})
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")