osintengine 1.0.2__tar.gz → 1.0.4__tar.gz

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 (115) hide show
  1. {osintengine-1.0.2 → osintengine-1.0.4}/PKG-INFO +4 -4
  2. {osintengine-1.0.2 → osintengine-1.0.4}/README.md +3 -3
  3. {osintengine-1.0.2 → osintengine-1.0.4}/pyproject.toml +1 -1
  4. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/__init__.py +1 -1
  5. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/orchestration/engine.py +47 -11
  6. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/orchestration/synthesis.py +28 -20
  7. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/pipeline/pre_synthesis.py +1 -1
  8. {osintengine-1.0.2 → osintengine-1.0.4}/.env.example +0 -0
  9. {osintengine-1.0.2 → osintengine-1.0.4}/LICENSE +0 -0
  10. {osintengine-1.0.2 → osintengine-1.0.4}/MANIFEST.in +0 -0
  11. {osintengine-1.0.2 → osintengine-1.0.4}/SELF_HOSTING.md +0 -0
  12. {osintengine-1.0.2 → osintengine-1.0.4}/WATSON_ARCHITECTURE.md +0 -0
  13. {osintengine-1.0.2 → osintengine-1.0.4}/deploy.sh +0 -0
  14. {osintengine-1.0.2 → osintengine-1.0.4}/osintengine.egg-info/SOURCES.txt +0 -0
  15. {osintengine-1.0.2 → osintengine-1.0.4}/requirements.txt +0 -0
  16. {osintengine-1.0.2 → osintengine-1.0.4}/setup.cfg +0 -0
  17. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/agent/__init__.py +0 -0
  18. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/agents/__init__.py +0 -0
  19. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/agents/protocol.py +0 -0
  20. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/auth/__init__.py +0 -0
  21. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/auth/store.py +0 -0
  22. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/conversation.py +0 -0
  23. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/core/__init__.py +0 -0
  24. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/core/models.py +0 -0
  25. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/ethics.py +0 -0
  26. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/exports.py +0 -0
  27. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/graph/__init__.py +0 -0
  28. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/graph/entities.py +0 -0
  29. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/graph/graph.py +0 -0
  30. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/graph/osint_framework.py +0 -0
  31. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/graph/relationships.py +0 -0
  32. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/graph/transforms.py +0 -0
  33. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/infra/__init__.py +0 -0
  34. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/infra/cache.py +0 -0
  35. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/infra/ratelimit.py +0 -0
  36. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/infra/resilience.py +0 -0
  37. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/infra/retry.py +0 -0
  38. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/metrics.py +0 -0
  39. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/opsec/__init__.py +0 -0
  40. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/orchestration/__init__.py +0 -0
  41. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/orchestration/executor.py +0 -0
  42. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/orchestration/intent.py +0 -0
  43. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/orchestration/intent_classifier.py +0 -0
  44. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/orchestration/llm_config.py +0 -0
  45. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/orchestration/resolution.py +0 -0
  46. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/orchestration/scraper.py +0 -0
  47. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/orchestration/target_profile.py +0 -0
  48. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/persistence/__init__.py +0 -0
  49. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/persistence/models.py +0 -0
  50. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/persistence/store.py +0 -0
  51. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/pipeline/__init__.py +0 -0
  52. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/search.py +0 -0
  53. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/serializers/stix.py +0 -0
  54. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/tools/__init__.py +0 -0
  55. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/tools/base.py +0 -0
  56. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/tools/blockchain.py +0 -0
  57. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/tools/captcha.py +0 -0
  58. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/tools/conflict.py +0 -0
  59. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/tools/corporate.py +0 -0
  60. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/tools/darkweb.py +0 -0
  61. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/tools/geolocation.py +0 -0
  62. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/tools/image_video.py +0 -0
  63. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/tools/marinetraffic.py +0 -0
  64. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/tools/people.py +0 -0
  65. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/tools/registry.py +0 -0
  66. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/tools/satellite.py +0 -0
  67. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/tools/scraper.py +0 -0
  68. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/tools/shodan.py +0 -0
  69. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/tools/social_media.py +0 -0
  70. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/tools/websites.py +0 -0
  71. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/tools/wikidata.py +0 -0
  72. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/utils/__init__.py +0 -0
  73. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/utils/helpers.py +0 -0
  74. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/utils/http.py +0 -0
  75. {osintengine-1.0.2 → osintengine-1.0.4}/src/watson/verification/__init__.py +0 -0
  76. {osintengine-1.0.2 → osintengine-1.0.4}/tests/conftest.py +0 -0
  77. {osintengine-1.0.2 → osintengine-1.0.4}/tests/test_infra.py +0 -0
  78. {osintengine-1.0.2 → osintengine-1.0.4}/tests/test_integration.py +0 -0
  79. {osintengine-1.0.2 → osintengine-1.0.4}/tests/test_persistence.py +0 -0
  80. {osintengine-1.0.2 → osintengine-1.0.4}/tests/test_resolution.py +0 -0
  81. {osintengine-1.0.2 → osintengine-1.0.4}/tests/test_synthesis.py +0 -0
  82. {osintengine-1.0.2 → osintengine-1.0.4}/watson/__init__.py +0 -0
  83. {osintengine-1.0.2 → osintengine-1.0.4}/watson/agents/__init__.py +0 -0
  84. {osintengine-1.0.2 → osintengine-1.0.4}/watson/agents/base.py +0 -0
  85. {osintengine-1.0.2 → osintengine-1.0.4}/watson/agents/direct.py +0 -0
  86. {osintengine-1.0.2 → osintengine-1.0.4}/watson/agents/hermes.py +0 -0
  87. {osintengine-1.0.2 → osintengine-1.0.4}/watson/agents/hermes_mcp.py +0 -0
  88. {osintengine-1.0.2 → osintengine-1.0.4}/watson/api_keys.py +0 -0
  89. {osintengine-1.0.2 → osintengine-1.0.4}/watson/browser_scraper.py +0 -0
  90. {osintengine-1.0.2 → osintengine-1.0.4}/watson/cli.py +0 -0
  91. {osintengine-1.0.2 → osintengine-1.0.4}/watson/crossref.py +0 -0
  92. {osintengine-1.0.2 → osintengine-1.0.4}/watson/ethics.py +0 -0
  93. {osintengine-1.0.2 → osintengine-1.0.4}/watson/graph.py +0 -0
  94. {osintengine-1.0.2 → osintengine-1.0.4}/watson/mcp_server.py +0 -0
  95. {osintengine-1.0.2 → osintengine-1.0.4}/watson/memory.py +0 -0
  96. {osintengine-1.0.2 → osintengine-1.0.4}/watson/neo4j_graph.py +0 -0
  97. {osintengine-1.0.2 → osintengine-1.0.4}/watson/opsec/__init__.py +0 -0
  98. {osintengine-1.0.2 → osintengine-1.0.4}/watson/reporter.py +0 -0
  99. {osintengine-1.0.2 → osintengine-1.0.4}/watson/scheduler.py +0 -0
  100. {osintengine-1.0.2 → osintengine-1.0.4}/watson/serializers/stix.py +0 -0
  101. {osintengine-1.0.2 → osintengine-1.0.4}/watson/terminal.py +0 -0
  102. {osintengine-1.0.2 → osintengine-1.0.4}/watson/toolkit.py +0 -0
  103. {osintengine-1.0.2 → osintengine-1.0.4}/watson/toolkit_api.py +0 -0
  104. {osintengine-1.0.2 → osintengine-1.0.4}/watson/toolkit_automation.py +0 -0
  105. {osintengine-1.0.2 → osintengine-1.0.4}/watson/toolkit_registry.py +0 -0
  106. {osintengine-1.0.2 → osintengine-1.0.4}/watson/verification/__init__.py +0 -0
  107. {osintengine-1.0.2 → osintengine-1.0.4}/watson/web/__init__.py +0 -0
  108. {osintengine-1.0.2 → osintengine-1.0.4}/watson/web/app.py +0 -0
  109. {osintengine-1.0.2 → osintengine-1.0.4}/watson/web/middleware.py +0 -0
  110. {osintengine-1.0.2 → osintengine-1.0.4}/watson/web/static/assets/index-B7hPOc0z.js +0 -0
  111. {osintengine-1.0.2 → osintengine-1.0.4}/watson/web/static/assets/index-CJ6FP8Mp.css +0 -0
  112. {osintengine-1.0.2 → osintengine-1.0.4}/watson/web/static/assets/watson-logo-Dk9tawHb.png +0 -0
  113. {osintengine-1.0.2 → osintengine-1.0.4}/watson/web/static/index.html +0 -0
  114. {osintengine-1.0.2 → osintengine-1.0.4}/watson/web/templates/chat.html +0 -0
  115. {osintengine-1.0.2 → osintengine-1.0.4}/watson/web/templates/investigation-map.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: osintengine
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: The OSINT investigation engine — 7-phase pipeline, persistent knowledge graph, community intelligence
5
5
  Author-email: Lorenzo Baron <baron.lorenzo99@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -65,9 +65,9 @@ Dynamic: license-file
