Skip to content

Commit a46a344

Browse files
committed
Fix a few bugs
1 parent a15499f commit a46a344

4 files changed

Lines changed: 59 additions & 10 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![Tests](https://img.shields.io/github/actions/workflow/status/IssunDB/example-apps/tests.yml?label=tests&style=flat&labelColor=282c34&logo=github)](https://github.com/IssunDB/example-apps/actions/workflows/tests.yml)
44
[![Code Coverage](https://img.shields.io/codecov/c/github/IssunDB/example-apps?label=coverage&style=flat&labelColor=282c34&logo=codecov)](https://codecov.io/gh/IssunDB/example-apps)
55
[![Python version](https://img.shields.io/badge/python-%3E=3.10-3776ab?style=flat&labelColor=282c34&logo=python)](https://github.com/IssunDB/example-apps)
6-
[![License: MIT](https://img.shields.io/badge/license-MIT-ffd343?style=flat&labelColor=282c34&logo=open-source-initiative)](LICENSE)
6+
[![License: MIT](https://img.shields.io/badge/license-MIT-3776ab?style=flat&labelColor=282c34&logo=open-source-initiative)](LICENSE)
77

88
This repository includes a collection of example applications that use the [IssunDB](https://github.com/IssunDB/issun-db) graph database.
99

python/fraud/src/fraud_detection_stream/main.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,15 @@ def main(argv: list[str] | None = None) -> int:
101101
if not batch:
102102
break
103103
batch_no += 1
104+
batch_first_ts = batch[0].ts
104105
for event in batch:
105106
store.apply(event)
106107
last_ts = max(last_ts, event.ts)
107108

108-
since_ts = last_ts - args.window
109+
# Look back `window` seconds, but never start later than the oldest
110+
# event in this batch: a batch whose span exceeds the window would
111+
# otherwise drop patterns injected early in it before they are scored.
112+
since_ts = min(last_ts - args.window, batch_first_ts)
109113
new_alerts: list[Alert] = []
110114
for alert in run_all_detectors(db, since_ts):
111115
key = (alert.rule, alert.subject)

rust/code-explorer/src/main.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,12 @@ enum Command {
3838
Dead,
3939
/// Everything transitively affected if this function changes.
4040
Impact { name: String },
41-
/// Most structurally important functions (PageRank over the call graph).
41+
/// Most structurally important functions (PageRank over the code graph).
4242
Rank {
4343
#[arg(long, default_value_t = 15)]
4444
top: usize,
4545
},
46-
/// Connected components and recursion cycles in the call graph.
46+
/// Connected components and reference cycles across the code graph.
4747
Structure,
4848
/// Shortest call path between two functions.
4949
Path { from: String, to: String },
@@ -176,6 +176,9 @@ fn impact(graph: &Graph, name: &str) -> Result<()> {
176176
}
177177

178178
fn rank(graph: &Graph, top: usize) -> Result<()> {
179+
// PageRank runs over every edge type (CALLS, CONTAINS, METHOD_OF,
180+
// IMPLEMENTS), so the score is whole-code-graph centrality, not pure
181+
// call-graph importance; we then keep only the Function nodes.
179182
let scores = graph.page_rank(30, 0.85)?;
180183
let functions = graph.nodes_by_label("Function")?;
181184
let mut ranked: Vec<(NodeId, f32)> = functions
@@ -184,7 +187,7 @@ fn rank(graph: &Graph, top: usize) -> Result<()> {
184187
.collect();
185188
ranked.sort_by(|a, b| b.1.total_cmp(&a.1));
186189

187-
println!("most depended-upon functions (PageRank over the call graph):");
190+
println!("most central functions (PageRank over the code graph):");
188191
for (node, score) in ranked.into_iter().take(top) {
189192
println!(
190193
" {score:.5} {:<36} {}",
@@ -208,8 +211,10 @@ fn structure(graph: &Graph) -> Result<()> {
208211
sizes.len(),
209212
&sizes[..sizes.len().min(8)]
210213
);
214+
// detect_cycle spans all edge types; in this schema a cycle is almost
215+
// always CALLS recursion, but it is not restricted to it.
211216
println!(
212-
"recursion present: {}",
217+
"cycle present: {}",
213218
if graph.detect_cycle()? { "yes" } else { "no" }
214219
);
215220
Ok(())

rust/graphrag-agent/src/ingest.rs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,10 @@ fn chunk(text: &str) -> Vec<String> {
170170
current.push_str("\n\n");
171171
}
172172
current.push_str(para);
173-
// Hard-split oversized paragraphs on word boundaries.
173+
// Hard-split oversized paragraphs, cutting only at UTF-8 char
174+
// boundaries so multi-byte text never panics the slice.
174175
while current.len() > CHUNK_MAX {
175-
let cut = current[..CHUNK_MAX]
176-
.rfind(char::is_whitespace)
177-
.unwrap_or(CHUNK_MAX);
176+
let cut = split_point(&current);
178177
let rest = current.split_off(cut);
179178
chunks.push(std::mem::take(&mut current));
180179
current = rest.trim_start().to_owned();
@@ -186,6 +185,32 @@ fn chunk(text: &str) -> Vec<String> {
186185
chunks
187186
}
188187

188+
/// Byte index at or below `CHUNK_MAX` at which to split a too-long chunk.
189+
///
190+
/// Prefers the last whitespace boundary within the limit and always lands on a
191+
/// UTF-8 char boundary, so a chunk containing multi-byte characters cannot
192+
/// trigger a panic when sliced. The result is at least 1, so the split loop
193+
/// always makes progress.
194+
fn split_point(s: &str) -> usize {
195+
if let Some((idx, _)) = s
196+
.char_indices()
197+
.take_while(|(i, _)| *i <= CHUNK_MAX)
198+
.filter(|(_, c)| c.is_whitespace())
199+
.last()
200+
{
201+
if idx > 0 {
202+
return idx;
203+
}
204+
}
205+
// No usable whitespace in range: fall back to the largest char boundary
206+
// at or below the limit.
207+
let mut cut = CHUNK_MAX;
208+
while cut > 1 && !s.is_char_boundary(cut) {
209+
cut -= 1;
210+
}
211+
cut
212+
}
213+
189214
/// Heuristic named-entity extraction: runs of capitalized words.
190215
///
191216
/// A real pipeline would use an NER model or an LLM here; runs of TitleCase
@@ -258,4 +283,19 @@ mod tests {
258283
assert_eq!(chunks.len(), 1);
259284
assert!(chunks[0].contains("para three"));
260285
}
286+
287+
#[test]
288+
fn chunking_splits_multibyte_text_without_panic() {
289+
// A whitespace-free run of 3-byte characters: byte CHUNK_MAX lands
290+
// mid-character, which a naive byte slice would panic on.
291+
let text = "\u{4f60}".repeat(400); // 1200 bytes, 400 chars
292+
let chunks = chunk(&text);
293+
assert!(
294+
chunks.len() > 1,
295+
"expected a hard split, got {}",
296+
chunks.len()
297+
);
298+
// No whitespace in the input, so the split is lossless.
299+
assert_eq!(chunks.concat().chars().count(), 400);
300+
}
261301
}

0 commit comments

Comments
 (0)