Marius16 commited on
Commit
fcbc9fa
·
1 Parent(s): 31b3ba4

Persist article title, source and analyzed_at in Article node

Browse files
backend/pipeline/extraction/spacy_extractor.py CHANGED
@@ -248,11 +248,20 @@ class SpacyExtractor(AbstractExtractor):
248
 
249
  # Relation classification
250
  def _classify_relation(self, verb: Token) -> RelationType:
251
- """Determine the relation type based on the verb lemma."""
252
  lemma = verb.lemma_.lower()
253
 
 
 
 
 
 
 
 
254
  if lemma in POSITION_VERBS:
255
  return RelationType.HOLDS_POSITION
 
 
256
  elif lemma in MEMBERSHIP_VERBS:
257
  return RelationType.MEMBER_OF
258
  elif lemma in EVENT_VERBS:
 
248
 
249
  # Relation classification
250
  def _classify_relation(self, verb: Token) -> RelationType:
251
+ """Determine the relation type based on the verb lemma and compound phrases."""
252
  lemma = verb.lemma_.lower()
253
 
254
+ # Compound verb phrases take priority over single-lemma lookup
255
+ phrase = " ".join(t.lemma_.lower() for t in verb.subtree if t.dep_ in {"aux", "prt", "compound"} or t == verb)
256
+ if any(p in phrase for p in ("swear in", "take office", "be elect", "be appoint", "be inaugurate", "step down", "take over")):
257
+ return RelationType.HOLDS_POSITION
258
+ if any(p in phrase for p in ("win election", "win race", "defeat opponent")):
259
+ return RelationType.HOLDS_POSITION
260
+
261
  if lemma in POSITION_VERBS:
262
  return RelationType.HOLDS_POSITION
263
+ elif lemma in {"win", "defeat", "beat"}:
264
+ return RelationType.HOLDS_POSITION
265
  elif lemma in MEMBERSHIP_VERBS:
266
  return RelationType.MEMBER_OF
267
  elif lemma in EVENT_VERBS:
backend/pipeline/graph/neo4j_store.py CHANGED
@@ -52,13 +52,22 @@ class Neo4jTKGStore(AbstractTKGStore):
52
 
53
  # Write operations
54
 
55
- def add_fact(self, fact: TemporalFact, article_id: str | None = None) -> None:
 
 
 
 
 
 
56
  """Store a fact as two Entity nodes + a TEMPORAL_RELATION edge."""
57
  with self._driver.session() as session:
58
- session.execute_write(self._create_fact_tx, fact, article_id)
59
 
60
  @staticmethod
61
- def _create_fact_tx(tx, fact: TemporalFact, article_id: str | None) -> None:
 
 
 
62
  def to_iso(expr: TemporalExpression | None) -> str | None:
63
  if expr and expr.normalized_date:
64
  return expr.normalized_date.isoformat()
@@ -113,11 +122,27 @@ class Neo4jTKGStore(AbstractTKGStore):
113
  """, **params)
114
 
115
  if article_id:
116
- tx.run("MERGE (a:Article {article_id: $article_id})", article_id=article_id)
 
 
 
 
 
 
 
 
 
 
117
 
118
- def add_facts(self, facts: list[TemporalFact], article_id: str | None = None) -> None:
 
 
 
 
 
 
119
  for fact in facts:
120
- self.add_fact(fact, article_id)
121
  logger.info(
122
  f"Neo4jTKGStore: {len(facts)} facts written"
123
  + (f" for article {article_id}" if article_id else "")
 
52
 
53
  # Write operations
54
 
55
+ def add_fact(
56
+ self,
57
+ fact: TemporalFact,
58
+ article_id: str | None = None,
59
+ title: str | None = None,
60
+ source: str | None = None,
61
+ ) -> None:
62
  """Store a fact as two Entity nodes + a TEMPORAL_RELATION edge."""
63
  with self._driver.session() as session:
64
+ session.execute_write(self._create_fact_tx, fact, article_id, title, source)
65
 
66
  @staticmethod
67
+ def _create_fact_tx(
68
+ tx, fact: TemporalFact, article_id: str | None,
69
+ title: str | None = None, source: str | None = None,
70
+ ) -> None:
71
  def to_iso(expr: TemporalExpression | None) -> str | None:
72
  if expr and expr.normalized_date:
73
  return expr.normalized_date.isoformat()
 
122
  """, **params)
123
 
124
  if article_id:
125
+ tx.run("""
126
+ MERGE (a:Article {article_id: $article_id})
127
+ ON CREATE SET a.title = $title,
128
+ a.source = $source,
129
+ a.analyzed_at = $analyzed_at
130
+ """,
131
+ article_id=article_id,
132
+ title=title or "",
133
+ source=source or "",
134
+ analyzed_at=datetime.utcnow().isoformat(),
135
+ )
136
 