65
65
  </p>
66
66
 
67
67
  <p align="center">
68
- <a href="https://pypi.org/project/osintengine/"><img src="https://img.shields.io/badge/pypi-v1.0.2-blue" alt="PyPI"></a>
68
+ <a href="https://pypi.org/project/osintengine/"><img src="https://img.shields.io/badge/pypi-v1.0.3-blue" alt="PyPI"></a>
69
69
  <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10%2B-blue" alt="Python"></a>
70
- <a href="LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-green" alt="License"></a>
70
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-green" alt="License"></a>
71
71
  <a href="https://github.com/Lorenzobaron99/watson-osint"><img src="https://img.shields.io/badge/status-active-brightgreen" alt="Status"></a>
72
72
  </p>
73
73
 
@@ -85,7 +85,7 @@ One target. One command. A dossier that would take an analyst hours.
85
85
 
86
86
  | You type... | Most tools return... | Watson returns... |
87
87
  |---|---|---|
88
- | "Paolo Trecate" | Wikipedia link, maybe a news article | Employment history, board positions, corporate registrations, domain ownerships, sanctions screening, adverse media, timeline of professional milestones, entity relationship graph |
88
+ | "John Chen" | Wikipedia link, maybe a news article | Employment history, board positions, corporate registrations, domain ownerships, sanctions screening, adverse media, timeline of professional milestones, entity relationship graph |
89
89
  | "example.com" | WHOIS record | Domain history, SSL certificates, subdomains, DNS records, IP geolocation, hosting provider, Wayback snapshots, linked social accounts, VirusTotal reputation |
