This commit adds the read path (Cortex) to complement the write path (Spine): ## Crates - stemedb-api: HTTP API with axum + utoipa OpenAPI - /v1/assert, /v1/query, /v1/epoch, /v1/skeptic, /v1/trace, /v1/audit - Metered endpoints with quota enforcement - Ed25519 signature verification - stemedb-lens: Truth resolution lenses - RecencyLens, ConsensusLens, ConfidenceLens - VoteAwareConsensusLens (Ballot Box pattern) - TrustAwareAuthorityLens (The Hive pattern) - SkepticLens (conflict analysis) - EpochAwareLens (paradigm-safe queries) - stemedb-query: Query engine with materialized views ## Storage Extensions - VoteStore: Vote aggregation with cached counts - TrustRankStore: Agent reputation with decay - AuditStore: Query audit trail - IndexStore: SP/P/S index structures - SupersessionStore: Epoch supersession chains ## SDKs - sdk/go/steme: Go HTTP client with Ed25519 signing - sdk/go/adk: ADK-Go tools for AI agents ## Documentation - Updated CLAUDE.md, architecture.md, roadmap.md - New ai-lookup entries for all services - Use case docs for consumer health intelligence - Arena roadmap for simulation advancement Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
779 lines
32 KiB
Markdown
779 lines
32 KiB
Markdown
# Consumer Health Intelligence: Application Layer Guide
|
|
|
|
> **Vertical:** Consumer-facing health information
|
|
> **Use Case:** [consumer-health-intelligence.md](../../use-cases/consumer-health-intelligence.md)
|
|
> **Status:** Design spec — implementation not started
|
|
|
|
This guide describes the **application layer components** needed to build a Consumer Health Intelligence platform on Episteme. It covers what you build, not what Episteme provides.
|
|
|
|
---
|
|
|
|
## Architecture Overview
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ CONSUMER HEALTH APP │
|
|
├─────────────────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ PubMed │ │ Reddit │ │ FAERS │ │ FDA │ │
|
|
│ │ Crawler │ │ Crawler │ │ Crawler │ │ Crawler │ │
|
|
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
|
│ │ │ │ │ │
|
|
│ └────────────┬────┴────────┬────────┴─────────┬───────┘ │
|
|
│ │ │ │ │
|
|
│ ▼ ▼ ▼ │
|
|
│ ┌───────────────────────────────────────────────┐ │
|
|
│ │ NLP Extraction Pipeline │ │
|
|
│ │ (claim identification, confidence scoring) │ │
|
|
│ └───────────────────────┬───────────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌───────────────────────────────────────────────┐ │
|
|
│ │ Source-Class Classifier │ │
|
|
│ │ (tier assignment: 0=regulatory → 6=media) │ │
|
|
│ └───────────────────────┬───────────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌───────────────────────────────────────────────┐ │
|
|
│ │ Metadata Enrichment Service │ │
|
|
│ │ (DOI lookup, journal info, engagement stats) │ │
|
|
│ └───────────────────────┬───────────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌───────────────────────────────────────────────┐ │
|
|
│ │ Agent Wallet │ │
|
|
│ │ (key management, Ed25519 signing) │ │
|
|
│ └───────────────────────┬───────────────────────┘ │
|
|
│ │ │
|
|
└──────────────────────────────────────┼───────────────────────────────────────┘
|
|
│ POST /assert
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ EPISTEME DATABASE │
|
|
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
|
│ │ WAL │ │ KV │ │ Indexes │ │ Lenses │ │ MV │ │
|
|
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
│ GET /query
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ CONSUMER HEALTH APP │
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────┐ │
|
|
│ │ Background Gardener │ │
|
|
│ │ (cluster detection, escalation assertions) │ │
|
|
│ └───────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────┐ │
|
|
│ │ LLM Summary Generator │ │
|
|
│ │ (plain-language synthesis of query results) │ │
|
|
│ └───────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────┐ │
|
|
│ │ Disagreement Dashboard │ │
|
|
│ │ (web UI for consumers) │ │
|
|
│ └───────────────────────────────────────────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Component 1: Ingestion Pipeline
|
|
|
|
### 1.1 Data Sources
|
|
|
|
| Source | API/Method | Volume Estimate | Source Class |
|
|
|--------|------------|-----------------|--------------|
|
|
| **PubMed/biorxiv** | NCBI E-utilities API | ~5,000 papers for GLP-1 | Tier 1-2 |
|
|
| **ClinicalTrials.gov** | ClinicalTrials.gov API | ~800 registered trials | Tier 1-2 |
|
|
| **FDA Labels** | DailyMed API / EDGAR scraping | ~50 documents | Tier 0 |
|
|
| **FAERS** | openFDA API | ~50,000 reports for semaglutide | Tier 3 |
|
|
| **Reddit** | Reddit API (r/Ozempic, r/loseit) | ~500,000 posts/comments | Tier 5 |
|
|
| **Patient Forums** | Web scraping | ~100,000 posts | Tier 5 |
|
|
| **News** | News API | ~10,000 articles | Tier 6 |
|
|
| **Social Media** | Platform APIs / Firehose | ~1,000,000+ mentions | Tier 6 |
|
|
|
|
### 1.2 Crawler Implementation
|
|
|
|
Each source needs a dedicated crawler. Here's the pattern:
|
|
|
|
```go
|
|
// crawler/pubmed/crawler.go
|
|
|
|
type PubMedCrawler struct {
|
|
apiKey string
|
|
baseURL string
|
|
rateLimit time.Duration
|
|
}
|
|
|
|
func (c *PubMedCrawler) FetchArticles(query string, since time.Time) ([]RawArticle, error) {
|
|
// Call NCBI E-utilities API
|
|
// Respect rate limits (3 req/sec with API key)
|
|
// Return raw article data
|
|
}
|
|
|
|
type RawArticle struct {
|
|
PMID string
|
|
Title string
|
|
Abstract string
|
|
Authors []string
|
|
Journal string
|
|
DOI string
|
|
PubDate time.Time
|
|
MeSHTerms []string
|
|
StudyDesign string // RCT, observational, meta-analysis, etc.
|
|
}
|
|
```
|
|
|
|
### 1.3 NLP Extraction Pipeline
|
|
|
|
Transforms raw documents into structured claims.
|
|
|
|
```go
|
|
// extraction/pipeline.go
|
|
|
|
type ClaimExtractor struct {
|
|
llm LLMClient // Claude, GPT-4, or local model
|
|
promptStore PromptStore // Domain-specific extraction prompts
|
|
}
|
|
|
|
type ExtractedClaim struct {
|
|
Subject string // e.g., "semaglutide/adverse-effects/gastroparesis"
|
|
Predicate string // e.g., "risk_level"
|
|
Object string // e.g., "No statistically significant increase"
|
|
Confidence float32 // Extraction confidence, not source authority
|
|
Evidence string // Quote from source supporting the claim
|
|
}
|
|
|
|
func (e *ClaimExtractor) Extract(doc RawDocument) ([]ExtractedClaim, error) {
|
|
// Use LLM to identify claims in the document
|
|
// Map claims to subject/predicate ontology
|
|
// Assign extraction confidence based on clarity
|
|
// Return structured claims
|
|
}
|
|
```
|
|
|
|
**Prompt Engineering:** The extraction prompt is critical. It must:
|
|
- Define the claim ontology (what subjects/predicates are valid)
|
|
- Distinguish claims from context
|
|
- Extract supporting evidence for provenance
|
|
- Rate extraction confidence (not source authority)
|
|
|
|
**Example prompt structure:**
|
|
```
|
|
You are extracting health claims from a medical document.
|
|
|
|
Ontology:
|
|
- Subjects: {drug}/adverse-effects/{condition}, {drug}/efficacy/{outcome}
|
|
- Predicates: risk_level, incidence_rate, relative_risk, clinical_significance
|
|
|
|
For each claim found, return:
|
|
- subject: The specific topic (e.g., "semaglutide/adverse-effects/gastroparesis")
|
|
- predicate: What aspect is being claimed
|
|
- object: The claim value (use exact quotes where possible)
|
|
- confidence: Your confidence in the extraction accuracy (0.0-1.0)
|
|
- evidence: The exact text supporting this claim
|
|
|
|
Document:
|
|
{document_text}
|
|
```
|
|
|
|
### 1.4 Source-Class Classifier
|
|
|
|
Assigns tier based on source metadata.
|
|
|
|
```go
|
|
// classification/source_class.go
|
|
|
|
type SourceClassifier struct {
|
|
rules []ClassificationRule
|
|
}
|
|
|
|
type ClassificationRule struct {
|
|
Name string
|
|
Predicate func(SourceMetadata) bool
|
|
Tier uint8
|
|
}
|
|
|
|
func (c *SourceClassifier) Classify(meta SourceMetadata) uint8 {
|
|
for _, rule := range c.rules {
|
|
if rule.Predicate(meta) {
|
|
return rule.Tier
|
|
}
|
|
}
|
|
return 6 // Default to lowest tier
|
|
}
|
|
|
|
// Default pharma rules
|
|
var PharmaRules = []ClassificationRule{
|
|
{
|
|
Name: "FDA Label",
|
|
Predicate: func(m SourceMetadata) bool {
|
|
return m.Source == "dailymed" || m.Source == "fda.gov"
|
|
},
|
|
Tier: 0,
|
|
},
|
|
{
|
|
Name: "Peer-Reviewed RCT",
|
|
Predicate: func(m SourceMetadata) bool {
|
|
return m.DOI != "" && m.StudyDesign == "RCT" && m.PeerReviewed
|
|
},
|
|
Tier: 1,
|
|
},
|
|
{
|
|
Name: "Meta-Analysis",
|
|
Predicate: func(m SourceMetadata) bool {
|
|
return m.StudyDesign == "meta-analysis" && m.PeerReviewed
|
|
},
|
|
Tier: 1,
|
|
},
|
|
{
|
|
Name: "Observational Study",
|
|
Predicate: func(m SourceMetadata) bool {
|
|
return m.DOI != "" && (m.StudyDesign == "cohort" || m.StudyDesign == "case-control")
|
|
},
|
|
Tier: 2,
|
|
},
|
|
{
|
|
Name: "FAERS Report",
|
|
Predicate: func(m SourceMetadata) bool {
|
|
return m.Source == "openfda" || m.Source == "faers"
|
|
},
|
|
Tier: 3,
|
|
},
|
|
{
|
|
Name: "Case Report",
|
|
Predicate: func(m SourceMetadata) bool {
|
|
return m.StudyDesign == "case-report"
|
|
},
|
|
Tier: 4,
|
|
},
|
|
{
|
|
Name: "Reddit",
|
|
Predicate: func(m SourceMetadata) bool {
|
|
return strings.Contains(m.URL, "reddit.com")
|
|
},
|
|
Tier: 5,
|
|
},
|
|
{
|
|
Name: "Patient Forum",
|
|
Predicate: func(m SourceMetadata) bool {
|
|
return m.Source == "patient-forum"
|
|
},
|
|
Tier: 5,
|
|
},
|
|
{
|
|
Name: "News",
|
|
Predicate: func(m SourceMetadata) bool {
|
|
return m.Source == "news-api"
|
|
},
|
|
Tier: 6,
|
|
},
|
|
{
|
|
Name: "Social Media",
|
|
Predicate: func(m SourceMetadata) bool {
|
|
return m.Source == "tiktok" || m.Source == "instagram" || m.Source == "twitter"
|
|
},
|
|
Tier: 6,
|
|
},
|
|
}
|
|
```
|
|
|
|
### 1.5 Metadata Enrichment
|
|
|
|
Adds structured metadata before submission.
|
|
|
|
```go
|
|
// enrichment/service.go
|
|
|
|
type EnrichmentService struct {
|
|
crossref CrossRefClient
|
|
pubmed PubMedClient
|
|
reddit RedditClient
|
|
}
|
|
|
|
type EnrichedMetadata struct {
|
|
// Academic sources
|
|
Journal string `json:"journal,omitempty"`
|
|
DOI string `json:"doi,omitempty"`
|
|
SampleSize int `json:"sample_size,omitempty"`
|
|
StudyDesign string `json:"study_design,omitempty"`
|
|
|
|
// Social sources
|
|
Platform string `json:"platform,omitempty"`
|
|
Subreddit string `json:"subreddit,omitempty"`
|
|
Upvotes int `json:"upvotes,omitempty"`
|
|
Replies int `json:"replies,omitempty"`
|
|
|
|
// Sentiment (computed during extraction)
|
|
Sentiment string `json:"sentiment,omitempty"`
|
|
SentimentPolarity float32 `json:"sentiment_polarity,omitempty"`
|
|
}
|
|
|
|
func (e *EnrichmentService) Enrich(source SourceMetadata) (*EnrichedMetadata, error) {
|
|
meta := &EnrichedMetadata{}
|
|
|
|
if source.DOI != "" {
|
|
// CrossRef lookup for journal, citations
|
|
crossrefData, _ := e.crossref.GetByDOI(source.DOI)
|
|
meta.Journal = crossrefData.ContainerTitle
|
|
}
|
|
|
|
if strings.Contains(source.URL, "reddit.com") {
|
|
// Reddit API for engagement metrics
|
|
redditData, _ := e.reddit.GetPost(source.URL)
|
|
meta.Upvotes = redditData.Score
|
|
meta.Replies = redditData.NumComments
|
|
meta.Subreddit = redditData.Subreddit
|
|
}
|
|
|
|
return meta, nil
|
|
}
|
|
```
|
|
|
|
### 1.6 Assembly & Submission
|
|
|
|
Combines all components into a signed assertion.
|
|
|
|
```go
|
|
// ingestion/submit.go
|
|
|
|
type AssertionSubmitter struct {
|
|
episteme EpistemeClient
|
|
wallet AgentWallet
|
|
classifier SourceClassifier
|
|
enricher EnrichmentService
|
|
}
|
|
|
|
func (s *AssertionSubmitter) Submit(claim ExtractedClaim, source SourceMetadata) error {
|
|
// 1. Classify source tier
|
|
tier := s.classifier.Classify(source)
|
|
|
|
// 2. Enrich metadata
|
|
meta, _ := s.enricher.Enrich(source)
|
|
metaJSON, _ := json.Marshal(meta)
|
|
|
|
// 3. Compute source hash
|
|
sourceHash := blake3.Sum256([]byte(source.URL + source.Content))
|
|
|
|
// 4. Build assertion
|
|
assertion := &CreateAssertionRequest{
|
|
Subject: claim.Subject,
|
|
Predicate: claim.Predicate,
|
|
Object: ObjectText(claim.Object),
|
|
Confidence: claim.Confidence,
|
|
SourceClass: &tier,
|
|
SourceHash: hex.EncodeToString(sourceHash[:]),
|
|
SourceMetadata: string(metaJSON),
|
|
Lifecycle: "Proposed",
|
|
}
|
|
|
|
// 5. Sign with agent key
|
|
signature, _ := s.wallet.Sign(assertion)
|
|
assertion.Signatures = []SignatureDTO{signature}
|
|
|
|
// 6. Submit to Episteme
|
|
return s.episteme.Assert(assertion)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Component 2: Background Gardener
|
|
|
|
Monitors the knowledge graph for emerging signals.
|
|
|
|
### 2.1 Cluster Detection
|
|
|
|
```go
|
|
// gardener/cluster_detector.go
|
|
|
|
type ClusterDetector struct {
|
|
episteme EpistemeClient
|
|
thresholds ClusterThresholds
|
|
}
|
|
|
|
type ClusterThresholds struct {
|
|
MinAssertions int // Minimum assertions to be considered a cluster
|
|
TimeWindow time.Duration // Window for growth rate calculation
|
|
GrowthRateHigh float64 // Assertions per month considered "high"
|
|
TierGapTrigger bool // Trigger if Tier 5+ exists but Tier 1-2 doesn't
|
|
}
|
|
|
|
type DetectedCluster struct {
|
|
Subject string
|
|
Predicate string
|
|
Tier5Count int
|
|
Tier1Count int
|
|
GrowthRate float64 // Assertions per month
|
|
HasClinicalGap bool // Tier 5+ exists, no Tier 1-2
|
|
EarliestReport time.Time
|
|
}
|
|
|
|
func (d *ClusterDetector) Scan() ([]DetectedCluster, error) {
|
|
// Query Episteme for all subject+predicate pairs
|
|
// For each pair, count assertions by tier
|
|
// Calculate growth rate over time window
|
|
// Flag clusters that meet thresholds
|
|
}
|
|
```
|
|
|
|
### 2.2 Escalation Assertion Generation
|
|
|
|
```go
|
|
// gardener/escalation.go
|
|
|
|
func (g *Gardener) GenerateEscalation(cluster DetectedCluster) *CreateAssertionRequest {
|
|
meta := map[string]interface{}{
|
|
"trigger": "cluster_threshold",
|
|
"tier_5_count": cluster.Tier5Count,
|
|
"tier_1_count": cluster.Tier1Count,
|
|
"growth_rate": fmt.Sprintf("%.0f/month", cluster.GrowthRate),
|
|
"clinical_gap": cluster.HasClinicalGap,
|
|
"earliest_report": cluster.EarliestReport.Format(time.RFC3339),
|
|
}
|
|
metaJSON, _ := json.Marshal(meta)
|
|
|
|
return &CreateAssertionRequest{
|
|
Subject: cluster.Subject,
|
|
Predicate: "escalation_signal",
|
|
Object: ObjectText("Anecdotal cluster detected"),
|
|
Confidence: 0.6, // Moderate confidence in the signal
|
|
SourceClass: ptr(uint8(255)), // Meta-tier for system-generated
|
|
SourceMetadata: string(metaJSON),
|
|
Lifecycle: "UnderReview",
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2.3 Scheduled Tasks
|
|
|
|
```go
|
|
// gardener/scheduler.go
|
|
|
|
func (g *Gardener) RunSchedule(ctx context.Context) {
|
|
// Every hour: Scan for new clusters
|
|
go g.runEvery(ctx, 1*time.Hour, g.ScanClusters)
|
|
|
|
// Every day: Decay trust ranks
|
|
go g.runEvery(ctx, 24*time.Hour, g.DecayTrustRanks)
|
|
|
|
// Every week: Generate summary reports
|
|
go g.runEvery(ctx, 7*24*time.Hour, g.GenerateReports)
|
|
}
|
|
|
|
func (g *Gardener) DecayTrustRanks(ctx context.Context) error {
|
|
return g.episteme.Post("/v1/admin/decay-trust-ranks", nil)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Component 3: Disagreement Dashboard
|
|
|
|
### 3.1 Query Patterns
|
|
|
|
The dashboard needs these Episteme queries:
|
|
|
|
```go
|
|
// dashboard/queries.go
|
|
|
|
// Get layered consensus for a topic
|
|
func (d *Dashboard) GetLayeredConsensus(subject string) (*LayeredResponse, error) {
|
|
return d.episteme.Query(&QueryParams{
|
|
Subject: subject,
|
|
Lens: "LayeredConsensus",
|
|
})
|
|
}
|
|
|
|
// Get conflict map using Skeptic lens
|
|
func (d *Dashboard) GetConflictMap(subject string) (*SkepticResponse, error) {
|
|
return d.episteme.Query(&QueryParams{
|
|
Subject: subject,
|
|
Lens: "Skeptic",
|
|
})
|
|
}
|
|
|
|
// Get historical state at a point in time
|
|
func (d *Dashboard) GetHistoricalState(subject string, asOf time.Time) (*QueryResponse, error) {
|
|
return d.episteme.Query(&QueryParams{
|
|
Subject: subject,
|
|
Lens: "LayeredConsensus",
|
|
AsOf: asOf.Unix(),
|
|
})
|
|
}
|
|
|
|
// Get changes since last visit
|
|
func (d *Dashboard) GetChangesSince(subject string, since time.Time) (*QueryResponse, error) {
|
|
return d.episteme.Query(&QueryParams{
|
|
Subject: subject,
|
|
Lens: "LayeredConsensus",
|
|
Since: since.Unix(),
|
|
})
|
|
}
|
|
```
|
|
|
|
### 3.2 Response Transformation
|
|
|
|
```go
|
|
// dashboard/transform.go
|
|
|
|
type ConsumerView struct {
|
|
Topic string `json:"topic"`
|
|
Summary string `json:"summary"` // LLM-generated
|
|
ConflictScore float32 `json:"conflict_score"`
|
|
TierPositions []TierPosition `json:"tier_positions"`
|
|
ResolvedTopics []ResolvedTopic `json:"resolved"`
|
|
ActiveDisputes []ActiveDispute `json:"active_disputes"`
|
|
EmergingSignals []EmergingSignal `json:"emerging_signals"`
|
|
LastUpdated time.Time `json:"last_updated"`
|
|
}
|
|
|
|
type TierPosition struct {
|
|
TierName string `json:"tier_name"` // "Clinical Evidence", "Patient Community"
|
|
TierNumber uint8 `json:"tier_number"`
|
|
Position string `json:"position"`
|
|
Confidence float32 `json:"confidence"`
|
|
AssertionCount int `json:"assertion_count"`
|
|
}
|
|
|
|
func TransformForConsumer(layered *LayeredResponse, skeptic *SkepticResponse) *ConsumerView {
|
|
view := &ConsumerView{
|
|
ConflictScore: layered.OverallConflictScore,
|
|
}
|
|
|
|
for _, tier := range layered.Tiers {
|
|
view.TierPositions = append(view.TierPositions, TierPosition{
|
|
TierName: tierToName(tier.Tier),
|
|
TierNumber: tier.Tier,
|
|
Position: summarizePosition(tier.Winner),
|
|
Confidence: tier.Winner.Confidence,
|
|
AssertionCount: tier.CandidatesCount,
|
|
})
|
|
}
|
|
|
|
// Categorize by conflict level
|
|
for _, topic := range skeptic.Topics {
|
|
if topic.ConflictScore < 0.2 {
|
|
view.ResolvedTopics = append(view.ResolvedTopics, ...)
|
|
} else if topic.ConflictScore < 0.7 {
|
|
view.ActiveDisputes = append(view.ActiveDisputes, ...)
|
|
} else {
|
|
view.EmergingSignals = append(view.EmergingSignals, ...)
|
|
}
|
|
}
|
|
|
|
return view
|
|
}
|
|
|
|
func tierToName(tier uint8) string {
|
|
names := map[uint8]string{
|
|
0: "Regulatory",
|
|
1: "Clinical Evidence",
|
|
2: "Real-World Evidence",
|
|
3: "Pharmacovigilance",
|
|
4: "Clinical Anecdote",
|
|
5: "Patient Community",
|
|
6: "Media",
|
|
}
|
|
return names[tier]
|
|
}
|
|
```
|
|
|
|
### 3.3 LLM Summary Generation
|
|
|
|
```go
|
|
// dashboard/summary.go
|
|
|
|
type SummaryGenerator struct {
|
|
llm LLMClient
|
|
}
|
|
|
|
func (g *SummaryGenerator) GenerateSummary(view *ConsumerView) (string, error) {
|
|
prompt := fmt.Sprintf(`
|
|
Summarize the following health topic for a consumer in 2-3 sentences.
|
|
Be factual. Acknowledge uncertainty where it exists. Do not give medical advice.
|
|
|
|
Topic: %s
|
|
Conflict Score: %.2f (0 = agreement, 1 = max disagreement)
|
|
|
|
Tier Positions:
|
|
%s
|
|
|
|
Output a concise, balanced summary.
|
|
`, view.Topic, view.ConflictScore, formatTierPositions(view.TierPositions))
|
|
|
|
return g.llm.Complete(prompt)
|
|
}
|
|
```
|
|
|
|
**Example output:**
|
|
> "Clinical trials show low incidence of gastroparesis with semaglutide; post-marketing reports and patient communities report higher rates. The FDA added gastroparesis to the label in January 2024. There is moderate disagreement between clinical evidence and real-world reports."
|
|
|
|
---
|
|
|
|
## Component 4: Agent Wallet
|
|
|
|
Manages signing keys for the ingestion pipeline.
|
|
|
|
### 4.1 Key Storage
|
|
|
|
```go
|
|
// wallet/wallet.go
|
|
|
|
type AgentWallet struct {
|
|
keyPath string
|
|
privateKey ed25519.PrivateKey
|
|
publicKey ed25519.PublicKey
|
|
}
|
|
|
|
func NewAgentWallet(keyPath string) (*AgentWallet, error) {
|
|
// Load or generate Ed25519 keypair
|
|
// Store private key securely (file permissions, encryption at rest)
|
|
}
|
|
|
|
func (w *AgentWallet) Sign(assertion *CreateAssertionRequest) (*SignatureDTO, error) {
|
|
// Serialize assertion for signing
|
|
message := fmt.Sprintf("%s:%s", assertion.Subject, assertion.Predicate)
|
|
sig := ed25519.Sign(w.privateKey, []byte(message))
|
|
|
|
return &SignatureDTO{
|
|
AgentID: hex.EncodeToString(w.publicKey),
|
|
Signature: hex.EncodeToString(sig),
|
|
Timestamp: time.Now().Unix(),
|
|
}, nil
|
|
}
|
|
```
|
|
|
|
### 4.2 Multi-Agent Setup
|
|
|
|
For different source types, use different agent identities:
|
|
|
|
```go
|
|
// wallet/multi.go
|
|
|
|
type MultiAgentWallet struct {
|
|
agents map[string]*AgentWallet
|
|
}
|
|
|
|
func (m *MultiAgentWallet) SignAs(agentName string, assertion *CreateAssertionRequest) (*SignatureDTO, error) {
|
|
agent, ok := m.agents[agentName]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown agent: %s", agentName)
|
|
}
|
|
return agent.Sign(assertion)
|
|
}
|
|
|
|
// Usage:
|
|
// - "pubmed-crawler" agent for academic sources
|
|
// - "reddit-crawler" agent for social sources
|
|
// - "gardener" agent for escalation assertions
|
|
```
|
|
|
|
---
|
|
|
|
## Deployment Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ Kubernetes Cluster │
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
│ │ PubMed │ │ Reddit │ │ FAERS │ Crawlers │
|
|
│ │ Crawler │ │ Crawler │ │ Crawler │ (CronJob) │
|
|
│ │ Pod │ │ Pod │ │ Pod │ │
|
|
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
|
│ │ │ │ │
|
|
│ └────────────────┼────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
│ │ Extraction Service │ │
|
|
│ │ (Deployment, 3 replicas) │ │
|
|
│ └───────────────────────────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
│ │ Episteme API │ │
|
|
│ │ (Deployment, 3 replicas) │ │
|
|
│ └───────────────────────────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
│ │ Dashboard API │ │
|
|
│ │ (Deployment, 2 replicas) │ │
|
|
│ └───────────────────────────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
│ │ Dashboard UI │ │
|
|
│ │ (Static, CDN) │ │
|
|
│ └───────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
│ │ Background Gardener │ │
|
|
│ │ (CronJob, hourly) │ │
|
|
│ └───────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Checklist: What You Need to Build
|
|
|
|
### Crawlers (one per source)
|
|
- [ ] PubMed/biorxiv crawler (NCBI E-utilities)
|
|
- [ ] ClinicalTrials.gov crawler
|
|
- [ ] FDA DailyMed scraper
|
|
- [ ] openFDA FAERS consumer
|
|
- [ ] Reddit API consumer
|
|
- [ ] Patient forum scrapers
|
|
- [ ] News API consumer
|
|
- [ ] Social media sampler
|
|
|
|
### Extraction Pipeline
|
|
- [ ] LLM-based claim extractor
|
|
- [ ] Subject/predicate ontology for pharma domain
|
|
- [ ] Extraction prompt library
|
|
- [ ] Confidence calibration
|
|
|
|
### Classification & Enrichment
|
|
- [ ] Source-class classifier with pharma rules
|
|
- [ ] CrossRef integration for DOI lookup
|
|
- [ ] PubMed integration for study metadata
|
|
- [ ] Reddit API for engagement metrics
|
|
- [ ] Sentiment analysis model
|
|
|
|
### Infrastructure
|
|
- [ ] Agent wallet with secure key storage
|
|
- [ ] Multi-agent identity management
|
|
- [ ] Rate limiting for external APIs
|
|
- [ ] Retry logic and error handling
|
|
|
|
### Gardener
|
|
- [ ] Cluster detection algorithm
|
|
- [ ] Escalation assertion generator
|
|
- [ ] TrustRank decay scheduler
|
|
- [ ] Alert/notification system
|
|
|
|
### Dashboard
|
|
- [ ] Query orchestration layer
|
|
- [ ] Response transformation
|
|
- [ ] LLM summary generator
|
|
- [ ] React/Vue frontend
|
|
- [ ] Time-travel UI
|
|
- [ ] Change notification system
|
|
|
|
---
|
|
|
|
## See Also
|
|
|
|
- [Use Case: Consumer Health Intelligence](../../use-cases/consumer-health-intelligence.md) — Full scenario walkthrough
|
|
- [App Concepts Index](./index.md) — General application layer patterns
|
|
- [Roadmap: Phase 3](../../roadmap.md) — Database features needed
|
|
- [ADK-Go Integration](../../.claude/guides/integrations/adk-go-episteme.md) — Agent framework integration
|