137
+ def add_facts(
138
+ self,
139
+ facts: list[TemporalFact],
140
+ article_id: str | None = None,
141
+ title: str | None = None,
142
+ source: str | None = None,
143
+ ) -> None:
144
  for fact in facts:
145
+ self.add_fact(fact, article_id, title=title, source=source)
146
  logger.info(
147
  f"Neo4jTKGStore: {len(facts)} facts written"
148
  + (f" for article {article_id}" if article_id else "")
backend/pipeline/orchestrator.py CHANGED
@@ -151,7 +151,12 @@ class PipelineOrchestrator:
151
  # Persist facts after scoring
152
  if self._persistent_store and article_id:
153
  try:
154
- self._persistent_store.add_facts(all_facts, article_id=article_id)
 
 
 
 
 
155
  logger.info(f"Neo4j: {len(all_facts)} facts persisted (article_id={article_id})")
156
  except Exception as e:
157
  logger.error(f"Neo4j persistence failed: {e}")
 
151
  # Persist facts after scoring
152
  if self._persistent_store and article_id:
153
  try:
154
+ self._persistent_store.add_facts(
155
+ all_facts,
156
+ article_id=article_id,
157
+ title=article.title or "",
158
+ source=article.source or "",
159
+ )
160
  logger.info(f"Neo4j: {len(all_facts)} facts persisted (article_id={article_id})")
161
  except Exception as e:
162
  logger.error(f"Neo4j persistence failed: {e}")
backend/pipeline/scoring/tcs.py CHANGED
@@ -42,10 +42,10 @@ class TCSCalculator:
42
 
43
  # Severity weights
44
  SEVERITY_WEIGHTS = {
 
 
45
  Severity.HIGH: 1.0,
46
- Severity.MEDIUM: 0.6,
47
- Severity.LOW: 0.3,
48
- Severity.CRITICAL: 1.0
49
  }
50
 
51
  # Weighted penalty
@@ -53,19 +53,21 @@ class TCSCalculator:
53
  # Worst-case: every claim has a critical inconsistency
54
  max_possible_penalty = n_claims * max(SEVERITY_WEIGHTS.values())
55
 
56
- # Coverage factor: proportion of facts verified; clamped to min 0.3
57
  if facts_total > 0:
58
- coverage_factor = max(0.3, facts_verified / facts_total)
 
59
  else:
60
- # Default when verification coverage is unknown
61
- coverage_factor = 0.5
62
 
63
  # Main formula
64
  penalty_ratio = min(1.0, weighted_penalty / max_possible_penalty) if max_possible_penalty > 0 else 0.0
65
  raw_tcs = (1.0 - penalty_ratio) * score_coherence
66
 
67
- # Blend with neutral 0.5 on the unverified portion
68
- tcs = raw_tcs * coverage_factor + (1.0 - coverage_factor) * 0.5
 
 
69
 
70
  # Clamp
71
  tcs = max(0.0, min(1.0, tcs))
 
42
 
43
  # Severity weights
44
  SEVERITY_WEIGHTS = {
45
+ Severity.LOW: 0.2,
46
+ Severity.MEDIUM: 0.5,
47
  Severity.HIGH: 1.0,
48
+ Severity.CRITICAL: 1.5,
 
 
49
  }
50
 
51
  # Weighted penalty
 
53
  # Worst-case: every claim has a critical inconsistency
54
  max_possible_penalty = n_claims * max(SEVERITY_WEIGHTS.values())
55
 
56
+ # Coverage factor: sqrt-softened ratio; higher coverage amplifies the raw signal
57
  if facts_total > 0:
58
+ raw_coverage = facts_verified / facts_total
59
+ coverage_factor = max(0.5, raw_coverage ** 0.5)
60
  else:
61
+ coverage_factor = 0.7
 
62
 
63
  # Main formula
64
  penalty_ratio = min(1.0, weighted_penalty / max_possible_penalty) if max_possible_penalty > 0 else 0.0
65
  raw_tcs = (1.0 - penalty_ratio) * score_coherence
66
 
67
+ # Confidence multiplier: amplify deviation from 0.5 proportionally to coverage
68
+ tcs = raw_tcs + coverage_factor * (raw_tcs - 0.5) * 0.3
69
+ if facts_verified == 0:
70
+ tcs = raw_tcs
71
 
72
  # Clamp
73
  tcs = max(0.0, min(1.0, tcs))
backend/pipeline/verification/internal.py CHANGED
@@ -228,7 +228,8 @@ class InternalVerifier:
228
  if SequenceMatcher(None, subj_h, subj_e).ratio() >= 0.85:
229
  if _is_inauguration_or_election(e_fact):
230
  point_e = _extract_point_time(e_fact)
231
- if point_e and point_e > start_h:
 
232
  inconsistencies.append(Inconsistency(
233
  inconsistency_type=InconsistencyType.IMPLICIT_CONTRADICTION,
234
  severity=Severity.MEDIUM,
 
228
  if SequenceMatcher(None, subj_h, subj_e).ratio() >= 0.85:
229
  if _is_inauguration_or_election(e_fact):
230
  point_e = _extract_point_time(e_fact)
231
+ # 180-day buffer: election -> inauguration transition is normal (up to ~6 months)
232
+ if point_e and (point_e - start_h).days > 180:
233
  inconsistencies.append(Inconsistency(
234
  inconsistency_type=InconsistencyType.IMPLICIT_CONTRADICTION,
235
  severity=Severity.MEDIUM,
frontend/src/components/ArticleHistory.tsx CHANGED
@@ -147,7 +147,9 @@ function ArticleHistory({ open, onClose }: ArticleHistoryProps): React.ReactElem
147
  <ListItemText
148
  primary={
149
  <Typography variant="body2" noWrap sx={{ fontWeight: "medium" }}>
150
- {article.title ?? <em>Untitled</em>}
 
 
151
  </Typography>
152
  }
153
  secondary={
 
147
  <ListItemText
148
  primary={
149
  <Typography variant="body2" noWrap sx={{ fontWeight: "medium" }}>
150
+ {article.title && article.title.trim()
151
+ ? article.title.trim().slice(0, 60)
152
+ : <em>Untitled</em>}
153
  </Typography>
154
  }
155
  secondary={
start.ps1 DELETED
@@ -1,149 +0,0 @@
1
- # Usage: .\start.ps1
2
- # Starts Ollama, pulls required models, verifies spaCy models,
3
- # then launches the backend (FastAPI :8000) and frontend (React :3000).
4
-
5
- $BACKEND_URL = "http://localhost:8000"
6
- $FRONTEND_URL = "http://localhost:3000"
7
-
8
- function Write-Step { param($msg) Write-Host "[*] $msg" -ForegroundColor Cyan }
9
- function Write-Ok { param($msg) Write-Host "[OK] $msg" -ForegroundColor Green }
10
- function Write-Warn { param($msg) Write-Host "[!] $msg" -ForegroundColor Yellow }
11
- function Write-Err { param($msg) Write-Host "[ERR] $msg" -ForegroundColor Red }
12
-
13
- # Load .env into the current process environment
14
- $envFile = Join-Path $PSScriptRoot ".env"
15
- if (Test-Path $envFile) {
16
- Get-Content $envFile | Where-Object { $_ -notmatch '^\s*#' -and $_ -match '=' } | ForEach-Object {
17
- if ($_ -match '^([^=]+)=(.*)$') {
18
- [System.Environment]::SetEnvironmentVariable($Matches[1].Trim(), $Matches[2].Trim(), 'Process')
19
- }
20
- }
21
- }
22
-
23
- # Resolve values from env with fallbacks
24
- $OLLAMA_URL = if ($env:OLLAMA_HOST) { $env:OLLAMA_HOST } else { "http://localhost:11434" }
25
- $LLM_MODELS = if ($env:LLM_MODELS) { $env:LLM_MODELS -split ',' | ForEach-Object { $_.Trim() } } else { @("sciphi/triplex") }
26
- $SPACY_MODELS = if ($env:SPACY_MODELS) { $env:SPACY_MODELS -split ',' | ForEach-Object { $_.Trim() } } else { @("en_core_web_trf") }
27
-
28
- # Check if a URL responds with HTTP 200
29
- function Test-Http {
30
- param($url)
31
- try {
32
- $resp = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop
33
- return $resp.StatusCode -eq 200
34
- } catch {
35
- return $false
36
- }
37
- }
38
-
39
- # [1/5] Ollama daemon
40
- Write-Step "[1/5] Checking Ollama..."
41
- if (-not (Test-Http "$OLLAMA_URL/api/tags")) {
42
- Write-Warn "Ollama not running — starting ollama serve"
43
- Start-Process -FilePath "ollama" -ArgumentList "serve" -WindowStyle Hidden
44
- Start-Sleep -Seconds 3
45
- if (-not (Test-Http "$OLLAMA_URL/api/tags")) {
46
- Write-Err "Ollama failed to start. Is it installed?"
47
- exit 1
48
- }
49
- }
50
- Write-Ok "Ollama is running"
51
-
52
- # [2/5] Ollama LLM models
53
- Write-Step "[2/5] Checking Ollama LLM models..."
54
- try {
55
- $tagsJson = Invoke-RestMethod -Uri "$OLLAMA_URL/api/tags" -TimeoutSec 5
56
- $pulledNames = $tagsJson.models | ForEach-Object { $_.name -replace ":latest$", "" }
57
- } catch {
58
- $pulledNames = @()
59
- }
60
-
61
- foreach ($model in $LLM_MODELS) {
62
- $shortName = $model -replace ":latest$", ""
63
- if ($pulledNames -contains $shortName) {
64
- Write-Ok "LLM model present: $model"
65
- } else {
66
- Write-Warn "Pulling LLM model: $model"
67
- & ollama pull $model
68
- if ($LASTEXITCODE -ne 0) {
69
- Write-Err "Failed to pull $model"
70
- exit 1
71
- }
72
- Write-Ok "Pulled: $model"
73
- }
74
- }
75
-
76
- # [3/5] spaCy NLP models
77
- Write-Step "[3/5] Checking spaCy models..."
78
- foreach ($model in $SPACY_MODELS) {
79
- $result = & python -c "import spacy; spacy.load('$model'); print('ok')" 2>&1
80
- if ("$result" -match "ok") {
81
- Write-Ok "spaCy model present: $model"
82
- } else {
83
- Write-Warn "spaCy model not found: $model — downloading..."
84
- & python -m spacy download $model
85
- if ($LASTEXITCODE -ne 0) {
86
- Write-Err "Failed to download spaCy model: $model"
87
- exit 1
88
- }
89
- Write-Ok "Downloaded spaCy model: $model"
90
- }
91
- }
92
-
93
- # [4/5] Backend
94
- Write-Step "[4/5] Starting backend (uvicorn on :8000)..."
95
- $backendJob = Start-Job -ScriptBlock {
96
- param($root)
97
- Set-Location $root
98
- & "$root\venv\Scripts\Activate.ps1"
99
- & "$root\venv\Scripts\uvicorn.exe" backend.main:app --host 0.0.0.0 --port 8000
100
- } -ArgumentList $PWD.Path
101
-
102
- Start-Sleep -Seconds 5
103
- if (Test-Http "$BACKEND_URL/health") {
104
- Write-Ok "Backend healthy at $BACKEND_URL"
105
- } else {
106
- Write-Warn "Backend did not respond in time — check logs below"
107
- }
108
-
109
- # [5/5] Frontend
110
- Write-Step "[5/5] Starting frontend (npm start on :3000)..."
111
- $frontendJob = Start-Job -ScriptBlock {
112
- param($root)
113
- Set-Location "$root\frontend"
114
- & npm start
115
- } -ArgumentList $PWD.Path
116
-
117
- # Summary
118
- Write-Host ""
119
- Write-Host "======================================" -ForegroundColor Cyan
120
- Write-Host " Backend $BACKEND_URL" -ForegroundColor Green
121
- Write-Host " Frontend $FRONTEND_URL" -ForegroundColor Green
122
- Write-Host " Ollama $OLLAMA_URL" -ForegroundColor Green
123
- Write-Host " Press Ctrl+C to stop all services" -ForegroundColor Yellow
124
- Write-Host "======================================" -ForegroundColor Cyan
125
- Write-Host ""
126
-
127
- # Stream job output until Ctrl+C
128
- try {
129
- while ($true) {
130
- Receive-Job -Job $backendJob | ForEach-Object { Write-Host "[backend] $_" }
131
- Receive-Job -Job $frontendJob | ForEach-Object { Write-Host "[frontend] $_" }
132
- Start-Sleep -Milliseconds 500
133
-
134
- if ($backendJob.State -eq "Failed") {
135
- Write-Err "Backend job failed"
136
- break
137
- }
138
- if ($frontendJob.State -eq "Failed") {
139
- Write-Err "Frontend job failed"
140
- break
141
- }
142
- }
143
- } finally {
144
- Write-Host ""
145
- Write-Step "Shutting down..."
146
- Stop-Job -Job $backendJob, $frontendJob -ErrorAction SilentlyContinue
147
- Remove-Job -Job $backendJob, $frontendJob -ErrorAction SilentlyContinue -Force
148
- Write-Ok "All services stopped"
149
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
start.sh CHANGED
@@ -41,7 +41,7 @@ fi
41
  # Resolve values from env with fallbacks
42
  OLLAMA_URL="${OLLAMA_HOST:-http://localhost:11434}"
43
 
44
- IFS=',' read -ra LLM_MODELS <<< "${LLM_MODELS:-sciphi/triplex}"
45
  IFS=',' read -ra SPACY_MODELS <<< "${SPACY_MODELS:-en_core_web_trf}"
46
 
47
  # Trim whitespace from each element
 
41
  # Resolve values from env with fallbacks
42
  OLLAMA_URL="${OLLAMA_HOST:-http://localhost:11434}"
43
 
44
+ IFS=',' read -ra LLM_MODELS <<< "${LLM_MODELS:-llama3}"
45
  IFS=',' read -ra SPACY_MODELS <<< "${SPACY_MODELS:-en_core_web_trf}"
46
 
47
  # Trim whitespace from each element