90
90
  | "crypto-wallet-0x..." | Nothing useful | Transaction graph, exchange links, associated addresses, risk scoring, dark web mentions |
91
91
 
@@ -9,9 +9,9 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <a href="https://pypi.org/project/osintengine/"><img src="https://img.shields.io/badge/pypi-v1.0.2-blue" alt="PyPI"></a>
12
+ <a href="https://pypi.org/project/osintengine/"><img src="https://img.shields.io/badge/pypi-v1.0.3-blue" alt="PyPI"></a>
13
13
  <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10%2B-blue" alt="Python"></a>
14
- <a href="LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-green" alt="License"></a>
14
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-green" alt="License"></a>
15
15
  <a href="https://github.com/Lorenzobaron99/watson-osint"><img src="https://img.shields.io/badge/status-active-brightgreen" alt="Status"></a>
16
16
  </p>
17
17
 
@@ -29,7 +29,7 @@ One target. One command. A dossier that would take an analyst hours.
29
29
 
30
30
  | You type... | Most tools return... | Watson returns... |
31
31
  |---|---|---|
32
- | "Paolo Trecate" | Wikipedia link, maybe a news article | Employment history, board positions, corporate registrations, domain ownerships, sanctions screening, adverse media, timeline of professional milestones, entity relationship graph |
32
+ | "John Chen" | Wikipedia link, maybe a news article | Employment history, board positions, corporate registrations, domain ownerships, sanctions screening, adverse media, timeline of professional milestones, entity relationship graph |
33
33
  | "example.com" | WHOIS record | Domain history, SSL certificates, subdomains, DNS records, IP geolocation, hosting provider, Wayback snapshots, linked social accounts, VirusTotal reputation |
