osintengine 1.0.2__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 (103) hide show
  1. osintengine-1.0.2.dist-info/METADATA +311 -0
  2. osintengine-1.0.2.dist-info/RECORD +103 -0
  3. osintengine-1.0.2.dist-info/WHEEL +5 -0
  4. osintengine-1.0.2.dist-info/entry_points.txt +2 -0
  5. osintengine-1.0.2.dist-info/licenses/LICENSE +202 -0
  6. osintengine-1.0.2.dist-info/top_level.txt +2 -0
  7. src/watson/__init__.py +10 -0
  8. src/watson/agent/__init__.py +96 -0
  9. src/watson/agents/__init__.py +101 -0
  10. src/watson/agents/protocol.py +196 -0
  11. src/watson/auth/__init__.py +68 -0
  12. src/watson/auth/store.py +145 -0
  13. src/watson/conversation.py +68 -0
  14. src/watson/core/__init__.py +0 -0
  15. src/watson/core/models.py +96 -0
  16. src/watson/ethics.py +205 -0
  17. src/watson/exports.py +182 -0
  18. src/watson/graph/__init__.py +69 -0
  19. src/watson/graph/entities.py +207 -0
  20. src/watson/graph/graph.py +489 -0
  21. src/watson/graph/osint_framework.py +266 -0
  22. src/watson/graph/relationships.py +116 -0
  23. src/watson/graph/transforms.py +787 -0
  24. src/watson/infra/__init__.py +43 -0
  25. src/watson/infra/cache.py +171 -0
  26. src/watson/infra/ratelimit.py +125 -0
  27. src/watson/infra/resilience.py +239 -0
  28. src/watson/infra/retry.py +186 -0
  29. src/watson/metrics.py +128 -0
  30. src/watson/opsec/__init__.py +290 -0
  31. src/watson/orchestration/__init__.py +23 -0
  32. src/watson/orchestration/engine.py +5587 -0
  33. src/watson/orchestration/executor.py +96 -0
  34. src/watson/orchestration/intent.py +139 -0
  35. src/watson/orchestration/intent_classifier.py +45 -0
  36. src/watson/orchestration/llm_config.py +160 -0
  37. src/watson/orchestration/resolution.py +555 -0
  38. src/watson/orchestration/scraper.py +94 -0
  39. src/watson/orchestration/synthesis.py +726 -0
  40. src/watson/orchestration/target_profile.py +748 -0
  41. src/watson/persistence/__init__.py +18 -0
  42. src/watson/persistence/models.py +161 -0
  43. src/watson/persistence/store.py +230 -0
  44. src/watson/pipeline/__init__.py +16 -0
  45. src/watson/pipeline/pre_synthesis.py +621 -0
  46. src/watson/search.py +103 -0
  47. src/watson/serializers/stix.py +451 -0
  48. src/watson/tools/__init__.py +31 -0
  49. src/watson/tools/base.py +97 -0
  50. src/watson/tools/blockchain.py +359 -0
  51. src/watson/tools/captcha.py +276 -0
  52. src/watson/tools/conflict.py +107 -0
  53. src/watson/tools/corporate.py +287 -0
  54. src/watson/tools/darkweb.py +144 -0
  55. src/watson/tools/geolocation.py +347 -0
  56. src/watson/tools/image_video.py +108 -0
  57. src/watson/tools/marinetraffic.py +142 -0
  58. src/watson/tools/people.py +476 -0
  59. src/watson/tools/registry.py +56 -0
  60. src/watson/tools/satellite.py +118 -0
  61. src/watson/tools/scraper.py +745 -0
  62. src/watson/tools/shodan.py +129 -0
  63. src/watson/tools/social_media.py +160 -0
  64. src/watson/tools/websites.py +291 -0
  65. src/watson/tools/wikidata.py +398 -0
  66. src/watson/utils/__init__.py +1 -0
  67. src/watson/utils/helpers.py +49 -0
  68. src/watson/utils/http.py +119 -0
  69. src/watson/verification/__init__.py +245 -0
  70. watson/__init__.py +6 -0
  71. watson/agents/__init__.py +20 -0
  72. watson/agents/base.py +103 -0
  73. watson/agents/direct.py +225 -0
  74. watson/agents/hermes.py +275 -0
  75. watson/agents/hermes_mcp.py +292 -0
  76. watson/api_keys.py +176 -0
  77. watson/browser_scraper.py +481 -0
  78. watson/cli.py +836 -0
  79. watson/crossref.py +175 -0
  80. watson/ethics.py +20 -0
  81. watson/graph.py +406 -0
  82. watson/mcp_server.py +601 -0
  83. watson/memory.py +552 -0
  84. watson/neo4j_graph.py +124 -0
  85. watson/opsec/__init__.py +307 -0
  86. watson/reporter.py +537 -0
  87. watson/scheduler.py +486 -0
  88. watson/serializers/stix.py +451 -0
  89. watson/terminal.py +410 -0
  90. watson/toolkit.py +252 -0
  91. watson/toolkit_api.py +347 -0
  92. watson/toolkit_automation.py +95 -0
  93. watson/toolkit_registry.py +345 -0
  94. watson/verification/__init__.py +251 -0
  95. watson/web/__init__.py +1 -0
  96. watson/web/app.py +1650 -0
  97. watson/web/middleware.py +301 -0
  98. watson/web/static/assets/index-B7hPOc0z.js +278 -0
  99. watson/web/static/assets/index-CJ6FP8Mp.css +1 -0
  100. watson/web/static/assets/watson-logo-Dk9tawHb.png +0 -0
  101. watson/web/static/index.html +14 -0
  102. watson/web/templates/chat.html +2 -0
  103. watson/web/templates/investigation-map.html +429 -0
