cortad 0.3.1-rc.8 → 0.3.1-rc.9
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.
- package/lib/pyhook/sitecustomize.py +126 -21
- package/lib/replay.mjs +5 -2
- package/lib/text.mjs +13 -4
- package/lib/trace.cjs +63 -16
- package/package.json +1 -1
|
@@ -579,7 +579,7 @@ def _install():
|
|
|
579
579
|
# person's latest message on, and Anthropic document and search_result blocks. The app's own
|
|
580
580
|
# instructions are not passages: only what sits under such a label is.
|
|
581
581
|
passage_text, passages_max = 3000, 6
|
|
582
|
-
material = re.compile(r"\b(?:retriev\w*|context|knowledge|documents?|sources?|search[ _-]?results?|references?|passages?|excerpts?|snippets?|chunks?|background|faq)\b
|
|
582
|
+
material = re.compile(r"\b(?:retriev\w*|context|knowledge|documents?|sources?|search[ _-]?results?|references?|passages?|excerpts?|snippets?|chunks?|background|faq|relevant)\b|检索|知识|参考资料|资料|上下文|文档|背景|相关", re.I)
|
|
583
583
|
heading = re.compile(r"^\s*(?:#{1,6}\s+[^\n]{1,80}|[^\n]{1,80}[::]\s*(?:\([^\n)]*\))?|\[[^\n\]]{1,80}\]|={2,}\s*[^\n]{1,80}?\s*={2,})\s*$")
|
|
584
584
|
# An inline label is a noun phrase that ends in the material word ("context:", "background:",
|
|
585
585
|
# "retrieved passages:"): "4. Knowledge:" in a numbered instruction and a JSON key are not.
|
|
@@ -587,6 +587,36 @@ def _install():
|
|
|
587
587
|
named = re.compile(r"(?:" + material.pattern + r")\s*$", re.I)
|
|
588
588
|
tagged = re.compile(r"<([A-Za-z][\w-]*)[^>]*>([\s\S]*?)</\1>")
|
|
589
589
|
|
|
590
|
+
# A label's block runs on to the next heading of its own kind: "资料:" over retrieved chunks
|
|
591
|
+
# that open on markdown headings holds every chunk, where stopping at its first paragraph kept
|
|
592
|
+
# one chunk of five and the reader called the knowledge base's own 1% fee made up. A markdown
|
|
593
|
+
# label holds deeper headings. A closing paragraph that opens on a label of its own ("问题:...",
|
|
594
|
+
# "Question: ...") is the prompt's ask, not material. A long block is several passages, cut
|
|
595
|
+
# between its paragraphs.
|
|
596
|
+
markdown = re.compile(r"^\s*(#{1,6})\s")
|
|
597
|
+
|
|
598
|
+
def ends_block(label, line):
|
|
599
|
+
if not heading.match(line):
|
|
600
|
+
return False
|
|
601
|
+
outer, inner = markdown.match(label), markdown.match(line)
|
|
602
|
+
return not inner or bool(outer and len(inner.group(1)) <= len(outer.group(1)))
|
|
603
|
+
|
|
604
|
+
def asks(para):
|
|
605
|
+
m = inline.match(para.split("\n")[0])
|
|
606
|
+
return bool(m and not named.search(m.group(1)))
|
|
607
|
+
|
|
608
|
+
# A heading inside a fenced block is the material's own text: a README chunk's "## Features".
|
|
609
|
+
fence = re.compile(r"^\s*(?:```|~~~)", re.M)
|
|
610
|
+
|
|
611
|
+
def add_block(add, name, paras):
|
|
612
|
+
text = ""
|
|
613
|
+
for p in paras:
|
|
614
|
+
if text and len(text) + len(p) + 2 > passage_text:
|
|
615
|
+
add(name, text)
|
|
616
|
+
text = ""
|
|
617
|
+
text += ("\n\n" if text else "") + p
|
|
618
|
+
add(name, text)
|
|
619
|
+
|
|
590
620
|
def labelled(text, add):
|
|
591
621
|
def lift(m):
|
|
592
622
|
if material.search(m.group(1).replace("_", " ")):
|
|
@@ -599,13 +629,14 @@ def _install():
|
|
|
599
629
|
lines = paras[i].split("\n")
|
|
600
630
|
head = lines[0]
|
|
601
631
|
if heading.match(head) and material.search(head):
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
632
|
+
block = ["\n".join(lines[1:])]
|
|
633
|
+
fenced = len(fence.findall(block[0])) % 2 == 1
|
|
634
|
+
while i + 1 < len(paras) and (fenced or (not ends_block(head, paras[i + 1].split("\n")[0]) and not (i + 2 == len(paras) and asks(paras[i + 1])))):
|
|
635
|
+
i += 1
|
|
636
|
+
block.append(paras[i])
|
|
637
|
+
if len(fence.findall(paras[i])) % 2 == 1:
|
|
638
|
+
fenced = not fenced
|
|
639
|
+
add_block(add, re.sub(r"[#::\[\]=]", "", head).strip(), block)
|
|
609
640
|
else:
|
|
610
641
|
label = next((m for m in (inline.match(l) for l in lines) if m and named.search(m.group(1))), None)
|
|
611
642
|
if label:
|
|
@@ -855,7 +886,76 @@ def _install():
|
|
|
855
886
|
except Exception:
|
|
856
887
|
return ""
|
|
857
888
|
|
|
858
|
-
|
|
889
|
+
# Which of the app's own lines made a model call, nearest first: the run names the code path that
|
|
890
|
+
# ran by it, where the read could only name the door. Its frames on this stack outside installed
|
|
891
|
+
# packages, then, when the call runs in a task another task awaits (LangChain runs each step of a
|
|
892
|
+
# chain as its own task), the awaiting task's. Relative to the folder the app was started in.
|
|
893
|
+
root = os.getcwd().replace(os.sep, "/").rstrip("/") + "/"
|
|
894
|
+
installed = re.compile(r"/(?:site-packages|dist-packages|\.venv|venv|__pypackages__)/|/lib/python\d")
|
|
895
|
+
|
|
896
|
+
def ours(frame, out):
|
|
897
|
+
f = frame.f_code.co_filename.replace(os.sep, "/")
|
|
898
|
+
if f.startswith(root) and not installed.search(f[len(root) - 1:]):
|
|
899
|
+
at = "%s:%d" % (f[len(root):], frame.f_lineno)
|
|
900
|
+
if at not in out:
|
|
901
|
+
out.append(at)
|
|
902
|
+
return len(out) >= 3
|
|
903
|
+
|
|
904
|
+
# The task that waits on this one: its wake-up is among this one's callbacks, directly, or behind
|
|
905
|
+
# the future an asyncio.gather callback closes over.
|
|
906
|
+
def awaiting(fut, depth=0):
|
|
907
|
+
for cb in getattr(fut, "_callbacks", None) or ():
|
|
908
|
+
fn = cb[0] if isinstance(cb, tuple) else cb
|
|
909
|
+
owner = getattr(fn, "__self__", None)
|
|
910
|
+
if owner is not None and owner is not fut and hasattr(owner, "get_coro"):
|
|
911
|
+
return owner
|
|
912
|
+
for cell in (getattr(fn, "__closure__", None) or ()) if depth < 2 else ():
|
|
913
|
+
try:
|
|
914
|
+
inner = cell.cell_contents
|
|
915
|
+
except ValueError:
|
|
916
|
+
continue
|
|
917
|
+
found = awaiting(inner, depth + 1) if inner is not fut and hasattr(inner, "_callbacks") else None
|
|
918
|
+
if found is not None:
|
|
919
|
+
return found
|
|
920
|
+
return None
|
|
921
|
+
|
|
922
|
+
# An embedding call answers nobody, so where it was made names no path a reply came down.
|
|
923
|
+
embedding = re.compile(r"/embeddings$", re.I)
|
|
924
|
+
|
|
925
|
+
def callers_now(url):
|
|
926
|
+
if embedding.search(urlsplit(str(url)).path or ""):
|
|
927
|
+
return None
|
|
928
|
+
out = []
|
|
929
|
+
try:
|
|
930
|
+
# Below the event loop's own frames is whatever started the loop, not this call's path.
|
|
931
|
+
frame = sys._getframe(1)
|
|
932
|
+
while frame is not None and not frame.f_code.co_filename.replace(os.sep, "/").endswith("/asyncio/events.py"):
|
|
933
|
+
if ours(frame, out):
|
|
934
|
+
return out
|
|
935
|
+
frame = frame.f_back
|
|
936
|
+
aio = sys.modules.get("asyncio")
|
|
937
|
+
try:
|
|
938
|
+
task = aio.current_task() if aio else None
|
|
939
|
+
except RuntimeError:
|
|
940
|
+
task = None
|
|
941
|
+
for _ in range(8):
|
|
942
|
+
task = awaiting(task) if task is not None else None
|
|
943
|
+
if task is None:
|
|
944
|
+
break
|
|
945
|
+
chain, coro = [], task.get_coro()
|
|
946
|
+
while coro is not None and len(chain) < 64:
|
|
947
|
+
f = getattr(coro, "cr_frame", None) or getattr(coro, "gi_frame", None) or getattr(coro, "ag_frame", None)
|
|
948
|
+
if f is not None:
|
|
949
|
+
chain.append(f)
|
|
950
|
+
coro = getattr(coro, "cr_await", None) or getattr(coro, "gi_yieldfrom", None) or getattr(coro, "ag_await", None)
|
|
951
|
+
for f in reversed(chain):
|
|
952
|
+
if ours(f, out):
|
|
953
|
+
return out
|
|
954
|
+
except Exception:
|
|
955
|
+
pass
|
|
956
|
+
return out or None
|
|
957
|
+
|
|
958
|
+
def meter(url, sent, status, kind, raw, turn=None, caller=None):
|
|
859
959
|
try:
|
|
860
960
|
tokens, model, events = read_reply(kind, (raw or "")[:reply_max])
|
|
861
961
|
parts = urlsplit(str(url))
|
|
@@ -878,6 +978,8 @@ def _install():
|
|
|
878
978
|
passages = passages_in(as_text)
|
|
879
979
|
if passages:
|
|
880
980
|
row["passages"] = passages
|
|
981
|
+
if caller:
|
|
982
|
+
row["caller"] = caller
|
|
881
983
|
write({"call": row})
|
|
882
984
|
except Exception:
|
|
883
985
|
pass
|
|
@@ -1425,7 +1527,7 @@ def _install():
|
|
|
1425
1527
|
finally:
|
|
1426
1528
|
self.finish()
|
|
1427
1529
|
|
|
1428
|
-
def watch(request, response, sent):
|
|
1530
|
+
def watch(request, response, sent, caller):
|
|
1429
1531
|
model = is_model_call(request.url)
|
|
1430
1532
|
if not model and not is_retrieval(request.url):
|
|
1431
1533
|
dep(request.url, response.status_code)
|
|
@@ -1433,13 +1535,13 @@ def _install():
|
|
|
1433
1535
|
kind = response.headers.get("content-type", "")
|
|
1434
1536
|
if getattr(response, "_content", None) is not None:
|
|
1435
1537
|
if model:
|
|
1436
|
-
meter(request.url, sent, response.status_code, kind, response.content.decode("utf-8", "replace"))
|
|
1538
|
+
meter(request.url, sent, response.status_code, kind, response.content.decode("utf-8", "replace"), caller=caller)
|
|
1437
1539
|
else:
|
|
1438
1540
|
dep(request.url, response.status_code, passages=passages_from(response.content.decode("utf-8", "replace")))
|
|
1439
1541
|
return response
|
|
1440
1542
|
turn = turn_now()
|
|
1441
1543
|
if model:
|
|
1442
|
-
done = lambda raw: meter(request.url, sent, response.status_code, kind, unpacked(raw, response.headers.get("content-encoding")), turn)
|
|
1544
|
+
done = lambda raw: meter(request.url, sent, response.status_code, kind, unpacked(raw, response.headers.get("content-encoding")), turn, caller)
|
|
1443
1545
|
else:
|
|
1444
1546
|
done = lambda raw: dep(request.url, response.status_code, passages=passages_from(unpacked(raw, response.headers.get("content-encoding"))), turn=turn)
|
|
1445
1547
|
stream = response.stream
|
|
@@ -1448,7 +1550,7 @@ def _install():
|
|
|
1448
1550
|
elif hasattr(stream, "__iter__"):
|
|
1449
1551
|
response.stream = SyncKept(stream, done)
|
|
1450
1552
|
elif model:
|
|
1451
|
-
meter(request.url, sent, response.status_code, kind, "")
|
|
1553
|
+
meter(request.url, sent, response.status_code, kind, "", caller=caller)
|
|
1452
1554
|
else:
|
|
1453
1555
|
dep(request.url, response.status_code)
|
|
1454
1556
|
return response
|
|
@@ -1458,6 +1560,7 @@ def _install():
|
|
|
1458
1560
|
if cls is module.Client:
|
|
1459
1561
|
def sync_send(self, request, *a, _send=send, **k):
|
|
1460
1562
|
sent = sent_of(request)
|
|
1563
|
+
caller = callers_now(request.url) if is_model_call(request.url) else None
|
|
1461
1564
|
try:
|
|
1462
1565
|
note(request.url, sent)
|
|
1463
1566
|
except Exception:
|
|
@@ -1466,18 +1569,19 @@ def _install():
|
|
|
1466
1569
|
response = _send(self, request, *a, **k)
|
|
1467
1570
|
except Exception as err:
|
|
1468
1571
|
if is_model_call(request.url):
|
|
1469
|
-
meter(request.url, sent, 0, "", "")
|
|
1572
|
+
meter(request.url, sent, 0, "", "", caller=caller)
|
|
1470
1573
|
else:
|
|
1471
1574
|
dep(request.url, 0, type(err).__name__)
|
|
1472
1575
|
raise
|
|
1473
1576
|
try:
|
|
1474
|
-
return watch(request, response, sent)
|
|
1577
|
+
return watch(request, response, sent, caller)
|
|
1475
1578
|
except Exception:
|
|
1476
1579
|
return response
|
|
1477
1580
|
cls.send = sync_send
|
|
1478
1581
|
else:
|
|
1479
1582
|
async def async_send(self, request, *a, _send=send, **k):
|
|
1480
1583
|
sent = sent_of(request)
|
|
1584
|
+
caller = callers_now(request.url) if is_model_call(request.url) else None
|
|
1481
1585
|
try:
|
|
1482
1586
|
note(request.url, sent)
|
|
1483
1587
|
except Exception:
|
|
@@ -1486,12 +1590,12 @@ def _install():
|
|
|
1486
1590
|
response = await _send(self, request, *a, **k)
|
|
1487
1591
|
except Exception as err:
|
|
1488
1592
|
if is_model_call(request.url):
|
|
1489
|
-
meter(request.url, sent, 0, "", "")
|
|
1593
|
+
meter(request.url, sent, 0, "", "", caller=caller)
|
|
1490
1594
|
else:
|
|
1491
1595
|
dep(request.url, 0, type(err).__name__)
|
|
1492
1596
|
raise
|
|
1493
1597
|
try:
|
|
1494
|
-
return watch(request, response, sent)
|
|
1598
|
+
return watch(request, response, sent, caller)
|
|
1495
1599
|
except Exception:
|
|
1496
1600
|
return response
|
|
1497
1601
|
cls.send = async_send
|
|
@@ -1500,6 +1604,7 @@ def _install():
|
|
|
1500
1604
|
send = module.Session.send
|
|
1501
1605
|
|
|
1502
1606
|
def sent(self, request, *a, **k):
|
|
1607
|
+
caller = callers_now(request.url) if is_model_call(request.url) else None
|
|
1503
1608
|
try:
|
|
1504
1609
|
note(request.url, request.body)
|
|
1505
1610
|
except Exception:
|
|
@@ -1517,7 +1622,7 @@ def _install():
|
|
|
1517
1622
|
if is_model_call(request.url):
|
|
1518
1623
|
# A streamed reply is counted as a call whose counts were not read.
|
|
1519
1624
|
raw = response.content.decode("utf-8", "replace") if getattr(response, "_content_consumed", False) else ""
|
|
1520
|
-
meter(request.url, text(request.body), response.status_code, response.headers.get("content-type", ""), raw)
|
|
1625
|
+
meter(request.url, text(request.body), response.status_code, response.headers.get("content-type", ""), raw, caller=caller)
|
|
1521
1626
|
except Exception:
|
|
1522
1627
|
pass
|
|
1523
1628
|
return response
|
|
@@ -1540,7 +1645,7 @@ def _install():
|
|
|
1540
1645
|
dep(str_or_url, 0, type(err).__name__)
|
|
1541
1646
|
raise
|
|
1542
1647
|
if is_model_call(str_or_url):
|
|
1543
|
-
response._cortad = (str_or_url, text(body), turn_now())
|
|
1648
|
+
response._cortad = (str_or_url, text(body), turn_now(), callers_now(str_or_url))
|
|
1544
1649
|
elif is_retrieval(str_or_url):
|
|
1545
1650
|
response._cortad_dep = (str_or_url, turn_now())
|
|
1546
1651
|
else:
|
|
@@ -1559,7 +1664,7 @@ def _install():
|
|
|
1559
1664
|
call = getattr(self, "_cortad", None)
|
|
1560
1665
|
if call:
|
|
1561
1666
|
self._cortad = None
|
|
1562
|
-
meter(call[0], call[1], self.status, self.headers.get("content-type", ""), (raw or b"").decode("utf-8", "replace"), call[2])
|
|
1667
|
+
meter(call[0], call[1], self.status, self.headers.get("content-type", ""), (raw or b"").decode("utf-8", "replace"), call[2], call[3])
|
|
1563
1668
|
return raw
|
|
1564
1669
|
|
|
1565
1670
|
def release_kept(self, *a, **k):
|
|
@@ -1570,7 +1675,7 @@ def _install():
|
|
|
1570
1675
|
call = getattr(self, "_cortad", None)
|
|
1571
1676
|
if call:
|
|
1572
1677
|
self._cortad = None
|
|
1573
|
-
meter(call[0], call[1], self.status, self.headers.get("content-type", ""), "", call[2])
|
|
1678
|
+
meter(call[0], call[1], self.status, self.headers.get("content-type", ""), "", call[2], call[3])
|
|
1574
1679
|
return release(self, *a, **k)
|
|
1575
1680
|
|
|
1576
1681
|
module.ClientSession._request = requested
|
package/lib/replay.mjs
CHANGED
|
@@ -121,6 +121,9 @@ const calledOf = (v) => {
|
|
|
121
121
|
return called.length ? { called } : {};
|
|
122
122
|
};
|
|
123
123
|
const passagesOf = (v) => (texts(v).length ? { passages: texts(v).slice(0, 6) } : {});
|
|
124
|
+
// Which of the app's own lines made the call, nearest first, as "path:line".
|
|
125
|
+
const AT = /^[^\n\r\t]{1,300}:\d{1,7}$/;
|
|
126
|
+
const callerOf = (v) => { const caller = Array.isArray(v) ? v.filter((c) => typeof c === "string" && AT.test(c)).slice(0, 3) : []; return caller.length ? { caller } : {}; };
|
|
124
127
|
const meter = (() => {
|
|
125
128
|
const rows = [];
|
|
126
129
|
const deps = [];
|
|
@@ -131,7 +134,7 @@ const meter = (() => {
|
|
|
131
134
|
rows.push({
|
|
132
135
|
at: count(call.at), host: call.host.slice(0, 253), model: String(call.model ?? "").slice(0, 160), status: count(call.status),
|
|
133
136
|
promptTokens: count(call.promptTokens), cachedTokens: count(call.cachedTokens), completionTokens: count(call.completionTokens),
|
|
134
|
-
usage: call.usage === true, ...turnOf(call.turn), ...rulesOf(call.rules), ...toolsOf(call.tools), ...calledOf(call.called), ...passagesOf(call.passages),
|
|
137
|
+
usage: call.usage === true, ...turnOf(call.turn), ...rulesOf(call.rules), ...toolsOf(call.tools), ...calledOf(call.called), ...passagesOf(call.passages), ...callerOf(call.caller),
|
|
135
138
|
});
|
|
136
139
|
if (rows.length > ROWS) rows.splice(0, rows.length - ROWS);
|
|
137
140
|
},
|
|
@@ -159,7 +162,7 @@ const meter = (() => {
|
|
|
159
162
|
}
|
|
160
163
|
return {
|
|
161
164
|
totals: [...totals.values()].sort((a, b) => b.calls - a.calls),
|
|
162
|
-
rows: rows.slice(-400).reverse().map(({ at, host, model, status, turn, rules, tools, called, passages }) => ({ at, host, model, status, ...(turn ? { turn } : {}), ...(rules ? { rules } : {}), ...(tools ? { tools } : {}), ...(called ? { called } : {}), ...(passages ? { passages } : {}) })),
|
|
165
|
+
rows: rows.slice(-400).reverse().map(({ at, host, model, status, turn, rules, tools, called, passages, caller }) => ({ at, host, model, status, ...(turn ? { turn } : {}), ...(rules ? { rules } : {}), ...(tools ? { tools } : {}), ...(called ? { called } : {}), ...(passages ? { passages } : {}), ...(caller ? { caller } : {}) })),
|
|
163
166
|
deps: deps.slice(-400).reverse(),
|
|
164
167
|
};
|
|
165
168
|
},
|
package/lib/text.mjs
CHANGED
|
@@ -159,13 +159,19 @@ const sideText = (m) => (m.rate ? rateText(m.rate) : m.unavailable ? null : move
|
|
|
159
159
|
// Over zero pairs there is no move to print: the line says verification is unavailable and why,
|
|
160
160
|
// never "no change, 0 points" measured on nothing. The visible side's reason is the said line.
|
|
161
161
|
function verifyLines(v) {
|
|
162
|
-
// An access verify is a re-knock, not a trial move: the routes side by side, then the count.
|
|
162
|
+
// An access verify is a re-knock, not a trial move: the routes side by side, then the count. Only
|
|
163
|
+
// a route knocked and answered both times is compared; one the app no longer has or did not answer
|
|
164
|
+
// is named as such, never read as a closed gate.
|
|
163
165
|
if (v.access) {
|
|
164
166
|
const lines = [`Verify of ${v.findingId}.`];
|
|
165
167
|
for (const r of v.routes ?? []) {
|
|
166
|
-
const then = r.wasOpen ? `answered ${r.before} with no sign-in` : `refused (${r.before
|
|
167
|
-
const now =
|
|
168
|
-
|
|
168
|
+
const then = r.wasOpen ? `answered ${r.before} with no sign-in` : `refused (${r.before})`;
|
|
169
|
+
const now =
|
|
170
|
+
r.state === "answered" ? (r.nowOpen ? `now answers ${r.after} with no sign-in` : `now refuses (${r.after})`)
|
|
171
|
+
: r.state === "gone" ? "your app no longer has this route"
|
|
172
|
+
: r.state === "no-answer" ? "your app did not answer this time"
|
|
173
|
+
: "was not knocked again";
|
|
174
|
+
lines.push(` ${r.method} ${r.route}: ${then}, ${now}.`);
|
|
169
175
|
}
|
|
170
176
|
if (v.said) lines.push(v.said);
|
|
171
177
|
return lines;
|
|
@@ -355,6 +361,9 @@ function findingBlock(f, placed) {
|
|
|
355
361
|
// investigation itself.
|
|
356
362
|
if (q.asked) lines.push(` Asked: "${clip(q.asked, 300)}"`);
|
|
357
363
|
lines.push(` Reply ${q.reply}: "${clip(q.quote, 400)}"${about ? ` (${about})` : ""}`);
|
|
364
|
+
// What the reader checked the reply against: what their app retrieved for it, the start of each.
|
|
365
|
+
if (q.checked) lines.push(` ${q.checked}`);
|
|
366
|
+
for (const p of (q.passages ?? []).slice(0, 3)) lines.push(` Passage${p.source ? ` (${clip(p.source, 60)})` : ""}: "${clip(p.text, 240)}"`);
|
|
358
367
|
// What a developer reproduces it from: the request as sent, the body back, the app's own log.
|
|
359
368
|
if (q.request) lines.push(` Request: ${q.request.method} ${q.request.path}${q.request.body ? ` ${clip(q.request.body, 600)}` : ""}`);
|
|
360
369
|
if (has(q.response)) lines.push(` Response: ${q.response ? clip(q.response, 600) : "(empty body)"}`);
|
package/lib/trace.cjs
CHANGED
|
@@ -119,15 +119,33 @@ if (FILE) {
|
|
|
119
119
|
.replace(/\*(\w+)/g, "{$1}");
|
|
120
120
|
const joined = (a, b) => ("/" + [a, b].map((s) => String(s).replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/"));
|
|
121
121
|
const nodePath = require("node:path");
|
|
122
|
+
// The frames of the stack it is called from, nearest first, outside this hook and node_modules,
|
|
123
|
+
// relative to the folder the app was started in. An awaited call keeps its awaiting frames in
|
|
124
|
+
// the stack, and an SDK is deeper than the default ten frames.
|
|
125
|
+
const stackFrames = () => {
|
|
126
|
+
const limit = Error.stackTraceLimit;
|
|
127
|
+
Error.stackTraceLimit = 60;
|
|
128
|
+
const stack = String(new Error().stack);
|
|
129
|
+
Error.stackTraceLimit = limit;
|
|
130
|
+
const out = [];
|
|
131
|
+
for (const line of stack.split("\n")) {
|
|
132
|
+
const m = /\(?(?:file:\/\/)?(\/[^()]+?):(\d+):\d+\)?$/.exec(line.trim());
|
|
133
|
+
if (m && m[1] !== __filename && !m[1].includes("/node_modules/")) out.push({ file: nodePath.relative(process.cwd(), decodeURI(m[1])), line: Number(m[2]) });
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
};
|
|
122
137
|
// The file that registered a route: the first frame outside this hook and outside node_modules.
|
|
123
|
-
const callerFile = () => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
const callerFile = () => { const f = stackFrames()[0]; return f && !f.file.startsWith("..") ? f.file : ""; };
|
|
139
|
+
// Which of the app's own lines made a model call, nearest first: the run names the code path that
|
|
140
|
+
// ran by it, where the read could only name the door.
|
|
141
|
+
const callers = () => {
|
|
142
|
+
const out = [];
|
|
143
|
+
for (const f of stackFrames()) {
|
|
144
|
+
const at = `${f.file}:${f.line}`;
|
|
145
|
+
if (!f.file.startsWith("..") && !out.includes(at)) out.push(at);
|
|
146
|
+
if (out.length === 3) break;
|
|
129
147
|
}
|
|
130
|
-
return
|
|
148
|
+
return out.length ? out : undefined;
|
|
131
149
|
};
|
|
132
150
|
const seen = new Set();
|
|
133
151
|
let nest = false;
|
|
@@ -561,13 +579,36 @@ if (FILE) {
|
|
|
561
579
|
// person's latest message on, and Anthropic document and search_result blocks. The app's own
|
|
562
580
|
// instructions are not passages: only what sits under such a label is.
|
|
563
581
|
const PASSAGE_TEXT = 3000, PASSAGES_MAX = 6;
|
|
564
|
-
const MATERIAL = /\b(?:retriev\w*|context|knowledge|documents?|sources?|search[ _-]?results?|references?|passages?|excerpts?|snippets?|chunks?|background|faq)\b
|
|
582
|
+
const MATERIAL = /\b(?:retriev\w*|context|knowledge|documents?|sources?|search[ _-]?results?|references?|passages?|excerpts?|snippets?|chunks?|background|faq|relevant)\b|检索|知识|参考资料|资料|上下文|文档|背景|相关/i;
|
|
565
583
|
const HEADING = /^\s*(?:#{1,6}\s+[^\n]{1,80}|[^\n]{1,80}[::]\s*(?:\([^\n)]*\))?|\[[^\n\]]{1,80}\]|={2,}\s*[^\n]{1,80}?\s*={2,})\s*$/;
|
|
566
584
|
// An inline label is a noun phrase that ends in the material word ("context:", "background:",
|
|
567
585
|
// "retrieved passages:"): "4. Knowledge:" in a numbered instruction and a JSON key are not.
|
|
568
586
|
const INLINE = /^\s*(?:[-*]\s+)?([A-Za-z\u4e00-\u9fff][A-Za-z\u4e00-\u9fff '’_-]{0,29})[::]\s*\S/;
|
|
569
587
|
const NAMED = new RegExp(`(?:${MATERIAL.source})\\s*$`, "i");
|
|
570
588
|
const TAGGED = /<([A-Za-z][\w-]*)[^>]*>([\s\S]*?)<\/\1>/g;
|
|
589
|
+
// A label's block runs on to the next heading of its own kind: "资料:" over retrieved chunks
|
|
590
|
+
// that open on markdown headings holds every chunk, where stopping at its first paragraph kept
|
|
591
|
+
// one chunk of five and the reader called the knowledge base's own 1% fee made up. A markdown
|
|
592
|
+
// label holds deeper headings. A closing paragraph that opens on a label of its own ("问题:...",
|
|
593
|
+
// "Question: ...") is the prompt's ask, not material. A long block is several passages, cut
|
|
594
|
+
// between its paragraphs.
|
|
595
|
+
const MARKDOWN = /^\s*(#{1,6})\s/;
|
|
596
|
+
const endsBlock = (label, line) => {
|
|
597
|
+
if (!HEADING.test(line)) return false;
|
|
598
|
+
const outer = MARKDOWN.exec(label), inner = MARKDOWN.exec(line);
|
|
599
|
+
return !inner || Boolean(outer && inner[1].length <= outer[1].length);
|
|
600
|
+
};
|
|
601
|
+
const asks = (para) => { const m = INLINE.exec(para.split("\n")[0]); return Boolean(m && !NAMED.test(m[1])); };
|
|
602
|
+
// A heading inside a fenced block is the material's own text: a README chunk's "## Features".
|
|
603
|
+
const fences = (para) => (para.match(/^\s*(?:```|~~~)/gm) || []).length;
|
|
604
|
+
const addBlock = (add, name, paras) => {
|
|
605
|
+
let text = "";
|
|
606
|
+
for (const p of paras) {
|
|
607
|
+
if (text && text.length + p.length + 2 > PASSAGE_TEXT) { add(name, text); text = ""; }
|
|
608
|
+
text += (text ? "\n\n" : "") + p;
|
|
609
|
+
}
|
|
610
|
+
add(name, text);
|
|
611
|
+
};
|
|
571
612
|
const labelled = (text, add) => {
|
|
572
613
|
const rest = String(text || "").replace(TAGGED, (all, tag, inner) => { if (MATERIAL.test(tag.replace(/_/g, " "))) { add(tag, inner); return ""; } return all; });
|
|
573
614
|
const paras = rest.split(/\n\s*\n/);
|
|
@@ -575,10 +616,13 @@ if (FILE) {
|
|
|
575
616
|
const lines = paras[i].split("\n");
|
|
576
617
|
const head = lines[0];
|
|
577
618
|
if (HEADING.test(head) && MATERIAL.test(head)) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
619
|
+
const block = [lines.slice(1).join("\n")];
|
|
620
|
+
let fenced = fences(block[0]) % 2 === 1;
|
|
621
|
+
while (i + 1 < paras.length && (fenced || (!endsBlock(head, paras[i + 1].split("\n")[0]) && !(i + 2 === paras.length && asks(paras[i + 1]))))) {
|
|
622
|
+
block.push(paras[++i]);
|
|
623
|
+
if (fences(paras[i]) % 2 === 1) fenced = !fenced;
|
|
624
|
+
}
|
|
625
|
+
addBlock(add, head.replace(/[#::[\]=]/g, "").trim(), block);
|
|
582
626
|
continue;
|
|
583
627
|
}
|
|
584
628
|
const inline = lines.map((l) => INLINE.exec(l)).find((m) => m && NAMED.test(m[1]));
|
|
@@ -636,6 +680,8 @@ if (FILE) {
|
|
|
636
680
|
// Where models are served, by host, and the paths every OpenAI-shaped or vendor endpoint ends with.
|
|
637
681
|
const MODEL_HOST = /(?:^|\.)(?:openai\.com|anthropic\.com|fireworks\.ai|openrouter\.ai|groq\.com|mistral\.ai|together\.xyz|together\.ai|deepseek\.com|cohere\.ai|cohere\.com|perplexity\.ai|x\.ai|googleapis\.com|openai\.azure\.com|cognitiveservices\.azure\.com|amazonaws\.com|replicate\.com|huggingface\.co|cerebras\.ai|deepinfra\.com|novita\.ai|moonshot\.cn|dashscope\.aliyuncs\.com|bigmodel\.cn|ai-gateway\.vercel\.sh|gateway\.ai\.cloudflare\.com|helicone\.ai|portkey\.ai)$/i;
|
|
638
682
|
const MODEL_PATH = /\/(?:chat\/completions|completions|responses|messages|embeddings)$|:(?:generateContent|streamGenerateContent)|\/invoke(?:-with-response-stream)?$|\/api\/(?:chat|generate)$/i;
|
|
683
|
+
// An embedding call answers nobody, so where it was made names no path a reply came down.
|
|
684
|
+
const EMBEDDING = /\/embeddings$/i;
|
|
639
685
|
const isModelCall = (host, path) => {
|
|
640
686
|
const h = String(host || "").replace(/:\d+$/, "");
|
|
641
687
|
const p = String(path || "").split("?")[0];
|
|
@@ -790,14 +836,14 @@ if (FILE) {
|
|
|
790
836
|
const m = /\/models\/([^/:]+):|\/model\/([^/]+)\/(?:invoke|converse)/.exec(path || "");
|
|
791
837
|
return m ? decodeURIComponent(m[1] || m[2]) : "";
|
|
792
838
|
};
|
|
793
|
-
const meter = ({ host, path, sent, status, type, body, turn }) => {
|
|
839
|
+
const meter = ({ host, path, sent, status, type, body, turn, caller }) => {
|
|
794
840
|
try {
|
|
795
841
|
const reply = readReply(type, String(body || "").slice(0, REPLY_MAX));
|
|
796
842
|
const rules = rulesIn(sent);
|
|
797
843
|
const tools = toolsIn(sent);
|
|
798
844
|
const called = calledIn(sent, reply.events);
|
|
799
845
|
const passages = passagesIn(sent);
|
|
800
|
-
row({ call: { at: Date.now(), host: String(host).replace(/:443$/, ""), model: String(askedFor(path, sent) || reply.model || "").slice(0, 160), status, ...(reply.tokens || { promptTokens: 0, cachedTokens: 0, completionTokens: 0 }), usage: Boolean(reply.tokens), ...(turn ? { turn } : {}), ...(rules ? { rules } : {}), ...(tools ? { tools } : {}), ...(called ? { called } : {}), ...(passages ? { passages } : {}) } });
|
|
846
|
+
row({ call: { at: Date.now(), host: String(host).replace(/:443$/, ""), model: String(askedFor(path, sent) || reply.model || "").slice(0, 160), status, ...(reply.tokens || { promptTokens: 0, cachedTokens: 0, completionTokens: 0 }), usage: Boolean(reply.tokens), ...(turn ? { turn } : {}), ...(rules ? { rules } : {}), ...(tools ? { tools } : {}), ...(called ? { called } : {}), ...(passages ? { passages } : {}), ...(caller ? { caller } : {}) } });
|
|
801
847
|
} catch { /* the command is gone */ }
|
|
802
848
|
};
|
|
803
849
|
|
|
@@ -1014,7 +1060,7 @@ if (FILE) {
|
|
|
1014
1060
|
}
|
|
1015
1061
|
const sent = bodyText(init && init.body);
|
|
1016
1062
|
note(als.getStore(), sent);
|
|
1017
|
-
const call = { host: url.host, path: url.pathname, sent, turn: (als.getStore() || {}).turn };
|
|
1063
|
+
const call = { host: url.host, path: url.pathname, sent, turn: (als.getStore() || {}).turn, caller: EMBEDDING.test(url.pathname) ? undefined : callers() };
|
|
1018
1064
|
return realFetch.apply(this, arguments).then((res) => {
|
|
1019
1065
|
try {
|
|
1020
1066
|
// A clone is a tee: your app's branch gets every byte as fast as it reads, this one is read to the end.
|
|
@@ -1043,7 +1089,8 @@ if (FILE) {
|
|
|
1043
1089
|
const end = req.end;
|
|
1044
1090
|
req.write = function (chunk, ...r) { if (chunk && typeof chunk !== "function") parts.push(Buffer.from(chunk)); return write.call(this, chunk, ...r); };
|
|
1045
1091
|
req.end = function (chunk, ...r) { if (chunk && typeof chunk !== "function") parts.push(Buffer.from(chunk)); sent = Buffer.concat(parts).toString("utf8"); note(ctx, sent); return end.call(this, chunk, ...r); };
|
|
1046
|
-
const
|
|
1092
|
+
const bare = String(path || "").split("?")[0];
|
|
1093
|
+
const call = { host: String(host || ""), path: bare, turn: ctx && ctx.turn, caller: EMBEDDING.test(bare) ? undefined : callers() };
|
|
1047
1094
|
let done = false;
|
|
1048
1095
|
const once = (row) => { if (!done) { done = true; meter({ ...call, sent, ...row }); } };
|
|
1049
1096
|
req.once("error", () => once({ status: 0, type: "", body: "" }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortad",
|
|
3
|
-
"version": "0.3.1-rc.
|
|
3
|
+
"version": "0.3.1-rc.9",
|
|
4
4
|
"description": "Connects the AI app on your machine to Cortad for test conversations, and gives your coding agent the MCP and skill to run them. No dependencies.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cortad": "local.mjs"
|