34
34
  | "crypto-wallet-0x..." | Nothing useful | Transaction graph, exchange links, associated addresses, risk scoring, dark web mentions |
35
35
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "osintengine"
3
- version = "1.0.2"
3
+ version = "1.0.4"
4
4
  description = "The OSINT investigation engine — 7-phase pipeline, persistent knowledge graph, community intelligence"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -7,4 +7,4 @@ must be the truth." — Sherlock Holmes
7
7
  Autonomous open-source intelligence with 338 Bellingcat tools.
8
8
  Knowledge graph, lead tracking, recursive investigation.
9
9
  """
10
- __version__ = "1.0.2"
10
+ __version__ = "1.0.4"
@@ -1927,7 +1927,7 @@ Examples:
1927
1927
  # FORCE a targeted person search excluding the non-person Wikipedia hit
1928
1928
  # Universal fix: when Wikipedia returns a location/org/thing instead of a person,
1929
1929
  # search with -site:wikipedia.org + person keywords. Works for:
1930
- # - Paolo Trecate (comune) → "Paolo Trecate" analyst defense
1930
+ # - Marco Rossi (comune) → "Marco Rossi" analyst defense
1931
1931
  # - Austin Texas (city) → "Austin Texas" linkedin
1932
1932
  # - Paris Hilton (city) → "Paris Hilton" celebrity (though common enough to find anyway)
1933
1933
  try:
@@ -2808,7 +2808,7 @@ Examples:
2808
2808
  # employment, board positions) — essential context even for criminal
2809
2809
  # investigations. Criminal queries add sanctions, court records, and
2810
2810
  # wanted notices. Without professional queries, deep investigations
2811
- # miss entire professional profiles (e.g. "Paolo Trecate" → only found
2811
+ # miss entire professional profiles (e.g. "Marco Rossi" → only found
2812
2812
  # jail lookup sites, never discovered eurodefense.tech).
2813
2813
  search_label = "searching professional history + court records, legal databases"
2814
2814
  search_mode = "both"
@@ -3616,7 +3616,9 @@ Examples:
3616
3616
  except Exception as e:
3617
3617
  logger.warning("resolution_failed: %s", e)
3618
3618
 
3619
- # Synthesis
3619
+ # Synthesis — filter noise before feeding to LLM
3620
+ findings = self._filter_quality(findings, query=query)
3621
+
3620
3622
  try:
3621
3623
  from .synthesis import synthesize_brief
3622
3624
  from .llm_config import call_llm
@@ -3802,14 +3804,14 @@ Examples:
3802
3804
  primary = sum(1 for f in findings if f.source_tier == "PRIMARY")
3803
3805
  has_url = sum(1 for f in findings if f.source_url)
3804
3806
 
3805
- # Verifiability score — weighted: URL sources + high-confidence tiers
3806
- # Most OSINT findings are SECONDARY (news, Wikipedia) — PRIMARY tier
3807
- # (court docs, sanctions lists) is rare. Weight PROBABLE heavily.
3807
+ # Verifiability score — weighted: confirmed findings + primary sources dominate.
3808
+ # CONFIRMED findings are the gold standard; PRIMARY sources (court docs, sanctions)
3809
+ # carry the most weight. PROBABLE adds marginal confidence.
3808
3810
  report.verifiability_score = (
3809
- 0.25 * (primary / max(total, 1)) +
3811
+ 0.35 * (confirmed / max(total, 1)) +
3812
+ 0.30 * (primary / max(total, 1)) +
3810
3813
  0.25 * (has_url / max(total, 1)) +
3811
- 0.15 * (confirmed / max(total, 1)) +
3812
- 0.35 * (probable / max(total, 1))
3814
+ 0.10 * (probable / max(total, 1))
3813
3815
  )
3814
3816
 
3815
3817
  lines = [
@@ -4830,7 +4832,7 @@ and confidence assessments. Follow the OUTPUT FORMAT specified above."""
4830
4832
  """Compute token-overlap relevance between target query and finding text.
4831
4833
 
4832
4834
  Returns 0.0 – 1.0. A score of 0 means zero word overlap — the finding
4833
- is almost certainly unrelated (e.g., "Idaho murders" for "BÓDI Ildikó").
4835
+ is almost certainly unrelated (e.g., "Idaho murders" for "KOVÁCS Anna").
4834
4836
  """