@@ -0,0 +1,96 @@
1
+ """Core models for Watson — Pydantic data structures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from enum import Enum
7
+ from typing import Optional
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class FindingSeverity(str, Enum):
13
+ CRITICAL = "critical"
14
+ HIGH = "high"
15
+ MEDIUM = "medium"
16
+ LOW = "low"
17
+ INFO = "info"
18
+
19
+
20
+ class FindingSource(str, Enum):
21
+ SATELLITE = "satellite"
22
+ GEOLOCATION = "geolocation"
23
+ IMAGE_VIDEO = "image_video"
24
+ SOCIAL_MEDIA = "social_media"
25
+ PEOPLE = "people"
26
+ WEBSITES = "websites"
27
+ CORPORATE = "corporate"
28
+ CONFLICT = "conflict"
29
+ OSINT = "osint"
30
+ BELLINGCAT = "bellingcat"
31
+ SOCMINT = "socmint"
32
+ CROSS_REF = "cross_ref"
33
+
34
+
35
+ class Finding(BaseModel):
36
+ """A single finding from an investigation tool."""
37
+
38
+ id: str = Field(description="Unique finding ID")
39
+ source: FindingSource
40
+ tool: str = Field(description="Tool name that produced this finding")
41
+ title: str
42
+ description: str
43
+ evidence: list[str] = Field(default_factory=list, description="URLs or references")
44
+ severity: FindingSeverity = FindingSeverity.INFO
45
+ confidence: float = Field(default=0.5, ge=0.0, le=1.0)
46
+ timestamp: datetime = Field(default_factory=datetime.now)
47
+ metadata: dict = Field(default_factory=dict)
48
+
49
+
50
+ class InvestigationTask(BaseModel):
51
+ """A decomposed investigation subtask dispatched to a tool."""
52
+
53
+ id: str
54
+ tool_category: FindingSource
55
+ tool_name: str = ""
56
+ query: str
57
+ context: str = ""
58
+ priority: int = 1
59
+
60
+
61
+ class InvestigationRequest(BaseModel):
62
+ """Top-level investigation request from user."""
63
+
64
+ query: str
65
+ tools: list[FindingSource] | None = Field(default=None)
66
+ max_findings_per_tool: int = 10
67
+ cross_reference: bool = True
68
+
69
+
70
+ class Report(BaseModel):
71
+ """Final investigation report."""
72
+
73
+ query: str
74
+ generated_at: datetime = Field(default_factory=datetime.now)
75
+ findings: list[Finding] = Field(default_factory=list)
76
+ cross_references: list[Finding] = Field(default_factory=list)
77
+ summary: str = ""
78
+ tool_stats: dict[str, int] = Field(default_factory=dict)
79
+
80
+ @property
81
+ def total_findings(self) -> int:
82
+ return len(self.findings) + len(self.cross_references)
83
+
84
+ @property
85
+ def by_severity(self) -> dict[str, int]:
86
+ counts: dict[str, int] = {}
87
+ for f in self.findings + self.cross_references:
88
+ counts[f.severity.value] = counts.get(f.severity.value, 0) + 1
89
+ return counts
90
+
91
+ @property
92
+ def by_source(self) -> dict[str, int]:
93
+ counts: dict[str, int] = {}
94
+ for f in self.findings:
95
+ counts[f.source.value] = counts.get(f.source.value, 0) + 1
96
+ return counts
src/watson/ethics.py ADDED
@@ -0,0 +1,205 @@
1
+ """Sidney Ledger — editorial compliance and entity tracking across investigations.
2
+
3
+ Every entity that appears in a Watson investigation is recorded here.
4
+ Cross-case patterns, harm assessments, and editorial gates live here.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ import json
9
+ import logging
10
+ import os
11
+ from pathlib import Path
12
+ from datetime import datetime, timezone
13
+
14
+ logger = logging.getLogger("watson.ethics")
15
+
16
+ # ── Ledger ─────────────────────────────────────────────────────
17
+
18
+ _LEDGER_PATH = Path(os.environ.get("WATSON_LEDGER_PATH",
19
+ os.path.expanduser("~/.watson/ledger.json")))
20
+
21
+
22
+ class SidneyLedger:
23
+ """Cross-case entity ledger with editorial compliance."""
24
+
25
+ def __init__(self, path: Path | None = None):
26
+ self.path = path or _LEDGER_PATH
27
+ self.path.parent.mkdir(parents=True, exist_ok=True)
28
+ self._data = self._load()
29
+
30
+ def _load(self) -> dict:
31
+ if self.path.exists():
32
+ try:
33
+ return json.loads(self.path.read_text())
34
+ except (json.JSONDecodeError, OSError):
35
+ pass
36
+ return {"entities": {}, "cases": {}, "version": "1.0"}
37
+
38
+ def _save(self):
39
+ try:
40
+ self.path.write_text(json.dumps(self._data, indent=2, default=str))
41
+ except OSError as e:
42
+ logger.warning("ledger_save_failed: %s", e)
43
+
44
+ def record_entity(self, name: str, entity_type: str, case_id: str, confidence: float,
45
+ appears_in: list[str] | None = None):
46
+ """Record an entity found in an investigation."""
47
+ key = name.lower().strip()
48
+ if key not in self._data["entities"]:
49
+ self._data["entities"][key] = {
50
+ "canonical": name,
51
+ "type": entity_type,
52
+ "first_seen": datetime.now(timezone.utc).isoformat(),
53
+ "cases": [],
54
+ "total_appearances": 0,
55
+ "confidence_high": confidence,
56
+ }
57
+
58
+ entry = self._data["entities"][key]
59
+ if case_id not in entry["cases"]:
60
+ entry["cases"].append(case_id)
61
+ entry["total_appearances"] = len(entry["cases"])
62
+ entry["confidence_high"] = max(entry["confidence_high"], confidence)
63
+ entry["last_seen"] = datetime.now(timezone.utc).isoformat()
64
+
65
+ if appears_in:
66
+ existing = entry.get("appears_in", [])
67
+ for url in appears_in:
68
+ if url not in existing:
69
+ existing.append(url)
70
+ entry["appears_in"] = existing[:50]
71
+
72
+ self._save()
73
+
74
+ def record_case(self, case_id: str, query: str, finding_count: int,
75
+ confirmed_count: int, entities: list[str]):
76
+ """Record a completed investigation case."""
77
+ self._data["cases"][case_id] = {
78
+ "query": query,
79
+ "completed_at": datetime.now(timezone.utc).isoformat(),
80
+ "finding_count": finding_count,
81
+ "confirmed_count": confirmed_count,
82
+ "entities": entities,
83
+ }
84
+ self._save()
85
+
86
+ def get_entity(self, name: str) -> dict | None:
87
+ """Look up an entity's cross-case history."""
88
+ return self._data["entities"].get(name.lower().strip())
89
+
90
+ def get_case(self, case_id: str) -> dict | None:
91
+ """Look up a case."""
92
+ return self._data["cases"].get(case_id)
93
+
94
+ def entity_appears_across_cases(self, name: str) -> list[str]:
95
+ """List all cases where this entity appears."""
96
+ entry = self.get_entity(name)
97
+ return entry["cases"] if entry else []
98
+
99
+ def stats(self) -> dict:
100
+ return {
101
+ "total_entities": len(self._data["entities"]),
102
+ "total_cases": len(self._data["cases"]),
103
+ "cross_case_entities": sum(
104
+ 1 for e in self._data["entities"].values()
105
+ if len(e.get("cases", [])) > 1
106
+ ),
107
+ }
108
+
109
+ def get_stats(self) -> dict:
110
+ """Alias for stats() — used by health endpoint."""
111
+ return self.stats()
112
+
113
+
114
+ # ── Editorial checks ───────────────────────────────────────────
115
+
116
+ def apply_editorial_checks(findings: list, query: str) -> dict:
117
+ """Apply editorial compliance checks to findings. Returns assessment dict."""
118
+
119
+ # Harm indicators — terms that suggest sensitive content
120
+ harm_indicators = {
121
+ "victim": "VULNERABLE ENTITIES DETECTED",
122
+ "minor": "MINOR DETECTED",
123
+ "child": "MINOR DETECTED",
124
+ "survivor": "VULNERABLE ENTITIES DETECTED",
125
+ "phone_number": "PII DETECTED",
126
+ "email": "PII DETECTED",
127
+ "address": "PII DETECTED",
128
+ "passport": "PII DETECTED",
129
+ }
130
+
131
+ checks = {
132
+ "publication_assessment": "RECOMMENDED",
133
+ "harm_level": "LOW",
134
+ "public_interest": True,
135
+ "warnings": [],
136
+ "redactions_needed": 0,
137
+ }
138
+
139
+ all_text = " ".join(
140
+ f"{getattr(f, 'title', '')} {getattr(f, 'description', '')}"
141
+ for f in findings
142
+ ).lower()
143
+
144
+ for indicator, warning in harm_indicators.items():
145
+ if indicator in all_text:
146
+ checks["warnings"].append(warning)
147
+
148
+ if checks["warnings"]:
149
+ checks["harm_level"] = "HIGH" if len(checks["warnings"]) > 2 else "MEDIUM"
150
+ checks["redactions_needed"] = len(checks["warnings"])
151
+
152
+ return checks
153
+
154
+
155
+ # ── Singleton ──────────────────────────────────────────────────
156
+
157
+ _ledger: SidneyLedger | None = None
158
+
159
+
160
+ def get_ledger() -> SidneyLedger:
161
+ global _ledger
162
+ if _ledger is None:
163
+ _ledger = SidneyLedger()
164
+ return _ledger
165
+
166
+
167
+ # ── Editorial Framework (reporter integration) ──────────────────
168
+ # These are lightweight stubs — the real editorial pipeline lives in
169
+ # watson/reporter.py with the harm indicators in src/watson/ethics.py.
170
+
171
+ BELLINGCAT_DATA_ETHICS_APPENDIX = """
172
+ ## Data Ethics & Methodology
173
+
174
+ This investigation follows the Bellingcat Digital Research Ethics Framework:
175
+
176
+ - **Open Source**: All data sourced from publicly available information
177
+ - **Verification**: Cross-referenced across multiple independent sources
178
+ - **Privacy**: Personal identifying information redacted unless clearly in the public interest
179
+ - **Attribution**: Sources documented for reproducibility
180
+ - **Correction**: Errors will be promptly corrected when identified
181
+ - **Proportionality**: Only information relevant to the investigation is included
182
+
183
+ Methodology: Multi-agent OSINT pipeline using web scraping, API queries,
184
+ DNS/WHOIS lookups, certificate transparency logs, and dark web monitoring.
185
+ """
186
+
187
+
188
+ def generate_compliance_header(assessment: dict) -> str:
189
+ """Generate a markdown compliance header from the assessment."""
190
+ warnings = assessment.get("warnings", [])
191
+ if not warnings:
192
+ return "✅ **Editorial Compliance**: No concerns detected.\n"
193
+
194
+ lines = [f"⚠️ **Editorial Compliance** — {len(warnings)} concern(s):"]
195
+ for w in warnings:
196
+ lines.append(f"- {w}")
197
+ return "\n".join(lines) + "\n"
198
+
199
+
200
+ class EditorialFramework:
201
+ """Editorial compliance framework for pre-publication assessment."""
202
+
203
+ def assess(self, query: str, findings: list, narrative: str, ai_used: bool) -> dict:
204
+ """Assess a report for editorial compliance before publication."""
205
+ return apply_editorial_checks(findings, query)
src/watson/exports.py ADDED
@@ -0,0 +1,182 @@
1
+ """Export — generate reports in multiple formats.
2
+
3
+ BellingcatReport class for structured OSINT report generation.
4
+ Delegates to watson.reporter for core report logic.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Any, Dict, List, Optional
12
+
13
+
14
+ class BellingcatReport:
15
+ """Bellingcat-style structured OSINT report with multi-format export.
16
+
17
+ Supports: JSON, STIX, MISP, PDF, Markdown.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ query: str = "",
23
+ investigation_id: str = "",
24
+ target_type: str = "unknown",
25
+ target_value: str = "",
26
+ ):
27
+ self.query = query
28
+ self.investigation_id = investigation_id
29
+ self.target_type = target_type
30
+ self.target_value = target_value
31
+ self.findings: List[Dict[str, Any]] = []
32
+ self.cross_references: List[Dict[str, Any]] = []
33
+ self.hops: int = 0
34
+
35
+ def add_findings(self, findings: List[Dict[str, Any]]):
36
+ self.findings.extend(findings)
37
+
38
+ def add_cross_references(self, refs: List[Dict[str, Any]]):
39
+ self.cross_references.extend(refs)
40
+
41
+ def to_dict(self) -> Dict[str, Any]:
42
+ """Return full report as dict."""
43
+ return {
44
+ "case_id": self.investigation_id,
45
+ "query": self.query,
46
+ "target_type": self.target_type,
47
+ "target_value": self.target_value,
48
+ "hops": self.hops,
49
+ "total_findings": len(self.findings),
50
+ "confirmed": sum(1 for f in self.findings if f.get("tier") == "CONFIRMED"),
51
+ "findings": self.findings,
52
+ "cross_references": self.cross_references,
53
+ "verifiability": self._verifiability(),
54
+ }
55
+
56
+ def to_json(self, path: Path | str) -> Path:
57
+ """Export as JSON."""
58
+ path = Path(path)
59
+ path.write_text(json.dumps(self.to_dict(), indent=2, default=str))
60
+ return path
61
+
62
+ def to_stix(self, path: Path | str) -> Path:
63
+ """Export as STIX 2.1 bundle."""
64
+ import uuid
65
+ from datetime import datetime, timezone
66
+
67
+ now = datetime.now(timezone.utc).isoformat()
68
+ bundle = {
69
+ "type": "bundle",
70
+ "id": f"bundle--{uuid.uuid4()}",
71
+ "spec_version": "2.1",
72
+ "objects": [
73
+ {
74
+ "type": "report",
75
+ "id": f"report--{uuid.uuid4()}",
76
+ "created": now,
77
+ "modified": now,
78
+ "name": f"Watson Investigation: {self.query[:80]}",
79
+ "description": f"OSINT investigation of {self.target_value}",
80
+ "report_types": ["investigation"],
81
+ "object_refs": [],
82
+ }
83
+ ],
84
+ }
85
+ path = Path(path)
86
+ path.write_text(json.dumps(bundle, indent=2))
87
+ return path
88
+
89
+ def to_misp(self, path: Path | str) -> Path:
90
+ """Export as MISP event JSON."""
91
+ import uuid
92
+
93
+ event = {
94
+ "Event": {
95
+ "uuid": str(uuid.uuid4()),
96
+ "info": f"Watson Investigation: {self.query[:80]}",
97
+ "analysis": "2", # Completed
98
+ "threat_level_id": "3", # Low
99
+ "published": False,
100
+ "Attribute": [
101
+ {
102
+ "type": "text",
103
+ "category": "External analysis",
104
+ "value": f["title"],
105
+ "comment": f["description"],
106
+ }
107
+ for f in self.findings[:50]
108
+ ],
109
+ }
110
+ }
111
+ path = Path(path)
112
+ path.write_text(json.dumps(event, indent=2))
113
+ return path
114
+
115
+ def to_pdf(self, path: Path | str) -> Path:
116
+ """Export as PDF (saves as JSON with .pdf extension if fpdf unavailable)."""
117
+ path = Path(path)
118
+ try:
119
+ from fpdf import FPDF
120
+ pdf = FPDF()
121
+ pdf.add_page()
122
+ pdf.set_font("Helvetica", size=12)
123
+ pdf.cell(200, 10, text=f"Watson OSINT Report: {self.query[:80]}", align="C")
124
+ pdf.ln(10)
125
+ for f in self.findings:
126
+ pdf.set_font("Helvetica", "B", size=10)
127
+ pdf.cell(200, 8, text=f["title"][:100])
128
+ pdf.ln(8)
129
+ pdf.set_font("Helvetica", size=9)
130
+ pdf.multi_cell(0, 6, text=f.get("description", "")[:300])
131
+ pdf.ln(4)
132
+ pdf.output(str(path))
133
+ except ImportError:
134
+ # Fallback: save as markdown with .pdf extension
135
+ path.write_text(self.to_markdown())
136
+ return path
137
+
138
+ def to_markdown(self) -> str:
139
+ """Export as Markdown."""
140
+ lines = [
141
+ f"# Watson OSINT Investigation Report",
142
+ f"",
143
+ f"**Case ID:** {self.investigation_id}",
144
+ f"**Query:** {self.query}",
145
+ f"**Target Type:** {self.target_type}",
146
+ f"**Target:** {self.target_value}",
147
+ f"**Hops:** {self.hops}",
148
+ f"**Findings:** {len(self.findings)}",
149
+ f"**Verifiability:** {self._verifiability():.0%}",
150
+ f"",
151
+ f"## Findings",
152
+ f"",
153
+ ]
154
+ for i, f in enumerate(self.findings, 1):
155
+ tier = f.get("tier", "UNSUBSTANTIATED")
156
+ lines.append(f"### Finding {i}: {f.get('title', 'Untitled')}")
157
+ lines.append(f"")
158
+ lines.append(f"- **Tier:** {tier}")
159
+ lines.append(f"- **Confidence:** {f.get('confidence', 0):.1%}")
160
+ lines.append(f"- **Source:** {f.get('source_type', 'unknown')}")
161
+ if f.get("source_url"):
162
+ lines.append(f"- **URL:** {f.get('source_url')}")
163
+ lines.append(f"")
164
+ lines.append(f.get("description", ""))
165
+ lines.append(f"")
166
+
167
+ if self.cross_references:
168
+ lines.append(f"## Cross-References")
169
+ lines.append(f"")
170
+ for cr in self.cross_references:
171
+ lines.append(f"- {cr}")
172
+
173
+ return "\n".join(lines)
174
+
175
+ def _verifiability(self) -> float:
176
+ """Calculate the verifiability score (0-1)."""
177
+ if not self.findings:
178
+ return 0.0
179
+ sources = sum(1 for f in self.findings if f.get("source_url"))
180
+ primary = sum(1 for f in self.findings if f.get("source_type") == "PRIMARY")
181
+ score = (sources / len(self.findings) * 0.5 + primary / max(len(self.findings), 1) * 0.5)
182
+ return min(score, 1.0)
@@ -0,0 +1,69 @@
1
+ """Watson Entity Graph — typed entities, relationships, and transform engine.
2
+
3
+ This module provides Maltego-style graph-based OSINT discovery:
4
+ - Typed entities (Domain, IPAddress, Email, Person, Website, etc.)
5
+ - Typed relationships (RESOLVES_TO, HAS_SUBDOMAIN, LOCATED_IN, etc.)
6
+ - EntityGraph: a networkx-backed graph with type-safe operations
7
+ - TransformEngine: recursive entity→entity transform chaining
8
+
9
+ Integration: the graph engine runs as an enrichment layer AFTER the surface
10
+ phase of the existing pipeline. It does NOT modify the pipeline — it adds
11
+ new findings derived from the graph transforms, while all existing phases
12
+ continue unchanged.
13
+
14
+ Usage:
15
+ from watson.graph import EntityGraph, TransformEngine
16
+ g = EntityGraph()
17
+ g.add_entity(Domain("automattic.com"))
18
+ engine = TransformEngine(g)
19
+ await engine.run_transforms(max_depth=3)
20
+ findings = g.to_findings()
21
+ """
22
+
23
+ from .entities import (
24
+ Entity,
25
+ Domain,
26
+ IPAddress,
27
+ Email,
28
+ Person,
29
+ Organization,
30
+ Website,
31
+ Location,
32
+ Document,
33
+ )
34
+ from .relationships import (
35
+ Relationship,
36
+ RESOLVES_TO,
37
+ HAS_SUBDOMAIN,
38
+ LOCATED_IN,
39
+ HAS_EMAIL,
40
+ MENTIONS,
41
+ CONTAINS,
42
+ OWNS,
43
+ RELATED_TO,
44
+ )
45
+ from .graph import EntityGraph
46
+ from .transforms import TransformEngine
47
+
48
+ __all__ = [
49
+ "Entity",
50
+ "Domain",
51
+ "IPAddress",
52
+ "Email",
53
+ "Person",
54
+ "Organization",
55
+ "Website",
56
+ "Location",
57
+ "Document",
58
+ "Relationship",
59
+ "RESOLVES_TO",
60
+ "HAS_SUBDOMAIN",
61
+ "LOCATED_IN",
62
+ "HAS_EMAIL",
63
+ "MENTIONS",
64
+ "CONTAINS",
65
+ "OWNS",
66
+ "RELATED_TO",
67
+ "EntityGraph",
68
+ "TransformEngine",
69
+ ]