rnsr 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.
Files changed (72) hide show
  1. rnsr/__init__.py +118 -0
  2. rnsr/__main__.py +242 -0
  3. rnsr/agent/__init__.py +218 -0
  4. rnsr/agent/cross_doc_navigator.py +767 -0
  5. rnsr/agent/graph.py +1557 -0
  6. rnsr/agent/llm_cache.py +575 -0
  7. rnsr/agent/navigator_api.py +497 -0
  8. rnsr/agent/provenance.py +772 -0
  9. rnsr/agent/query_clarifier.py +617 -0
  10. rnsr/agent/reasoning_memory.py +736 -0
  11. rnsr/agent/repl_env.py +709 -0
  12. rnsr/agent/rlm_navigator.py +2108 -0
  13. rnsr/agent/self_reflection.py +602 -0
  14. rnsr/agent/variable_store.py +308 -0
  15. rnsr/benchmarks/__init__.py +118 -0
  16. rnsr/benchmarks/comprehensive_benchmark.py +733 -0
  17. rnsr/benchmarks/evaluation_suite.py +1210 -0
  18. rnsr/benchmarks/finance_bench.py +147 -0
  19. rnsr/benchmarks/pdf_merger.py +178 -0
  20. rnsr/benchmarks/performance.py +321 -0
  21. rnsr/benchmarks/quality.py +321 -0
  22. rnsr/benchmarks/runner.py +298 -0
  23. rnsr/benchmarks/standard_benchmarks.py +995 -0
  24. rnsr/client.py +560 -0
  25. rnsr/document_store.py +394 -0
  26. rnsr/exceptions.py +74 -0
  27. rnsr/extraction/__init__.py +172 -0
  28. rnsr/extraction/candidate_extractor.py +357 -0
  29. rnsr/extraction/entity_extractor.py +581 -0
  30. rnsr/extraction/entity_linker.py +825 -0
  31. rnsr/extraction/grounded_extractor.py +722 -0
  32. rnsr/extraction/learned_types.py +599 -0
  33. rnsr/extraction/models.py +232 -0
  34. rnsr/extraction/relationship_extractor.py +600 -0
  35. rnsr/extraction/relationship_patterns.py +511 -0
  36. rnsr/extraction/relationship_validator.py +392 -0
  37. rnsr/extraction/rlm_extractor.py +589 -0
  38. rnsr/extraction/rlm_unified_extractor.py +990 -0
  39. rnsr/extraction/tot_validator.py +610 -0
  40. rnsr/extraction/unified_extractor.py +342 -0
  41. rnsr/indexing/__init__.py +60 -0
  42. rnsr/indexing/knowledge_graph.py +1128 -0
  43. rnsr/indexing/kv_store.py +313 -0
  44. rnsr/indexing/persistence.py +323 -0
  45. rnsr/indexing/semantic_retriever.py +237 -0
  46. rnsr/indexing/semantic_search.py +320 -0
  47. rnsr/indexing/skeleton_index.py +395 -0
  48. rnsr/ingestion/__init__.py +161 -0
  49. rnsr/ingestion/chart_parser.py +569 -0
  50. rnsr/ingestion/document_boundary.py +662 -0
  51. rnsr/ingestion/font_histogram.py +334 -0
  52. rnsr/ingestion/header_classifier.py +595 -0
  53. rnsr/ingestion/hierarchical_cluster.py +515 -0
  54. rnsr/ingestion/layout_detector.py +356 -0
  55. rnsr/ingestion/layout_model.py +379 -0
  56. rnsr/ingestion/ocr_fallback.py +177 -0
  57. rnsr/ingestion/pipeline.py +936 -0
  58. rnsr/ingestion/semantic_fallback.py +417 -0
  59. rnsr/ingestion/table_parser.py +799 -0
  60. rnsr/ingestion/text_builder.py +460 -0
  61. rnsr/ingestion/tree_builder.py +402 -0
  62. rnsr/ingestion/vision_retrieval.py +965 -0
  63. rnsr/ingestion/xy_cut.py +555 -0
  64. rnsr/llm.py +733 -0
  65. rnsr/models.py +167 -0
  66. rnsr/py.typed +2 -0
  67. rnsr-0.1.0.dist-info/METADATA +592 -0
  68. rnsr-0.1.0.dist-info/RECORD +72 -0
  69. rnsr-0.1.0.dist-info/WHEEL +5 -0
  70. rnsr-0.1.0.dist-info/entry_points.txt +2 -0
  71. rnsr-0.1.0.dist-info/licenses/LICENSE +21 -0
  72. rnsr-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,600 @@