4835
4837
  import unicodedata
4836
4838
  if not target or not text:
@@ -5004,7 +5006,7 @@ and confidence assessments. Follow the OUTPUT FORMAT specified above."""
5004
5006
  "no family members identified",
5005
5007
  "no specific co-conspirator names",
5006
5008
  "no specific fine", "no specific asset",
5007
- # ── Leaked through BÓDI Ildikó investigation ──
5009
+ # ── Leaked through KOVÁCS investigation ──
5008
5010
  "no specific legal citations",
5009
5011
  "no court identifiers",
5010
5012
  "no child details",
@@ -5111,6 +5113,24 @@ and confidence assessments. Follow the OUTPUT FORMAT specified above."""
5111
5113
  "i will run", "i will simulate", "i must ask",
5112
5114
  "you have not", "if you can", "instruct me",
5113
5115
  "once you provide", "example of what",
5116
+ # ── Leaked through Mohammadzadeh investigation ──
5117
+ "let's first extract", "let's first",
5118
+ "i will open", "i'll open", "i can use python",
5119
+ "i'm not actually able", "i recall the",
5120
+ "for each fact", "i'll also", "i'll keep",
5121
+ "i'll treat", "i'll use", "i can use",
5122
+ "the youtube video is likely",
5123
+ "however, the user also",
5124
+ "i will incorporate", "i can incorporate",
5125
+ "i could open", "i could use",
5126
+ "i don't have the full", "i can view",
5127
+ "i could mention", "i'll rely on",
5128
+ "but i can't see", "i can't see the full",
5129
+ "the search result snippet says",
5130
+ "for iran:", "i can mention that",
5131
+ "as an ai in this simulation",
5132
+ "i need to fetch", "i might need",
5133
+ "i will treat", "i can mention",
5114
5134
  ])
5115
5135
 
5116
5136
  if is_meta and not (has_proper_names or has_dates or has_source_ref):
