Instructions to use FerrellSyntheticIntelligence/fsi-anomaly with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use FerrellSyntheticIntelligence/fsi-anomaly with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf FerrellSyntheticIntelligence/fsi-anomaly # Run inference directly in the terminal: llama cli -hf FerrellSyntheticIntelligence/fsi-anomaly
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf FerrellSyntheticIntelligence/fsi-anomaly # Run inference directly in the terminal: llama cli -hf FerrellSyntheticIntelligence/fsi-anomaly
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf FerrellSyntheticIntelligence/fsi-anomaly # Run inference directly in the terminal: ./llama-cli -hf FerrellSyntheticIntelligence/fsi-anomaly
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf FerrellSyntheticIntelligence/fsi-anomaly # Run inference directly in the terminal: ./build/bin/llama-cli -hf FerrellSyntheticIntelligence/fsi-anomaly
Use Docker
docker model run hf.co/FerrellSyntheticIntelligence/fsi-anomaly
- LM Studio
- Jan
- Ollama
How to use FerrellSyntheticIntelligence/fsi-anomaly with Ollama:
ollama run hf.co/FerrellSyntheticIntelligence/fsi-anomaly
- Unsloth Desktop
- Docker Model Runner
How to use FerrellSyntheticIntelligence/fsi-anomaly with Docker Model Runner:
docker model run hf.co/FerrellSyntheticIntelligence/fsi-anomaly
- Lemonade
How to use FerrellSyntheticIntelligence/fsi-anomaly with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull FerrellSyntheticIntelligence/fsi-anomaly
Run and chat with the model
lemonade run user.fsi-anomaly-{{QUANT_TAG}}List all available models
lemonade list
- Atomic Chat
| """Claim Graph β deterministic contradiction elevation for the FSI suit. | |
| The model does NOT resolve contradictions. The system SURFACES them: | |
| "Doc A says X. Doc B says NOT X." β elevated CONTRADICTION card for the human. | |
| Big-tech basis (tiny-model-suit): claim graph extraction + provenance tiering. | |
| Deterministic, no model call needed for the elevation step. | |
| """ | |
| import re | |
| import json | |
| from collections import defaultdict | |
| from dataclasses import dataclass, asdict | |
| from typing import List, Dict, Optional | |
| from pathlib import Path | |
| # --- Claim extraction (deterministic) --- | |
| CLAIM_PATTERNS = [ | |
| # "X is Y" / "X was Y" / "X will be Y" | |
| (re.compile(r'\b([A-Z][a-zA-Z\s]{2,50}?)\s+(is|was|will be|has been|had been)\s+([^.]{5,100})', re.IGNORECASE), 'assertion'), | |
| # "X said Y" / "X stated Y" / "X claimed Y" | |
| (re.compile(r'\b([A-Z][a-zA-Z\s]{2,50}?)\s+(said|stated|claimed|asserted|reported|wrote)\s+(?:that\s+)?([^.]{5,100})', re.IGNORECASE), 'attribution'), | |
| # Time expressions: "at 9am", "ended at 11am", "ran until noon", "by 5pm" - FULL sentence capture | |
| (re.compile(r'(?:^|[.!?]\s+)([^.]*?(?:at|by|until|ended at|ran until)\s+\d{1,2}:\d{2}|\d{1,2}(?:am|pm)|noon|midnight)[^.]*\.', re.IGNORECASE), 'time'), | |
| # Numbers/dates that are checkable | |
| (re.compile(r'\b(\d{1,3}(?:,\d{3})*(?:\.\d+)?%?)\b'), 'number'), | |
| (re.compile(r'\b(?:19|20)\d{2}\b'), 'year'), | |
| # Quoted claims | |
| (re.compile(r'"([^"]{10,200})"'), 'quote'), | |
| ] | |
| # Key nouns that indicate the SUBJECT of a claim (for cross-doc matching) | |
| SUBJECT_NOUNS = { | |
| 'meeting', 'budget', 'file', 'incident', 'report', 'document', 'memo', 'record', | |
| 'account', 'statement', 'testimony', 'evidence', 'claim', 'assertion', 'allegation', | |
| 'event', 'occurrence', 'deletion', 'modification', 'change', 'update', 'revision', | |
| 'investigation', 'audit', 'review', 'analysis', 'study', 'survey', 'assessment', | |
| 'timeline', 'schedule', 'agenda', 'minutes', 'transcript', 'recording', 'log', | |
| 'entry', 'transaction', 'transfer', 'payment', 'deposit', 'withdrawal', 'balance', | |
| 'amount', 'total', 'sum', 'figure', 'number', 'value', 'cost', 'price', 'fee', | |
| 'date', 'time', 'year', 'month', 'day', 'hour', 'minute', 'deadline', 'period', | |
| 'person', 'individual', 'official', 'witness', 'source', 'author', 'speaker', | |
| 'agency', 'department', 'organization', 'company', 'institution', 'government', | |
| 'program', 'project', 'operation', 'initiative', 'policy', 'rule', 'regulation', | |
| 'law', 'statute', 'order', 'directive', 'memo', 'memorandum', 'letter', 'email', | |
| 'message', 'communication', 'notification', 'alert', 'warning', 'report', 'filing' | |
| } | |
| class Claim: | |
| text: str | |
| claim_type: str | |
| source_doc: str | |
| source_id: str | |
| entities: List[str] | |
| subjects: List[str] | |
| values: List[str] | |
| times: List[str] | |
| span_start: int | |
| span_end: int | |
| class Contradiction: | |
| claim_a: Claim | |
| claim_b: Claim | |
| contradiction_type: str # 'direct' | 'numeric' | 'temporal' | 'attribution' | 'time' | |
| severity: str # 'high' | 'medium' | 'low' | |
| explanation: str | |
| def extract_entities(text: str) -> List[str]: | |
| """Extract proper nouns and key entities from text.""" | |
| words = re.findall(r'\b[A-Z][a-zA-Z]{2,}\b', text) | |
| stop = {'The', 'This', 'That', 'These', 'Those', 'It', 'He', 'She', 'We', 'They', 'You', 'I', | |
| 'At', 'By', 'Until', 'Ended', 'Ran', 'Official', 'Witness', 'Stated', 'Said'} | |
| return list(set(w for w in words if w not in stop)) | |
| def extract_subjects(text: str) -> List[str]: | |
| """Extract subject nouns (meeting, budget, file, etc.) for cross-doc matching.""" | |
| text_lower = text.lower() | |
| subjects = [] | |
| for noun in SUBJECT_NOUNS: | |
| if noun in text_lower: | |
| subjects.append(noun) | |
| return subjects | |
| def extract_values(text: str) -> List[str]: | |
| """Extract checkable values: numbers, years, percentages.""" | |
| values = [] | |
| values.extend(re.findall(r'\b\d{1,3}(?:,\d{3})*(?:\.\d+)?%?\b', text)) | |
| values.extend(re.findall(r'\b(?:19|20)\d{2}\b', text)) | |
| return values | |
| def extract_times(text: str) -> List[str]: | |
| """Extract time expressions.""" | |
| times = re.findall(r'\d{1,2}:\d{2}|\d{1,2}(?:am|pm)|noon|midnight', text, re.IGNORECASE) | |
| return [t.lower() for t in times] | |
| def extract_claims(text: str, source_doc: str, source_id: str) -> List[Claim]: | |
| """Extract atomic claims from document text.""" | |
| claims = [] | |
| seen_texts = set() | |
| for pattern, ctype in CLAIM_PATTERNS: | |
| for match in pattern.finditer(text): | |
| claim_text = match.group(0).strip() | |
| if len(claim_text) < 15: | |
| continue | |
| # Deduplicate by normalized text | |
| norm_text = re.sub(r'\s+', ' ', claim_text.lower()) | |
| if norm_text in seen_texts: | |
| continue | |
| seen_texts.add(norm_text) | |
| entities = extract_entities(claim_text) | |
| subjects = extract_subjects(claim_text) | |
| values = extract_values(claim_text) | |
| times = extract_times(claim_text) | |
| claims.append(Claim( | |
| text=claim_text, | |
| claim_type=ctype, | |
| source_doc=source_doc, | |
| source_id=source_id, | |
| entities=entities, | |
| subjects=subjects, | |
| values=values, | |
| times=times, | |
| span_start=match.start(), | |
| span_end=match.end() | |
| )) | |
| return claims | |
| def claims_overlap(claim_a: Claim, claim_b: Claim) -> bool: | |
| """Check if two claims are about the same subject.""" | |
| # Check subject overlap (primary for cross-doc matching) | |
| shared_subjects = set(claim_a.subjects) & set(claim_b.subjects) | |
| if shared_subjects: | |
| return True | |
| # Also check entity overlap | |
| shared_entities = set(claim_a.entities) & set(claim_b.entities) | |
| if shared_entities: | |
| return True | |
| # Also check value overlap for numeric claims | |
| if claim_a.values and claim_b.values: | |
| shared_values = set(claim_a.values) & set(claim_b.values) | |
| if shared_values: | |
| return True | |
| return False | |
| def detect_contradiction(claim_a: Claim, claim_b: Claim) -> Optional[Contradiction]: | |
| """Detect if two claims about the same subject contradict.""" | |
| if not claims_overlap(claim_a, claim_b): | |
| return None | |
| text_a = claim_a.text.lower() | |
| text_b = claim_b.text.lower() | |
| # Direct negation patterns | |
| negations = [ | |
| (r'\bis\b', r'\bis not\b|\bwas not\b|\bwill not be\b'), | |
| (r'\bwas\b', r'\bwas not\b|\bis not\b'), | |
| (r'\bhas\b', r'\bhas not\b|\bhave not\b'), | |
| (r'\bcan\b', r'\bcannot\b|\bcan\'t\b'), | |
| (r'\bwill\b', r'\bwill not\b|\bwon\'t\b'), | |
| (r'\btrue\b', r'\bfalse\b'), | |
| (r'\byes\b', r'\bno\b'), | |
| ] | |
| for pos, neg in negations: | |
| if re.search(pos, text_a) and re.search(neg, text_b): | |
| return Contradiction( | |
| claim_a=claim_a, claim_b=claim_b, | |
| contradiction_type='direct', | |
| severity='high', | |
| explanation=f"Direct negation: '{claim_a.text[:80]}...' vs '{claim_b.text[:80]}...'" | |
| ) | |
| if re.search(pos, text_b) and re.search(neg, text_a): | |
| return Contradiction( | |
| claim_a=claim_a, claim_b=claim_b, | |
| contradiction_type='direct', | |
| severity='high', | |
| explanation=f"Direct negation: '{claim_a.text[:80]}...' vs '{claim_b.text[:80]}...'" | |
| ) | |
| # Numeric contradiction (same subject, different numbers) | |
| if claim_a.values and claim_b.values: | |
| shared_vals = set(claim_a.values) & set(claim_b.values) | |
| a_only = set(claim_a.values) - set(claim_b.values) | |
| b_only = set(claim_b.values) - set(claim_a.values) | |
| if a_only and b_only and not shared_vals: | |
| return Contradiction( | |
| claim_a=claim_a, claim_b=claim_b, | |
| contradiction_type='numeric', | |
| severity='high', | |
| explanation=f"Conflicting values: {claim_a.values} vs {claim_b.values}" | |
| ) | |
| # Time contradiction (same subject, different times) | |
| if claim_a.times and claim_b.times: | |
| shared_times = set(claim_a.times) & set(claim_b.times) | |
| a_only = set(claim_a.times) - set(claim_b.times) | |
| b_only = set(claim_b.times) - set(claim_a.times) | |
| if a_only and b_only and not shared_times: | |
| return Contradiction( | |
| claim_a=claim_a, claim_b=claim_b, | |
| contradiction_type='time', | |
| severity='high', | |
| explanation=f"Conflicting times: {claim_a.times} vs {claim_b.times}" | |
| ) | |
| # Temporal contradiction (same subject, different dates) | |
| years_a = [v for v in claim_a.values if re.match(r'^(?:19|20)\d{2}$', v)] | |
| years_b = [v for v in claim_b.values if re.match(r'^(?:19|20)\d{2}$', v)] | |
| if years_a and years_b: | |
| if set(years_a) != set(years_b): | |
| return Contradiction( | |
| claim_a=claim_a, claim_b=claim_b, | |
| contradiction_type='temporal', | |
| severity='medium', | |
| explanation=f"Conflicting dates: {years_a} vs {years_b}" | |
| ) | |
| # Attribution contradiction (same quote, different sources) | |
| if claim_a.claim_type == 'attribution' and claim_b.claim_type == 'attribution': | |
| if claim_a.text == claim_b.text and claim_a.source_id != claim_b.source_id: | |
| return Contradiction( | |
| claim_a=claim_a, claim_b=claim_b, | |
| contradiction_type='attribution', | |
| severity='medium', | |
| explanation=f"Same claim attributed to different sources: {claim_a.source_id} vs {claim_b.source_id}" | |
| ) | |
| return None | |
| def build_claim_graph(documents: Dict[str, str]) -> Dict: | |
| """Build claim graph from documents, return all claims and contradictions. | |
| Args: | |
| documents: {source_id: text} mapping | |
| Returns: | |
| { | |
| 'claims': [...], | |
| 'contradictions': [...], | |
| 'by_subject': {subject: [claim_ids]}, | |
| 'by_entity': {entity: [claim_ids]}, | |
| 'by_source': {source_id: [claim_ids]} | |
| } | |
| """ | |
| all_claims = [] | |
| claim_id = 0 | |
| by_subject = defaultdict(list) | |
| by_entity = defaultdict(list) | |
| by_source = defaultdict(list) | |
| for source_id, text in documents.items(): | |
| claims = extract_claims(text, source_id, source_id) | |
| for claim in claims: | |
| claim_dict = asdict(claim) | |
| claim_dict['id'] = claim_id | |
| all_claims.append(claim_dict) | |
| for subject in claim.subjects: | |
| by_subject[subject].append(claim_id) | |
| for entity in claim.entities: | |
| by_entity[entity].append(claim_id) | |
| by_source[source_id].append(claim_id) | |
| claim_id += 1 | |
| # Find contradictions | |
| contradictions = [] | |
| for i, claim_a in enumerate(all_claims): | |
| for j, claim_b in enumerate(all_claims[i+1:], i+1): | |
| if claim_a['source_id'] == claim_b['source_id']: | |
| continue # Same source, skip | |
| contr = detect_contradiction( | |
| Claim(**{k:v for k,v in claim_a.items() if k!='id'}), | |
| Claim(**{k:v for k,v in claim_b.items() if k!='id'}) | |
| ) | |
| if contr: | |
| c_dict = asdict(contr) | |
| c_dict['claim_a_id'] = i | |
| c_dict['claim_b_id'] = j | |
| contradictions.append(c_dict) | |
| return { | |
| 'claims': all_claims, | |
| 'contradictions': contradictions, | |
| 'by_subject': dict(by_subject), | |
| 'by_entity': dict(by_entity), | |
| 'by_source': dict(by_source), | |
| 'stats': { | |
| 'total_claims': len(all_claims), | |
| 'total_contradictions': len(contradictions), | |
| 'by_type': defaultdict(int, {c['contradiction_type']: 1 for c in contradictions}) | |
| } | |
| } | |
| def format_contradiction_card(contradiction: Dict, claims: List[Dict]) -> str: | |
| """Format a contradiction as a human-readable card for the TUI.""" | |
| a = claims[contradiction['claim_a_id']] | |
| b = claims[contradiction['claim_b_id']] | |
| severity_marker = {'high': 'π΄', 'medium': 'π‘', 'low': 'π’'}.get(contradiction['severity'], 'βͺ') | |
| card = f""" | |
| {severity_marker} CONTRADICTION [{contradiction['contradiction_type'].upper()}] {severity_marker} | |
| ββββββββββββββββββββββββββββββββββββββββ | |
| Source A ({a['source_id']}): | |
| {a['text'][:120]}... | |
| Source B ({b['source_id']}): | |
| {b['text'][:120]}... | |
| Explanation: {contradiction['explanation']} | |
| Shared Subjects: {', '.join(set(a['subjects']) & set(b['subjects']))} | |
| Shared Entities: {', '.join(set(a['entities']) & set(b['entities']))} | |
| Values A: {a['values'] or 'none'} | |
| Values B: {b['values'] or 'none'} | |
| Times A: {a['times'] or 'none'} | |
| Times B: {b['times'] or 'none'} | |
| """ | |
| return card | |
| if __name__ == "__main__": | |
| # Demo with test documents | |
| test_docs = { | |
| "doc_a": "The meeting started at 9am and ended at 11am. Official John Smith stated the budget was $50 million.", | |
| "doc_b": "The meeting started at 9am and ran until noon. Witness Jane Doe said the budget was $75 million.", | |
| "doc_c": "The 1993 incident file was deleted. Agency reports confirm the deletion occurred in 1995.", | |
| } | |
| graph = build_claim_graph(test_docs) | |
| print(f"Claims extracted: {graph['stats']['total_claims']}") | |
| print(f"Contradictions found: {graph['stats']['total_contradictions']}") | |
| print(f"By type: {dict(graph['stats']['by_type'])}") | |
| print() | |
| for contr in graph['contradictions']: | |
| print(format_contradiction_card(contr, graph['claims'])) | |