1
+ """
2
+ RNSR Relationship Extractor
3
+
4
+ DEPRECATED: This extractor uses LLM-first approach which can hallucinate.
5
+ Use RLMUnifiedExtractor instead for grounded, accurate extraction.
6
+
7
+ LLM-based relationship extraction between entities and sections.
8
+ Extracts temporal, causal, semantic, and reference relationships.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import re
15
+ import time
16
+ import warnings
17
+ from typing import Any
18
+
19
+ import structlog
20
+
21
+ from rnsr.extraction.models import (
22
+ Entity,
23
+ EntityType,
24
+ Relationship,
25
+ RelationType,
26
+ )
27
+ from rnsr.llm import get_llm
28
+
29
+ logger = structlog.get_logger(__name__)
30
+
31
+ # Deprecation warning
32
+ _DEPRECATION_WARNING = """
33
+ RelationshipExtractor is deprecated and may hallucinate relationships.
34
+ Use RLMUnifiedExtractor instead for grounded, accurate extraction:
35
+
36
+ from rnsr.extraction import RLMUnifiedExtractor
37
+ extractor = RLMUnifiedExtractor()
38
+ result = extractor.extract(node_id, doc_id, header, content)
39
+ # result.relationships contains validated relationships
40
+ """
41
+
42
+
43
+ # Relationship extraction prompt template
44
+ RELATIONSHIP_EXTRACTION_PROMPT = """You are an expert relationship extractor for legal and business documents.
45
+
46
+ Analyze the following document section and extract relationships between:
47
+ 1. Entities (people, organizations, dates, events, legal concepts)
48
+ 2. This section and other referenced sections/documents
49
+
50
+ Document Section:
51
+ ---
52
+ {content}
53
+ ---
54
+
55
+ Section ID: {node_id}
56
+ Document ID: {doc_id}
57
+ Section Header: {header}
58
+
59
+ Known entities in this section:
60
+ {entities_json}
61
+
62
+ Extract relationships of the following types:
63
+
64
+ ENTITY-TO-ENTITY RELATIONSHIPS:
65
+ - TEMPORAL_BEFORE: Event/date X occurred before Event/date Y
66
+ - TEMPORAL_AFTER: Event/date X occurred after Event/date Y
67
+ - CAUSAL: Action X caused/led to Outcome Y (e.g., breach led to damages)
68
+ - AFFILIATED_WITH: Person X is affiliated with Organization Y
69
+ - PARTY_TO: Entity X is party to Document/Event Y (e.g., signatory, defendant)
70
+
71
+ SECTION/DOCUMENT RELATIONSHIPS:
72
+ - SUPPORTS: This section supports a claim or finding in another section
73
+ - CONTRADICTS: This section contradicts another section
74
+ - REFERENCES: This section references another document or section (exhibit, citation)
75
+ - SUPERSEDES: This section supersedes/overrides another
76
+ - AMENDS: This section amends another section/document
77
+
78
+ For each relationship, provide:
79
+ 1. type: One of the types above
80
+ 2. source_id: ID of the source entity (from the known entities list) or "{node_id}" for this section
81
+ 3. source_type: "entity" or "node"
82
+ 4. target_id: ID of the target entity or node ID of the target section (use descriptive placeholder if referencing external doc, e.g., "exhibit_a")
83
+ 5. target_type: "entity" or "node"
84
+ 6. confidence: 0.0-1.0 based on how explicit the relationship is
85
+ 7. evidence: The exact quote that establishes this relationship
86
+
87
+ Return your response as a JSON array:
88
+ ```json
89
+ [
90
+ {{
91
+ "type": "CAUSAL",
92
+ "source_id": "ent_abc123",
93
+ "source_type": "entity",
94
+ "target_id": "ent_def456",
95
+ "target_type": "entity",
96
+ "confidence": 0.9,
97
+ "evidence": "The breach of contract by Defendant led to significant damages..."
98
+ }},
99
+ {{
100
+ "type": "REFERENCES",
101
+ "source_id": "{node_id}",
102
+ "source_type": "node",
103
+ "target_id": "exhibit_a",
104
+ "target_type": "node",
105
+ "confidence": 1.0,
106
+ "evidence": "As shown in Exhibit A..."
107
+ }}
108
+ ]
109
+ ```
110
+
111
+ If no relationships are found, return an empty array: []
112
+
113
+ Important:
114
+ - Only extract relationships that are explicitly stated or strongly implied
115
+ - Include the exact quote as evidence
116
+ - Use entity IDs from the provided list when possible
117
+ - For temporal relationships, be precise about the direction (BEFORE vs AFTER)
118
+ - For causal relationships, the source is the cause and target is the effect
119
+ """
120
+
121
+
122
+ class RelationshipExtractor:
123
+ """
124
+ DEPRECATED: Extracts relationships from document sections using LLM-first approach.
125
+
126
+ This extractor can hallucinate relationships. Use RLMUnifiedExtractor instead.
127
+
128
+ Identifies connections between:
129
+ - Entities (temporal, causal, affiliation)
130
+ - Sections (supports, contradicts, references)
131
+ - Documents (cross-references, amendments)
132
+ """
133
+
134
+ def __init__(
135
+ self,
136
+ llm: Any | None = None,
137
+ min_content_length: int = 50,
138
+ max_content_length: int = 8000,
139
+ suppress_deprecation_warning: bool = False,
140
+ ):
141
+ # Emit deprecation warning
142
+ if not suppress_deprecation_warning:
143
+ warnings.warn(
144
+ _DEPRECATION_WARNING,
145
+ DeprecationWarning,
146
+ stacklevel=2,
147
+ )
148
+ logger.warning("deprecated_extractor_used", extractor="RelationshipExtractor")
149
+ """
150
+ Initialize the relationship extractor.
151
+
152
+ Args:
153
+ llm: LLM instance to use. If None, uses get_llm().
154
+ min_content_length: Minimum content length to process.
155
+ max_content_length: Maximum content length per extraction call.
156
+ """
157
+ self.llm = llm or get_llm()
158
+ self.min_content_length = min_content_length
159
+ self.max_content_length = max_content_length
160
+
161
+ # Cache for extracted relationships (node_id -> relationships)
162
+ self._cache: dict[str, list[Relationship]] = {}
163
+
164
+ def extract_from_node(
165
+ self,
166
+ node_id: str,
167
+ doc_id: str,
168
+ header: str,
169
+ content: str,
170
+ entities: list[Entity],
171
+ ) -> list[Relationship]:
172
+ """
173
+ Extract relationships from a single document node.
174
+
175
+ Args:
176
+ node_id: Skeleton node ID.
177
+ doc_id: Document ID.
178
+ header: Section header text.
179
+ content: Full section content.
180
+ entities: Entities already extracted from this node.
181
+
182
+ Returns:
183
+ List of extracted Relationship objects.
184
+ """
185
+ start_time = time.time()
186
+
187
+ # Skip very short content
188
+ if len(content.strip()) < self.min_content_length:
189
+ logger.debug(
190
+ "skipping_short_content",
191
+ node_id=node_id,
192
+ content_length=len(content),
193
+ )
194
+ return []
195
+
196
+ # Check cache
197
+ cache_key = f"{doc_id}:{node_id}"
198
+ if cache_key in self._cache:
199
+ logger.debug("using_cached_relationships", node_id=node_id)
200
+ return self._cache[cache_key]
201
+
202
+ # Truncate content if too long
203
+ if len(content) > self.max_content_length:
204
+ content = content[:self.max_content_length] + "..."
205
+
206
+ try:
207
+ relationships = self._extract_with_llm(
208
+ node_id=node_id,
209
+ doc_id=doc_id,
210
+ header=header,
211
+ content=content,
212
+ entities=entities,
213
+ )
214
+
215
+ # Cache results
216
+ self._cache[cache_key] = relationships
217
+
218
+ except Exception as e:
219
+ logger.error(
220
+ "relationship_extraction_failed",
221
+ node_id=node_id,
222
+ error=str(e),
223
+ )
224
+ relationships = []
225
+
226
+ processing_time_ms = (time.time() - start_time) * 1000
227
+
228
+ logger.info(
229
+ "relationships_extracted",
230
+ node_id=node_id,
231
+ relationship_count=len(relationships),
232
+ processing_time_ms=processing_time_ms,
233
+ )
234
+
235
+ return relationships
236
+
237
+ def _extract_with_llm(
238
+ self,
239
+ node_id: str,
240
+ doc_id: str,
241
+ header: str,
242
+ content: str,
243
+ entities: list[Entity],
244
+ ) -> list[Relationship]:
245
+ """
246
+ Use LLM to extract relationships from content.
247
+
248
+ Args:
249
+ node_id: Skeleton node ID.
250
+ doc_id: Document ID.
251
+ header: Section header.
252
+ content: Section content.
253
+ entities: Known entities in this section.
254
+
255
+ Returns:
256
+ List of extracted Relationship objects.
257
+ """
258
+ # Format entities for prompt
259
+ entities_json = json.dumps([
260
+ {
261
+ "id": e.id,
262
+ "type": e.type.value,
263
+ "name": e.canonical_name,
264
+ "aliases": e.aliases,
265
+ }
266
+ for e in entities
267
+ ], indent=2)
268
+
269
+ prompt = RELATIONSHIP_EXTRACTION_PROMPT.format(
270
+ content=content,
271
+ node_id=node_id,
272
+ doc_id=doc_id,
273
+ header=header,
274
+ entities_json=entities_json,
275
+ )
276
+
277
+ # Call LLM
278
+ response = self.llm.complete(prompt)
279
+ response_text = str(response) if not isinstance(response, str) else response
280
+
281
+ # Parse JSON from response
282
+ relationships = self._parse_llm_response(
283
+ response_text=response_text,
284
+ node_id=node_id,
285
+ doc_id=doc_id,
286
+ entities=entities,
287
+ )
288
+
289
+ return relationships
290
+
291
+ def _parse_llm_response(
292
+ self,
293
+ response_text: str,
294
+ node_id: str,
295
+ doc_id: str,
296
+ entities: list[Entity],
297
+ ) -> list[Relationship]:
298
+ """
299
+ Parse LLM response into Relationship objects.
300
+
301
+ Args:
302
+ response_text: Raw LLM response.
303
+ node_id: Source node ID.
304
+ doc_id: Source document ID.
305
+ entities: Known entities.
306
+
307
+ Returns:
308
+ List of Relationship objects.
309
+ """
310
+ # Extract JSON from response
311
+ json_match = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', response_text)
312
+ if json_match:
313
+ json_str = json_match.group(1)
314
+ else:
315
+ json_match = re.search(r'\[[\s\S]*\]', response_text)
316
+ if json_match:
317
+ json_str = json_match.group(0)
318
+ else:
319
+ logger.warning(
320
+ "no_json_found_in_response",
321
+ response_preview=response_text[:200],
322
+ )
323
+ return []
324
+
325
+ try:
326
+ raw_relationships = json.loads(json_str)
327
+ except json.JSONDecodeError as e:
328
+ logger.warning(
329
+ "json_parse_error",
330
+ error=str(e),
331
+ json_preview=json_str[:200],
332
+ )
333
+ return []
334
+
335
+ if not isinstance(raw_relationships, list):
336
+ logger.warning("expected_list_of_relationships", got=type(raw_relationships).__name__)
337
+ return []
338
+
339
+ # Create a set of valid entity IDs
340
+ valid_entity_ids = {e.id for e in entities}
341
+
342
+ relationships = []
343
+ for raw in raw_relationships:
344
+ try:
345
+ relationship = self._create_relationship_from_raw(
346
+ raw=raw,
347
+ node_id=node_id,
348
+ doc_id=doc_id,
349
+ valid_entity_ids=valid_entity_ids,
350
+ )
351
+ if relationship:
352
+ relationships.append(relationship)
353
+ except Exception as e:
354
+ logger.debug(
355
+ "failed_to_create_relationship",
356
+ raw=raw,
357
+ error=str(e),
358
+ )
359
+
360
+ return relationships
361
+
362
+ def _create_relationship_from_raw(
363
+ self,
364
+ raw: dict[str, Any],
365
+ node_id: str,
366
+ doc_id: str,
367
+ valid_entity_ids: set[str],
368
+ ) -> Relationship | None:
369
+ """
370
+ Create a Relationship object from raw LLM output.
371
+
372
+ Args:
373
+ raw: Raw relationship dict from LLM.
374
+ node_id: Source node ID.
375
+ doc_id: Source document ID.
376
+ valid_entity_ids: Set of valid entity IDs.
377
+
378
+ Returns:
379
+ Relationship object or None if invalid.
380
+ """
381
+ # Parse relationship type
382
+ type_str = raw.get("type", "").upper()
383
+
384
+ type_mapping = {
385
+ "TEMPORAL_BEFORE": RelationType.TEMPORAL_BEFORE,
386
+ "TEMPORAL_AFTER": RelationType.TEMPORAL_AFTER,
387
+ "BEFORE": RelationType.TEMPORAL_BEFORE,
388
+ "AFTER": RelationType.TEMPORAL_AFTER,
389
+ "CAUSAL": RelationType.CAUSAL,
390
+ "CAUSED": RelationType.CAUSAL,
391
+ "AFFILIATED_WITH": RelationType.AFFILIATED_WITH,
392
+ "AFFILIATION": RelationType.AFFILIATED_WITH,
393
+ "PARTY_TO": RelationType.PARTY_TO,
394
+ "PARTY": RelationType.PARTY_TO,
395
+ "SUPPORTS": RelationType.SUPPORTS,
396
+ "SUPPORT": RelationType.SUPPORTS,
397
+ "CONTRADICTS": RelationType.CONTRADICTS,
398
+ "CONTRADICT": RelationType.CONTRADICTS,
399
+ "REFERENCES": RelationType.REFERENCES,
400
+ "REFERENCE": RelationType.REFERENCES,
401
+ "CITES": RelationType.REFERENCES,
402
+ "SUPERSEDES": RelationType.SUPERSEDES,
403
+ "SUPERSEDE": RelationType.SUPERSEDES,
404
+ "AMENDS": RelationType.AMENDS,
405
+ "AMEND": RelationType.AMENDS,
406
+ "MENTIONS": RelationType.MENTIONS,
407
+ "DEFINED_IN": RelationType.DEFINED_IN,
408
+ }
409
+
410
+ rel_type = type_mapping.get(type_str)
411
+ if not rel_type:
412
+ logger.debug("unknown_relationship_type", type=type_str)
413
+ return None
414
+
415
+ # Get source and target
416
+ source_id = raw.get("source_id", "")
417
+ target_id = raw.get("target_id", "")
418
+ source_type = raw.get("source_type", "entity").lower()
419
+ target_type = raw.get("target_type", "entity").lower()
420
+
421
+ if not source_id or not target_id:
422
+ return None
423
+
424
+ # Validate source_type and target_type
425
+ if source_type not in ("entity", "node"):
426
+ source_type = "entity"
427
+ if target_type not in ("entity", "node"):
428
+ target_type = "entity"
429
+
430
+ # Get confidence and evidence
431
+ confidence = raw.get("confidence", 0.8)
432
+ if not isinstance(confidence, (int, float)):
433
+ confidence = 0.8
434
+ confidence = max(0.0, min(1.0, float(confidence)))
435
+
436
+ evidence = raw.get("evidence", "").strip()
437
+
438
+ # Create relationship
439
+ relationship = Relationship(
440
+ type=rel_type,
441
+ source_id=source_id,
442
+ target_id=target_id,
443
+ source_type=source_type,
444
+ target_type=target_type,
445
+ doc_id=doc_id,
446
+ confidence=confidence,
447
+ evidence=evidence,
448
+ )
449
+
450
+ return relationship
451
+
452
+ def extract_entity_to_section_relationships(
453
+ self,
454
+ entities: list[Entity],
455
+ ) -> list[Relationship]:
456
+ """
457
+ Create MENTIONS relationships linking entities to their sections.
458
+
459
+ Args:
460
+ entities: List of entities with mentions.
461
+
462
+ Returns:
463
+ List of MENTIONS relationships.
464
+ """
465
+ relationships = []
466
+
467
+ for entity in entities:
468
+ for mention in entity.mentions:
469
+ rel = Relationship(
470
+ type=RelationType.MENTIONS,
471
+ source_id=mention.node_id,
472
+ target_id=entity.id,
473
+ source_type="node",
474
+ target_type="entity",
475
+ doc_id=mention.doc_id,
476
+ confidence=mention.confidence,
477
+ evidence=mention.context,
478
+ )
479
+ relationships.append(rel)
480
+
481
+ return relationships
482
+
483
+ def extract_batch(
484
+ self,
485
+ nodes: list[dict[str, Any]],
486
+ entities_by_node: dict[str, list[Entity]],
487
+ ) -> list[Relationship]:
488
+ """
489
+ Extract relationships from multiple nodes.
490
+
491
+ Args:
492
+ nodes: List of node dicts with keys: node_id, doc_id, header, content
493
+ entities_by_node: Mapping of node_id to entities in that node.
494
+
495
+ Returns:
496
+ List of all extracted Relationship objects.
497
+ """
498
+ all_relationships = []
499
+
500
+ for node in nodes:
501
+ node_id = node.get("node_id", "")
502
+ entities = entities_by_node.get(node_id, [])
503
+
504
+ relationships = self.extract_from_node(
505
+ node_id=node_id,
506
+ doc_id=node.get("doc_id", ""),
507
+ header=node.get("header", ""),
508
+ content=node.get("content", ""),
509
+ entities=entities,
510
+ )
511
+ all_relationships.extend(relationships)
512
+
513
+ return all_relationships
514
+
515
+ def clear_cache(self) -> None:
516
+ """Clear the relationship cache."""
517
+ self._cache.clear()
518
+
519
+
520
+ def extract_implicit_relationships(
521
+ entities: list[Entity],
522
+ doc_id: str,
523
+ ) -> list[Relationship]:
524
+ """
525
+ Extract implicit relationships based on entity metadata.
526
+
527
+ For example:
528
+ - PERSON with role "defendant" -> PARTY_TO -> any legal proceeding
529
+ - PERSON affiliated with ORGANIZATION
530
+
531
+ Args:
532
+ entities: List of entities.
533
+ doc_id: Document ID.
534
+
535
+ Returns:
536
+ List of implicit relationships.
537
+ """
538
+ relationships = []
539
+
540
+ # Group entities by type
541
+ persons = [e for e in entities if e.type == EntityType.PERSON]
542
+ orgs = [e for e in entities if e.type == EntityType.ORGANIZATION]
543
+ events = [e for e in entities if e.type == EntityType.EVENT]
544
+ documents = [e for e in entities if e.type == EntityType.DOCUMENT]
545
+
546
+ # Extract affiliations from person metadata
547
+ for person in persons:
548
+ # Check for organization affiliation in metadata
549
+ affiliated_org = person.metadata.get("organization") or person.metadata.get("employer")
550
+ if affiliated_org:
551
+ # Find matching org
552
+ for org in orgs:
553
+ if affiliated_org.lower() in org.canonical_name.lower() or any(
554
+ affiliated_org.lower() in alias.lower() for alias in org.aliases
555
+ ):
556
+ rel = Relationship(
557
+ type=RelationType.AFFILIATED_WITH,
558
+ source_id=person.id,
559
+ target_id=org.id,
560
+ source_type="entity",
561
+ target_type="entity",
562
+ doc_id=doc_id,
563
+ confidence=0.9,
564
+ evidence=f"{person.canonical_name} affiliated with {org.canonical_name}",
565
+ )
566
+ relationships.append(rel)
567
+ break
568
+
569
+ # Check for role-based party relationships
570
+ role = person.metadata.get("role", "").lower()
571
+ if role in ("defendant", "plaintiff", "respondent", "petitioner", "applicant"):
572
+ for event in events:
573
+ if any(kw in event.canonical_name.lower() for kw in ["case", "proceeding", "trial", "hearing"]):
574
+ rel = Relationship(
575
+ type=RelationType.PARTY_TO,
576
+ source_id=person.id,
577
+ target_id=event.id,
578
+ source_type="entity",
579
+ target_type="entity",
580
+ doc_id=doc_id,
581
+ confidence=0.85,
582
+ evidence=f"{person.canonical_name} is {role} in {event.canonical_name}",
583
+ )
584
+ relationships.append(rel)
585
+
586
+ for doc in documents:
587
+ if any(kw in doc.canonical_name.lower() for kw in ["complaint", "motion", "order", "judgment"]):
588
+ rel = Relationship(
589
+ type=RelationType.PARTY_TO,
590
+ source_id=person.id,
591
+ target_id=doc.id,
592
+ source_type="entity",
593
+ target_type="entity",
594
+ doc_id=doc_id,
595
+ confidence=0.85,
596
+ evidence=f"{person.canonical_name} is {role} in {doc.canonical_name}",
597
+ )
598
+ relationships.append(rel)
599
+
600
+ return relationships