@@ -5121,6 +5141,22 @@ and confidence assessments. Follow the OUTPUT FORMAT specified above."""
5121
5141
  f.source_tier = "UNVERIFIED"
5122
5142
  f.confidence = min(f.confidence, 0.25)
5123
5143
 
5144
+ # ── Drop: DNS/IP resolution noise when target is a person ──
5145
+ # Graph enrichment resolves every domain in findings to IPs and geolocates them.
5146
+ # For person investigations, this produces 14+ noise findings (Cloudflare → Toronto).
5147
+ # Only keep DNS/IP enrichment when the investigation target IS a domain or IP.
5148
+ if getattr(f, 'phase', '') == 'graph_enrich':
5149
+ tf = (f.title or '').lower()
5150
+ df = (f.description or '').lower()
5151
+ # DNS resolution or IP geolocation that's about infrastructure, not the target
5152
+ dns_patterns = [
5153
+ 'dns a record', 'resolves_to', '→ resolves_to',
5154
+ '→ located_in', 'located_in', 'ip geolocation',
5155
+ 'cloudflare, inc.', '→ toronto', '→ san francisco',
5156
+ ]
5157
+ if any(p in tf or p in df for p in dns_patterns):
5158
+ continue
5159
+
5124
5160
  kept.append(f)
5125
5161
 
5126
5162
  # ── Relevance check: universal pre-synthesis pipeline (any target type, any language) ──
@@ -560,37 +560,45 @@ def _fallback_brief(query: str, findings: list) -> dict:
560
560
  "Financial": ["revenue", "profit", "loss", "billion", "million", "funding", "valuation",
561
561
  "stock", "share", "investor", "ipo", "acquisition"],
562
562
  "Crime / Investigation": ["criminal", "arrest", "prison", "jail", "fraud", "money laundering",
563
- "investigation", "probe", "allegation", "misconduct"],
564
- "Infrastructure / Technical": ["dns", "ip address", "nameserver", "hosting", "cdn",
565
- "subdomain", "resolves_to", "mx record"],
563
+ "investigation", "probe", "allegation", "misconduct", "wanted", "fugitive",
564
+ "fbi", "hacker", "cyber"],
566
565
  }
567
566
  themes = []
568
567
  all_text = " ".join(titles + [getattr(f, "description", "") or "" for f in findings]).lower()
569
568
  for theme, keywords in theme_keywords.items():
570
569
  matches = [kw for kw in keywords if kw in all_text]
571
570
  if matches:
572
- # Extract a relevant finding snippet
571
+ # Find the best finding for this theme — prefer ones with real descriptions
572
+ best_summary = f"Evidence of {matches[0]} found across multiple sources."
573
+ best_source = ""
573
574
  for f in findings:
574
- desc = (getattr(f, "description", "") or "").lower()
575
+ desc = (getattr(f, "description", "") or "")
575
576
  title_l = (getattr(f, "title", "") or "").lower()
576
577
  for kw in matches:
577
- if kw in title_l or kw in desc:
578
- themes.append({
579
- "theme": theme,
580
- "severity": "HIGH" if theme.startswith("Crime") else "MEDIUM",
581
- "summary": getattr(f, "title", "")[:200] or f"Evidence of {kw} found",
582
- "source_titles": [getattr(f, "title", "")[:100]],
583
- })
578
+ if kw in title_l or kw in desc.lower():
579
+ # Use the description as summary (actual intelligence, not just title)
580
+ if desc and len(desc) > 20:
581
+ best_summary = desc[:300]
582
+ best_source = getattr(f, "source_url", "") or ""
584
583
  break
585
- if any(t["theme"] == theme for t in themes):
584
+ if best_source:
586
585
  break
587
- else:
588
- themes.append({
589
- "theme": theme,
590
- "severity": "MEDIUM",
591
- "summary": f"Keywords detected: {', '.join(matches[:4])}",
592
- "source_titles": [],
593
- })
586
+ themes.append({
587
+ "theme": theme,
588
+ "severity": "HIGH" if theme.startswith("Crime") else "MEDIUM",
589
+ "summary": best_summary,
590
+ "source_titles": [best_source] if best_source else [],
591
+ })
592
+
593
+ # Deduplicate themes — same summary shouldn't appear for multiple themes
594
+ seen_summaries = set()
595
+ deduped_themes = []
596
+ for t in themes:
597
+ key = t["summary"][:50]
598
+ if key not in seen_summaries:
599
+ seen_summaries.add(key)
600
+ deduped_themes.append(t)
601
+ themes = deduped_themes[:5]
594
602
 
595
603
  # Extract timeline events from findings (dates in titles/descriptions)
596
604
  timeline_events: list[dict] = []
@@ -501,7 +501,7 @@ def relevance_score(profile: TargetProfile, text: str) -> float:
501
501
 
502
502
  # ── Strategy 2.5: Surname-aware penalty (person targets only) ──
503
503
  # For multi-word person names, a finding that matches only ONE name part
504
- # (e.g., "Ildiko Szabo" matching only "Ildiko" from "BÓDI Ildikó") is a
504
+ # (e.g., "Anna Kovacs" matching only "Anna" from "KOVÁCS Anna") is a
505
505
  # completely different person. Given names are too common; the surname is
506
506
  # the primary disambiguator. This penalty prevents the trigram/Jaccard
507
507
  # overlap from giving high scores to clearly irrelevant findings.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes