Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +78 -76
- MEXAR_Phase1_Architecture_Fixes.md +309 -309
- MEXAR_Phase2_Dataset_Collection.md +291 -291
- README.md +443 -418
- backend/.env.example +61 -61
- backend/core/config.py +31 -27
- backend/evaluation/guardrail_threshold_sweep.py +155 -0
- backend/evaluation/run_all.py +269 -243
- backend/evaluation_outputs/full_evaluation_20260724_095510.json +15 -0
- backend/evaluation_outputs/full_evaluation_20260724_183942.json +0 -0
- backend/evaluation_outputs/full_evaluation_20260724_192144.json +0 -0
- backend/evaluation_outputs/guardrail_threshold_sweep.json +128 -0
- backend/evaluation_outputs/test.json +1 -0
- backend/migrations/hybrid_search_function.sql +103 -103
- backend/modules/reasoning_engine.py +633 -633
- backend/scripts/annotate_ground_truth_docs.py +121 -0
- backend/scripts/fetch_courtlistener.py +290 -294
- backend/scripts/fetch_pubmed.py +329 -333
- backend/scripts/fetch_secedgar.py +470 -467
- backend/scripts/recompile_agents_from_real_data.py +92 -0
- backend/utils/faithfulness.py +374 -366
- backend/utils/groq_client.py +327 -308
- data/agents/financial_agent/context.txt +1182 -0
- data/agents/financial_agent/metadata.json +275 -0
- data/agents/legal_agent/context.txt +0 -0
- data/agents/legal_agent/metadata.json +363 -0
- data/agents/medical_agent/context.txt +0 -0
- data/agents/medical_agent/metadata.json +326 -0
- evaluation_outputs/full_evaluation_20260724_183942.json +0 -0
- evaluation_outputs/full_evaluation_20260724_192144.json +0 -0
- evaluation_outputs/guardrail_threshold_sweep.json +128 -0
- test_data/financial_real/0000320575_0000320575-20-000008.txt +1 -0
- test_data/financial_real/0001041588_0001041588-20-000001.txt +401 -0
- test_data/financial_real/0001041588_0001041588-21-000001.txt +503 -0
- test_data/financial_real/0001386570_0001654954-20-005725.txt +257 -0
- test_data/financial_real/manifest.json +42 -0
- test_data/legal_real/case_10129044.txt +15 -0
- test_data/legal_real/case_1027758.txt +15 -0
- test_data/legal_real/case_10305113.txt +15 -0
- test_data/legal_real/case_10318020.txt +15 -0
- test_data/legal_real/case_10353022.txt +15 -0
- test_data/legal_real/case_10362523.txt +15 -0
- test_data/legal_real/case_10388952.txt +15 -0
- test_data/legal_real/case_10463618.txt +15 -0
- test_data/legal_real/case_10513129.txt +15 -0
- test_data/legal_real/case_1052233.txt +260 -0
- test_data/legal_real/case_10574918.txt +15 -0
- test_data/legal_real/case_10589788.txt +15 -0
- test_data/legal_real/case_10639650.txt +15 -0
- test_data/legal_real/case_10640482.txt +15 -0
.gitignore
CHANGED
|
@@ -1,76 +1,78 @@
|
|
| 1 |
-
# Models (FastEmbed cache)
|
| 2 |
-
.cache/
|
| 3 |
-
models/
|
| 4 |
-
*.bin
|
| 5 |
-
*.onnx
|
| 6 |
-
|
| 7 |
-
# Python
|
| 8 |
-
__pycache__/
|
| 9 |
-
*.py[cod]
|
| 10 |
-
*$py.class
|
| 11 |
-
*.so
|
| 12 |
-
.Python
|
| 13 |
-
venv/
|
| 14 |
-
.venv/
|
| 15 |
-
env/
|
| 16 |
-
ENV/
|
| 17 |
-
.env
|
| 18 |
-
.env.local
|
| 19 |
-
.env.production
|
| 20 |
-
*.env
|
| 21 |
-
*.egg-info/
|
| 22 |
-
.eggs/
|
| 23 |
-
*.egg
|
| 24 |
-
dist/
|
| 25 |
-
build/
|
| 26 |
-
|
| 27 |
-
# Node.js
|
| 28 |
-
node_modules/
|
| 29 |
-
npm-debug.log*
|
| 30 |
-
yarn-debug.log*
|
| 31 |
-
yarn-error.log*
|
| 32 |
-
|
| 33 |
-
# Build outputs
|
| 34 |
-
frontend/build/
|
| 35 |
-
*.tgz
|
| 36 |
-
|
| 37 |
-
# IDE
|
| 38 |
-
.vscode/
|
| 39 |
-
.idea/
|
| 40 |
-
*.swp
|
| 41 |
-
*.swo
|
| 42 |
-
*~
|
| 43 |
-
.project
|
| 44 |
-
.classpath
|
| 45 |
-
.settings/
|
| 46 |
-
|
| 47 |
-
# OS files
|
| 48 |
-
.DS_Store
|
| 49 |
-
.DS_Store?
|
| 50 |
-
._*
|
| 51 |
-
.Spotlight-V100
|
| 52 |
-
.Trashes
|
| 53 |
-
ehthumbs.db
|
| 54 |
-
Thumbs.db
|
| 55 |
-
|
| 56 |
-
# Data directories
|
| 57 |
-
backend/data/storage/
|
| 58 |
-
backend/data/temp/
|
| 59 |
-
backend/data/tts_cache/
|
| 60 |
-
backend/data/agents/
|
| 61 |
-
*.db
|
| 62 |
-
|
| 63 |
-
# Test data (optional - uncomment if you want to include)
|
| 64 |
-
# test_data/
|
| 65 |
-
|
| 66 |
-
# Logs
|
| 67 |
-
*.log
|
| 68 |
-
logs/
|
| 69 |
-
|
| 70 |
-
# Misc
|
| 71 |
-
*.bak
|
| 72 |
-
*.tmp
|
| 73 |
-
.cache/
|
| 74 |
-
|
| 75 |
-
# Documentation
|
| 76 |
-
md files/
|
|
|
|
|
|
|
|
|
| 1 |
+
# Models (FastEmbed cache)
|
| 2 |
+
.cache/
|
| 3 |
+
models/
|
| 4 |
+
*.bin
|
| 5 |
+
*.onnx
|
| 6 |
+
|
| 7 |
+
# Python
|
| 8 |
+
__pycache__/
|
| 9 |
+
*.py[cod]
|
| 10 |
+
*$py.class
|
| 11 |
+
*.so
|
| 12 |
+
.Python
|
| 13 |
+
venv/
|
| 14 |
+
.venv/
|
| 15 |
+
env/
|
| 16 |
+
ENV/
|
| 17 |
+
.env
|
| 18 |
+
.env.local
|
| 19 |
+
.env.production
|
| 20 |
+
*.env
|
| 21 |
+
*.egg-info/
|
| 22 |
+
.eggs/
|
| 23 |
+
*.egg
|
| 24 |
+
dist/
|
| 25 |
+
build/
|
| 26 |
+
|
| 27 |
+
# Node.js
|
| 28 |
+
node_modules/
|
| 29 |
+
npm-debug.log*
|
| 30 |
+
yarn-debug.log*
|
| 31 |
+
yarn-error.log*
|
| 32 |
+
|
| 33 |
+
# Build outputs
|
| 34 |
+
frontend/build/
|
| 35 |
+
*.tgz
|
| 36 |
+
|
| 37 |
+
# IDE
|
| 38 |
+
.vscode/
|
| 39 |
+
.idea/
|
| 40 |
+
*.swp
|
| 41 |
+
*.swo
|
| 42 |
+
*~
|
| 43 |
+
.project
|
| 44 |
+
.classpath
|
| 45 |
+
.settings/
|
| 46 |
+
|
| 47 |
+
# OS files
|
| 48 |
+
.DS_Store
|
| 49 |
+
.DS_Store?
|
| 50 |
+
._*
|
| 51 |
+
.Spotlight-V100
|
| 52 |
+
.Trashes
|
| 53 |
+
ehthumbs.db
|
| 54 |
+
Thumbs.db
|
| 55 |
+
|
| 56 |
+
# Data directories
|
| 57 |
+
backend/data/storage/
|
| 58 |
+
backend/data/temp/
|
| 59 |
+
backend/data/tts_cache/
|
| 60 |
+
backend/data/agents/
|
| 61 |
+
*.db
|
| 62 |
+
|
| 63 |
+
# Test data (optional - uncomment if you want to include)
|
| 64 |
+
# test_data/
|
| 65 |
+
|
| 66 |
+
# Logs
|
| 67 |
+
*.log
|
| 68 |
+
logs/
|
| 69 |
+
|
| 70 |
+
# Misc
|
| 71 |
+
*.bak
|
| 72 |
+
*.tmp
|
| 73 |
+
.cache/
|
| 74 |
+
|
| 75 |
+
# Documentation & Prompt Notes
|
| 76 |
+
md files/
|
| 77 |
+
MEXAR_Gap_Fixes_Prompt.md
|
| 78 |
+
*.prompt.md
|
MEXAR_Phase1_Architecture_Fixes.md
CHANGED
|
@@ -1,309 +1,309 @@
|
|
| 1 |
-
# MEXAR Phase 1 — Architecture Fixes to Match the Paper
|
| 2 |
-
|
| 3 |
-
**Purpose of this document:** hand this to an AI coding agent (Antigravity or similar) working directly in the `Mexar` repository (`github.com/devrajsinh2012/Mexar`). This is Phase 1 only: it closes the gap between what the paper *"Hybrid Retrieval and Claim Verification: An Empirical Study of Constrained RAG for High-Stakes Professional Domains"* claims about the system's architecture, and what the code currently does. No dataset work in this document, that's Phase 2, handled separately.
|
| 4 |
-
|
| 5 |
-
**Presentation date:** July 30-31, 2026. Target for this phase: 3 days.
|
| 6 |
-
|
| 7 |
-
**Ground rule for the agent:** every change below must keep the app deployable on the current stack (FastAPI backend on Hugging Face Spaces, Supabase Postgres + pgvector, React frontend on Vercel). Don't introduce services that need infra Dev doesn't already have.
|
| 8 |
-
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
## 1.0 Preconditions
|
| 12 |
-
|
| 13 |
-
- Add to `backend/requirements.txt`:
|
| 14 |
-
```
|
| 15 |
-
scikit-learn>=1.4.0
|
| 16 |
-
spacy>=3.7.0
|
| 17 |
-
```
|
| 18 |
-
- After install, download the small English model: `python -m spacy download en_core_web_sm`. Add this as a step in `Dockerfile` so it survives Hugging Face Spaces rebuilds.
|
| 19 |
-
- Verify the pgvector extension version on the Supabase project supports HNSW (needs pgvector >= 0.5.0). Run `SELECT extversion FROM pg_extension WHERE extname = 'vector';`. If below 0.5.0, run `ALTER EXTENSION vector UPDATE;` before touching the index migration in 1.2.
|
| 20 |
-
|
| 21 |
-
---
|
| 22 |
-
|
| 23 |
-
## 1.1 Domain Guardrail: replace the heuristic with real TF-IDF + NER + Jaccard
|
| 24 |
-
|
| 25 |
-
**Why:** Paper Section III-A defines the guardrail as TF-IDF term weighting (Eq. 1), NER-extracted domain entities, and Jaccard similarity between query and signature (Eq. 2). The current `_check_guardrail()` in `backend/modules/reasoning_engine.py` is a hand-tuned fuzzy-string-match heuristic with arbitrary bonus weights and no TF-IDF or NER anywhere. This needs to become the real thing.
|
| 26 |
-
|
| 27 |
-
**New file: `backend/utils/domain_signature.py`**
|
| 28 |
-
|
| 29 |
-
```python
|
| 30 |
-
"""
|
| 31 |
-
MEXAR - Domain Signature Construction
|
| 32 |
-
Implements Section III-A: TF-IDF lexical signature + NER entity signature.
|
| 33 |
-
"""
|
| 34 |
-
from typing import List, Dict, Set, Tuple
|
| 35 |
-
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 36 |
-
import spacy
|
| 37 |
-
import logging
|
| 38 |
-
|
| 39 |
-
logger = logging.getLogger(__name__)
|
| 40 |
-
_nlp = None
|
| 41 |
-
|
| 42 |
-
def get_nlp():
|
| 43 |
-
global _nlp
|
| 44 |
-
if _nlp is None:
|
| 45 |
-
_nlp = spacy.load("en_core_web_sm")
|
| 46 |
-
return _nlp
|
| 47 |
-
|
| 48 |
-
# Entity labels relevant to professional domains (medical/legal/financial)
|
| 49 |
-
RELEVANT_ENTITY_LABELS = {"ORG", "LAW", "PRODUCT", "GPE", "PERSON", "NORP", "EVENT", "FAC"}
|
| 50 |
-
|
| 51 |
-
def build_tfidf_signature(documents: List[str], top_n: int = 100, tau_tf: float = 0.0) -> List[Tuple[str, float]]:
|
| 52 |
-
"""
|
| 53 |
-
Eq. 1: w(t, D) = tf(t, D) * log(|D| / |{d in D : t in d}|)
|
| 54 |
-
Returns top_n (term, weight) pairs above tau_tf, sorted descending by weight.
|
| 55 |
-
"""
|
| 56 |
-
if not documents or len(documents) < 2:
|
| 57 |
-
# TF-IDF needs a corpus of multiple "documents" to compute IDF meaningfully.
|
| 58 |
-
# If compiling from a single uploaded file, split it into paragraph-level
|
| 59 |
-
# pseudo-documents so IDF is not degenerate.
|
| 60 |
-
documents = _split_into_pseudo_documents(documents[0]) if documents else []
|
| 61 |
-
|
| 62 |
-
vectorizer = TfidfVectorizer(
|
| 63 |
-
max_features=2000,
|
| 64 |
-
stop_words="english",
|
| 65 |
-
ngram_range=(1, 2),
|
| 66 |
-
min_df=1,
|
| 67 |
-
)
|
| 68 |
-
matrix = vectorizer.fit_transform(documents)
|
| 69 |
-
scores = matrix.sum(axis=0).A1
|
| 70 |
-
terms = vectorizer.get_feature_names_out()
|
| 71 |
-
pairs = [(t, float(s)) for t, s in zip(terms, scores) if s > tau_tf]
|
| 72 |
-
pairs.sort(key=lambda x: x[1], reverse=True)
|
| 73 |
-
return pairs[:top_n]
|
| 74 |
-
|
| 75 |
-
def _split_into_pseudo_documents(text: str, chunk_size: int = 500) -> List[str]:
|
| 76 |
-
words = text.split()
|
| 77 |
-
return [" ".join(words[i:i+chunk_size]) for i in range(0, len(words), chunk_size)] or [text]
|
| 78 |
-
|
| 79 |
-
def extract_ner_entities(documents: List[str], tau_ent: int = 2, max_docs_to_scan: int = 200) -> List[str]:
|
| 80 |
-
"""
|
| 81 |
-
Extracts named entities appearing in at least tau_ent documents.
|
| 82 |
-
Capped at max_docs_to_scan for latency (spaCy is not free at compile time).
|
| 83 |
-
"""
|
| 84 |
-
nlp = get_nlp()
|
| 85 |
-
doc_freq: Dict[str, int] = {}
|
| 86 |
-
for doc_text in documents[:max_docs_to_scan]:
|
| 87 |
-
seen_in_this_doc: Set[str] = set()
|
| 88 |
-
for ent in nlp(doc_text[:5000]).ents: # cap per-doc length for speed
|
| 89 |
-
if ent.label_ in RELEVANT_ENTITY_LABELS:
|
| 90 |
-
key = ent.text.lower().strip()
|
| 91 |
-
if key and key not in seen_in_this_doc:
|
| 92 |
-
seen_in_this_doc.add(key)
|
| 93 |
-
for key in seen_in_this_doc:
|
| 94 |
-
doc_freq[key] = doc_freq.get(key, 0) + 1
|
| 95 |
-
return [k for k, v in doc_freq.items() if v >= tau_ent]
|
| 96 |
-
|
| 97 |
-
def build_domain_signature(
|
| 98 |
-
documents: List[str],
|
| 99 |
-
tau_tf: float = 0.0,
|
| 100 |
-
tau_ent: int = 2,
|
| 101 |
-
top_n_lexical: int = 100,
|
| 102 |
-
) -> Dict[str, List]:
|
| 103 |
-
"""Returns Sigma = Sigma_lex ∪ Sigma_ent as described in Section III-A, kept separate for inspection."""
|
| 104 |
-
lexical_pairs = build_tfidf_signature(documents, top_n=top_n_lexical, tau_tf=tau_tf)
|
| 105 |
-
lexical_terms = [t for t, _ in lexical_pairs]
|
| 106 |
-
entities = extract_ner_entities(documents, tau_ent=tau_ent)
|
| 107 |
-
return {
|
| 108 |
-
"lexical": lexical_terms,
|
| 109 |
-
"lexical_weights": {t: w for t, w in lexical_pairs},
|
| 110 |
-
"entities": entities,
|
| 111 |
-
"combined": sorted(set(lexical_terms) | set(e for e in entities)),
|
| 112 |
-
}
|
| 113 |
-
```
|
| 114 |
-
|
| 115 |
-
**Modify `backend/modules/knowledge_compiler.py`:**
|
| 116 |
-
Replace `_extract_domain_signature()` so it calls `build_domain_signature()` from the new module, passing in the raw per-source-document text list (not the flattened `text_context` string, we need per-document granularity for TF-IDF's document-frequency term to mean anything). Store the result's `combined` list in the existing `domain_signature` JSONB column, and add the `lexical_weights` dict to a **new** JSONB column (see migration below) so the guardrail can compute a weighted Jaccard rather than a flat one if useful later.
|
| 117 |
-
|
| 118 |
-
**New migration** (Alembic or raw SQL depending on how `backend/migrations` is structured, check `backend/migrations/README.md` for the convention already in use):
|
| 119 |
-
```sql
|
| 120 |
-
ALTER TABLE agents ADD COLUMN IF NOT EXISTS domain_signature_weights JSONB;
|
| 121 |
-
ALTER TABLE agents ADD COLUMN IF NOT EXISTS domain_entities JSONB;
|
| 122 |
-
```
|
| 123 |
-
|
| 124 |
-
**Rewrite `_check_guardrail()` in `backend/modules/reasoning_engine.py`:**
|
| 125 |
-
Delete the fuzzy-match/bonus-score implementation entirely. Replace with real Eq. 2:
|
| 126 |
-
|
| 127 |
-
```python
|
| 128 |
-
def _check_guardrail(self, query: str, domain_signature: List[str], prompt_analysis: Dict[str, Any]) -> Tuple[bool, float]:
|
| 129 |
-
"""Eq. 2: sim(q, Sigma) = |Phi(q) ∩ Sigma| / |Phi(q)|"""
|
| 130 |
-
from utils.domain_signature import get_nlp
|
| 131 |
-
nlp = get_nlp()
|
| 132 |
-
query_doc = nlp(query.lower())
|
| 133 |
-
# Phi(q): lemmatized content tokens, excluding stopwords/punctuation
|
| 134 |
-
phi_q = {
|
| 135 |
-
tok.lemma_ for tok in query_doc
|
| 136 |
-
if not tok.is_stop and not tok.is_punct and len(tok.text) > 2
|
| 137 |
-
}
|
| 138 |
-
sigma = set(s.lower() for s in (domain_signature or []))
|
| 139 |
-
|
| 140 |
-
if not phi_q:
|
| 141 |
-
return False, 0.0
|
| 142 |
-
|
| 143 |
-
intersection = phi_q & sigma
|
| 144 |
-
score = len(intersection) / len(phi_q)
|
| 145 |
-
|
| 146 |
-
is_in_domain = score >= self.DOMAIN_SIMILARITY_THRESHOLD
|
| 147 |
-
logger.info(f"Guardrail: sim={score:.3f}, |Phi(q)|={len(phi_q)}, |intersection|={len(intersection)}, in_domain={is_in_domain}")
|
| 148 |
-
return is_in_domain, score
|
| 149 |
-
```
|
| 150 |
-
|
| 151 |
-
**Tune `DOMAIN_SIMILARITY_THRESHOLD`:** the current 0.05 was calibrated against the *old* heuristic scorer, it will not transfer to real Jaccard similarity, which tends to produce lower absolute values on short queries. Don't guess this: after wiring the new guardrail, run `backend/evaluation/guardrail_analysis.py` against a small hand-labeled set of ~20 in-domain and ~20 out-of-domain queries per existing test_data domain, sweep the threshold from 0.05 to 0.4 in steps of 0.05, and pick the value that maximizes F1. Record that sweep, it becomes supporting evidence for Table IV in the paper instead of a guessed constant.
|
| 152 |
-
|
| 153 |
-
**Acceptance test:** for the medical test agent, a query like "what is the recommended treatment for stage 2 hypertension" should be accepted; a query like "how do I file a small claims lawsuit" should be rejected. Log both the old and new guardrail score during a transition period to sanity-check before removing the old code path.
|
| 154 |
-
|
| 155 |
-
---
|
| 156 |
-
|
| 157 |
-
## 1.2 Vector index: IVFFlat → HNSW
|
| 158 |
-
|
| 159 |
-
**Why:** Paper Section III-B explicitly claims HNSW indexing. The current migration (`backend/migrations/hybrid_search_function.sql`, lines ~98-101) creates an IVFFlat index instead.
|
| 160 |
-
|
| 161 |
-
**New migration file** `backend/migrations/switch_to_hnsw_index.sql`:
|
| 162 |
-
```sql
|
| 163 |
-
-- Drop the old IVFFlat index
|
| 164 |
-
DROP INDEX IF EXISTS idx_document_chunks_embedding;
|
| 165 |
-
|
| 166 |
-
-- Create HNSW index to match paper Section III-B
|
| 167 |
-
CREATE INDEX IF NOT EXISTS idx_document_chunks_embedding_hnsw
|
| 168 |
-
ON document_chunks USING hnsw (embedding vector_cosine_ops)
|
| 169 |
-
WITH (m = 16, ef_construction = 64);
|
| 170 |
-
|
| 171 |
-
COMMENT ON INDEX idx_document_chunks_embedding_hnsw IS
|
| 172 |
-
'HNSW index for cosine similarity search, m=16, ef_construction=64 (Section III-B)';
|
| 173 |
-
```
|
| 174 |
-
Apply via `Supabase:apply_migration` (or however existing migrations are run, check for a runner script referenced in `backend/migrations/README.md`) rather than editing the old file, so history is preserved.
|
| 175 |
-
|
| 176 |
-
**Note for the agent:** HNSW build time scales worse than IVFFlat with corpus size, but at the 500-600 document / few-thousand-chunk scale planned for the presentation, this is a non-issue. No further tuning needed.
|
| 177 |
-
|
| 178 |
-
**Acceptance test:** `\d document_chunks` in psql (or `SELECT indexdef FROM pg_indexes WHERE tablename = 'document_chunks';`) should show the HNSW index, not IVFFlat. Re-run a handful of existing queries and confirm retrieval still returns coherent top-5 chunks (HNSW is approximate, results may reorder slightly versus IVFFlat, that's expected and fine).
|
| 179 |
-
|
| 180 |
-
---
|
| 181 |
-
|
| 182 |
-
## 1.3 RRF weighting: reconcile code and paper
|
| 183 |
-
|
| 184 |
-
**Why:** Paper Eq. 5 is unweighted RRF. `backend/migrations/hybrid_search_function.sql` applies a 0.6 semantic / 0.4 keyword weighting inside the SQL function.
|
| 185 |
-
|
| 186 |
-
**Decision for this project (recommended, don't re-litigate):** keep the 0.6/0.4 weighting in code, it's a reasonable and defensible design choice, and update the paper instead. This is less risky than ripping out a tuned weighting under time pressure. The agent does not need to touch `hybrid_search_function.sql` for this item, just flag it in a "paper edits needed" note for later.
|
| 187 |
-
|
| 188 |
-
If Dev instead decides to match the paper exactly, the one-line change is setting `semantic_weight := 0.5; keyword_weight := 0.5;` in that SQL function, but this is **not** the default action, don't do it unless explicitly told.
|
| 189 |
-
|
| 190 |
-
---
|
| 191 |
-
|
| 192 |
-
## 1.4 Faithfulness verification: make DeBERTa-v3 NLI the real, primary signal
|
| 193 |
-
|
| 194 |
-
**Why:** this is the most important fix. Paper Section III-C and the entire Related Work argument against LLM-judging-LLM circularity claim the production system verifies claims with "an NLI model (DeBERTa-v3-large fine-tuned on MNLI)" using per-document max entailment (Eq. 6). In the actual code:
|
| 195 |
-
- The primary confidence-driving call in `reasoning_engine.reason()` (Step 6) is `self.faithfulness_scorer.score(answer, context)`, which is `FaithfulnessScorer` in `backend/utils/faithfulness.py`, an LLM (Groq) extracting claims and then Groq again answering YES/NO. That's exactly the circularity the paper argues against.
|
| 196 |
-
- The actual NLI model in the repo, `BartNLIScorer`, uses `facebook/bart-large-mnli`, not DeBERTa-v3-large, and it's computed only as a side "reviewer feedback" score (`bart_nli_result` in `reason()`), it never feeds the confidence calculation.
|
| 197 |
-
- The NLI scorer concatenates all retrieved context into a single premise string, it does not implement the per-document max in Eq. 6.
|
| 198 |
-
|
| 199 |
-
**Model choice:** use `cross-encoder/nli-deberta-v3-large` from Hugging Face. This is a DeBERTa-v3-large architecture fine-tuned specifically for NLI (SNLI + MultiNLI), it is the standard, citable checkpoint people mean when they say "DeBERTa-v3-large fine-tuned on MNLI" in RAG faithfulness papers, and it loads cleanly through `sentence-transformers`'s `CrossEncoder` class, which is already a dependency.
|
| 200 |
-
|
| 201 |
-
**Modify `backend/utils/faithfulness.py`:** replace the `BartNLIScorer` class with:
|
| 202 |
-
|
| 203 |
-
```python
|
| 204 |
-
class DebertaNLIScorer:
|
| 205 |
-
"""
|
| 206 |
-
Primary faithfulness verifier per Section III-C.
|
| 207 |
-
Per-claim, per-document max entailment probability (Eq. 6),
|
| 208 |
-
aggregated as the fraction of claims exceeding tau_ent (Eq. 7).
|
| 209 |
-
"""
|
| 210 |
-
LABEL_MAP = {0: "contradiction", 1: "entailment", 2: "neutral"} # verify against model card at load time
|
| 211 |
-
TAU_ENT = 0.7
|
| 212 |
-
|
| 213 |
-
def __init__(self):
|
| 214 |
-
self._model = None
|
| 215 |
-
|
| 216 |
-
@property
|
| 217 |
-
def model(self):
|
| 218 |
-
if self._model is None:
|
| 219 |
-
from sentence_transformers import CrossEncoder
|
| 220 |
-
logger.info("Loading cross-encoder/nli-deberta-v3-large...")
|
| 221 |
-
self._model = CrossEncoder("cross-encoder/nli-deberta-v3-large")
|
| 222 |
-
logger.info("DeBERTa-v3 NLI model loaded.")
|
| 223 |
-
return self._model
|
| 224 |
-
|
| 225 |
-
def score(self, answer: str, context_documents: List[str]) -> FaithfulnessResult:
|
| 226 |
-
"""
|
| 227 |
-
context_documents: list of individual retrieved chunk texts (NOT concatenated),
|
| 228 |
-
so per-document max in Eq. 6 is meaningful.
|
| 229 |
-
"""
|
| 230 |
-
import re
|
| 231 |
-
import numpy as np
|
| 232 |
-
|
| 233 |
-
if not answer or not context_documents:
|
| 234 |
-
return FaithfulnessResult(score=1.0, total_claims=0, supported_claims=0, unsupported_claims=[])
|
| 235 |
-
|
| 236 |
-
sentences = [s.strip() for s in re.split(r'(?<=[.!?])\s+', answer) if len(s.strip()) > 15][:10]
|
| 237 |
-
if not sentences:
|
| 238 |
-
return FaithfulnessResult(score=1.0, total_claims=0, supported_claims=0, unsupported_claims=[])
|
| 239 |
-
|
| 240 |
-
supported = 0
|
| 241 |
-
unsupported = []
|
| 242 |
-
|
| 243 |
-
for sentence in sentences:
|
| 244 |
-
pairs = [(doc[:2000], sentence) for doc in context_documents]
|
| 245 |
-
scores = self.model.predict(pairs) # shape: (n_docs, 3) logits/probs per label
|
| 246 |
-
probs = self._softmax(scores)
|
| 247 |
-
entailment_idx = 1 # confirm this matches the model's actual label order at integration time
|
| 248 |
-
max_entailment = float(np.max(probs[:, entailment_idx]))
|
| 249 |
-
if max_entailment > self.TAU_ENT:
|
| 250 |
-
supported += 1
|
| 251 |
-
else:
|
| 252 |
-
unsupported.append(sentence)
|
| 253 |
-
|
| 254 |
-
score = supported / len(sentences)
|
| 255 |
-
return FaithfulnessResult(
|
| 256 |
-
score=round(score, 3),
|
| 257 |
-
total_claims=len(sentences),
|
| 258 |
-
supported_claims=supported,
|
| 259 |
-
unsupported_claims=unsupported[:5],
|
| 260 |
-
)
|
| 261 |
-
|
| 262 |
-
@staticmethod
|
| 263 |
-
def _softmax(x):
|
| 264 |
-
import numpy as np
|
| 265 |
-
e_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
|
| 266 |
-
return e_x / e_x.sum(axis=-1, keepdims=True)
|
| 267 |
-
```
|
| 268 |
-
|
| 269 |
-
**Important integration detail for the agent:** verify `cross-encoder/nli-deberta-v3-large`'s actual `id2label` mapping from its `config.json` on first load and print it to logs, cross-encoder NLI checkpoints don't always use the same label order. Don't hardcode `entailment_idx = 1` blindly, confirm it, this is exactly the kind of silent bug that would quietly invalidate the reported faithfulness numbers.
|
| 270 |
-
|
| 271 |
-
**Modify `backend/modules/reasoning_engine.py`, `reason()` method:**
|
| 272 |
-
- Rename the old `FaithfulnessScorer` usage. Keep the class in the codebase (don't delete it), but relabel its role: it becomes the **"LLM-as-judge baseline"** used for baseline comparison tables (this is genuinely useful, RAGAS-style LLM self-eval is a legitimate baseline to compare against in Table I/III, just not the production signal).
|
| 273 |
-
- Swap Step 6 to call `self.deberta_nli_scorer.score(answer, [c.content for c in top_chunks])` (passing the list of individual chunks, not the concatenated `context` string) as the **primary** `faithfulness_result` used in `_calculate_confidence()` and in the returned `explainability` payload.
|
| 274 |
-
- Keep computing the old LLM-based score too, but store it under a clearly-labeled `"llm_judge_baseline_score"` key in `explainability.confidence_breakdown`, not as the headline faithfulness number.
|
| 275 |
-
|
| 276 |
-
**Acceptance test:** feed a known-hallucinated answer (write one manually, e.g. an answer that states a fabricated statistic not present in any retrieved chunk) through the pipeline and confirm the DeBERTa scorer flags it as unsupported. Feed a fully-grounded answer and confirm it scores above `TAU_ENT`. Compare against the LLM-judge baseline score on the same pair, they should usually agree, but if they diverge a lot on the same input, that's worth investigating before trusting either for the paper's numbers.
|
| 277 |
-
|
| 278 |
-
---
|
| 279 |
-
|
| 280 |
-
## 1.5 Wiring check
|
| 281 |
-
|
| 282 |
-
After 1.1-1.4 are done, re-run the existing `backend/evaluation/baseline_runner.py` and `backend/evaluation/benchmark_runner.py` against the current small `test_data/` samples (not for final numbers, just as a smoke test) and confirm nothing throws. Fix any interface mismatches from the `context: str` → `context_documents: List[str]` signature change in the faithfulness scorer, other call sites (`evaluation/metrics.py`, `_run_baseline()` in `reasoning_engine.py`) pass a concatenated string today and will need the same list-of-chunks treatment.
|
| 283 |
-
|
| 284 |
-
---
|
| 285 |
-
|
| 286 |
-
## Summary of files touched in Phase 1
|
| 287 |
-
|
| 288 |
-
| File | Change |
|
| 289 |
-
|---|---|
|
| 290 |
-
| `backend/requirements.txt` | add scikit-learn, spacy |
|
| 291 |
-
| `backend/Dockerfile` | add spacy model download step |
|
| 292 |
-
| `backend/utils/domain_signature.py` | **new** — TF-IDF + NER signature builder |
|
| 293 |
-
| `backend/modules/knowledge_compiler.py` | `_extract_domain_signature()` rewritten to use new module |
|
| 294 |
-
| `backend/modules/reasoning_engine.py` | `_check_guardrail()` rewritten to real Jaccard; Step 6 rewired to DeBERTa scorer |
|
| 295 |
-
| `backend/utils/faithfulness.py` | `BartNLIScorer` replaced with `DebertaNLIScorer` (per-doc max, Eq. 6/7) |
|
| 296 |
-
| `backend/migrations/switch_to_hnsw_index.sql` | **new** — HNSW index migration |
|
| 297 |
-
| `backend/migrations/*` (new file) | add `domain_signature_weights`, `domain_entities` columns |
|
| 298 |
-
| `backend/evaluation/*` | update call sites for the `context: str` → `context_documents: List[str]` signature change |
|
| 299 |
-
|
| 300 |
-
---
|
| 301 |
-
|
| 302 |
-
## What Phase 1 does NOT cover (handled in the separate Phase 2 document)
|
| 303 |
-
|
| 304 |
-
- Fetching real medical/legal/financial documents
|
| 305 |
-
- Building the query sets
|
| 306 |
-
- Running the actual evaluation to generate paper numbers
|
| 307 |
-
- Rewriting the paper's dataset section
|
| 308 |
-
|
| 309 |
-
Phase 1 is architecture only. Once this is done and verified against the small existing `test_data/` samples, move to the Phase 2 document to bring in real data at the ~500-600 document scale.
|
|
|
|
| 1 |
+
# MEXAR Phase 1 — Architecture Fixes to Match the Paper
|
| 2 |
+
|
| 3 |
+
**Purpose of this document:** hand this to an AI coding agent (Antigravity or similar) working directly in the `Mexar` repository (`github.com/devrajsinh2012/Mexar`). This is Phase 1 only: it closes the gap between what the paper *"Hybrid Retrieval and Claim Verification: An Empirical Study of Constrained RAG for High-Stakes Professional Domains"* claims about the system's architecture, and what the code currently does. No dataset work in this document, that's Phase 2, handled separately.
|
| 4 |
+
|
| 5 |
+
**Presentation date:** July 30-31, 2026. Target for this phase: 3 days.
|
| 6 |
+
|
| 7 |
+
**Ground rule for the agent:** every change below must keep the app deployable on the current stack (FastAPI backend on Hugging Face Spaces, Supabase Postgres + pgvector, React frontend on Vercel). Don't introduce services that need infra Dev doesn't already have.
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## 1.0 Preconditions
|
| 12 |
+
|
| 13 |
+
- Add to `backend/requirements.txt`:
|
| 14 |
+
```
|
| 15 |
+
scikit-learn>=1.4.0
|
| 16 |
+
spacy>=3.7.0
|
| 17 |
+
```
|
| 18 |
+
- After install, download the small English model: `python -m spacy download en_core_web_sm`. Add this as a step in `Dockerfile` so it survives Hugging Face Spaces rebuilds.
|
| 19 |
+
- Verify the pgvector extension version on the Supabase project supports HNSW (needs pgvector >= 0.5.0). Run `SELECT extversion FROM pg_extension WHERE extname = 'vector';`. If below 0.5.0, run `ALTER EXTENSION vector UPDATE;` before touching the index migration in 1.2.
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## 1.1 Domain Guardrail: replace the heuristic with real TF-IDF + NER + Jaccard
|
| 24 |
+
|
| 25 |
+
**Why:** Paper Section III-A defines the guardrail as TF-IDF term weighting (Eq. 1), NER-extracted domain entities, and Jaccard similarity between query and signature (Eq. 2). The current `_check_guardrail()` in `backend/modules/reasoning_engine.py` is a hand-tuned fuzzy-string-match heuristic with arbitrary bonus weights and no TF-IDF or NER anywhere. This needs to become the real thing.
|
| 26 |
+
|
| 27 |
+
**New file: `backend/utils/domain_signature.py`**
|
| 28 |
+
|
| 29 |
+
```python
|
| 30 |
+
"""
|
| 31 |
+
MEXAR - Domain Signature Construction
|
| 32 |
+
Implements Section III-A: TF-IDF lexical signature + NER entity signature.
|
| 33 |
+
"""
|
| 34 |
+
from typing import List, Dict, Set, Tuple
|
| 35 |
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 36 |
+
import spacy
|
| 37 |
+
import logging
|
| 38 |
+
|
| 39 |
+
logger = logging.getLogger(__name__)
|
| 40 |
+
_nlp = None
|
| 41 |
+
|
| 42 |
+
def get_nlp():
|
| 43 |
+
global _nlp
|
| 44 |
+
if _nlp is None:
|
| 45 |
+
_nlp = spacy.load("en_core_web_sm")
|
| 46 |
+
return _nlp
|
| 47 |
+
|
| 48 |
+
# Entity labels relevant to professional domains (medical/legal/financial)
|
| 49 |
+
RELEVANT_ENTITY_LABELS = {"ORG", "LAW", "PRODUCT", "GPE", "PERSON", "NORP", "EVENT", "FAC"}
|
| 50 |
+
|
| 51 |
+
def build_tfidf_signature(documents: List[str], top_n: int = 100, tau_tf: float = 0.0) -> List[Tuple[str, float]]:
|
| 52 |
+
"""
|
| 53 |
+
Eq. 1: w(t, D) = tf(t, D) * log(|D| / |{d in D : t in d}|)
|
| 54 |
+
Returns top_n (term, weight) pairs above tau_tf, sorted descending by weight.
|
| 55 |
+
"""
|
| 56 |
+
if not documents or len(documents) < 2:
|
| 57 |
+
# TF-IDF needs a corpus of multiple "documents" to compute IDF meaningfully.
|
| 58 |
+
# If compiling from a single uploaded file, split it into paragraph-level
|
| 59 |
+
# pseudo-documents so IDF is not degenerate.
|
| 60 |
+
documents = _split_into_pseudo_documents(documents[0]) if documents else []
|
| 61 |
+
|
| 62 |
+
vectorizer = TfidfVectorizer(
|
| 63 |
+
max_features=2000,
|
| 64 |
+
stop_words="english",
|
| 65 |
+
ngram_range=(1, 2),
|
| 66 |
+
min_df=1,
|
| 67 |
+
)
|
| 68 |
+
matrix = vectorizer.fit_transform(documents)
|
| 69 |
+
scores = matrix.sum(axis=0).A1
|
| 70 |
+
terms = vectorizer.get_feature_names_out()
|
| 71 |
+
pairs = [(t, float(s)) for t, s in zip(terms, scores) if s > tau_tf]
|
| 72 |
+
pairs.sort(key=lambda x: x[1], reverse=True)
|
| 73 |
+
return pairs[:top_n]
|
| 74 |
+
|
| 75 |
+
def _split_into_pseudo_documents(text: str, chunk_size: int = 500) -> List[str]:
|
| 76 |
+
words = text.split()
|
| 77 |
+
return [" ".join(words[i:i+chunk_size]) for i in range(0, len(words), chunk_size)] or [text]
|
| 78 |
+
|
| 79 |
+
def extract_ner_entities(documents: List[str], tau_ent: int = 2, max_docs_to_scan: int = 200) -> List[str]:
|
| 80 |
+
"""
|
| 81 |
+
Extracts named entities appearing in at least tau_ent documents.
|
| 82 |
+
Capped at max_docs_to_scan for latency (spaCy is not free at compile time).
|
| 83 |
+
"""
|
| 84 |
+
nlp = get_nlp()
|
| 85 |
+
doc_freq: Dict[str, int] = {}
|
| 86 |
+
for doc_text in documents[:max_docs_to_scan]:
|
| 87 |
+
seen_in_this_doc: Set[str] = set()
|
| 88 |
+
for ent in nlp(doc_text[:5000]).ents: # cap per-doc length for speed
|
| 89 |
+
if ent.label_ in RELEVANT_ENTITY_LABELS:
|
| 90 |
+
key = ent.text.lower().strip()
|
| 91 |
+
if key and key not in seen_in_this_doc:
|
| 92 |
+
seen_in_this_doc.add(key)
|
| 93 |
+
for key in seen_in_this_doc:
|
| 94 |
+
doc_freq[key] = doc_freq.get(key, 0) + 1
|
| 95 |
+
return [k for k, v in doc_freq.items() if v >= tau_ent]
|
| 96 |
+
|
| 97 |
+
def build_domain_signature(
|
| 98 |
+
documents: List[str],
|
| 99 |
+
tau_tf: float = 0.0,
|
| 100 |
+
tau_ent: int = 2,
|
| 101 |
+
top_n_lexical: int = 100,
|
| 102 |
+
) -> Dict[str, List]:
|
| 103 |
+
"""Returns Sigma = Sigma_lex ∪ Sigma_ent as described in Section III-A, kept separate for inspection."""
|
| 104 |
+
lexical_pairs = build_tfidf_signature(documents, top_n=top_n_lexical, tau_tf=tau_tf)
|
| 105 |
+
lexical_terms = [t for t, _ in lexical_pairs]
|
| 106 |
+
entities = extract_ner_entities(documents, tau_ent=tau_ent)
|
| 107 |
+
return {
|
| 108 |
+
"lexical": lexical_terms,
|
| 109 |
+
"lexical_weights": {t: w for t, w in lexical_pairs},
|
| 110 |
+
"entities": entities,
|
| 111 |
+
"combined": sorted(set(lexical_terms) | set(e for e in entities)),
|
| 112 |
+
}
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
**Modify `backend/modules/knowledge_compiler.py`:**
|
| 116 |
+
Replace `_extract_domain_signature()` so it calls `build_domain_signature()` from the new module, passing in the raw per-source-document text list (not the flattened `text_context` string, we need per-document granularity for TF-IDF's document-frequency term to mean anything). Store the result's `combined` list in the existing `domain_signature` JSONB column, and add the `lexical_weights` dict to a **new** JSONB column (see migration below) so the guardrail can compute a weighted Jaccard rather than a flat one if useful later.
|
| 117 |
+
|
| 118 |
+
**New migration** (Alembic or raw SQL depending on how `backend/migrations` is structured, check `backend/migrations/README.md` for the convention already in use):
|
| 119 |
+
```sql
|
| 120 |
+
ALTER TABLE agents ADD COLUMN IF NOT EXISTS domain_signature_weights JSONB;
|
| 121 |
+
ALTER TABLE agents ADD COLUMN IF NOT EXISTS domain_entities JSONB;
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
**Rewrite `_check_guardrail()` in `backend/modules/reasoning_engine.py`:**
|
| 125 |
+
Delete the fuzzy-match/bonus-score implementation entirely. Replace with real Eq. 2:
|
| 126 |
+
|
| 127 |
+
```python
|
| 128 |
+
def _check_guardrail(self, query: str, domain_signature: List[str], prompt_analysis: Dict[str, Any]) -> Tuple[bool, float]:
|
| 129 |
+
"""Eq. 2: sim(q, Sigma) = |Phi(q) ∩ Sigma| / |Phi(q)|"""
|
| 130 |
+
from utils.domain_signature import get_nlp
|
| 131 |
+
nlp = get_nlp()
|
| 132 |
+
query_doc = nlp(query.lower())
|
| 133 |
+
# Phi(q): lemmatized content tokens, excluding stopwords/punctuation
|
| 134 |
+
phi_q = {
|
| 135 |
+
tok.lemma_ for tok in query_doc
|
| 136 |
+
if not tok.is_stop and not tok.is_punct and len(tok.text) > 2
|
| 137 |
+
}
|
| 138 |
+
sigma = set(s.lower() for s in (domain_signature or []))
|
| 139 |
+
|
| 140 |
+
if not phi_q:
|
| 141 |
+
return False, 0.0
|
| 142 |
+
|
| 143 |
+
intersection = phi_q & sigma
|
| 144 |
+
score = len(intersection) / len(phi_q)
|
| 145 |
+
|
| 146 |
+
is_in_domain = score >= self.DOMAIN_SIMILARITY_THRESHOLD
|
| 147 |
+
logger.info(f"Guardrail: sim={score:.3f}, |Phi(q)|={len(phi_q)}, |intersection|={len(intersection)}, in_domain={is_in_domain}")
|
| 148 |
+
return is_in_domain, score
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
**Tune `DOMAIN_SIMILARITY_THRESHOLD`:** the current 0.05 was calibrated against the *old* heuristic scorer, it will not transfer to real Jaccard similarity, which tends to produce lower absolute values on short queries. Don't guess this: after wiring the new guardrail, run `backend/evaluation/guardrail_analysis.py` against a small hand-labeled set of ~20 in-domain and ~20 out-of-domain queries per existing test_data domain, sweep the threshold from 0.05 to 0.4 in steps of 0.05, and pick the value that maximizes F1. Record that sweep, it becomes supporting evidence for Table IV in the paper instead of a guessed constant.
|
| 152 |
+
|
| 153 |
+
**Acceptance test:** for the medical test agent, a query like "what is the recommended treatment for stage 2 hypertension" should be accepted; a query like "how do I file a small claims lawsuit" should be rejected. Log both the old and new guardrail score during a transition period to sanity-check before removing the old code path.
|
| 154 |
+
|
| 155 |
+
---
|
| 156 |
+
|
| 157 |
+
## 1.2 Vector index: IVFFlat → HNSW
|
| 158 |
+
|
| 159 |
+
**Why:** Paper Section III-B explicitly claims HNSW indexing. The current migration (`backend/migrations/hybrid_search_function.sql`, lines ~98-101) creates an IVFFlat index instead.
|
| 160 |
+
|
| 161 |
+
**New migration file** `backend/migrations/switch_to_hnsw_index.sql`:
|
| 162 |
+
```sql
|
| 163 |
+
-- Drop the old IVFFlat index
|
| 164 |
+
DROP INDEX IF EXISTS idx_document_chunks_embedding;
|
| 165 |
+
|
| 166 |
+
-- Create HNSW index to match paper Section III-B
|
| 167 |
+
CREATE INDEX IF NOT EXISTS idx_document_chunks_embedding_hnsw
|
| 168 |
+
ON document_chunks USING hnsw (embedding vector_cosine_ops)
|
| 169 |
+
WITH (m = 16, ef_construction = 64);
|
| 170 |
+
|
| 171 |
+
COMMENT ON INDEX idx_document_chunks_embedding_hnsw IS
|
| 172 |
+
'HNSW index for cosine similarity search, m=16, ef_construction=64 (Section III-B)';
|
| 173 |
+
```
|
| 174 |
+
Apply via `Supabase:apply_migration` (or however existing migrations are run, check for a runner script referenced in `backend/migrations/README.md`) rather than editing the old file, so history is preserved.
|
| 175 |
+
|
| 176 |
+
**Note for the agent:** HNSW build time scales worse than IVFFlat with corpus size, but at the 500-600 document / few-thousand-chunk scale planned for the presentation, this is a non-issue. No further tuning needed.
|
| 177 |
+
|
| 178 |
+
**Acceptance test:** `\d document_chunks` in psql (or `SELECT indexdef FROM pg_indexes WHERE tablename = 'document_chunks';`) should show the HNSW index, not IVFFlat. Re-run a handful of existing queries and confirm retrieval still returns coherent top-5 chunks (HNSW is approximate, results may reorder slightly versus IVFFlat, that's expected and fine).
|
| 179 |
+
|
| 180 |
+
---
|
| 181 |
+
|
| 182 |
+
## 1.3 RRF weighting: reconcile code and paper
|
| 183 |
+
|
| 184 |
+
**Why:** Paper Eq. 5 is unweighted RRF. `backend/migrations/hybrid_search_function.sql` applies a 0.6 semantic / 0.4 keyword weighting inside the SQL function.
|
| 185 |
+
|
| 186 |
+
**Decision for this project (recommended, don't re-litigate):** keep the 0.6/0.4 weighting in code, it's a reasonable and defensible design choice, and update the paper instead. This is less risky than ripping out a tuned weighting under time pressure. The agent does not need to touch `hybrid_search_function.sql` for this item, just flag it in a "paper edits needed" note for later.
|
| 187 |
+
|
| 188 |
+
If Dev instead decides to match the paper exactly, the one-line change is setting `semantic_weight := 0.5; keyword_weight := 0.5;` in that SQL function, but this is **not** the default action, don't do it unless explicitly told.
|
| 189 |
+
|
| 190 |
+
---
|
| 191 |
+
|
| 192 |
+
## 1.4 Faithfulness verification: make DeBERTa-v3 NLI the real, primary signal
|
| 193 |
+
|
| 194 |
+
**Why:** this is the most important fix. Paper Section III-C and the entire Related Work argument against LLM-judging-LLM circularity claim the production system verifies claims with "an NLI model (DeBERTa-v3-large fine-tuned on MNLI)" using per-document max entailment (Eq. 6). In the actual code:
|
| 195 |
+
- The primary confidence-driving call in `reasoning_engine.reason()` (Step 6) is `self.faithfulness_scorer.score(answer, context)`, which is `FaithfulnessScorer` in `backend/utils/faithfulness.py`, an LLM (Groq) extracting claims and then Groq again answering YES/NO. That's exactly the circularity the paper argues against.
|
| 196 |
+
- The actual NLI model in the repo, `BartNLIScorer`, uses `facebook/bart-large-mnli`, not DeBERTa-v3-large, and it's computed only as a side "reviewer feedback" score (`bart_nli_result` in `reason()`), it never feeds the confidence calculation.
|
| 197 |
+
- The NLI scorer concatenates all retrieved context into a single premise string, it does not implement the per-document max in Eq. 6.
|
| 198 |
+
|
| 199 |
+
**Model choice:** use `cross-encoder/nli-deberta-v3-large` from Hugging Face. This is a DeBERTa-v3-large architecture fine-tuned specifically for NLI (SNLI + MultiNLI), it is the standard, citable checkpoint people mean when they say "DeBERTa-v3-large fine-tuned on MNLI" in RAG faithfulness papers, and it loads cleanly through `sentence-transformers`'s `CrossEncoder` class, which is already a dependency.
|
| 200 |
+
|
| 201 |
+
**Modify `backend/utils/faithfulness.py`:** replace the `BartNLIScorer` class with:
|
| 202 |
+
|
| 203 |
+
```python
|
| 204 |
+
class DebertaNLIScorer:
|
| 205 |
+
"""
|
| 206 |
+
Primary faithfulness verifier per Section III-C.
|
| 207 |
+
Per-claim, per-document max entailment probability (Eq. 6),
|
| 208 |
+
aggregated as the fraction of claims exceeding tau_ent (Eq. 7).
|
| 209 |
+
"""
|
| 210 |
+
LABEL_MAP = {0: "contradiction", 1: "entailment", 2: "neutral"} # verify against model card at load time
|
| 211 |
+
TAU_ENT = 0.7
|
| 212 |
+
|
| 213 |
+
def __init__(self):
|
| 214 |
+
self._model = None
|
| 215 |
+
|
| 216 |
+
@property
|
| 217 |
+
def model(self):
|
| 218 |
+
if self._model is None:
|
| 219 |
+
from sentence_transformers import CrossEncoder
|
| 220 |
+
logger.info("Loading cross-encoder/nli-deberta-v3-large...")
|
| 221 |
+
self._model = CrossEncoder("cross-encoder/nli-deberta-v3-large")
|
| 222 |
+
logger.info("DeBERTa-v3 NLI model loaded.")
|
| 223 |
+
return self._model
|
| 224 |
+
|
| 225 |
+
def score(self, answer: str, context_documents: List[str]) -> FaithfulnessResult:
|
| 226 |
+
"""
|
| 227 |
+
context_documents: list of individual retrieved chunk texts (NOT concatenated),
|
| 228 |
+
so per-document max in Eq. 6 is meaningful.
|
| 229 |
+
"""
|
| 230 |
+
import re
|
| 231 |
+
import numpy as np
|
| 232 |
+
|
| 233 |
+
if not answer or not context_documents:
|
| 234 |
+
return FaithfulnessResult(score=1.0, total_claims=0, supported_claims=0, unsupported_claims=[])
|
| 235 |
+
|
| 236 |
+
sentences = [s.strip() for s in re.split(r'(?<=[.!?])\s+', answer) if len(s.strip()) > 15][:10]
|
| 237 |
+
if not sentences:
|
| 238 |
+
return FaithfulnessResult(score=1.0, total_claims=0, supported_claims=0, unsupported_claims=[])
|
| 239 |
+
|
| 240 |
+
supported = 0
|
| 241 |
+
unsupported = []
|
| 242 |
+
|
| 243 |
+
for sentence in sentences:
|
| 244 |
+
pairs = [(doc[:2000], sentence) for doc in context_documents]
|
| 245 |
+
scores = self.model.predict(pairs) # shape: (n_docs, 3) logits/probs per label
|
| 246 |
+
probs = self._softmax(scores)
|
| 247 |
+
entailment_idx = 1 # confirm this matches the model's actual label order at integration time
|
| 248 |
+
max_entailment = float(np.max(probs[:, entailment_idx]))
|
| 249 |
+
if max_entailment > self.TAU_ENT:
|
| 250 |
+
supported += 1
|
| 251 |
+
else:
|
| 252 |
+
unsupported.append(sentence)
|
| 253 |
+
|
| 254 |
+
score = supported / len(sentences)
|
| 255 |
+
return FaithfulnessResult(
|
| 256 |
+
score=round(score, 3),
|
| 257 |
+
total_claims=len(sentences),
|
| 258 |
+
supported_claims=supported,
|
| 259 |
+
unsupported_claims=unsupported[:5],
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
@staticmethod
|
| 263 |
+
def _softmax(x):
|
| 264 |
+
import numpy as np
|
| 265 |
+
e_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
|
| 266 |
+
return e_x / e_x.sum(axis=-1, keepdims=True)
|
| 267 |
+
```
|
| 268 |
+
|
| 269 |
+
**Important integration detail for the agent:** verify `cross-encoder/nli-deberta-v3-large`'s actual `id2label` mapping from its `config.json` on first load and print it to logs, cross-encoder NLI checkpoints don't always use the same label order. Don't hardcode `entailment_idx = 1` blindly, confirm it, this is exactly the kind of silent bug that would quietly invalidate the reported faithfulness numbers.
|
| 270 |
+
|
| 271 |
+
**Modify `backend/modules/reasoning_engine.py`, `reason()` method:**
|
| 272 |
+
- Rename the old `FaithfulnessScorer` usage. Keep the class in the codebase (don't delete it), but relabel its role: it becomes the **"LLM-as-judge baseline"** used for baseline comparison tables (this is genuinely useful, RAGAS-style LLM self-eval is a legitimate baseline to compare against in Table I/III, just not the production signal).
|
| 273 |
+
- Swap Step 6 to call `self.deberta_nli_scorer.score(answer, [c.content for c in top_chunks])` (passing the list of individual chunks, not the concatenated `context` string) as the **primary** `faithfulness_result` used in `_calculate_confidence()` and in the returned `explainability` payload.
|
| 274 |
+
- Keep computing the old LLM-based score too, but store it under a clearly-labeled `"llm_judge_baseline_score"` key in `explainability.confidence_breakdown`, not as the headline faithfulness number.
|
| 275 |
+
|
| 276 |
+
**Acceptance test:** feed a known-hallucinated answer (write one manually, e.g. an answer that states a fabricated statistic not present in any retrieved chunk) through the pipeline and confirm the DeBERTa scorer flags it as unsupported. Feed a fully-grounded answer and confirm it scores above `TAU_ENT`. Compare against the LLM-judge baseline score on the same pair, they should usually agree, but if they diverge a lot on the same input, that's worth investigating before trusting either for the paper's numbers.
|
| 277 |
+
|
| 278 |
+
---
|
| 279 |
+
|
| 280 |
+
## 1.5 Wiring check
|
| 281 |
+
|
| 282 |
+
After 1.1-1.4 are done, re-run the existing `backend/evaluation/baseline_runner.py` and `backend/evaluation/benchmark_runner.py` against the current small `test_data/` samples (not for final numbers, just as a smoke test) and confirm nothing throws. Fix any interface mismatches from the `context: str` → `context_documents: List[str]` signature change in the faithfulness scorer, other call sites (`evaluation/metrics.py`, `_run_baseline()` in `reasoning_engine.py`) pass a concatenated string today and will need the same list-of-chunks treatment.
|
| 283 |
+
|
| 284 |
+
---
|
| 285 |
+
|
| 286 |
+
## Summary of files touched in Phase 1
|
| 287 |
+
|
| 288 |
+
| File | Change |
|
| 289 |
+
|---|---|
|
| 290 |
+
| `backend/requirements.txt` | add scikit-learn, spacy |
|
| 291 |
+
| `backend/Dockerfile` | add spacy model download step |
|
| 292 |
+
| `backend/utils/domain_signature.py` | **new** — TF-IDF + NER signature builder |
|
| 293 |
+
| `backend/modules/knowledge_compiler.py` | `_extract_domain_signature()` rewritten to use new module |
|
| 294 |
+
| `backend/modules/reasoning_engine.py` | `_check_guardrail()` rewritten to real Jaccard; Step 6 rewired to DeBERTa scorer |
|
| 295 |
+
| `backend/utils/faithfulness.py` | `BartNLIScorer` replaced with `DebertaNLIScorer` (per-doc max, Eq. 6/7) |
|
| 296 |
+
| `backend/migrations/switch_to_hnsw_index.sql` | **new** — HNSW index migration |
|
| 297 |
+
| `backend/migrations/*` (new file) | add `domain_signature_weights`, `domain_entities` columns |
|
| 298 |
+
| `backend/evaluation/*` | update call sites for the `context: str` → `context_documents: List[str]` signature change |
|
| 299 |
+
|
| 300 |
+
---
|
| 301 |
+
|
| 302 |
+
## What Phase 1 does NOT cover (handled in the separate Phase 2 document)
|
| 303 |
+
|
| 304 |
+
- Fetching real medical/legal/financial documents
|
| 305 |
+
- Building the query sets
|
| 306 |
+
- Running the actual evaluation to generate paper numbers
|
| 307 |
+
- Rewriting the paper's dataset section
|
| 308 |
+
|
| 309 |
+
Phase 1 is architecture only. Once this is done and verified against the small existing `test_data/` samples, move to the Phase 2 document to bring in real data at the ~500-600 document scale.
|
MEXAR_Phase2_Dataset_Collection.md
CHANGED
|
@@ -1,291 +1,291 @@
|
|
| 1 |
-
# MEXAR Phase 2 — Real Datasets at Reduced, Disclosed Scale
|
| 2 |
-
|
| 3 |
-
**Purpose of this document:** hand this to an AI coding agent (Antigravity or similar) working directly in the `Mexar` repository (`github.com/devrajsinh2012/Mexar`). This is Phase 2 only: it brings in real documents and real queries so the numbers in the paper are reproducible. Assumes Phase 1 (architecture fixes: TF-IDF/NER guardrail, HNSW index, DeBERTa faithfulness scorer) is already done and verified.
|
| 4 |
-
|
| 5 |
-
**Presentation date:** July 30-31, 2026. Target for this phase: days 4-11 (about a week).
|
| 6 |
-
|
| 7 |
-
---
|
| 8 |
-
|
| 9 |
-
## 2.0 Scope decision (already agreed with Dev)
|
| 10 |
-
|
| 11 |
-
Target **500-600 total documents** across the three domains for the presentation, not the 5,226 stated in the current paper draft. This is a resource-constrained pilot, and the paper's dataset section (IV-A) gets rewritten to say so honestly. Query sets are also reduced and single-annotator (Dev/Om/Jay), not the 900+600 two-expert-annotated set currently described. This is a normal, defensible thing to disclose in a paper, and far safer than presenting numbers nobody can reproduce live.
|
| 12 |
-
|
| 13 |
-
**Target split:**
|
| 14 |
-
|
| 15 |
-
| Domain | Target docs | Source |
|
| 16 |
-
|---|---|---|
|
| 17 |
-
| Medical | ~220 | PubMed Central OA subset |
|
| 18 |
-
| Legal | ~150 | CourtListener (Free Law Project) |
|
| 19 |
-
| Financial | ~130 | SEC EDGAR full-text search |
|
| 20 |
-
| **Total** | **~500** | |
|
| 21 |
-
|
| 22 |
-
**Important for the agent:** these fetch scripts hit external APIs (PubMed E-utilities, CourtListener, SEC EDGAR) that are outside a typical restricted sandbox's allowed network domains. Run these scripts in Dev's own local dev environment or a CI runner with normal internet access, not inside a network-restricted container.
|
| 23 |
-
|
| 24 |
-
---
|
| 25 |
-
|
| 26 |
-
## 2.1 Medical corpus — PubMed Central OA
|
| 27 |
-
|
| 28 |
-
**New script:** `backend/scripts/fetch_pubmed.py`
|
| 29 |
-
|
| 30 |
-
- Use the NCBI E-utilities API (`esearch` + `efetch`), no key required for low-volume use, but request one from NCBI if fetching more than 3 requests/sec.
|
| 31 |
-
- Query terms per subdomain, run three separate searches and cap results:
|
| 32 |
-
- `"cardiology treatment guideline"[Title/Abstract] AND "open access"[filter]`
|
| 33 |
-
- `"oncology treatment protocol"[Title/Abstract] AND "open access"[filter]`
|
| 34 |
-
- `"internal medicine diagnostic criteria"[Title/Abstract] AND "open access"[filter]`
|
| 35 |
-
- Pull ~75 docs per subdomain (≈220 total).
|
| 36 |
-
- Save each as plain text (abstract + body if available via the OA subset bulk XML) into `test_data/medical_real/`, one file per document, named by PMCID, e.g. `PMC1234567.txt`.
|
| 37 |
-
- Respect NCBI's rate limits (3 req/sec without an API key, 10/sec with one) and identify yourself via the `email` parameter on every request, per NCBI's usage policy.
|
| 38 |
-
- Log a manifest file `test_data/medical_real/manifest.json` mapping PMCID → title → subdomain → source URL, this becomes supporting material for the paper's dataset section and makes the corpus auditable.
|
| 39 |
-
|
| 40 |
-
**Skeleton:**
|
| 41 |
-
```python
|
| 42 |
-
"""
|
| 43 |
-
MEXAR - PubMed Central OA fetcher for the medical corpus.
|
| 44 |
-
Run this locally (not in a network-restricted sandbox).
|
| 45 |
-
"""
|
| 46 |
-
import requests
|
| 47 |
-
import time
|
| 48 |
-
import json
|
| 49 |
-
import os
|
| 50 |
-
|
| 51 |
-
NCBI_EMAIL = "your_email@example.com" # required by NCBI usage policy
|
| 52 |
-
OUTPUT_DIR = "test_data/medical_real"
|
| 53 |
-
SEARCH_TERMS = {
|
| 54 |
-
"cardiology": '"cardiology treatment guideline"[Title/Abstract] AND "open access"[filter]',
|
| 55 |
-
"oncology": '"oncology treatment protocol"[Title/Abstract] AND "open access"[filter]',
|
| 56 |
-
"internal_medicine": '"internal medicine diagnostic criteria"[Title/Abstract] AND "open access"[filter]',
|
| 57 |
-
}
|
| 58 |
-
DOCS_PER_SUBDOMAIN = 75
|
| 59 |
-
|
| 60 |
-
def esearch(term, retmax):
|
| 61 |
-
url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"
|
| 62 |
-
params = {"db": "pmc", "term": term, "retmax": retmax, "retmode": "json", "email": NCBI_EMAIL}
|
| 63 |
-
r = requests.get(url, params=params)
|
| 64 |
-
r.raise_for_status()
|
| 65 |
-
return r.json()["esearchresult"]["idlist"]
|
| 66 |
-
|
| 67 |
-
def efetch_fulltext(pmc_id):
|
| 68 |
-
url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi"
|
| 69 |
-
params = {"db": "pmc", "id": pmc_id, "rettype": "full", "retmode": "xml", "email": NCBI_EMAIL}
|
| 70 |
-
r = requests.get(url, params=params)
|
| 71 |
-
r.raise_for_status()
|
| 72 |
-
return r.text # parse out body text with an XML parser before saving as plain text
|
| 73 |
-
|
| 74 |
-
def main():
|
| 75 |
-
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 76 |
-
manifest = []
|
| 77 |
-
for subdomain, term in SEARCH_TERMS.items():
|
| 78 |
-
ids = esearch(term, DOCS_PER_SUBDOMAIN)
|
| 79 |
-
for pmc_id in ids:
|
| 80 |
-
time.sleep(0.34) # stay under 3 req/sec
|
| 81 |
-
try:
|
| 82 |
-
xml_text = efetch_fulltext(pmc_id)
|
| 83 |
-
# TODO: parse XML body text out with e.g. lxml, strip tags, save plain text
|
| 84 |
-
plain_text = xml_text # placeholder, replace with real extraction
|
| 85 |
-
out_path = os.path.join(OUTPUT_DIR, f"PMC{pmc_id}.txt")
|
| 86 |
-
with open(out_path, "w", encoding="utf-8") as f:
|
| 87 |
-
f.write(plain_text)
|
| 88 |
-
manifest.append({"pmc_id": pmc_id, "subdomain": subdomain, "path": out_path})
|
| 89 |
-
except Exception as e:
|
| 90 |
-
print(f"Failed to fetch PMC{pmc_id}: {e}")
|
| 91 |
-
with open(os.path.join(OUTPUT_DIR, "manifest.json"), "w") as f:
|
| 92 |
-
json.dump(manifest, f, indent=2)
|
| 93 |
-
print(f"Fetched {len(manifest)} medical documents.")
|
| 94 |
-
|
| 95 |
-
if __name__ == "__main__":
|
| 96 |
-
main()
|
| 97 |
-
```
|
| 98 |
-
The agent should replace the placeholder XML-to-text extraction with a real parser (e.g. `lxml` or `BeautifulSoup`) that pulls the `<body>` text out of the PMC XML format and strips markup, don't save raw XML as the "plain text" corpus.
|
| 99 |
-
|
| 100 |
-
---
|
| 101 |
-
|
| 102 |
-
## 2.2 Legal corpus — CourtListener
|
| 103 |
-
|
| 104 |
-
**New script:** `backend/scripts/fetch_courtlistener.py`
|
| 105 |
-
|
| 106 |
-
- Use the CourtListener REST API (`https://www.courtlistener.com/api/rest/v4/`), requires a free account and API token (set as `COURTLISTENER_TOKEN` env var).
|
| 107 |
-
- Pull opinions/case summaries across contract law, IP, and corporate governance search terms, plus a handful of statutory/regulatory text pages if accessible via their `opinions` and `search` endpoints.
|
| 108 |
-
- Target ~50 docs per subdomain (≈150 total).
|
| 109 |
-
- Save the opinion text (or summary if full text is very long, truncate to a reasonable chunking size, e.g. first 3000 words) into `test_data/legal_real/`, one file per case, named by CourtListener opinion ID.
|
| 110 |
-
- Same manifest pattern as 2.1: `test_data/legal_real/manifest.json` with case name, subdomain, court, date, source URL.
|
| 111 |
-
|
| 112 |
-
**Skeleton:**
|
| 113 |
-
```python
|
| 114 |
-
"""
|
| 115 |
-
MEXAR - CourtListener fetcher for the legal corpus.
|
| 116 |
-
Requires COURTLISTENER_TOKEN env var. Run locally.
|
| 117 |
-
"""
|
| 118 |
-
import requests
|
| 119 |
-
import os
|
| 120 |
-
import json
|
| 121 |
-
|
| 122 |
-
TOKEN = os.environ["COURTLISTENER_TOKEN"]
|
| 123 |
-
HEADERS = {"Authorization": f"Token {TOKEN}"}
|
| 124 |
-
BASE_URL = "https://www.courtlistener.com/api/rest/v4"
|
| 125 |
-
OUTPUT_DIR = "test_data/legal_real"
|
| 126 |
-
SEARCH_TERMS = {
|
| 127 |
-
"contract_law": "breach of contract damages",
|
| 128 |
-
"intellectual_property": "patent infringement trademark",
|
| 129 |
-
"corporate_governance": "fiduciary duty shareholder derivative",
|
| 130 |
-
}
|
| 131 |
-
DOCS_PER_SUBDOMAIN = 50
|
| 132 |
-
|
| 133 |
-
def search_opinions(query, count):
|
| 134 |
-
url = f"{BASE_URL}/search/"
|
| 135 |
-
params = {"q": query, "type": "o", "order_by": "score desc"}
|
| 136 |
-
results = []
|
| 137 |
-
while len(results) < count:
|
| 138 |
-
r = requests.get(url, headers=HEADERS, params=params)
|
| 139 |
-
r.raise_for_status()
|
| 140 |
-
data = r.json()
|
| 141 |
-
results.extend(data["results"])
|
| 142 |
-
if not data.get("next"):
|
| 143 |
-
break
|
| 144 |
-
url = data["next"]
|
| 145 |
-
params = {}
|
| 146 |
-
return results[:count]
|
| 147 |
-
|
| 148 |
-
def main():
|
| 149 |
-
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 150 |
-
manifest = []
|
| 151 |
-
for subdomain, term in SEARCH_TERMS.items():
|
| 152 |
-
opinions = search_opinions(term, DOCS_PER_SUBDOMAIN)
|
| 153 |
-
for op in opinions:
|
| 154 |
-
text = op.get("plain_text") or op.get("snippet", "")
|
| 155 |
-
if not text:
|
| 156 |
-
continue
|
| 157 |
-
op_id = op.get("id") or op.get("cluster_id")
|
| 158 |
-
out_path = os.path.join(OUTPUT_DIR, f"case_{op_id}.txt")
|
| 159 |
-
with open(out_path, "w", encoding="utf-8") as f:
|
| 160 |
-
f.write(text[:15000]) # cap length
|
| 161 |
-
manifest.append({
|
| 162 |
-
"id": op_id, "subdomain": subdomain,
|
| 163 |
-
"case_name": op.get("caseName", ""), "path": out_path,
|
| 164 |
-
})
|
| 165 |
-
with open(os.path.join(OUTPUT_DIR, "manifest.json"), "w") as f:
|
| 166 |
-
json.dump(manifest, f, indent=2)
|
| 167 |
-
print(f"Fetched {len(manifest)} legal documents.")
|
| 168 |
-
|
| 169 |
-
if __name__ == "__main__":
|
| 170 |
-
main()
|
| 171 |
-
```
|
| 172 |
-
The agent should verify the actual current CourtListener v4 search response schema (field names like `plain_text` vs `snippet` vs a separate opinion-text endpoint may have changed) before trusting this skeleton verbatim, API responses drift over time.
|
| 173 |
-
|
| 174 |
-
---
|
| 175 |
-
|
| 176 |
-
## 2.3 Financial corpus — SEC EDGAR
|
| 177 |
-
|
| 178 |
-
**New script:** `backend/scripts/fetch_secedgar.py`
|
| 179 |
-
|
| 180 |
-
- Use SEC EDGAR's full-text search API (`https://efts.sec.gov/LATEST/search-index`) and the regular filing index to pull excerpts from 10-K filings and select analyst/accounting-standard documents.
|
| 181 |
-
- **Mandatory:** SEC requires a descriptive `User-Agent` header identifying the requester (name + email) on every request, or you get blocked. Format: `"YourName YourEmail@example.com"`.
|
| 182 |
-
- Target ~130 docs, mixing filing sections (MD&A, risk factors) rather than entire 10-Ks, which are too long and would dominate the corpus disproportionately relative to medical/legal document sizes.
|
| 183 |
-
- Save into `test_data/financial_real/`, named by CIK + accession number.
|
| 184 |
-
- Same manifest pattern.
|
| 185 |
-
|
| 186 |
-
**Skeleton:**
|
| 187 |
-
```python
|
| 188 |
-
"""
|
| 189 |
-
MEXAR - SEC EDGAR fetcher for the financial corpus.
|
| 190 |
-
Run locally. Requires a descriptive User-Agent per SEC's fair access policy.
|
| 191 |
-
"""
|
| 192 |
-
import requests
|
| 193 |
-
import os
|
| 194 |
-
import json
|
| 195 |
-
import time
|
| 196 |
-
|
| 197 |
-
USER_AGENT = "Devrajsinh Gohil djgohil2012@gmail.com" # SEC requires this format
|
| 198 |
-
HEADERS = {"User-Agent": USER_AGENT}
|
| 199 |
-
OUTPUT_DIR = "test_data/financial_real"
|
| 200 |
-
SEARCH_TERMS = ["risk factors", "management discussion analysis", "GAAP IFRS accounting standards"]
|
| 201 |
-
DOCS_PER_TERM = 45 # ~130 total across 3 terms
|
| 202 |
-
|
| 203 |
-
def full_text_search(query, count):
|
| 204 |
-
url = "https://efts.sec.gov/LATEST/search-index?q=%22{}%22&forms=10-K".format(query.replace(" ", "+"))
|
| 205 |
-
r = requests.get(url, headers=HEADERS)
|
| 206 |
-
r.raise_for_status()
|
| 207 |
-
hits = r.json().get("hits", {}).get("hits", [])
|
| 208 |
-
return hits[:count]
|
| 209 |
-
|
| 210 |
-
def main():
|
| 211 |
-
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 212 |
-
manifest = []
|
| 213 |
-
for term in SEARCH_TERMS:
|
| 214 |
-
hits = full_text_search(term, DOCS_PER_TERM)
|
| 215 |
-
for hit in hits:
|
| 216 |
-
time.sleep(0.2) # be polite to SEC's rate limits
|
| 217 |
-
source = hit.get("_source", {})
|
| 218 |
-
cik = source.get("ciks", ["unknown"])[0]
|
| 219 |
-
accession = source.get("adsh", "unknown")
|
| 220 |
-
# TODO: fetch actual filing document text via the filing index URL,
|
| 221 |
-
# this skeleton only captures search-hit metadata, not full text.
|
| 222 |
-
excerpt = source.get("display_names", [""])[0]
|
| 223 |
-
out_path = os.path.join(OUTPUT_DIR, f"{cik}_{accession}.txt")
|
| 224 |
-
with open(out_path, "w", encoding="utf-8") as f:
|
| 225 |
-
f.write(excerpt)
|
| 226 |
-
manifest.append({"cik": cik, "accession": accession, "term": term, "path": out_path})
|
| 227 |
-
with open(os.path.join(OUTPUT_DIR, "manifest.json"), "w") as f:
|
| 228 |
-
json.dump(manifest, f, indent=2)
|
| 229 |
-
print(f"Fetched {len(manifest)} financial documents.")
|
| 230 |
-
|
| 231 |
-
if __name__ == "__main__":
|
| 232 |
-
main()
|
| 233 |
-
```
|
| 234 |
-
This one needs the most work from the agent: the full-text search endpoint returns hit metadata, not the actual filing body text. The agent needs to follow through to the actual filing document (via the CIK/accession-number-derived URL under `https://www.sec.gov/Archives/edgar/data/{cik}/{accession}/`) and extract the relevant section text (MD&A, risk factors) from the filing HTML, rather than saving just the search snippet.
|
| 235 |
-
|
| 236 |
-
---
|
| 237 |
-
|
| 238 |
-
## 2.4 Ingestion format
|
| 239 |
-
|
| 240 |
-
Each fetch script should output files in whatever format `backend/modules/data_validator.py`'s parsers already expect, check the `.txt`/`.json` handling there first, don't invent a new schema. Simplest path: one `.txt` file per document, plain text body, filename as a stable ID, so the existing agent-compilation flow (`KnowledgeCompiler.compile()`) can ingest them exactly like a user-uploaded file batch, no special-casing needed in the app itself.
|
| 241 |
-
|
| 242 |
-
---
|
| 243 |
-
|
| 244 |
-
## 2.5 Query set authoring
|
| 245 |
-
|
| 246 |
-
Create `test_data/query_sets/{medical,legal,financial}_queries.json` with this schema:
|
| 247 |
-
```json
|
| 248 |
-
[
|
| 249 |
-
{
|
| 250 |
-
"query": "What is the first-line treatment for stage 2 hypertension?",
|
| 251 |
-
"domain": "medical",
|
| 252 |
-
"is_in_domain": true,
|
| 253 |
-
"expected_source_docs": ["PMC1234567"],
|
| 254 |
-
"annotator": "devrajsinh"
|
| 255 |
-
}
|
| 256 |
-
]
|
| 257 |
-
```
|
| 258 |
-
- ~40-50 in-domain queries per domain (authored by whoever on the team is most comfortable with that domain, split the load across Dev/Om/Jay).
|
| 259 |
-
- ~25-35 out-of-domain queries per domain-pair (medical→legal, legal→financial, etc.) for guardrail testing, reusing the structure already implied by Table IV in the paper, just at a smaller count.
|
| 260 |
-
- Single annotator per query is fine at this scale, note this explicitly as a limitation instead of claiming inter-annotator agreement you didn't actually measure.
|
| 261 |
-
|
| 262 |
-
---
|
| 263 |
-
|
| 264 |
-
## 2.6 Suggested paper text for Section IV-A
|
| 265 |
-
|
| 266 |
-
Drafted now so it's ready when the paper gets updated in Phase 4:
|
| 267 |
-
|
| 268 |
-
> *"Due to resource constraints, this study evaluates on a reduced corpus of approximately 500 documents (220 medical, 150 legal, 130 financial) sourced from PubMed Central Open Access, CourtListener, and SEC EDGAR respectively, with 120-150 in-domain queries and approximately 90 out-of-domain queries authored by the paper's co-authors. We report this as a pilot-scale evaluation and note that a larger, multi-annotator study is needed to confirm these findings generalize at production scale."*
|
| 269 |
-
|
| 270 |
-
Adjust the exact counts once the fetch scripts finish and real numbers are known.
|
| 271 |
-
|
| 272 |
-
---
|
| 273 |
-
|
| 274 |
-
## Summary of new files in Phase 2
|
| 275 |
-
|
| 276 |
-
| File | Purpose |
|
| 277 |
-
|---|---|
|
| 278 |
-
| `backend/scripts/fetch_pubmed.py` | pulls ~220 medical docs |
|
| 279 |
-
| `backend/scripts/fetch_courtlistener.py` | pulls ~150 legal docs |
|
| 280 |
-
| `backend/scripts/fetch_secedgar.py` | pulls ~130 financial docs |
|
| 281 |
-
| `test_data/medical_real/`, `test_data/legal_real/`, `test_data/financial_real/` | fetched corpora + manifests |
|
| 282 |
-
| `test_data/query_sets/*.json` | authored query sets with domain/expected-source labels |
|
| 283 |
-
|
| 284 |
-
---
|
| 285 |
-
|
| 286 |
-
## What Phase 2 does NOT cover (handled later)
|
| 287 |
-
|
| 288 |
-
- Running `baseline_runner.py`, `benchmark_runner.py`, `guardrail_analysis.py`, `statistical_tests.py` against this real data to produce actual paper numbers, that's Phase 3.
|
| 289 |
-
- Rewriting Tables I-V and Figures 2-4 in the paper, and rehearsing the live demo, that's Phase 4.
|
| 290 |
-
|
| 291 |
-
Once Phase 1 (architecture) and Phase 2 (data) are both done, the evaluation scripts already in `backend/evaluation/` should be able to run against real inputs with minimal further changes, since Phase 1 kept their function signatures stable.
|
|
|
|
| 1 |
+
# MEXAR Phase 2 — Real Datasets at Reduced, Disclosed Scale
|
| 2 |
+
|
| 3 |
+
**Purpose of this document:** hand this to an AI coding agent (Antigravity or similar) working directly in the `Mexar` repository (`github.com/devrajsinh2012/Mexar`). This is Phase 2 only: it brings in real documents and real queries so the numbers in the paper are reproducible. Assumes Phase 1 (architecture fixes: TF-IDF/NER guardrail, HNSW index, DeBERTa faithfulness scorer) is already done and verified.
|
| 4 |
+
|
| 5 |
+
**Presentation date:** July 30-31, 2026. Target for this phase: days 4-11 (about a week).
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## 2.0 Scope decision (already agreed with Dev)
|
| 10 |
+
|
| 11 |
+
Target **500-600 total documents** across the three domains for the presentation, not the 5,226 stated in the current paper draft. This is a resource-constrained pilot, and the paper's dataset section (IV-A) gets rewritten to say so honestly. Query sets are also reduced and single-annotator (Dev/Om/Jay), not the 900+600 two-expert-annotated set currently described. This is a normal, defensible thing to disclose in a paper, and far safer than presenting numbers nobody can reproduce live.
|
| 12 |
+
|
| 13 |
+
**Target split:**
|
| 14 |
+
|
| 15 |
+
| Domain | Target docs | Source |
|
| 16 |
+
|---|---|---|
|
| 17 |
+
| Medical | ~220 | PubMed Central OA subset |
|
| 18 |
+
| Legal | ~150 | CourtListener (Free Law Project) |
|
| 19 |
+
| Financial | ~130 | SEC EDGAR full-text search |
|
| 20 |
+
| **Total** | **~500** | |
|
| 21 |
+
|
| 22 |
+
**Important for the agent:** these fetch scripts hit external APIs (PubMed E-utilities, CourtListener, SEC EDGAR) that are outside a typical restricted sandbox's allowed network domains. Run these scripts in Dev's own local dev environment or a CI runner with normal internet access, not inside a network-restricted container.
|
| 23 |
+
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
## 2.1 Medical corpus — PubMed Central OA
|
| 27 |
+
|
| 28 |
+
**New script:** `backend/scripts/fetch_pubmed.py`
|
| 29 |
+
|
| 30 |
+
- Use the NCBI E-utilities API (`esearch` + `efetch`), no key required for low-volume use, but request one from NCBI if fetching more than 3 requests/sec.
|
| 31 |
+
- Query terms per subdomain, run three separate searches and cap results:
|
| 32 |
+
- `"cardiology treatment guideline"[Title/Abstract] AND "open access"[filter]`
|
| 33 |
+
- `"oncology treatment protocol"[Title/Abstract] AND "open access"[filter]`
|
| 34 |
+
- `"internal medicine diagnostic criteria"[Title/Abstract] AND "open access"[filter]`
|
| 35 |
+
- Pull ~75 docs per subdomain (≈220 total).
|
| 36 |
+
- Save each as plain text (abstract + body if available via the OA subset bulk XML) into `test_data/medical_real/`, one file per document, named by PMCID, e.g. `PMC1234567.txt`.
|
| 37 |
+
- Respect NCBI's rate limits (3 req/sec without an API key, 10/sec with one) and identify yourself via the `email` parameter on every request, per NCBI's usage policy.
|
| 38 |
+
- Log a manifest file `test_data/medical_real/manifest.json` mapping PMCID → title → subdomain → source URL, this becomes supporting material for the paper's dataset section and makes the corpus auditable.
|
| 39 |
+
|
| 40 |
+
**Skeleton:**
|
| 41 |
+
```python
|
| 42 |
+
"""
|
| 43 |
+
MEXAR - PubMed Central OA fetcher for the medical corpus.
|
| 44 |
+
Run this locally (not in a network-restricted sandbox).
|
| 45 |
+
"""
|
| 46 |
+
import requests
|
| 47 |
+
import time
|
| 48 |
+
import json
|
| 49 |
+
import os
|
| 50 |
+
|
| 51 |
+
NCBI_EMAIL = "your_email@example.com" # required by NCBI usage policy
|
| 52 |
+
OUTPUT_DIR = "test_data/medical_real"
|
| 53 |
+
SEARCH_TERMS = {
|
| 54 |
+
"cardiology": '"cardiology treatment guideline"[Title/Abstract] AND "open access"[filter]',
|
| 55 |
+
"oncology": '"oncology treatment protocol"[Title/Abstract] AND "open access"[filter]',
|
| 56 |
+
"internal_medicine": '"internal medicine diagnostic criteria"[Title/Abstract] AND "open access"[filter]',
|
| 57 |
+
}
|
| 58 |
+
DOCS_PER_SUBDOMAIN = 75
|
| 59 |
+
|
| 60 |
+
def esearch(term, retmax):
|
| 61 |
+
url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"
|
| 62 |
+
params = {"db": "pmc", "term": term, "retmax": retmax, "retmode": "json", "email": NCBI_EMAIL}
|
| 63 |
+
r = requests.get(url, params=params)
|
| 64 |
+
r.raise_for_status()
|
| 65 |
+
return r.json()["esearchresult"]["idlist"]
|
| 66 |
+
|
| 67 |
+
def efetch_fulltext(pmc_id):
|
| 68 |
+
url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi"
|
| 69 |
+
params = {"db": "pmc", "id": pmc_id, "rettype": "full", "retmode": "xml", "email": NCBI_EMAIL}
|
| 70 |
+
r = requests.get(url, params=params)
|
| 71 |
+
r.raise_for_status()
|
| 72 |
+
return r.text # parse out body text with an XML parser before saving as plain text
|
| 73 |
+
|
| 74 |
+
def main():
|
| 75 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 76 |
+
manifest = []
|
| 77 |
+
for subdomain, term in SEARCH_TERMS.items():
|
| 78 |
+
ids = esearch(term, DOCS_PER_SUBDOMAIN)
|
| 79 |
+
for pmc_id in ids:
|
| 80 |
+
time.sleep(0.34) # stay under 3 req/sec
|
| 81 |
+
try:
|
| 82 |
+
xml_text = efetch_fulltext(pmc_id)
|
| 83 |
+
# TODO: parse XML body text out with e.g. lxml, strip tags, save plain text
|
| 84 |
+
plain_text = xml_text # placeholder, replace with real extraction
|
| 85 |
+
out_path = os.path.join(OUTPUT_DIR, f"PMC{pmc_id}.txt")
|
| 86 |
+
with open(out_path, "w", encoding="utf-8") as f:
|
| 87 |
+
f.write(plain_text)
|
| 88 |
+
manifest.append({"pmc_id": pmc_id, "subdomain": subdomain, "path": out_path})
|
| 89 |
+
except Exception as e:
|
| 90 |
+
print(f"Failed to fetch PMC{pmc_id}: {e}")
|
| 91 |
+
with open(os.path.join(OUTPUT_DIR, "manifest.json"), "w") as f:
|
| 92 |
+
json.dump(manifest, f, indent=2)
|
| 93 |
+
print(f"Fetched {len(manifest)} medical documents.")
|
| 94 |
+
|
| 95 |
+
if __name__ == "__main__":
|
| 96 |
+
main()
|
| 97 |
+
```
|
| 98 |
+
The agent should replace the placeholder XML-to-text extraction with a real parser (e.g. `lxml` or `BeautifulSoup`) that pulls the `<body>` text out of the PMC XML format and strips markup, don't save raw XML as the "plain text" corpus.
|
| 99 |
+
|
| 100 |
+
---
|
| 101 |
+
|
| 102 |
+
## 2.2 Legal corpus — CourtListener
|
| 103 |
+
|
| 104 |
+
**New script:** `backend/scripts/fetch_courtlistener.py`
|
| 105 |
+
|
| 106 |
+
- Use the CourtListener REST API (`https://www.courtlistener.com/api/rest/v4/`), requires a free account and API token (set as `COURTLISTENER_TOKEN` env var).
|
| 107 |
+
- Pull opinions/case summaries across contract law, IP, and corporate governance search terms, plus a handful of statutory/regulatory text pages if accessible via their `opinions` and `search` endpoints.
|
| 108 |
+
- Target ~50 docs per subdomain (≈150 total).
|
| 109 |
+
- Save the opinion text (or summary if full text is very long, truncate to a reasonable chunking size, e.g. first 3000 words) into `test_data/legal_real/`, one file per case, named by CourtListener opinion ID.
|
| 110 |
+
- Same manifest pattern as 2.1: `test_data/legal_real/manifest.json` with case name, subdomain, court, date, source URL.
|
| 111 |
+
|
| 112 |
+
**Skeleton:**
|
| 113 |
+
```python
|
| 114 |
+
"""
|
| 115 |
+
MEXAR - CourtListener fetcher for the legal corpus.
|
| 116 |
+
Requires COURTLISTENER_TOKEN env var. Run locally.
|
| 117 |
+
"""
|
| 118 |
+
import requests
|
| 119 |
+
import os
|
| 120 |
+
import json
|
| 121 |
+
|
| 122 |
+
TOKEN = os.environ["COURTLISTENER_TOKEN"]
|
| 123 |
+
HEADERS = {"Authorization": f"Token {TOKEN}"}
|
| 124 |
+
BASE_URL = "https://www.courtlistener.com/api/rest/v4"
|
| 125 |
+
OUTPUT_DIR = "test_data/legal_real"
|
| 126 |
+
SEARCH_TERMS = {
|
| 127 |
+
"contract_law": "breach of contract damages",
|
| 128 |
+
"intellectual_property": "patent infringement trademark",
|
| 129 |
+
"corporate_governance": "fiduciary duty shareholder derivative",
|
| 130 |
+
}
|
| 131 |
+
DOCS_PER_SUBDOMAIN = 50
|
| 132 |
+
|
| 133 |
+
def search_opinions(query, count):
|
| 134 |
+
url = f"{BASE_URL}/search/"
|
| 135 |
+
params = {"q": query, "type": "o", "order_by": "score desc"}
|
| 136 |
+
results = []
|
| 137 |
+
while len(results) < count:
|
| 138 |
+
r = requests.get(url, headers=HEADERS, params=params)
|
| 139 |
+
r.raise_for_status()
|
| 140 |
+
data = r.json()
|
| 141 |
+
results.extend(data["results"])
|
| 142 |
+
if not data.get("next"):
|
| 143 |
+
break
|
| 144 |
+
url = data["next"]
|
| 145 |
+
params = {}
|
| 146 |
+
return results[:count]
|
| 147 |
+
|
| 148 |
+
def main():
|
| 149 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 150 |
+
manifest = []
|
| 151 |
+
for subdomain, term in SEARCH_TERMS.items():
|
| 152 |
+
opinions = search_opinions(term, DOCS_PER_SUBDOMAIN)
|
| 153 |
+
for op in opinions:
|
| 154 |
+
text = op.get("plain_text") or op.get("snippet", "")
|
| 155 |
+
if not text:
|
| 156 |
+
continue
|
| 157 |
+
op_id = op.get("id") or op.get("cluster_id")
|
| 158 |
+
out_path = os.path.join(OUTPUT_DIR, f"case_{op_id}.txt")
|
| 159 |
+
with open(out_path, "w", encoding="utf-8") as f:
|
| 160 |
+
f.write(text[:15000]) # cap length
|
| 161 |
+
manifest.append({
|
| 162 |
+
"id": op_id, "subdomain": subdomain,
|
| 163 |
+
"case_name": op.get("caseName", ""), "path": out_path,
|
| 164 |
+
})
|
| 165 |
+
with open(os.path.join(OUTPUT_DIR, "manifest.json"), "w") as f:
|
| 166 |
+
json.dump(manifest, f, indent=2)
|
| 167 |
+
print(f"Fetched {len(manifest)} legal documents.")
|
| 168 |
+
|
| 169 |
+
if __name__ == "__main__":
|
| 170 |
+
main()
|
| 171 |
+
```
|
| 172 |
+
The agent should verify the actual current CourtListener v4 search response schema (field names like `plain_text` vs `snippet` vs a separate opinion-text endpoint may have changed) before trusting this skeleton verbatim, API responses drift over time.
|
| 173 |
+
|
| 174 |
+
---
|
| 175 |
+
|
| 176 |
+
## 2.3 Financial corpus — SEC EDGAR
|
| 177 |
+
|
| 178 |
+
**New script:** `backend/scripts/fetch_secedgar.py`
|
| 179 |
+
|
| 180 |
+
- Use SEC EDGAR's full-text search API (`https://efts.sec.gov/LATEST/search-index`) and the regular filing index to pull excerpts from 10-K filings and select analyst/accounting-standard documents.
|
| 181 |
+
- **Mandatory:** SEC requires a descriptive `User-Agent` header identifying the requester (name + email) on every request, or you get blocked. Format: `"YourName YourEmail@example.com"`.
|
| 182 |
+
- Target ~130 docs, mixing filing sections (MD&A, risk factors) rather than entire 10-Ks, which are too long and would dominate the corpus disproportionately relative to medical/legal document sizes.
|
| 183 |
+
- Save into `test_data/financial_real/`, named by CIK + accession number.
|
| 184 |
+
- Same manifest pattern.
|
| 185 |
+
|
| 186 |
+
**Skeleton:**
|
| 187 |
+
```python
|
| 188 |
+
"""
|
| 189 |
+
MEXAR - SEC EDGAR fetcher for the financial corpus.
|
| 190 |
+
Run locally. Requires a descriptive User-Agent per SEC's fair access policy.
|
| 191 |
+
"""
|
| 192 |
+
import requests
|
| 193 |
+
import os
|
| 194 |
+
import json
|
| 195 |
+
import time
|
| 196 |
+
|
| 197 |
+
USER_AGENT = "Devrajsinh Gohil djgohil2012@gmail.com" # SEC requires this format
|
| 198 |
+
HEADERS = {"User-Agent": USER_AGENT}
|
| 199 |
+
OUTPUT_DIR = "test_data/financial_real"
|
| 200 |
+
SEARCH_TERMS = ["risk factors", "management discussion analysis", "GAAP IFRS accounting standards"]
|
| 201 |
+
DOCS_PER_TERM = 45 # ~130 total across 3 terms
|
| 202 |
+
|
| 203 |
+
def full_text_search(query, count):
|
| 204 |
+
url = "https://efts.sec.gov/LATEST/search-index?q=%22{}%22&forms=10-K".format(query.replace(" ", "+"))
|
| 205 |
+
r = requests.get(url, headers=HEADERS)
|
| 206 |
+
r.raise_for_status()
|
| 207 |
+
hits = r.json().get("hits", {}).get("hits", [])
|
| 208 |
+
return hits[:count]
|
| 209 |
+
|
| 210 |
+
def main():
|
| 211 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 212 |
+
manifest = []
|
| 213 |
+
for term in SEARCH_TERMS:
|
| 214 |
+
hits = full_text_search(term, DOCS_PER_TERM)
|
| 215 |
+
for hit in hits:
|
| 216 |
+
time.sleep(0.2) # be polite to SEC's rate limits
|
| 217 |
+
source = hit.get("_source", {})
|
| 218 |
+
cik = source.get("ciks", ["unknown"])[0]
|
| 219 |
+
accession = source.get("adsh", "unknown")
|
| 220 |
+
# TODO: fetch actual filing document text via the filing index URL,
|
| 221 |
+
# this skeleton only captures search-hit metadata, not full text.
|
| 222 |
+
excerpt = source.get("display_names", [""])[0]
|
| 223 |
+
out_path = os.path.join(OUTPUT_DIR, f"{cik}_{accession}.txt")
|
| 224 |
+
with open(out_path, "w", encoding="utf-8") as f:
|
| 225 |
+
f.write(excerpt)
|
| 226 |
+
manifest.append({"cik": cik, "accession": accession, "term": term, "path": out_path})
|
| 227 |
+
with open(os.path.join(OUTPUT_DIR, "manifest.json"), "w") as f:
|
| 228 |
+
json.dump(manifest, f, indent=2)
|
| 229 |
+
print(f"Fetched {len(manifest)} financial documents.")
|
| 230 |
+
|
| 231 |
+
if __name__ == "__main__":
|
| 232 |
+
main()
|
| 233 |
+
```
|
| 234 |
+
This one needs the most work from the agent: the full-text search endpoint returns hit metadata, not the actual filing body text. The agent needs to follow through to the actual filing document (via the CIK/accession-number-derived URL under `https://www.sec.gov/Archives/edgar/data/{cik}/{accession}/`) and extract the relevant section text (MD&A, risk factors) from the filing HTML, rather than saving just the search snippet.
|
| 235 |
+
|
| 236 |
+
---
|
| 237 |
+
|
| 238 |
+
## 2.4 Ingestion format
|
| 239 |
+
|
| 240 |
+
Each fetch script should output files in whatever format `backend/modules/data_validator.py`'s parsers already expect, check the `.txt`/`.json` handling there first, don't invent a new schema. Simplest path: one `.txt` file per document, plain text body, filename as a stable ID, so the existing agent-compilation flow (`KnowledgeCompiler.compile()`) can ingest them exactly like a user-uploaded file batch, no special-casing needed in the app itself.
|
| 241 |
+
|
| 242 |
+
---
|
| 243 |
+
|
| 244 |
+
## 2.5 Query set authoring
|
| 245 |
+
|
| 246 |
+
Create `test_data/query_sets/{medical,legal,financial}_queries.json` with this schema:
|
| 247 |
+
```json
|
| 248 |
+
[
|
| 249 |
+
{
|
| 250 |
+
"query": "What is the first-line treatment for stage 2 hypertension?",
|
| 251 |
+
"domain": "medical",
|
| 252 |
+
"is_in_domain": true,
|
| 253 |
+
"expected_source_docs": ["PMC1234567"],
|
| 254 |
+
"annotator": "devrajsinh"
|
| 255 |
+
}
|
| 256 |
+
]
|
| 257 |
+
```
|
| 258 |
+
- ~40-50 in-domain queries per domain (authored by whoever on the team is most comfortable with that domain, split the load across Dev/Om/Jay).
|
| 259 |
+
- ~25-35 out-of-domain queries per domain-pair (medical→legal, legal→financial, etc.) for guardrail testing, reusing the structure already implied by Table IV in the paper, just at a smaller count.
|
| 260 |
+
- Single annotator per query is fine at this scale, note this explicitly as a limitation instead of claiming inter-annotator agreement you didn't actually measure.
|
| 261 |
+
|
| 262 |
+
---
|
| 263 |
+
|
| 264 |
+
## 2.6 Suggested paper text for Section IV-A
|
| 265 |
+
|
| 266 |
+
Drafted now so it's ready when the paper gets updated in Phase 4:
|
| 267 |
+
|
| 268 |
+
> *"Due to resource constraints, this study evaluates on a reduced corpus of approximately 500 documents (220 medical, 150 legal, 130 financial) sourced from PubMed Central Open Access, CourtListener, and SEC EDGAR respectively, with 120-150 in-domain queries and approximately 90 out-of-domain queries authored by the paper's co-authors. We report this as a pilot-scale evaluation and note that a larger, multi-annotator study is needed to confirm these findings generalize at production scale."*
|
| 269 |
+
|
| 270 |
+
Adjust the exact counts once the fetch scripts finish and real numbers are known.
|
| 271 |
+
|
| 272 |
+
---
|
| 273 |
+
|
| 274 |
+
## Summary of new files in Phase 2
|
| 275 |
+
|
| 276 |
+
| File | Purpose |
|
| 277 |
+
|---|---|
|
| 278 |
+
| `backend/scripts/fetch_pubmed.py` | pulls ~220 medical docs |
|
| 279 |
+
| `backend/scripts/fetch_courtlistener.py` | pulls ~150 legal docs |
|
| 280 |
+
| `backend/scripts/fetch_secedgar.py` | pulls ~130 financial docs |
|
| 281 |
+
| `test_data/medical_real/`, `test_data/legal_real/`, `test_data/financial_real/` | fetched corpora + manifests |
|
| 282 |
+
| `test_data/query_sets/*.json` | authored query sets with domain/expected-source labels |
|
| 283 |
+
|
| 284 |
+
---
|
| 285 |
+
|
| 286 |
+
## What Phase 2 does NOT cover (handled later)
|
| 287 |
+
|
| 288 |
+
- Running `baseline_runner.py`, `benchmark_runner.py`, `guardrail_analysis.py`, `statistical_tests.py` against this real data to produce actual paper numbers, that's Phase 3.
|
| 289 |
+
- Rewriting Tables I-V and Figures 2-4 in the paper, and rehearsing the live demo, that's Phase 4.
|
| 290 |
+
|
| 291 |
+
Once Phase 1 (architecture) and Phase 2 (data) are both done, the evaluation scripts already in `backend/evaluation/` should be able to run against real inputs with minimal further changes, since Phase 1 kept their function signatures stable.
|
README.md
CHANGED
|
@@ -1,418 +1,443 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Mexar
|
| 3 |
-
emoji: 🧠
|
| 4 |
-
colorFrom: blue
|
| 5 |
-
colorTo: indigo
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
license: mit
|
| 9 |
-
---
|
| 10 |
-
# MEXAR Ultimate 🧠
|
| 11 |
-
|
| 12 |
-
**Multimodal Explainable AI Reasoning Assistant**
|
| 13 |
-
|
| 14 |
-
[](https://www.python.org/downloads/)
|
| 15 |
-
[](https://reactjs.org/)
|
| 16 |
-
[](https://fastapi.tiangolo.com/)
|
| 17 |
-
[](LICENSE)
|
| 18 |
-
[](https://mexar.vercel.app)
|
| 19 |
-
|
| 20 |
-
> Create domain-specific intelligent agents from your data with transparent, explainable AI responses using RAG (Retrieval-Augmented Generation) with source attribution and faithfulness scoring.
|
| 21 |
-
|
| 22 |
-
**🚀 Live Demo**: [https://mexar.vercel.app](https://mexar.vercel.app)
|
| 23 |
-
**📡 Backend API**: [https://devrajsinh2012-mexar.hf.space](https://devrajsinh2012-mexar.hf.space)
|
| 24 |
-
|
| 25 |
-
---
|
| 26 |
-
|
| 27 |
-
## ✨ Key Features
|
| 28 |
-
|
| 29 |
-
| Feature | Description |
|
| 30 |
-
|---------|-------------|
|
| 31 |
-
| 🔍 **Hybrid Search** | Combines semantic (vector) + keyword search with RRF fusion for optimal retrieval |
|
| 32 |
-
| 🎯 **Cross-Encoder Reranking** | Improves retrieval precision using sentence-transformers |
|
| 33 |
-
| 📊 **Source Attribution** | Inline citations `[1]`, `[2]` linking answers to source data |
|
| 34 |
-
| ✅ **Faithfulness Scoring** | Measures how well answers are grounded in retrieved context |
|
| 35 |
-
| 🗣️ **Multimodal Input** | Audio (Whisper), Images (Vision), Video support |
|
| 36 |
-
| 🔐 **Domain Guardrails** | Prevents hallucinations outside knowledge base |
|
| 37 |
-
| 🔊 **Text-to-Speech** | ElevenLabs + Web Speech API integration |
|
| 38 |
-
| 📁 **5 File Types** | CSV, PDF, DOCX, JSON, TXT |
|
| 39 |
-
|
| 40 |
-
---
|
| 41 |
-
|
| 42 |
-
##
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
│
|
| 72 |
-
|
| 73 |
-
│
|
| 74 |
-
│
|
| 75 |
-
│
|
| 76 |
-
│
|
| 77 |
-
│
|
| 78 |
-
│
|
| 79 |
-
│
|
| 80 |
-
│
|
| 81 |
-
│
|
| 82 |
-
│
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
#
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
#
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
#
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
│
|
| 207 |
-
│ │
|
| 208 |
-
│ ├──
|
| 209 |
-
│
|
| 210 |
-
│
|
| 211 |
-
|
| 212 |
-
│ ├──
|
| 213 |
-
│ │ ├──
|
| 214 |
-
│ │
|
| 215 |
-
│ │
|
| 216 |
-
│
|
| 217 |
-
│ │
|
| 218 |
-
│ │
|
| 219 |
-
│ │
|
| 220 |
-
│ │
|
| 221 |
-
│
|
| 222 |
-
│ │ ├──
|
| 223 |
-
│ │
|
| 224 |
-
│ ├──
|
| 225 |
-
│
|
| 226 |
-
│
|
| 227 |
-
├──
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
-
|
| 231 |
-
#
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
#
|
| 243 |
-
|
| 244 |
-
#
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
#
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
-
|
| 276 |
-
|
| 277 |
-
##
|
| 278 |
-
|
| 279 |
-
### Backend
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
##
|
| 303 |
-
|
| 304 |
-
`
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
- **
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
##
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
-
|
| 404 |
-
|
| 405 |
-
--
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Mexar
|
| 3 |
+
emoji: 🧠
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
---
|
| 10 |
+
# MEXAR Ultimate 🧠
|
| 11 |
+
|
| 12 |
+
**Multimodal Explainable AI Reasoning Assistant**
|
| 13 |
+
|
| 14 |
+
[](https://www.python.org/downloads/)
|
| 15 |
+
[](https://reactjs.org/)
|
| 16 |
+
[](https://fastapi.tiangolo.com/)
|
| 17 |
+
[](LICENSE)
|
| 18 |
+
[](https://mexar.vercel.app)
|
| 19 |
+
|
| 20 |
+
> Create domain-specific intelligent agents from your data with transparent, explainable AI responses using RAG (Retrieval-Augmented Generation) with source attribution and faithfulness scoring.
|
| 21 |
+
|
| 22 |
+
**🚀 Live Demo**: [https://mexar.vercel.app](https://mexar.vercel.app)
|
| 23 |
+
**📡 Backend API**: [https://devrajsinh2012-mexar.hf.space](https://devrajsinh2012-mexar.hf.space)
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
## ✨ Key Features
|
| 28 |
+
|
| 29 |
+
| Feature | Description |
|
| 30 |
+
|---------|-------------|
|
| 31 |
+
| 🔍 **Hybrid Search** | Combines semantic (vector) + keyword search with RRF fusion for optimal retrieval |
|
| 32 |
+
| 🎯 **Cross-Encoder Reranking** | Improves retrieval precision using sentence-transformers |
|
| 33 |
+
| 📊 **Source Attribution** | Inline citations `[1]`, `[2]` linking answers to source data |
|
| 34 |
+
| ✅ **Faithfulness Scoring** | Measures how well answers are grounded in retrieved context |
|
| 35 |
+
| 🗣️ **Multimodal Input** | Audio (Whisper), Images (Vision), Video support |
|
| 36 |
+
| 🔐 **Domain Guardrails** | Prevents hallucinations outside knowledge base |
|
| 37 |
+
| 🔊 **Text-to-Speech** | ElevenLabs + Web Speech API integration |
|
| 38 |
+
| 📁 **5 File Types** | CSV, PDF, DOCX, JSON, TXT |
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## 📊 Empirical Evaluation Results & Benchmarks
|
| 43 |
+
|
| 44 |
+
MEXAR has been evaluated across real multi-domain datasets sourced from **NCBI PubMed**, **CourtListener REST API v4**, and **SEC EDGAR 10-K Filings**.
|
| 45 |
+
|
| 46 |
+
### 1. Real Multi-Domain Knowledge Base Stats
|
| 47 |
+
| Domain | Real API Data Source | Files Processed | Indexed Vector Chunks | Domain Terms |
|
| 48 |
+
| :--- | :--- | :---: | :---: | :---: |
|
| 49 |
+
| 🏥 **Medical** | PubMed Central Open Access (NCBI) | 31 papers | 556 chunks | 127 terms |
|
| 50 |
+
| ⚖️ **Legal** | CourtListener v4 REST API | 148 opinions | 157 chunks | 152 terms |
|
| 51 |
+
| 📈 **Financial** | SEC EDGAR 10-K Filings | 4 filings | 68 chunks | 119 terms |
|
| 52 |
+
|
| 53 |
+
### 2. Multi-System Faithfulness Comparison
|
| 54 |
+
| Baseline System | Medical Faithfulness | Legal Faithfulness | Financial Faithfulness |
|
| 55 |
+
| :--- | :---: | :---: | :---: |
|
| 56 |
+
| **Naive RAG** | 0.0222 | 0.0333 | 0.0000 |
|
| 57 |
+
| **BM25 Only** | 0.0000 | 0.0000 | 0.0000 |
|
| 58 |
+
| **LangChain** | 0.5000 | 0.5000 | 0.5000 |
|
| 59 |
+
| **Self-RAG** | 0.2380 | 0.0833 | N/A |
|
| 60 |
+
| 🧠 **MEXAR (Ours)** | **0.1000** | **0.1000** | N/A |
|
| 61 |
+
|
| 62 |
+
### 3. System Latency & Calibration Benchmarks
|
| 63 |
+
* ⚡ **NLI Faithfulness Evaluation Latency**: ~**1.2s / query** (~50x speedup via PyTorch vectorized batching vs 70s baseline).
|
| 64 |
+
* 🛡️ **Domain Guardrail Overhead**: Mean **113.49 ms** latency.
|
| 65 |
+
* 🎯 **Expected Calibration Error (ECE)**: **0.1000** (Confidence calibration vs empirical accuracy).
|
| 66 |
+
|
| 67 |
+
## 🏗️ Architecture
|
| 68 |
+
|
| 69 |
+
```
|
| 70 |
+
┌────────────────────────────────────────────────────────────────────────────┐
|
| 71 |
+
│ MEXAR Ultimate Stack │
|
| 72 |
+
├────────────────────────────────────────────────────────────────────────────┤
|
| 73 |
+
│ │
|
| 74 |
+
│ ┌────────────────────────────────────────────┐ │
|
| 75 |
+
│ │ React Frontend (Vercel) │ │
|
| 76 |
+
│ └────────────────────────────────────────────┘ │
|
| 77 |
+
│ │ │
|
| 78 |
+
│ ▼ │
|
| 79 |
+
│ ┌────────────────────────────────────────────┐ │
|
| 80 |
+
│ │ FastAPI Backend (Hugging Face Spaces) │ │
|
| 81 |
+
│ └────────────────────────────────────────────┘ │
|
| 82 |
+
│ │ │
|
| 83 |
+
│ ▼ │
|
| 84 |
+
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
| 85 |
+
│ │ Core Intelligence Layer │ │
|
| 86 |
+
│ │ │ │
|
| 87 |
+
│ │ 🔄 Data Validator (PDF / DOCX / CSV / TXT / JSON) │ │
|
| 88 |
+
│ │ 🤖 Prompt Analyzer (LLM-based Intent Parsing) │ │
|
| 89 |
+
│ │ 📦 Knowledge Compiler (FastEmbed + Chunking) │ │
|
| 90 |
+
│ │ 🧠 Reasoning Engine │ │
|
| 91 |
+
│ │ ├─ Hybrid Search (Dense + Sparse) │ │
|
| 92 |
+
│ │ ├─ Cross-Encoder (Re-ranking) │ │
|
| 93 |
+
│ │ ├─ Source Attribution (Citation Tracking) │ │
|
| 94 |
+
│ │ └─ Faithfulness Scorer (Hallucination Control) │ │
|
| 95 |
+
│ │ │ │
|
| 96 |
+
│ └────────────────────────────────────────────────────────────────────┘ │
|
| 97 |
+
│ │ │
|
| 98 |
+
│ ▼ │
|
| 99 |
+
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
| 100 |
+
│ │ External Services Layer │ │
|
| 101 |
+
│ │ │ │
|
| 102 |
+
│ │ 🗄 Supabase → PostgreSQL + pgvector + File Storage │ │
|
| 103 |
+
│ │ ⚡ Groq API → LLM Inference + Whisper + Vision │ │
|
| 104 |
+
│ │ 🔊 ElevenLabs → Text-to-Speech │ │
|
| 105 |
+
│ │ │ │
|
| 106 |
+
│ └──────────────────────────────────��─────────────────────────────────┘ │
|
| 107 |
+
│ │
|
| 108 |
+
└────────────────────────────────────────────────────────────────────────────┘
|
| 109 |
+
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
---
|
| 113 |
+
|
| 114 |
+
## 🚀 Quick Start
|
| 115 |
+
|
| 116 |
+
### Prerequisites
|
| 117 |
+
|
| 118 |
+
- **Python 3.9+** with pip
|
| 119 |
+
- **Node.js 18+** with npm
|
| 120 |
+
- **PostgreSQL** with `pgvector` extension (or use Supabase)
|
| 121 |
+
- **Groq API Key** - Get free at [console.groq.com](https://console.groq.com)
|
| 122 |
+
|
| 123 |
+
### Local Development
|
| 124 |
+
|
| 125 |
+
#### 1. Backend Setup
|
| 126 |
+
|
| 127 |
+
```bash
|
| 128 |
+
cd backend
|
| 129 |
+
|
| 130 |
+
# Create virtual environment
|
| 131 |
+
python -m venv venv
|
| 132 |
+
|
| 133 |
+
# Activate (Windows)
|
| 134 |
+
.\venv\Scripts\activate
|
| 135 |
+
# Activate (macOS/Linux)
|
| 136 |
+
source venv/bin/activate
|
| 137 |
+
|
| 138 |
+
# Install dependencies
|
| 139 |
+
pip install -r requirements.txt
|
| 140 |
+
|
| 141 |
+
# Configure environment
|
| 142 |
+
cp .env.example .env
|
| 143 |
+
# Edit .env and add your API keys
|
| 144 |
+
|
| 145 |
+
# Run server
|
| 146 |
+
python -m uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
**Backend will run at**: [http://localhost:8000](http://localhost:8000)
|
| 150 |
+
|
| 151 |
+
#### 2. Frontend Setup
|
| 152 |
+
|
| 153 |
+
```bash
|
| 154 |
+
cd frontend
|
| 155 |
+
|
| 156 |
+
# Install dependencies
|
| 157 |
+
npm install
|
| 158 |
+
|
| 159 |
+
# Start development server
|
| 160 |
+
npm start
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
**Frontend will run at**: [http://localhost:3000](http://localhost:3000)
|
| 164 |
+
|
| 165 |
+
---
|
| 166 |
+
|
| 167 |
+
## 📈 Evaluation Workflows
|
| 168 |
+
|
| 169 |
+
The scripts in `backend/evaluation` support baseline comparison, guardrail checks, benchmark runs, and ablation studies.
|
| 170 |
+
|
| 171 |
+
Run from project root:
|
| 172 |
+
|
| 173 |
+
```bash
|
| 174 |
+
cd backend
|
| 175 |
+
|
| 176 |
+
# Baseline comparison: MEXAR vs CRAG vs RAPTOR
|
| 177 |
+
python evaluation/baseline_runner.py
|
| 178 |
+
|
| 179 |
+
# Backbone comparison (restores original backbone after completion)
|
| 180 |
+
python evaluation/backbone_comparison.py
|
| 181 |
+
|
| 182 |
+
# Guardrail boundary query analysis
|
| 183 |
+
python evaluation/guardrail_analysis.py
|
| 184 |
+
|
| 185 |
+
# Benchmark dataset run (all rows by default) + save report
|
| 186 |
+
python evaluation/benchmark_runner.py --dataset-path ../test_data/medqa_sample.json --agent-name medical_agent --output evaluation_outputs/medqa_report.json
|
| 187 |
+
|
| 188 |
+
# Quick benchmark smoke test
|
| 189 |
+
python evaluation/benchmark_runner.py --dataset-path ../test_data/medqa_sample.json --agent-name medical_agent --max-samples 25
|
| 190 |
+
|
| 191 |
+
# McNemar significance helper
|
| 192 |
+
python evaluation/statistical_tests.py
|
| 193 |
+
```
|
| 194 |
+
|
| 195 |
+
Notes:
|
| 196 |
+
- Faithfulness values are read from `explainability.confidence_breakdown.faithfulness` when available.
|
| 197 |
+
- Benchmark reports include per-query status and aggregate summary metrics.
|
| 198 |
+
|
| 199 |
+
---
|
| 200 |
+
|
| 201 |
+
## 📁 Project Structure
|
| 202 |
+
|
| 203 |
+
```
|
| 204 |
+
mexar_ultimate/
|
| 205 |
+
├── backend/ # FastAPI Backend
|
| 206 |
+
│ ├── api/ # REST API endpoints
|
| 207 |
+
│ │ ├── auth.py # Authentication (JWT)
|
| 208 |
+
│ │ ├── agents.py # Agent CRUD
|
| 209 |
+
│ │ ├── chat.py # Chat + multimodal
|
| 210 |
+
│ │ ├── compile.py # Knowledge compilation
|
| 211 |
+
│ │ └── websocket.py # Real-time updates
|
| 212 |
+
│ ├── core/ # Core configuration
|
| 213 |
+
│ │ ├── config.py # Settings
|
| 214 |
+
│ │ ├── database.py # SQLAlchemy setup
|
| 215 |
+
│ │ └── security.py # JWT handling
|
| 216 |
+
│ ├── models/ # Database models
|
| 217 |
+
│ │ ├── user.py # User model
|
| 218 |
+
│ │ ├── agent.py # Agent + CompilationJob
|
| 219 |
+
│ │ ├── chunk.py # DocumentChunk (pgvector)
|
| 220 |
+
│ │ └── conversation.py # Chat history
|
| 221 |
+
│ ├── modules/ # Core AI modules
|
| 222 |
+
│ │ ├── data_validator.py # File parsing
|
| 223 |
+
│ │ ├── prompt_analyzer.py # Domain extraction
|
| 224 |
+
│ │ ├── knowledge_compiler.py # Vector embeddings
|
| 225 |
+
│ │ ├── reasoning_engine.py # RAG pipeline
|
| 226 |
+
│ │ └── explainability.py # UI formatting
|
| 227 |
+
│ ├── utils/ # Utilities
|
| 228 |
+
│ │ ├── groq_client.py # Groq API wrapper
|
| 229 |
+
│ │ ├── hybrid_search.py # RRF search fusion
|
| 230 |
+
│ │ ├── reranker.py # Cross-encoder
|
| 231 |
+
│ │ ├── faithfulness.py # Claim verification
|
| 232 |
+
│ │ └── source_attribution.py # Citation extraction
|
| 233 |
+
│ ├── main.py # FastAPI entry point
|
| 234 |
+
│ └── requirements.txt # Python dependencies
|
| 235 |
+
│
|
| 236 |
+
├── frontend/ # React Frontend
|
| 237 |
+
│ ├── src/
|
| 238 |
+
│ │ ├── pages/ # React pages
|
| 239 |
+
│ │ │ ├── Landing.jsx # Home page
|
| 240 |
+
│ │ │ ├── Login.jsx # Authentication
|
| 241 |
+
│ │ │ ├── Dashboard.jsx # User dashboard
|
| 242 |
+
│ │ │ ├── AgentCreation.jsx # Create agent
|
| 243 |
+
│ │ │ ├── CompilationProgress.jsx # Build progress
|
| 244 |
+
│ │ │ └── Chat.jsx # Chat interface
|
| 245 |
+
│ │ ├── components/ # Reusable UI
|
| 246 |
+
│ │ ├── contexts/ # React contexts
|
| 247 |
+
│ │ ├── api/ # API client
|
| 248 |
+
│ │ └── App.jsx # Main component
|
| 249 |
+
│ ├── package.json # Node dependencies
|
| 250 |
+
│ └── vercel.json # Vercel config
|
| 251 |
+
│
|
| 252 |
+
├── Dockerfile # Docker config for HF Spaces
|
| 253 |
+
└── README.md # This file
|
| 254 |
+
```
|
| 255 |
+
---
|
| 256 |
+
### 📸 Screenshot
|
| 257 |
+
<img width="1863" height="918" alt="image" src="https://github.com/user-attachments/assets/75ff2c32-4c90-4e65-ab5f-10c94bad2e65" />
|
| 258 |
+
<img width="330" height="330" alt="image" src="https://github.com/user-attachments/assets/2d9846c9-b002-4f05-b632-6dc1c8c58ad1" />
|
| 259 |
+
<img width="330" height="330" alt="image" src="https://github.com/user-attachments/assets/810de78d-4725-494e-94b5-e8aa71b8b7fd" />
|
| 260 |
+
<img width="330" height="330" alt="image" src="https://github.com/user-attachments/assets/155ea162-d8b9-4b45-acf9-42afc47c8498" />
|
| 261 |
+
<img width="330" height="330" alt="image" src="https://github.com/user-attachments/assets/cadf46a8-c679-4a76-a58d-a956f9cb9cc1" />
|
| 262 |
+
<img width="330" height="330" alt="image" src="https://github.com/user-attachments/assets/598b7925-8001-44a7-ae34-6fddb85b7531" />
|
| 263 |
+
<img width="330" height="330" alt="image" src="https://github.com/user-attachments/assets/e9e99b9d-05b0-4ae5-9990-ed9c9cc124c6" />
|
| 264 |
+
|
| 265 |
+
---
|
| 266 |
+
|
| 267 |
+
## 🌐 Deployment
|
| 268 |
+
|
| 269 |
+
### Current Deployment (Free Tier)
|
| 270 |
+
|
| 271 |
+
- **Frontend**: Vercel - [https://mexar.vercel.app](https://mexar.vercel.app)
|
| 272 |
+
- **Backend**: Hugging Face Spaces - [https://devrajsinh2012-mexar.hf.space](https://devrajsinh2012-mexar.hf.space)
|
| 273 |
+
- **Database**: Supabase (PostgreSQL with pgvector)
|
| 274 |
+
- **Storage**: Supabase Storage
|
| 275 |
+
- **Total Cost**: $0/month
|
| 276 |
+
|
| 277 |
+
### Deploy Your Own Instance
|
| 278 |
+
|
| 279 |
+
#### Deploy Backend to Hugging Face Spaces
|
| 280 |
+
|
| 281 |
+
1. Fork this repository
|
| 282 |
+
2. Create a new Space at [huggingface.co/new-space](https://huggingface.co/new-space)
|
| 283 |
+
3. Select **Docker** as SDK
|
| 284 |
+
4. Connect your GitHub repository
|
| 285 |
+
5. Add Repository Secrets:
|
| 286 |
+
- `GROQ_API_KEY`
|
| 287 |
+
- `DATABASE_URL`
|
| 288 |
+
- `SUPABASE_URL`
|
| 289 |
+
- `SUPABASE_KEY`
|
| 290 |
+
- `SECRET_KEY`
|
| 291 |
+
- `FRONTEND_URL`
|
| 292 |
+
|
| 293 |
+
#### Deploy Frontend to Vercel
|
| 294 |
+
|
| 295 |
+
1. Import repository at [vercel.com](https://vercel.com)
|
| 296 |
+
2. Set **Root Directory** to `frontend`
|
| 297 |
+
3. Add Environment Variable:
|
| 298 |
+
- `REACT_APP_API_URL` = Your HF Spaces URL
|
| 299 |
+
|
| 300 |
+
---
|
| 301 |
+
|
| 302 |
+
## 🔧 Environment Variables
|
| 303 |
+
|
| 304 |
+
### Backend (`backend/.env`)
|
| 305 |
+
|
| 306 |
+
```env
|
| 307 |
+
# Required: Get from console.groq.com
|
| 308 |
+
GROQ_API_KEY=your_groq_api_key_here
|
| 309 |
+
|
| 310 |
+
# Supabase Database
|
| 311 |
+
DATABASE_URL=postgresql://user:password@host:5432/database
|
| 312 |
+
|
| 313 |
+
# JWT Security
|
| 314 |
+
SECRET_KEY=generate-a-secure-random-key
|
| 315 |
+
|
| 316 |
+
# Supabase Storage
|
| 317 |
+
SUPABASE_URL=https://your-project.supabase.co
|
| 318 |
+
SUPABASE_KEY=your_supabase_service_role_key
|
| 319 |
+
|
| 320 |
+
# Optional: ElevenLabs TTS
|
| 321 |
+
ELEVENLABS_API_KEY=your_elevenlabs_api_key_here
|
| 322 |
+
|
| 323 |
+
# Frontend URL for CORS
|
| 324 |
+
FRONTEND_URL=https://mexar.vercel.app
|
| 325 |
+
```
|
| 326 |
+
|
| 327 |
+
### Frontend (`frontend/.env`)
|
| 328 |
+
|
| 329 |
+
```env
|
| 330 |
+
# Backend API URL
|
| 331 |
+
REACT_APP_API_URL=https://your-backend.hf.space
|
| 332 |
+
```
|
| 333 |
+
|
| 334 |
+
---
|
| 335 |
+
|
| 336 |
+
## 🔍 API Documentation
|
| 337 |
+
|
| 338 |
+
Once the backend is running, interactive API docs are available at:
|
| 339 |
+
|
| 340 |
+
- **Swagger UI**: `http://localhost:8000/docs`
|
| 341 |
+
- **ReDoc**: `http://localhost:8000/redoc`
|
| 342 |
+
|
| 343 |
+
### Key Endpoints
|
| 344 |
+
|
| 345 |
+
| Method | Endpoint | Description |
|
| 346 |
+
|--------|----------|-------------|
|
| 347 |
+
| POST | `/api/auth/register` | Register new user |
|
| 348 |
+
| POST | `/api/auth/login` | Login (returns JWT) |
|
| 349 |
+
| GET | `/api/agents/` | List all agents |
|
| 350 |
+
| POST | `/api/compile/` | Start agent compilation |
|
| 351 |
+
| GET | `/api/compile/{name}/status` | Check compilation status |
|
| 352 |
+
| POST | `/api/chat/` | Send message to agent |
|
| 353 |
+
| POST | `/api/chat/multimodal` | Send with audio/image |
|
| 354 |
+
|
| 355 |
+
---
|
| 356 |
+
|
| 357 |
+
## 🧪 Technologies
|
| 358 |
+
|
| 359 |
+
### Backend
|
| 360 |
+
- **FastAPI** - Modern async Python web framework
|
| 361 |
+
- **SQLAlchemy** - ORM for PostgreSQL
|
| 362 |
+
- **pgvector** - Vector similarity search
|
| 363 |
+
- **FastEmbed** - Local embedding generation (BAAI/bge-small-en-v1.5)
|
| 364 |
+
- **sentence-transformers** - Cross-encoder reranking
|
| 365 |
+
- **Groq API** - LLM (Llama 3.1/3.3), Whisper (audio), Vision (images)
|
| 366 |
+
|
| 367 |
+
### Frontend
|
| 368 |
+
- **React 18** - UI framework
|
| 369 |
+
- **Material-UI (MUI)** - Component library
|
| 370 |
+
- **React Router** - Navigation
|
| 371 |
+
- **Axios** - HTTP client
|
| 372 |
+
|
| 373 |
+
### External Services
|
| 374 |
+
- **Supabase** - Managed PostgreSQL + Storage
|
| 375 |
+
- **Groq** - Fast AI inference (LPU architecture)
|
| 376 |
+
- **ElevenLabs** - Text-to-Speech (optional)
|
| 377 |
+
|
| 378 |
+
---
|
| 379 |
+
|
| 380 |
+
## 📊 How It Works
|
| 381 |
+
|
| 382 |
+
### 1. Agent Creation Flow
|
| 383 |
+
```
|
| 384 |
+
User uploads files → DataValidator parses content
|
| 385 |
+
→ PromptAnalyzer extracts domain & keywords
|
| 386 |
+
→ KnowledgeCompiler creates embeddings
|
| 387 |
+
→ Stored in pgvector database
|
| 388 |
+
```
|
| 389 |
+
|
| 390 |
+
### 2. Query Processing Flow
|
| 391 |
+
```
|
| 392 |
+
User query → Domain Guardrail check
|
| 393 |
+
→ Hybrid Search (semantic + keyword)
|
| 394 |
+
→ Cross-Encoder Reranking (top 5 results)
|
| 395 |
+
→ LLM Generation with retrieved context
|
| 396 |
+
→ Source Attribution (extract citations)
|
| 397 |
+
→ Faithfulness Scoring (verify grounding)
|
| 398 |
+
→ Explainability Formatting
|
| 399 |
+
```
|
| 400 |
+
|
| 401 |
+
### 3. Confidence Calculation
|
| 402 |
+
Confidence score is calculated from:
|
| 403 |
+
- **Retrieval Quality** (35%) - Relevance of retrieved chunks
|
| 404 |
+
- **Rerank Score** (30%) - Cross-encoder confidence
|
| 405 |
+
- **Faithfulness** (25%) - Answer grounding in context
|
| 406 |
+
- **Base Floor** (10%) - For in-domain queries
|
| 407 |
+
|
| 408 |
+
---
|
| 409 |
+
|
| 410 |
+
## ⚠️ Known Limitations (Free Tier)
|
| 411 |
+
|
| 412 |
+
1. **Cold Start Delay**: First request after 15 min idle takes 45-90 seconds
|
| 413 |
+
2. **Model Download**: Initial startup takes 3-5 minutes (FastEmbed caching)
|
| 414 |
+
3. **Groq Rate Limits**: 30 requests/min, 14,400/day (free tier)
|
| 415 |
+
4. **Concurrent Users**: 1-2 recommended on free tier (2GB RAM limit)
|
| 416 |
+
5. **Ephemeral Storage**: HF Spaces `/tmp` data lost on restart (Supabase used for persistence)
|
| 417 |
+
|
| 418 |
+
**Production Migration**: Upgrade to paid tiers for ~$54/month (persistent instances, higher limits)
|
| 419 |
+
|
| 420 |
+
---
|
| 421 |
+
|
| 422 |
+
## 🙏 Acknowledgments
|
| 423 |
+
|
| 424 |
+
- [Groq](https://groq.com) - Fast AI inference with LPU technology
|
| 425 |
+
- [Supabase](https://supabase.com) - PostgreSQL + Storage platform
|
| 426 |
+
- [FastEmbed](https://github.com/qdrant/fastembed) - Lightweight embeddings library
|
| 427 |
+
- [sentence-transformers](https://www.sbert.net) - Reranking models
|
| 428 |
+
- [Hugging Face](https://huggingface.co) - Free ML model hosting
|
| 429 |
+
|
| 430 |
+
---
|
| 431 |
+
|
| 432 |
+
## 🤝 Contribution
|
| 433 |
+
|
| 434 |
+
This project is built by Devrajsinh Gohil and Jay Nasit under the guidance of Prof OM Prakash Suthar.
|
| 435 |
+
|
| 436 |
+
**Built with ❤️ using modern AI technologies**
|
| 437 |
+
|
| 438 |
+
---
|
| 439 |
+
|
| 440 |
+
## 📄 License
|
| 441 |
+
|
| 442 |
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
| 443 |
+
|
backend/.env.example
CHANGED
|
@@ -1,61 +1,61 @@
|
|
| 1 |
-
# MEXAR Core Engine - Backend Environment Variables
|
| 2 |
-
# Copy this file to .env and fill in your values
|
| 3 |
-
|
| 4 |
-
# ===========================================
|
| 5 |
-
# REQUIRED: Groq API (LLM, Whisper, Vision)
|
| 6 |
-
# ===========================================
|
| 7 |
-
# Get your free API key at: https://console.groq.com
|
| 8 |
-
GROQ_API_KEY=your_groq_api_key_here
|
| 9 |
-
|
| 10 |
-
# ===========================================
|
| 11 |
-
# REQUIRED: Database
|
| 12 |
-
# ===========================================
|
| 13 |
-
# PostgreSQL with pgvector extension
|
| 14 |
-
# For Supabase: Copy from Settings > Database > Connection string
|
| 15 |
-
DATABASE_URL=postgresql://user:password@host:5432/database
|
| 16 |
-
|
| 17 |
-
# ===========================================
|
| 18 |
-
# REQUIRED: Security
|
| 19 |
-
# ===========================================
|
| 20 |
-
# Generate a secure random key for JWT tokens
|
| 21 |
-
# Example: python -c "import secrets; print(secrets.token_urlsafe(32))"
|
| 22 |
-
SECRET_KEY=your_secure_secret_key_here
|
| 23 |
-
|
| 24 |
-
# ===========================================
|
| 25 |
-
# REQUIRED: Supabase Storage
|
| 26 |
-
# ===========================================
|
| 27 |
-
# Get from Supabase Dashboard > Settings > API
|
| 28 |
-
SUPABASE_URL=https://your-project-id.supabase.co
|
| 29 |
-
SUPABASE_KEY=your_supabase_service_role_key
|
| 30 |
-
|
| 31 |
-
# ===========================================
|
| 32 |
-
# OPTIONAL: Text-to-Speech
|
| 33 |
-
# ===========================================
|
| 34 |
-
# ElevenLabs API (10,000 chars/month free)
|
| 35 |
-
# Get at: https://elevenlabs.io
|
| 36 |
-
ELEVENLABS_API_KEY=your_elevenlabs_api_key_here
|
| 37 |
-
|
| 38 |
-
# ===========================================
|
| 39 |
-
# OPTIONAL: Local Storage Path
|
| 40 |
-
# ===========================================
|
| 41 |
-
# For development only, production uses Supabase Storage
|
| 42 |
-
STORAGE_PATH=./data/storage
|
| 43 |
-
|
| 44 |
-
# ===========================================
|
| 45 |
-
# PHASE 2: Dataset Collection Scripts
|
| 46 |
-
# ===========================================
|
| 47 |
-
# Required for backend/scripts/fetch_courtlistener.py
|
| 48 |
-
# Free account + token at: https://www.courtlistener.com/sign-in/
|
| 49 |
-
COURTLISTENER_TOKEN=
|
| 50 |
-
|
| 51 |
-
# Required for backend/scripts/fetch_pubmed.py
|
| 52 |
-
# NCBI usage policy requires a valid email address on every request.
|
| 53 |
-
NCBI_EMAIL=
|
| 54 |
-
|
| 55 |
-
# Optional: NCBI API key raises rate limit from 3 req/sec → 10 req/sec.
|
| 56 |
-
# Register at: https://www.ncbi.nlm.nih.gov/account/
|
| 57 |
-
NCBI_API_KEY=
|
| 58 |
-
|
| 59 |
-
# Required for backend/scripts/fetch_secedgar.py
|
| 60 |
-
# SEC fair access policy: format must be "FirstName LastName email@example.com"
|
| 61 |
-
SEC_USER_AGENT=YourName your@email.com
|
|
|
|
| 1 |
+
# MEXAR Core Engine - Backend Environment Variables
|
| 2 |
+
# Copy this file to .env and fill in your values
|
| 3 |
+
|
| 4 |
+
# ===========================================
|
| 5 |
+
# REQUIRED: Groq API (LLM, Whisper, Vision)
|
| 6 |
+
# ===========================================
|
| 7 |
+
# Get your free API key at: https://console.groq.com
|
| 8 |
+
GROQ_API_KEY=your_groq_api_key_here
|
| 9 |
+
|
| 10 |
+
# ===========================================
|
| 11 |
+
# REQUIRED: Database
|
| 12 |
+
# ===========================================
|
| 13 |
+
# PostgreSQL with pgvector extension
|
| 14 |
+
# For Supabase: Copy from Settings > Database > Connection string
|
| 15 |
+
DATABASE_URL=postgresql://user:password@host:5432/database
|
| 16 |
+
|
| 17 |
+
# ===========================================
|
| 18 |
+
# REQUIRED: Security
|
| 19 |
+
# ===========================================
|
| 20 |
+
# Generate a secure random key for JWT tokens
|
| 21 |
+
# Example: python -c "import secrets; print(secrets.token_urlsafe(32))"
|
| 22 |
+
SECRET_KEY=your_secure_secret_key_here
|
| 23 |
+
|
| 24 |
+
# ===========================================
|
| 25 |
+
# REQUIRED: Supabase Storage
|
| 26 |
+
# ===========================================
|
| 27 |
+
# Get from Supabase Dashboard > Settings > API
|
| 28 |
+
SUPABASE_URL=https://your-project-id.supabase.co
|
| 29 |
+
SUPABASE_KEY=your_supabase_service_role_key
|
| 30 |
+
|
| 31 |
+
# ===========================================
|
| 32 |
+
# OPTIONAL: Text-to-Speech
|
| 33 |
+
# ===========================================
|
| 34 |
+
# ElevenLabs API (10,000 chars/month free)
|
| 35 |
+
# Get at: https://elevenlabs.io
|
| 36 |
+
ELEVENLABS_API_KEY=your_elevenlabs_api_key_here
|
| 37 |
+
|
| 38 |
+
# ===========================================
|
| 39 |
+
# OPTIONAL: Local Storage Path
|
| 40 |
+
# ===========================================
|
| 41 |
+
# For development only, production uses Supabase Storage
|
| 42 |
+
STORAGE_PATH=./data/storage
|
| 43 |
+
|
| 44 |
+
# ===========================================
|
| 45 |
+
# PHASE 2: Dataset Collection Scripts
|
| 46 |
+
# ===========================================
|
| 47 |
+
# Required for backend/scripts/fetch_courtlistener.py
|
| 48 |
+
# Free account + token at: https://www.courtlistener.com/sign-in/
|
| 49 |
+
COURTLISTENER_TOKEN=token_api
|
| 50 |
+
|
| 51 |
+
# Required for backend/scripts/fetch_pubmed.py
|
| 52 |
+
# NCBI usage policy requires a valid email address on every request.
|
| 53 |
+
NCBI_EMAIL=example@example.com
|
| 54 |
+
|
| 55 |
+
# Optional: NCBI API key raises rate limit from 3 req/sec → 10 req/sec.
|
| 56 |
+
# Register at: https://www.ncbi.nlm.nih.gov/account/
|
| 57 |
+
NCBI_API_KEY=sample_api_key
|
| 58 |
+
|
| 59 |
+
# Required for backend/scripts/fetch_secedgar.py
|
| 60 |
+
# SEC fair access policy: format must be "FirstName LastName email@example.com"
|
| 61 |
+
SEC_USER_AGENT=YourName your@email.com
|
backend/core/config.py
CHANGED
|
@@ -1,27 +1,31 @@
|
|
| 1 |
-
|
| 2 |
-
import os
|
| 3 |
-
from
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import os
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
env_path = Path(__file__).resolve().parent.parent / ".env"
|
| 7 |
+
if env_path.exists():
|
| 8 |
+
load_dotenv(dotenv_path=env_path)
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
class Config:
|
| 12 |
+
# Database (Default to SQLite for dev)
|
| 13 |
+
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./mexar.db")
|
| 14 |
+
|
| 15 |
+
# Security
|
| 16 |
+
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-in-production")
|
| 17 |
+
ALGORITHM = "HS256"
|
| 18 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 # 1 day
|
| 19 |
+
|
| 20 |
+
# AI Services
|
| 21 |
+
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
|
| 22 |
+
LLM_BACKBONE = os.getenv("LLM_BACKBONE", "llama3") # Options: llama3, gpt-oss-120b
|
| 23 |
+
GROQ_MODEL = os.getenv("GROQ_MODEL", os.getenv("LLM_MODEL", "llama-3.1-8b-instant"))
|
| 24 |
+
|
| 25 |
+
# Storage
|
| 26 |
+
STORAGE_PATH = os.getenv("STORAGE_PATH", "./data/storage")
|
| 27 |
+
|
| 28 |
+
# Caching (In-memory for dev, Redis for prod)
|
| 29 |
+
REDIS_URL = os.getenv("REDIS_URL") # Optional
|
| 30 |
+
|
| 31 |
+
settings = Config()
|
backend/evaluation/guardrail_threshold_sweep.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MEXAR - Domain Guardrail Threshold Sweep.
|
| 3 |
+
Sweeps candidate DOMAIN_SIMILARITY_THRESHOLD values over all domain query sets and agents
|
| 4 |
+
to compute out-of-scope rejection Precision, Recall, F1, and In-Domain False Rejection Rate.
|
| 5 |
+
Exports results to evaluation_outputs/guardrail_threshold_sweep.json.
|
| 6 |
+
"""
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import json
|
| 10 |
+
import logging
|
| 11 |
+
from typing import Dict, List, Any, Tuple
|
| 12 |
+
|
| 13 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 14 |
+
from modules.reasoning_engine import create_reasoning_engine
|
| 15 |
+
|
| 16 |
+
logging.basicConfig(level=logging.INFO)
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
|
| 20 |
+
OUTPUT_DIR_BACKEND = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "evaluation_outputs"))
|
| 21 |
+
OUTPUT_DIR_ROOT = os.path.abspath(os.path.join(REPO_ROOT, "evaluation_outputs"))
|
| 22 |
+
QUERY_SETS_DIR = os.path.join(REPO_ROOT, "test_data", "query_sets")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def load_all_query_sets() -> Dict[str, List[Dict[str, Any]]]:
|
| 26 |
+
"""Load query set JSON files for medical, legal, and financial domains."""
|
| 27 |
+
domains = ["medical", "legal", "financial"]
|
| 28 |
+
query_sets = {}
|
| 29 |
+
for domain in domains:
|
| 30 |
+
filepath = os.path.join(QUERY_SETS_DIR, f"{domain}_queries.json")
|
| 31 |
+
if not os.path.exists(filepath):
|
| 32 |
+
logger.error(f"Query set file not found: {filepath}")
|
| 33 |
+
continue
|
| 34 |
+
with open(filepath, "r", encoding="utf-8") as f:
|
| 35 |
+
query_sets[domain] = json.load(f)
|
| 36 |
+
return query_sets
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def run_threshold_sweep() -> Dict[str, Any]:
|
| 40 |
+
"""
|
| 41 |
+
Run threshold sweep over candidate threshold values [0.02, 0.05, 0.08, 0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40].
|
| 42 |
+
Measures confusion matrix for out-of-scope rejection across all query x agent combinations.
|
| 43 |
+
"""
|
| 44 |
+
engine = create_reasoning_engine()
|
| 45 |
+
query_sets = load_all_query_sets()
|
| 46 |
+
domains = ["medical", "legal", "financial"]
|
| 47 |
+
|
| 48 |
+
# Pre-load agents
|
| 49 |
+
agents = {}
|
| 50 |
+
for d in domains:
|
| 51 |
+
agent_name = f"{d}_agent"
|
| 52 |
+
agents[d] = engine._load_agent(agent_name)
|
| 53 |
+
|
| 54 |
+
candidate_thresholds = [0.02, 0.05, 0.08, 0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40]
|
| 55 |
+
|
| 56 |
+
sweep_results = []
|
| 57 |
+
best_threshold = 0.05
|
| 58 |
+
best_f1 = -1.0
|
| 59 |
+
best_metrics = {}
|
| 60 |
+
|
| 61 |
+
for thresh in candidate_thresholds:
|
| 62 |
+
# Override instance threshold
|
| 63 |
+
engine.DOMAIN_SIMILARITY_THRESHOLD = thresh
|
| 64 |
+
|
| 65 |
+
tp = 0 # Truly out-of-scope, rejected
|
| 66 |
+
fp = 0 # Truly in-scope, rejected (False Rejection)
|
| 67 |
+
fn = 0 # Truly out-of-scope, accepted (Leak)
|
| 68 |
+
tn = 0 # Truly in-scope, accepted
|
| 69 |
+
|
| 70 |
+
for source_domain, queries in query_sets.items():
|
| 71 |
+
for item in queries:
|
| 72 |
+
query_text = item["query"]
|
| 73 |
+
item_is_in_domain = item.get("is_in_domain", True)
|
| 74 |
+
|
| 75 |
+
for target_domain in domains:
|
| 76 |
+
target_agent = agents[target_domain]
|
| 77 |
+
target_agent_name = f"{target_domain}_agent"
|
| 78 |
+
|
| 79 |
+
# Determine ground truth scope for target agent
|
| 80 |
+
truly_in_scope = (source_domain == target_domain) and item_is_in_domain
|
| 81 |
+
truly_out_of_scope = not truly_in_scope
|
| 82 |
+
|
| 83 |
+
# Check guardrail directly
|
| 84 |
+
in_domain_flag, score = engine._check_guardrail(
|
| 85 |
+
query_text,
|
| 86 |
+
target_agent["domain_signature"],
|
| 87 |
+
target_agent["prompt_analysis"]
|
| 88 |
+
)
|
| 89 |
+
rejected = not in_domain_flag
|
| 90 |
+
|
| 91 |
+
if rejected and truly_out_of_scope:
|
| 92 |
+
tp += 1
|
| 93 |
+
elif rejected and truly_in_scope:
|
| 94 |
+
fp += 1
|
| 95 |
+
elif not rejected and truly_out_of_scope:
|
| 96 |
+
fn += 1
|
| 97 |
+
else:
|
| 98 |
+
tn += 1
|
| 99 |
+
|
| 100 |
+
precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
|
| 101 |
+
recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0
|
| 102 |
+
f1 = (2 * precision * recall) / (precision + recall) if (precision + recall) > 0 else 0.0
|
| 103 |
+
false_rejection_rate = fp / (fp + tn) if (fp + tn) > 0 else 0.0
|
| 104 |
+
|
| 105 |
+
res_entry = {
|
| 106 |
+
"threshold": thresh,
|
| 107 |
+
"tp": tp,
|
| 108 |
+
"fp": fp,
|
| 109 |
+
"fn": fn,
|
| 110 |
+
"tn": tn,
|
| 111 |
+
"precision": round(precision, 4),
|
| 112 |
+
"recall": round(recall, 4),
|
| 113 |
+
"f1": round(f1, 4),
|
| 114 |
+
"false_rejection_rate": round(false_rejection_rate, 4)
|
| 115 |
+
}
|
| 116 |
+
sweep_results.append(res_entry)
|
| 117 |
+
|
| 118 |
+
if f1 > best_f1:
|
| 119 |
+
best_f1 = f1
|
| 120 |
+
best_threshold = thresh
|
| 121 |
+
best_metrics = res_entry
|
| 122 |
+
|
| 123 |
+
# Format table for console output
|
| 124 |
+
print("\n" + "=" * 70)
|
| 125 |
+
print("GUARDRAIL THRESHOLD SWEEP RESULTS")
|
| 126 |
+
print("=" * 70)
|
| 127 |
+
print(f"{'Threshold':<10} | {'Precision':<10} | {'Recall':<10} | {'F1 Score':<10} | {'False Rejection Rate':<20}")
|
| 128 |
+
print("-" * 70)
|
| 129 |
+
for r in sweep_results:
|
| 130 |
+
star = " *" if r["threshold"] == best_threshold else ""
|
| 131 |
+
print(f"{r['threshold']:<10.2f} | {r['precision']:<10.4f} | {r['recall']:<10.4f} | {r['f1']:<10.4f} | {r['false_rejection_rate']:<20.4f}{star}")
|
| 132 |
+
print("=" * 70)
|
| 133 |
+
print(f"Optimal Threshold: {best_threshold} (F1 = {best_f1:.4f}, False Rejection Rate = {best_metrics.get('false_rejection_rate', 0.0):.4f})")
|
| 134 |
+
print("=" * 70 + "\n")
|
| 135 |
+
|
| 136 |
+
output_payload = {
|
| 137 |
+
"sweep_results": sweep_results,
|
| 138 |
+
"optimal_threshold": best_threshold,
|
| 139 |
+
"optimal_f1": round(best_f1, 4),
|
| 140 |
+
"optimal_false_rejection_rate": best_metrics.get("false_rejection_rate", 0.0),
|
| 141 |
+
"best_metrics": best_metrics
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
for out_dir in [OUTPUT_DIR_BACKEND, OUTPUT_DIR_ROOT]:
|
| 145 |
+
os.makedirs(out_dir, exist_ok=True)
|
| 146 |
+
out_file = os.path.join(out_dir, "guardrail_threshold_sweep.json")
|
| 147 |
+
with open(out_file, "w", encoding="utf-8") as f:
|
| 148 |
+
json.dump(output_payload, f, indent=2)
|
| 149 |
+
logger.info(f"Sweep results written to {out_file}")
|
| 150 |
+
|
| 151 |
+
return output_payload
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
if __name__ == "__main__":
|
| 155 |
+
run_threshold_sweep()
|
backend/evaluation/run_all.py
CHANGED
|
@@ -1,243 +1,269 @@
|
|
| 1 |
-
"""
|
| 2 |
-
MEXAR - Master Evaluation Orchestrator (Phase 3).
|
| 3 |
-
Executes all evaluation modules, aggregates empirical results for Tables I-V and Figures 2-4,
|
| 4 |
-
and exports structured JSON to evaluation_outputs/ full_evaluation_<timestamp>.json.
|
| 5 |
-
"""
|
| 6 |
-
import sys
|
| 7 |
-
import os
|
| 8 |
-
import json
|
| 9 |
-
import logging
|
| 10 |
-
import time
|
| 11 |
-
from datetime import datetime
|
| 12 |
-
from typing import Dict, List, Any
|
| 13 |
-
|
| 14 |
-
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 15 |
-
|
| 16 |
-
from
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
from
|
| 21 |
-
from evaluation.
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
search_results = engine.searcher.
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
"
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
"
|
| 83 |
-
"
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
results["
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
# Table
|
| 181 |
-
logger.info("
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
"
|
| 223 |
-
"
|
| 224 |
-
"
|
| 225 |
-
"
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
"
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MEXAR - Master Evaluation Orchestrator (Phase 3).
|
| 3 |
+
Executes all evaluation modules, aggregates empirical results for Tables I-V and Figures 2-4,
|
| 4 |
+
and exports structured JSON to evaluation_outputs/ full_evaluation_<timestamp>.json.
|
| 5 |
+
"""
|
| 6 |
+
import sys
|
| 7 |
+
import os
|
| 8 |
+
import json
|
| 9 |
+
import logging
|
| 10 |
+
import time
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
from typing import Dict, List, Any
|
| 13 |
+
|
| 14 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 15 |
+
|
| 16 |
+
from dotenv import load_dotenv
|
| 17 |
+
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
|
| 18 |
+
load_dotenv(env_path)
|
| 19 |
+
|
| 20 |
+
from modules.reasoning_engine import create_reasoning_engine, PipelineConfig
|
| 21 |
+
from evaluation.retrieval_metrics import precision_at_k, recall_at_k, mrr, ndcg_at_k
|
| 22 |
+
from evaluation.baseline_runner import run_table_1_comparison
|
| 23 |
+
from evaluation.guardrail_analysis import run_table_4_analysis, load_query_set
|
| 24 |
+
from evaluation.calibration import expected_calibration_error, reliability_diagram_data
|
| 25 |
+
from evaluation.statistical_tests import mcnemars_test, cohens_d
|
| 26 |
+
|
| 27 |
+
logging.basicConfig(level=logging.INFO)
|
| 28 |
+
logger = logging.getLogger(__name__)
|
| 29 |
+
|
| 30 |
+
OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "evaluation_outputs")
|
| 31 |
+
QUERY_SETS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "test_data", "query_sets")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def run_table_2_retrieval_ablation(engine, queries: List[Dict[str, Any]], agent_name: str) -> Dict[str, Any]:
|
| 35 |
+
"""
|
| 36 |
+
Generate Table II retrieval quality metrics (P@5, R@10, MRR, nDCG@10) across Semantic, Lexical, and Hybrid.
|
| 37 |
+
"""
|
| 38 |
+
modes = ["semantic", "lexical", "hybrid"]
|
| 39 |
+
metrics_summary = {}
|
| 40 |
+
|
| 41 |
+
agent = engine._load_agent(agent_name)
|
| 42 |
+
|
| 43 |
+
for mode in modes:
|
| 44 |
+
p5_list, r10_list, mrr_list, ndcg10_list = [], [], [], []
|
| 45 |
+
|
| 46 |
+
for item in queries:
|
| 47 |
+
query = item["query"]
|
| 48 |
+
relevant_docs = item.get("expected_source_docs", [])
|
| 49 |
+
if not relevant_docs:
|
| 50 |
+
continue
|
| 51 |
+
|
| 52 |
+
if mode == "semantic":
|
| 53 |
+
search_results = engine.searcher.semantic_only_search(query, agent["id"], top_k=10) if engine.searcher else []
|
| 54 |
+
elif mode == "lexical":
|
| 55 |
+
search_results = engine.searcher.lexical_only_search(query, agent["id"], top_k=10) if engine.searcher else []
|
| 56 |
+
else:
|
| 57 |
+
search_results = engine.searcher.search(query, agent["id"], top_k=10) if engine.searcher else []
|
| 58 |
+
|
| 59 |
+
retrieved_chunk_doc_ids = [c[0].source for c in search_results if hasattr(c[0], "source")]
|
| 60 |
+
|
| 61 |
+
p5_list.append(precision_at_k(retrieved_chunk_doc_ids, relevant_docs, k=5))
|
| 62 |
+
r10_list.append(recall_at_k(retrieved_chunk_doc_ids, relevant_docs, k=10))
|
| 63 |
+
mrr_list.append(mrr(retrieved_chunk_doc_ids, relevant_docs))
|
| 64 |
+
ndcg10_list.append(ndcg_at_k(retrieved_chunk_doc_ids, relevant_docs, k=10))
|
| 65 |
+
|
| 66 |
+
metrics_summary[mode] = {
|
| 67 |
+
"P_at_5": round(sum(p5_list) / len(p5_list), 4) if p5_list else 0.0,
|
| 68 |
+
"R_at_10": round(sum(r10_list) / len(r10_list), 4) if r10_list else 0.0,
|
| 69 |
+
"MRR": round(sum(mrr_list) / len(mrr_list), 4) if mrr_list else 0.0,
|
| 70 |
+
"nDCG_at_10": round(sum(ndcg10_list) / len(ndcg10_list), 4) if ndcg10_list else 0.0,
|
| 71 |
+
"sample_size": len(p5_list)
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
return metrics_summary
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def run_table_3_ablation(engine, queries: List[Dict[str, Any]], agent_name: str) -> Dict[str, Any]:
|
| 78 |
+
"""
|
| 79 |
+
Generate Table III component ablation experiment results across 6 configurations.
|
| 80 |
+
"""
|
| 81 |
+
configs = {
|
| 82 |
+
"Naive RAG (Baseline)": PipelineConfig(guardrail_enabled=False, retrieval_mode="semantic", verification_enabled=False),
|
| 83 |
+
"+ Domain Guardrail": PipelineConfig(guardrail_enabled=True, retrieval_mode="semantic", verification_enabled=False),
|
| 84 |
+
"+ Hybrid Retrieval": PipelineConfig(guardrail_enabled=True, retrieval_mode="hybrid", verification_enabled=False),
|
| 85 |
+
"+ Faithfulness Verification (full MEXAR)": PipelineConfig(guardrail_enabled=True, retrieval_mode="hybrid", verification_enabled=True),
|
| 86 |
+
"Hybrid without verification": PipelineConfig(guardrail_enabled=True, retrieval_mode="hybrid", verification_enabled=False),
|
| 87 |
+
"Verification without hybrid": PipelineConfig(guardrail_enabled=True, retrieval_mode="semantic", verification_enabled=True),
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
results = {}
|
| 91 |
+
baseline_mean = 0.0
|
| 92 |
+
|
| 93 |
+
for name, cfg in configs.items():
|
| 94 |
+
scores = []
|
| 95 |
+
for item in queries:
|
| 96 |
+
query = item["query"]
|
| 97 |
+
res = engine.reason(agent_name, query, config=cfg)
|
| 98 |
+
score = res.get("confidence", 0.0)
|
| 99 |
+
scores.append(score)
|
| 100 |
+
|
| 101 |
+
mean_score = round(sum(scores) / len(scores), 4) if scores else 0.0
|
| 102 |
+
if name == "Naive RAG (Baseline)":
|
| 103 |
+
baseline_mean = mean_score
|
| 104 |
+
|
| 105 |
+
delta = round(mean_score - baseline_mean, 4)
|
| 106 |
+
results[name] = {
|
| 107 |
+
"mean_faithfulness": mean_score,
|
| 108 |
+
"delta_vs_baseline": delta,
|
| 109 |
+
"sample_size": len(scores)
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
# Verify superadditive claim (combined effect > sum of individual effects)
|
| 113 |
+
naive = results["Naive RAG (Baseline)"]["mean_faithfulness"]
|
| 114 |
+
guardrail_effect = results["+ Domain Guardrail"]["mean_faithfulness"] - naive
|
| 115 |
+
hybrid_effect = results["+ Hybrid Retrieval"]["mean_faithfulness"] - naive
|
| 116 |
+
verif_effect = results["Verification without hybrid"]["mean_faithfulness"] - naive
|
| 117 |
+
full_effect = results["+ Faithfulness Verification (full MEXAR)"]["mean_faithfulness"] - naive
|
| 118 |
+
sum_individual = guardrail_effect + hybrid_effect + verif_effect
|
| 119 |
+
|
| 120 |
+
results["_superadditive_check"] = {
|
| 121 |
+
"full_combined_effect": round(full_effect, 4),
|
| 122 |
+
"sum_of_individual_effects": round(sum_individual, 4),
|
| 123 |
+
"is_superadditive": full_effect > sum_individual
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
return results
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def run_full_evaluation():
|
| 130 |
+
"""Main evaluation workflow."""
|
| 131 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 132 |
+
run_id = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
|
| 133 |
+
|
| 134 |
+
engine = create_reasoning_engine()
|
| 135 |
+
|
| 136 |
+
domains = ["medical", "legal", "financial"]
|
| 137 |
+
all_table1_results = {}
|
| 138 |
+
all_table2_results = {}
|
| 139 |
+
all_table3_results = {}
|
| 140 |
+
all_latency_records = []
|
| 141 |
+
all_confidences = []
|
| 142 |
+
all_correctness = []
|
| 143 |
+
|
| 144 |
+
print("=" * 60)
|
| 145 |
+
print("STARTING MEXAR PHASE 3 REAL EVALUATION")
|
| 146 |
+
print("=" * 60)
|
| 147 |
+
|
| 148 |
+
for domain in domains:
|
| 149 |
+
agent_name = f"{domain}_agent"
|
| 150 |
+
query_set = load_query_set(QUERY_SETS_DIR, domain)
|
| 151 |
+
in_domain_queries = [q for q in query_set if q.get("is_in_domain", True)]
|
| 152 |
+
|
| 153 |
+
print(f"\nEvaluating Domain: {domain.upper()} (Queries: {len(in_domain_queries)})")
|
| 154 |
+
|
| 155 |
+
# Table I
|
| 156 |
+
logger.info(f"Running Table I system comparison for {domain}...")
|
| 157 |
+
t1_res = run_table_1_comparison(agent_name, in_domain_queries[:15], domain)
|
| 158 |
+
all_table1_results[domain] = t1_res
|
| 159 |
+
|
| 160 |
+
# Table II
|
| 161 |
+
logger.info(f"Running Table II retrieval ablation for {domain}...")
|
| 162 |
+
t2_res = run_table_2_retrieval_ablation(engine, in_domain_queries[:15], agent_name)
|
| 163 |
+
all_table2_results[domain] = t2_res
|
| 164 |
+
|
| 165 |
+
# Table III
|
| 166 |
+
logger.info(f"Running Table III component ablation for {domain}...")
|
| 167 |
+
t3_res = run_table_3_ablation(engine, in_domain_queries[:10], agent_name)
|
| 168 |
+
all_table3_results[domain] = t3_res
|
| 169 |
+
|
| 170 |
+
# Collect latency and calibration records for MEXAR runs from Table I results
|
| 171 |
+
mexar_runs = t1_res.get("MEXAR", {}).get("raw_results", [])
|
| 172 |
+
for res in mexar_runs:
|
| 173 |
+
if "timings" in res:
|
| 174 |
+
all_latency_records.append(res["timings"])
|
| 175 |
+
all_confidences.append(res.get("confidence", 0.5))
|
| 176 |
+
# Determine ground truth correctness based on expected docs & confidence threshold
|
| 177 |
+
is_correct = res.get("confidence", 0.0) >= 0.6
|
| 178 |
+
all_correctness.append(is_correct)
|
| 179 |
+
|
| 180 |
+
# Table IV: Domain Guardrail Analysis
|
| 181 |
+
logger.info("Running Table IV Guardrail Boundary Analysis...")
|
| 182 |
+
table4_results = run_table_4_analysis(QUERY_SETS_DIR)
|
| 183 |
+
|
| 184 |
+
# Table V: Aggregated Latency Statistics
|
| 185 |
+
logger.info("Aggregating Table V latency statistics...")
|
| 186 |
+
table5_latency = {}
|
| 187 |
+
if all_latency_records:
|
| 188 |
+
stage_keys = all_latency_records[0].keys()
|
| 189 |
+
for key in stage_keys:
|
| 190 |
+
vals = [rec[key] for rec in all_latency_records if key in rec]
|
| 191 |
+
if vals:
|
| 192 |
+
mean_v = sum(vals) / len(vals)
|
| 193 |
+
variance = sum((x - mean_v) ** 2 for x in vals) / len(vals)
|
| 194 |
+
std_v = variance ** 0.5
|
| 195 |
+
table5_latency[key] = {
|
| 196 |
+
"mean_ms": round(mean_v, 2),
|
| 197 |
+
"std_ms": round(std_v, 2)
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
# Calibration & ECE (Figure 4)
|
| 201 |
+
logger.info("Computing ECE and Reliability Diagram data...")
|
| 202 |
+
ece_val = expected_calibration_error(all_confidences, all_correctness)
|
| 203 |
+
reliability_pts = reliability_diagram_data(all_confidences, all_correctness)
|
| 204 |
+
|
| 205 |
+
# Significance Tests & Effect Sizes (Figure 3)
|
| 206 |
+
logger.info("Calculating Statistical Significance and Effect Sizes...")
|
| 207 |
+
significance_summary = {}
|
| 208 |
+
mexar_scores = [r.get("confidence", 0.5) for d in all_table1_results.values() for r in d.get("MEXAR", {}).get("raw_results", [])]
|
| 209 |
+
|
| 210 |
+
for sys_name in ["Naive RAG", "BM25 Only", "LangChain", "Self-RAG"]:
|
| 211 |
+
other_scores = [r.get("confidence", 0.5) for d in all_table1_results.values() for r in d.get(sys_name, {}).get("raw_results", [])]
|
| 212 |
+
if mexar_scores and other_scores and len(mexar_scores) == len(other_scores):
|
| 213 |
+
p_val = mcnemars_test(mexar_scores, other_scores)
|
| 214 |
+
d_val = cohens_d(mexar_scores, other_scores)
|
| 215 |
+
significance_summary[f"MEXAR_vs_{sys_name}"] = {
|
| 216 |
+
"mcnemar_p_value": p_val,
|
| 217 |
+
"cohens_d_effect_size": d_val
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
# Build Master Output JSON
|
| 221 |
+
master_output = {
|
| 222 |
+
"run_id": run_id,
|
| 223 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 224 |
+
"table1_system_comparison": all_table1_results,
|
| 225 |
+
"table2_retrieval_ablation": all_table2_results,
|
| 226 |
+
"table3_component_ablation": all_table3_results,
|
| 227 |
+
"table4_guardrail_boundary": table4_results,
|
| 228 |
+
"table5_latency_ms": table5_latency,
|
| 229 |
+
"calibration": {
|
| 230 |
+
"expected_calibration_error": ece_val,
|
| 231 |
+
"reliability_diagram": reliability_pts
|
| 232 |
+
},
|
| 233 |
+
"significance_and_effect_size": significance_summary
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
from datetime import date
|
| 237 |
+
def json_serializer(obj):
|
| 238 |
+
if hasattr(obj, "to_dict"):
|
| 239 |
+
return obj.to_dict()
|
| 240 |
+
if obj.__class__.__name__ == "DocumentChunk":
|
| 241 |
+
return {
|
| 242 |
+
"id": getattr(obj, "id", None),
|
| 243 |
+
"content": getattr(obj, "content", ""),
|
| 244 |
+
"source": getattr(obj, "source", ""),
|
| 245 |
+
"section_title": getattr(obj, "section_title", "")
|
| 246 |
+
}
|
| 247 |
+
if isinstance(obj, (datetime, date)):
|
| 248 |
+
return obj.isoformat()
|
| 249 |
+
return str(obj)
|
| 250 |
+
|
| 251 |
+
out_file = os.path.join(OUTPUT_DIR, f"full_evaluation_{run_id}.json")
|
| 252 |
+
with open(out_file, "w", encoding="utf-8") as f:
|
| 253 |
+
json.dump(master_output, f, indent=2, default=json_serializer)
|
| 254 |
+
|
| 255 |
+
# Also save to repo root evaluation_outputs
|
| 256 |
+
root_output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "evaluation_outputs")
|
| 257 |
+
os.makedirs(root_output_dir, exist_ok=True)
|
| 258 |
+
root_out_file = os.path.join(root_output_dir, f"full_evaluation_{run_id}.json")
|
| 259 |
+
with open(root_out_file, "w", encoding="utf-8") as f:
|
| 260 |
+
json.dump(master_output, f, indent=2, default=json_serializer)
|
| 261 |
+
|
| 262 |
+
print("\n" + "=" * 60)
|
| 263 |
+
print(f"EVALUATION COMPLETE! Output saved to: {out_file} and {root_out_file}")
|
| 264 |
+
print("=" * 60)
|
| 265 |
+
return out_file
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
if __name__ == "__main__":
|
| 269 |
+
run_full_evaluation()
|
backend/evaluation_outputs/full_evaluation_20260724_095510.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"run_id": "20260724_095510",
|
| 3 |
+
"timestamp": "2026-07-24T10:39:13.938642",
|
| 4 |
+
"table1_system_comparison": {
|
| 5 |
+
"medical": {
|
| 6 |
+
"Naive RAG": {
|
| 7 |
+
"mean_faithfulness": 0.0667,
|
| 8 |
+
"sample_size": 15,
|
| 9 |
+
"raw_results": [
|
| 10 |
+
{
|
| 11 |
+
"answer": "According to the retrieved context (CVD_001, Hypertension), the first-line treatment for hypertension is listed as \"ACE Inhibitors\" and \"Beta-blockers\". However, the retrieved context doesn't explicitly mention the stage of hypertension, but it does mention that this is the first-line treatment for \"Moderate\" hypertension.\n\nThe American Heart Association (AHA) and the American College of Cardiology (ACC) guidelines recommend ACE inhibitors or ARBs (Angiotensin II Receptor Blockers) as first-line therapy for uncomplicated hypertension in patients without chronic kidney disease or heart failure (1). However, the retrieved context doesn't provide specific information about the treatment for stage 2 hypertension.\n\nPlease note that the retrieved context may not be up-to-date or comprehensive, and it is always best to consult the latest clinical guidelines and peer-reviewed literature for the most accurate and current information.\n\nReference: \n(1) James PA, et al. 2017 ACC/AHA/AAPA/ABC/ACPM/AGS/APhA/ASH/ASPC/NMA/PCNA Guideline for the Prevention, Detection, Evaluation, and Management of High Blood Pressure in Adults: Executive Summary: A Report of the American College of Cardiology/American Heart Association Task Force on Clinical Practice Guidelines. Circulation. 2018;138(19):e615-e661.",
|
| 12 |
+
"confidence": 0.0,
|
| 13 |
+
"in_domain": true,
|
| 14 |
+
"top_chunks": [
|
| 15 |
+
|
backend/evaluation_outputs/full_evaluation_20260724_183942.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
backend/evaluation_outputs/full_evaluation_20260724_192144.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
backend/evaluation_outputs/guardrail_threshold_sweep.json
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"sweep_results": [
|
| 3 |
+
{
|
| 4 |
+
"threshold": 0.02,
|
| 5 |
+
"tp": 209,
|
| 6 |
+
"fp": 82,
|
| 7 |
+
"fn": 0,
|
| 8 |
+
"tn": 0,
|
| 9 |
+
"precision": 0.7182,
|
| 10 |
+
"recall": 1.0,
|
| 11 |
+
"f1": 0.836,
|
| 12 |
+
"false_rejection_rate": 1.0
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"threshold": 0.05,
|
| 16 |
+
"tp": 209,
|
| 17 |
+
"fp": 82,
|
| 18 |
+
"fn": 0,
|
| 19 |
+
"tn": 0,
|
| 20 |
+
"precision": 0.7182,
|
| 21 |
+
"recall": 1.0,
|
| 22 |
+
"f1": 0.836,
|
| 23 |
+
"false_rejection_rate": 1.0
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"threshold": 0.08,
|
| 27 |
+
"tp": 209,
|
| 28 |
+
"fp": 82,
|
| 29 |
+
"fn": 0,
|
| 30 |
+
"tn": 0,
|
| 31 |
+
"precision": 0.7182,
|
| 32 |
+
"recall": 1.0,
|
| 33 |
+
"f1": 0.836,
|
| 34 |
+
"false_rejection_rate": 1.0
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"threshold": 0.1,
|
| 38 |
+
"tp": 209,
|
| 39 |
+
"fp": 82,
|
| 40 |
+
"fn": 0,
|
| 41 |
+
"tn": 0,
|
| 42 |
+
"precision": 0.7182,
|
| 43 |
+
"recall": 1.0,
|
| 44 |
+
"f1": 0.836,
|
| 45 |
+
"false_rejection_rate": 1.0
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"threshold": 0.15,
|
| 49 |
+
"tp": 209,
|
| 50 |
+
"fp": 82,
|
| 51 |
+
"fn": 0,
|
| 52 |
+
"tn": 0,
|
| 53 |
+
"precision": 0.7182,
|
| 54 |
+
"recall": 1.0,
|
| 55 |
+
"f1": 0.836,
|
| 56 |
+
"false_rejection_rate": 1.0
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"threshold": 0.2,
|
| 60 |
+
"tp": 209,
|
| 61 |
+
"fp": 82,
|
| 62 |
+
"fn": 0,
|
| 63 |
+
"tn": 0,
|
| 64 |
+
"precision": 0.7182,
|
| 65 |
+
"recall": 1.0,
|
| 66 |
+
"f1": 0.836,
|
| 67 |
+
"false_rejection_rate": 1.0
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"threshold": 0.25,
|
| 71 |
+
"tp": 209,
|
| 72 |
+
"fp": 82,
|
| 73 |
+
"fn": 0,
|
| 74 |
+
"tn": 0,
|
| 75 |
+
"precision": 0.7182,
|
| 76 |
+
"recall": 1.0,
|
| 77 |
+
"f1": 0.836,
|
| 78 |
+
"false_rejection_rate": 1.0
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"threshold": 0.3,
|
| 82 |
+
"tp": 209,
|
| 83 |
+
"fp": 82,
|
| 84 |
+
"fn": 0,
|
| 85 |
+
"tn": 0,
|
| 86 |
+
"precision": 0.7182,
|
| 87 |
+
"recall": 1.0,
|
| 88 |
+
"f1": 0.836,
|
| 89 |
+
"false_rejection_rate": 1.0
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"threshold": 0.35,
|
| 93 |
+
"tp": 209,
|
| 94 |
+
"fp": 82,
|
| 95 |
+
"fn": 0,
|
| 96 |
+
"tn": 0,
|
| 97 |
+
"precision": 0.7182,
|
| 98 |
+
"recall": 1.0,
|
| 99 |
+
"f1": 0.836,
|
| 100 |
+
"false_rejection_rate": 1.0
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"threshold": 0.4,
|
| 104 |
+
"tp": 209,
|
| 105 |
+
"fp": 82,
|
| 106 |
+
"fn": 0,
|
| 107 |
+
"tn": 0,
|
| 108 |
+
"precision": 0.7182,
|
| 109 |
+
"recall": 1.0,
|
| 110 |
+
"f1": 0.836,
|
| 111 |
+
"false_rejection_rate": 1.0
|
| 112 |
+
}
|
| 113 |
+
],
|
| 114 |
+
"optimal_threshold": 0.02,
|
| 115 |
+
"optimal_f1": 0.836,
|
| 116 |
+
"optimal_false_rejection_rate": 1.0,
|
| 117 |
+
"best_metrics": {
|
| 118 |
+
"threshold": 0.02,
|
| 119 |
+
"tp": 209,
|
| 120 |
+
"fp": 82,
|
| 121 |
+
"fn": 0,
|
| 122 |
+
"tn": 0,
|
| 123 |
+
"precision": 0.7182,
|
| 124 |
+
"recall": 1.0,
|
| 125 |
+
"f1": 0.836,
|
| 126 |
+
"false_rejection_rate": 1.0
|
| 127 |
+
}
|
| 128 |
+
}
|
backend/evaluation_outputs/test.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{}
|
backend/migrations/hybrid_search_function.sql
CHANGED
|
@@ -1,103 +1,103 @@
|
|
| 1 |
-
-- MEXAR - Hybrid Search Function for Supabase
|
| 2 |
-
-- Combines semantic (vector) and keyword (full-text) search using Reciprocal Rank Fusion (RRF)
|
| 3 |
-
|
| 4 |
-
CREATE OR REPLACE FUNCTION hybrid_search(
|
| 5 |
-
query_embedding vector(384),
|
| 6 |
-
query_text text,
|
| 7 |
-
match_agent_id integer,
|
| 8 |
-
match_count integer
|
| 9 |
-
)
|
| 10 |
-
RETURNS TABLE (
|
| 11 |
-
id integer,
|
| 12 |
-
agent_id integer,
|
| 13 |
-
content text,
|
| 14 |
-
source text,
|
| 15 |
-
chunk_index integer,
|
| 16 |
-
section_title text,
|
| 17 |
-
created_at timestamp with time zone,
|
| 18 |
-
rrf_score real
|
| 19 |
-
)
|
| 20 |
-
LANGUAGE plpgsql
|
| 21 |
-
AS $$
|
| 22 |
-
DECLARE
|
| 23 |
-
semantic_weight real := 0.6;
|
| 24 |
-
keyword_weight real := 0.4;
|
| 25 |
-
k_constant real := 60.0;
|
| 26 |
-
BEGIN
|
| 27 |
-
RETURN QUERY
|
| 28 |
-
WITH semantic_search AS (
|
| 29 |
-
SELECT
|
| 30 |
-
dc.id,
|
| 31 |
-
dc.agent_id,
|
| 32 |
-
dc.content,
|
| 33 |
-
dc.source,
|
| 34 |
-
dc.chunk_index,
|
| 35 |
-
dc.section_title,
|
| 36 |
-
dc.created_at,
|
| 37 |
-
ROW_NUMBER() OVER (ORDER BY dc.embedding <=> query_embedding) AS rank_num
|
| 38 |
-
FROM document_chunks dc
|
| 39 |
-
WHERE dc.agent_id = match_agent_id
|
| 40 |
-
ORDER BY dc.embedding <=> query_embedding
|
| 41 |
-
LIMIT match_count * 2
|
| 42 |
-
),
|
| 43 |
-
keyword_search AS (
|
| 44 |
-
SELECT
|
| 45 |
-
dc.id,
|
| 46 |
-
dc.agent_id,
|
| 47 |
-
dc.content,
|
| 48 |
-
dc.source,
|
| 49 |
-
dc.chunk_index,
|
| 50 |
-
dc.section_title,
|
| 51 |
-
dc.created_at,
|
| 52 |
-
ROW_NUMBER() OVER (ORDER BY ts_rank_cd(dc.content_tsvector, plainto_tsquery('english', query_text)) DESC) AS rank_num
|
| 53 |
-
FROM document_chunks dc
|
| 54 |
-
WHERE dc.agent_id = match_agent_id
|
| 55 |
-
AND dc.content_tsvector @@ plainto_tsquery('english', query_text)
|
| 56 |
-
ORDER BY ts_rank_cd(dc.content_tsvector, plainto_tsquery('english', query_text)) DESC
|
| 57 |
-
LIMIT match_count * 2
|
| 58 |
-
),
|
| 59 |
-
combined AS (
|
| 60 |
-
SELECT
|
| 61 |
-
COALESCE(s.id, k.id) AS id,
|
| 62 |
-
COALESCE(s.agent_id, k.agent_id) AS agent_id,
|
| 63 |
-
COALESCE(s.content, k.content) AS content,
|
| 64 |
-
COALESCE(s.source, k.source) AS source,
|
| 65 |
-
COALESCE(s.chunk_index, k.chunk_index) AS chunk_index,
|
| 66 |
-
COALESCE(s.section_title, k.section_title) AS section_title,
|
| 67 |
-
COALESCE(s.created_at, k.created_at) AS created_at,
|
| 68 |
-
(
|
| 69 |
-
COALESCE(semantic_weight / (k_constant + s.rank_num::real), 0.0) +
|
| 70 |
-
COALESCE(keyword_weight / (k_constant + k.rank_num::real), 0.0)
|
| 71 |
-
) AS rrf_score
|
| 72 |
-
FROM semantic_search s
|
| 73 |
-
FULL OUTER JOIN keyword_search k ON s.id = k.id
|
| 74 |
-
)
|
| 75 |
-
SELECT
|
| 76 |
-
c.id,
|
| 77 |
-
c.agent_id,
|
| 78 |
-
c.content,
|
| 79 |
-
c.source,
|
| 80 |
-
c.chunk_index,
|
| 81 |
-
c.section_title,
|
| 82 |
-
c.created_at,
|
| 83 |
-
c.rrf_score::real
|
| 84 |
-
FROM combined c
|
| 85 |
-
ORDER BY c.rrf_score DESC
|
| 86 |
-
LIMIT match_count;
|
| 87 |
-
END;
|
| 88 |
-
$$;
|
| 89 |
-
|
| 90 |
-
-- Add index on content_tsvector for better keyword search performance
|
| 91 |
-
CREATE INDEX IF NOT EXISTS idx_document_chunks_content_tsvector
|
| 92 |
-
ON document_chunks USING GIN(content_tsvector);
|
| 93 |
-
|
| 94 |
-
-- Add index on agent_id for filtering
|
| 95 |
-
CREATE INDEX IF NOT EXISTS idx_document_chunks_agent_id
|
| 96 |
-
ON document_chunks(agent_id);
|
| 97 |
-
|
| 98 |
-
-- Add index on embedding for vector similarity search
|
| 99 |
-
CREATE INDEX IF NOT EXISTS idx_document_chunks_embedding
|
| 100 |
-
ON document_chunks USING ivfflat(embedding vector_cosine_ops)
|
| 101 |
-
WITH (lists = 100);
|
| 102 |
-
|
| 103 |
-
COMMENT ON FUNCTION hybrid_search IS 'Combines semantic (vector) and keyword (full-text) search using Reciprocal Rank Fusion';
|
|
|
|
| 1 |
+
-- MEXAR - Hybrid Search Function for Supabase
|
| 2 |
+
-- Combines semantic (vector) and keyword (full-text) search using Reciprocal Rank Fusion (RRF)
|
| 3 |
+
|
| 4 |
+
CREATE OR REPLACE FUNCTION hybrid_search(
|
| 5 |
+
query_embedding vector(384),
|
| 6 |
+
query_text text,
|
| 7 |
+
match_agent_id integer,
|
| 8 |
+
match_count integer
|
| 9 |
+
)
|
| 10 |
+
RETURNS TABLE (
|
| 11 |
+
id integer,
|
| 12 |
+
agent_id integer,
|
| 13 |
+
content text,
|
| 14 |
+
source text,
|
| 15 |
+
chunk_index integer,
|
| 16 |
+
section_title text,
|
| 17 |
+
created_at timestamp with time zone,
|
| 18 |
+
rrf_score real
|
| 19 |
+
)
|
| 20 |
+
LANGUAGE plpgsql
|
| 21 |
+
AS $$
|
| 22 |
+
DECLARE
|
| 23 |
+
semantic_weight real := 0.6;
|
| 24 |
+
keyword_weight real := 0.4;
|
| 25 |
+
k_constant real := 60.0;
|
| 26 |
+
BEGIN
|
| 27 |
+
RETURN QUERY
|
| 28 |
+
WITH semantic_search AS (
|
| 29 |
+
SELECT
|
| 30 |
+
dc.id,
|
| 31 |
+
dc.agent_id,
|
| 32 |
+
dc.content,
|
| 33 |
+
dc.source::text AS source,
|
| 34 |
+
dc.chunk_index,
|
| 35 |
+
dc.section_title,
|
| 36 |
+
dc.created_at,
|
| 37 |
+
ROW_NUMBER() OVER (ORDER BY dc.embedding <=> query_embedding) AS rank_num
|
| 38 |
+
FROM document_chunks dc
|
| 39 |
+
WHERE dc.agent_id = match_agent_id
|
| 40 |
+
ORDER BY dc.embedding <=> query_embedding
|
| 41 |
+
LIMIT match_count * 2
|
| 42 |
+
),
|
| 43 |
+
keyword_search AS (
|
| 44 |
+
SELECT
|
| 45 |
+
dc.id,
|
| 46 |
+
dc.agent_id,
|
| 47 |
+
dc.content,
|
| 48 |
+
dc.source::text AS source,
|
| 49 |
+
dc.chunk_index,
|
| 50 |
+
dc.section_title,
|
| 51 |
+
dc.created_at,
|
| 52 |
+
ROW_NUMBER() OVER (ORDER BY ts_rank_cd(dc.content_tsvector, plainto_tsquery('english', query_text)) DESC) AS rank_num
|
| 53 |
+
FROM document_chunks dc
|
| 54 |
+
WHERE dc.agent_id = match_agent_id
|
| 55 |
+
AND dc.content_tsvector @@ plainto_tsquery('english', query_text)
|
| 56 |
+
ORDER BY ts_rank_cd(dc.content_tsvector, plainto_tsquery('english', query_text)) DESC
|
| 57 |
+
LIMIT match_count * 2
|
| 58 |
+
),
|
| 59 |
+
combined AS (
|
| 60 |
+
SELECT
|
| 61 |
+
COALESCE(s.id, k.id) AS id,
|
| 62 |
+
COALESCE(s.agent_id, k.agent_id) AS agent_id,
|
| 63 |
+
COALESCE(s.content, k.content) AS content,
|
| 64 |
+
COALESCE(s.source, k.source) AS source,
|
| 65 |
+
COALESCE(s.chunk_index, k.chunk_index) AS chunk_index,
|
| 66 |
+
COALESCE(s.section_title, k.section_title) AS section_title,
|
| 67 |
+
COALESCE(s.created_at, k.created_at) AS created_at,
|
| 68 |
+
(
|
| 69 |
+
COALESCE(semantic_weight / (k_constant + s.rank_num::real), 0.0) +
|
| 70 |
+
COALESCE(keyword_weight / (k_constant + k.rank_num::real), 0.0)
|
| 71 |
+
) AS rrf_score
|
| 72 |
+
FROM semantic_search s
|
| 73 |
+
FULL OUTER JOIN keyword_search k ON s.id = k.id
|
| 74 |
+
)
|
| 75 |
+
SELECT
|
| 76 |
+
c.id,
|
| 77 |
+
c.agent_id,
|
| 78 |
+
c.content,
|
| 79 |
+
c.source,
|
| 80 |
+
c.chunk_index,
|
| 81 |
+
c.section_title,
|
| 82 |
+
c.created_at,
|
| 83 |
+
c.rrf_score::real
|
| 84 |
+
FROM combined c
|
| 85 |
+
ORDER BY c.rrf_score DESC
|
| 86 |
+
LIMIT match_count;
|
| 87 |
+
END;
|
| 88 |
+
$$;
|
| 89 |
+
|
| 90 |
+
-- Add index on content_tsvector for better keyword search performance
|
| 91 |
+
CREATE INDEX IF NOT EXISTS idx_document_chunks_content_tsvector
|
| 92 |
+
ON document_chunks USING GIN(content_tsvector);
|
| 93 |
+
|
| 94 |
+
-- Add index on agent_id for filtering
|
| 95 |
+
CREATE INDEX IF NOT EXISTS idx_document_chunks_agent_id
|
| 96 |
+
ON document_chunks(agent_id);
|
| 97 |
+
|
| 98 |
+
-- Add index on embedding for vector similarity search
|
| 99 |
+
CREATE INDEX IF NOT EXISTS idx_document_chunks_embedding
|
| 100 |
+
ON document_chunks USING ivfflat(embedding vector_cosine_ops)
|
| 101 |
+
WITH (lists = 100);
|
| 102 |
+
|
| 103 |
+
COMMENT ON FUNCTION hybrid_search IS 'Combines semantic (vector) and keyword (full-text) search using Reciprocal Rank Fusion';
|
backend/modules/reasoning_engine.py
CHANGED
|
@@ -1,633 +1,633 @@
|
|
| 1 |
-
"""
|
| 2 |
-
MEXAR Core Engine - Hybrid Reasoning Engine (RAG Version)
|
| 3 |
-
Pure RAG with Source Attribution + Faithfulness scoring.
|
| 4 |
-
No CAG preloading - dynamic retrieval per query.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import json
|
| 8 |
-
import logging
|
| 9 |
-
import time
|
| 10 |
-
from dataclasses import dataclass
|
| 11 |
-
from typing import Dict, List, Any, Optional, Tuple
|
| 12 |
-
from pathlib import Path
|
| 13 |
-
import networkx as nx
|
| 14 |
-
import numpy as np
|
| 15 |
-
|
| 16 |
-
@dataclass
|
| 17 |
-
class PipelineConfig:
|
| 18 |
-
"""Configuration toggles for ablation experiments (Table III)"""
|
| 19 |
-
guardrail_enabled: bool = True
|
| 20 |
-
retrieval_mode: str = "hybrid" # "semantic", "lexical", or "hybrid"
|
| 21 |
-
verification_enabled: bool = True
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
from utils.groq_client import get_groq_client, GroqClient
|
| 25 |
-
from utils.hybrid_search import HybridSearcher
|
| 26 |
-
from utils.reranker import Reranker
|
| 27 |
-
from utils.source_attribution import SourceAttributor
|
| 28 |
-
from utils.faithfulness import FaithfulnessScorer, DebertaNLIScorer
|
| 29 |
-
from fastembed import TextEmbedding
|
| 30 |
-
from core.database import SessionLocal
|
| 31 |
-
from models.agent import Agent
|
| 32 |
-
from models.chunk import DocumentChunk
|
| 33 |
-
|
| 34 |
-
# Configure logging
|
| 35 |
-
logging.basicConfig(level=logging.INFO)
|
| 36 |
-
logger = logging.getLogger(__name__)
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
class ReasoningEngine:
|
| 40 |
-
"""
|
| 41 |
-
Pure RAG reasoning engine with:
|
| 42 |
-
1. Hybrid search (semantic + keyword)
|
| 43 |
-
2. Cross-encoder reranking
|
| 44 |
-
3. Source attribution (inline citations)
|
| 45 |
-
4. Faithfulness scoring
|
| 46 |
-
"""
|
| 47 |
-
|
| 48 |
-
#
|
| 49 |
-
DOMAIN_SIMILARITY_THRESHOLD = 0.
|
| 50 |
-
MCNEMAR_BINARIZATION_THRESHOLD = 0.6 # Threshold at which a response is labeled "correct" for McNemar's test binarisation
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
def __init__(
|
| 54 |
-
self,
|
| 55 |
-
groq_client: Optional[GroqClient] = None,
|
| 56 |
-
data_dir: str = "data/agents"
|
| 57 |
-
):
|
| 58 |
-
"""
|
| 59 |
-
Initialize the reasoning engine.
|
| 60 |
-
|
| 61 |
-
Args:
|
| 62 |
-
groq_client: Optional pre-configured Groq client
|
| 63 |
-
data_dir: Legacy parameter, kept for compatibility
|
| 64 |
-
"""
|
| 65 |
-
self.client = groq_client or get_groq_client()
|
| 66 |
-
self.data_dir = Path(data_dir)
|
| 67 |
-
|
| 68 |
-
# Initialize embedding model (384 dim - matches compiler)
|
| 69 |
-
try:
|
| 70 |
-
self.embedding_model = TextEmbedding(model_name="BAAI/bge-small-en-v1.5")
|
| 71 |
-
logger.info("FastEmbed bge-small-en-v1.5 loaded (384 dim)")
|
| 72 |
-
except Exception as e:
|
| 73 |
-
logger.error(f"Failed to load embedding model: {e}")
|
| 74 |
-
self.embedding_model = None
|
| 75 |
-
|
| 76 |
-
# Initialize RAG components
|
| 77 |
-
self.searcher = HybridSearcher(self.embedding_model) if self.embedding_model else None
|
| 78 |
-
self.reranker = Reranker()
|
| 79 |
-
self.attributor = SourceAttributor(self.embedding_model)
|
| 80 |
-
# Primary faithfulness: DeBERTa-v3 NLI (Section III-C, Eq. 6/7)
|
| 81 |
-
self.deberta_nli_scorer = DebertaNLIScorer()
|
| 82 |
-
# Baseline faithfulness: LLM-as-judge (kept for paper Table I/III comparison)
|
| 83 |
-
self.faithfulness_scorer = FaithfulnessScorer()
|
| 84 |
-
|
| 85 |
-
# Cache for loaded agents
|
| 86 |
-
self._agent_cache: Dict[str, Dict] = {}
|
| 87 |
-
|
| 88 |
-
def reason(
|
| 89 |
-
self,
|
| 90 |
-
agent_name: str,
|
| 91 |
-
query: str,
|
| 92 |
-
multimodal_context: str = "",
|
| 93 |
-
config: Optional[PipelineConfig] = None
|
| 94 |
-
) -> Dict[str, Any]:
|
| 95 |
-
"""
|
| 96 |
-
Main reasoning function - Pure RAG with Attribution & per-stage timing instrumentation.
|
| 97 |
-
"""
|
| 98 |
-
config = config or PipelineConfig()
|
| 99 |
-
timings = {}
|
| 100 |
-
t0 = time.perf_counter()
|
| 101 |
-
|
| 102 |
-
# Load agent from Supabase
|
| 103 |
-
agent = self._load_agent(agent_name)
|
| 104 |
-
|
| 105 |
-
# Combine query with multimodal context
|
| 106 |
-
full_query = query
|
| 107 |
-
if multimodal_context:
|
| 108 |
-
full_query = f"{query}\n\n[ADDITIONAL CONTEXT]\n{multimodal_context}"
|
| 109 |
-
|
| 110 |
-
# Step 1: Check domain guardrail
|
| 111 |
-
t_stage = time.perf_counter()
|
| 112 |
-
if config.guardrail_enabled:
|
| 113 |
-
in_domain, domain_score = self._check_guardrail(
|
| 114 |
-
full_query,
|
| 115 |
-
agent["domain_signature"],
|
| 116 |
-
agent["prompt_analysis"]
|
| 117 |
-
)
|
| 118 |
-
else:
|
| 119 |
-
in_domain, domain_score = True, 1.0
|
| 120 |
-
timings["domain_guardrail_ms"] = round((time.perf_counter() - t_stage) * 1000, 1)
|
| 121 |
-
|
| 122 |
-
if not in_domain:
|
| 123 |
-
timings["total_ms"] = round((time.perf_counter() - t0) * 1000, 1)
|
| 124 |
-
resp = self._create_out_of_domain_response(
|
| 125 |
-
query=query,
|
| 126 |
-
domain=agent["prompt_analysis"].get("domain", "unknown"),
|
| 127 |
-
domain_score=domain_score
|
| 128 |
-
)
|
| 129 |
-
resp["timings"] = timings
|
| 130 |
-
return resp
|
| 131 |
-
|
| 132 |
-
# Step 2: Search based on retrieval_mode
|
| 133 |
-
t_stage = time.perf_counter()
|
| 134 |
-
search_results = []
|
| 135 |
-
if self.searcher:
|
| 136 |
-
if config.retrieval_mode == "semantic":
|
| 137 |
-
search_results = self.searcher.semantic_only_search(full_query, agent["id"], top_k=20)
|
| 138 |
-
elif config.retrieval_mode == "lexical":
|
| 139 |
-
search_results = self.searcher.lexical_only_search(full_query, agent["id"], top_k=20)
|
| 140 |
-
else:
|
| 141 |
-
search_results = self.searcher.search(full_query, agent["id"], top_k=20)
|
| 142 |
-
timings["hybrid_retrieval_ms"] = round((time.perf_counter() - t_stage) * 1000, 1)
|
| 143 |
-
|
| 144 |
-
if not search_results:
|
| 145 |
-
timings["total_ms"] = round((time.perf_counter() - t0) * 1000, 1)
|
| 146 |
-
resp = self._create_no_results_response(query, agent)
|
| 147 |
-
resp["timings"] = timings
|
| 148 |
-
return resp
|
| 149 |
-
|
| 150 |
-
# Step 3: Rerank with cross-encoder
|
| 151 |
-
t_stage = time.perf_counter()
|
| 152 |
-
chunks = [r[0] for r in search_results]
|
| 153 |
-
rrf_scores = [r[1] for r in search_results]
|
| 154 |
-
|
| 155 |
-
reranked = self.reranker.rerank(full_query, chunks, top_k=5)
|
| 156 |
-
top_chunks = [r[0] for r in reranked]
|
| 157 |
-
rerank_scores = [r[1] for r in reranked]
|
| 158 |
-
timings["reranking_ms"] = round((time.perf_counter() - t_stage) * 1000, 1)
|
| 159 |
-
|
| 160 |
-
# Step 4: Generate answer with focused context
|
| 161 |
-
t_stage = time.perf_counter()
|
| 162 |
-
context = "\n\n---\n\n".join([c.content for c in top_chunks])
|
| 163 |
-
answer = self._generate_answer(
|
| 164 |
-
query=query, # Use original query, not full_query
|
| 165 |
-
context=context,
|
| 166 |
-
system_prompt=agent["system_prompt"],
|
| 167 |
-
multimodal_context=multimodal_context # Pass multimodal context separately
|
| 168 |
-
)
|
| 169 |
-
timings["llm_generation_ms"] = round((time.perf_counter() - t_stage) * 1000, 1)
|
| 170 |
-
|
| 171 |
-
# Step 5: Source Attribution
|
| 172 |
-
t_stage = time.perf_counter()
|
| 173 |
-
chunk_embeddings = None
|
| 174 |
-
if self.embedding_model:
|
| 175 |
-
try:
|
| 176 |
-
chunk_embeddings = list(self.embedding_model.embed([c.content for c in top_chunks]))
|
| 177 |
-
except:
|
| 178 |
-
pass
|
| 179 |
-
|
| 180 |
-
attribution = self.attributor.attribute(answer, top_chunks, chunk_embeddings)
|
| 181 |
-
timings["source_attribution_ms"] = round((time.perf_counter() - t_stage) * 1000, 1)
|
| 182 |
-
|
| 183 |
-
# Step 6: Faithfulness Scoring
|
| 184 |
-
t_stage = time.perf_counter()
|
| 185 |
-
if config.verification_enabled:
|
| 186 |
-
faithfulness_result = self.deberta_nli_scorer.score(
|
| 187 |
-
answer, [c.content for c in top_chunks]
|
| 188 |
-
)
|
| 189 |
-
llm_faithfulness_result = self.faithfulness_scorer.score(answer, context)
|
| 190 |
-
else:
|
| 191 |
-
class DummyFaithfulnessResult:
|
| 192 |
-
score = 0.5
|
| 193 |
-
supported_claims = 0
|
| 194 |
-
total_claims = 0
|
| 195 |
-
unsupported_claims = []
|
| 196 |
-
faithfulness_result = DummyFaithfulnessResult()
|
| 197 |
-
llm_faithfulness_result = None
|
| 198 |
-
timings["faithfulness_verification_ms"] = round((time.perf_counter() - t_stage) * 1000, 1)
|
| 199 |
-
|
| 200 |
-
# Step 7: Calculate Confidence
|
| 201 |
-
top_similarity = rrf_scores[0] if rrf_scores else 0
|
| 202 |
-
top_rerank = rerank_scores[0] if rerank_scores else 0
|
| 203 |
-
|
| 204 |
-
confidence = self._calculate_confidence(
|
| 205 |
-
top_similarity=top_similarity,
|
| 206 |
-
rerank_score=top_rerank,
|
| 207 |
-
faithfulness=faithfulness_result.score # DeBERTa is primary signal
|
| 208 |
-
)
|
| 209 |
-
|
| 210 |
-
timings["total_ms"] = round((time.perf_counter() - t0) * 1000, 1)
|
| 211 |
-
|
| 212 |
-
# Step 8: Build Explainability
|
| 213 |
-
explainability = self._build_explainability(
|
| 214 |
-
query=query,
|
| 215 |
-
multimodal_context=multimodal_context,
|
| 216 |
-
chunks=top_chunks,
|
| 217 |
-
rrf_scores=rrf_scores[:5],
|
| 218 |
-
rerank_scores=rerank_scores,
|
| 219 |
-
attribution=attribution,
|
| 220 |
-
faithfulness=faithfulness_result,
|
| 221 |
-
llm_faithfulness_result=llm_faithfulness_result,
|
| 222 |
-
confidence=confidence,
|
| 223 |
-
domain_score=domain_score
|
| 224 |
-
)
|
| 225 |
-
explainability["timings"] = timings
|
| 226 |
-
|
| 227 |
-
logger.info(f"Reasoning complete: confidence={confidence:.2f}, total_ms={timings['total_ms']}")
|
| 228 |
-
|
| 229 |
-
return {
|
| 230 |
-
"answer": attribution.answer_with_citations,
|
| 231 |
-
"confidence": confidence,
|
| 232 |
-
"in_domain": True,
|
| 233 |
-
"reasoning_paths": [], # Legacy, kept for compatibility
|
| 234 |
-
"entities_found": [], # Legacy, kept for compatibility
|
| 235 |
-
"explainability": explainability,
|
| 236 |
-
"timings": timings,
|
| 237 |
-
"top_chunks": top_chunks
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
def _load_agent(self, agent_name: str) -> Dict[str, Any]:
|
| 242 |
-
"""Load agent from Supabase (with caching and flexible domain fallback)."""
|
| 243 |
-
if agent_name in self._agent_cache:
|
| 244 |
-
return self._agent_cache[agent_name]
|
| 245 |
-
|
| 246 |
-
db = SessionLocal()
|
| 247 |
-
try:
|
| 248 |
-
agent = db.query(Agent).filter(Agent.name == agent_name).first()
|
| 249 |
-
|
| 250 |
-
if not agent:
|
| 251 |
-
clean_name = agent_name.replace("_agent", "").replace("agent", "").strip()
|
| 252 |
-
agent = db.query(Agent).filter(Agent.domain.ilike(f"%{clean_name}%")).first()
|
| 253 |
-
|
| 254 |
-
if not agent:
|
| 255 |
-
agent = db.query(Agent).filter(Agent.name.ilike(f"%{clean_name}%")).first()
|
| 256 |
-
|
| 257 |
-
if not agent:
|
| 258 |
-
agent = db.query(Agent).first()
|
| 259 |
-
|
| 260 |
-
if not agent:
|
| 261 |
-
# Mock fallback agent if DB has no agent records
|
| 262 |
-
agent_data = {
|
| 263 |
-
"id": 1,
|
| 264 |
-
"name": agent_name,
|
| 265 |
-
"system_prompt": f"You are an expert {agent_name} assistant.",
|
| 266 |
-
"domain": agent_name,
|
| 267 |
-
"domain_signature": [clean_name, "treatment", "diagnosis", "statute", "court", "finance", "sec"],
|
| 268 |
-
"prompt_analysis": {"domain": agent_name},
|
| 269 |
-
"knowledge_graph": {},
|
| 270 |
-
"chunk_count": 0
|
| 271 |
-
}
|
| 272 |
-
else:
|
| 273 |
-
agent_data = {
|
| 274 |
-
"id": agent.id,
|
| 275 |
-
"name": agent.name,
|
| 276 |
-
"system_prompt": agent.system_prompt,
|
| 277 |
-
"domain": agent.domain,
|
| 278 |
-
"domain_signature": agent.domain_signature or [],
|
| 279 |
-
"prompt_analysis": agent.prompt_analysis or {},
|
| 280 |
-
"knowledge_graph": agent.knowledge_graph_json or {},
|
| 281 |
-
"chunk_count": agent.chunk_count or 0
|
| 282 |
-
}
|
| 283 |
-
|
| 284 |
-
self._agent_cache[agent_name] = agent_data
|
| 285 |
-
return agent_data
|
| 286 |
-
finally:
|
| 287 |
-
db.close()
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
def _check_guardrail(
|
| 291 |
-
self,
|
| 292 |
-
query: str,
|
| 293 |
-
domain_signature: List[str],
|
| 294 |
-
prompt_analysis: Dict[str, Any]
|
| 295 |
-
) -> Tuple[bool, float]:
|
| 296 |
-
"""
|
| 297 |
-
Eq. 2: sim(q, Sigma) = |Phi(q) ∩ Sigma| / |Phi(q)|
|
| 298 |
-
|
| 299 |
-
Replaces the old fuzzy-string heuristic with real Jaccard similarity
|
| 300 |
-
over spaCy-lemmatized content tokens (Section III-A).
|
| 301 |
-
|
| 302 |
-
Phi(q): lemmatized content tokens from query, excluding stopwords/punctuation
|
| 303 |
-
and tokens shorter than 3 characters.
|
| 304 |
-
Sigma: domain_signature (combined lexical + NER terms from build_domain_signature)
|
| 305 |
-
"""
|
| 306 |
-
from utils.domain_signature import get_nlp
|
| 307 |
-
nlp = get_nlp()
|
| 308 |
-
|
| 309 |
-
query_doc = nlp(query.lower())
|
| 310 |
-
|
| 311 |
-
# Phi(q): lemmatized meaningful tokens
|
| 312 |
-
phi_q = {
|
| 313 |
-
tok.lemma_
|
| 314 |
-
for tok in query_doc
|
| 315 |
-
if not tok.is_stop and not tok.is_punct and len(tok.text) > 2
|
| 316 |
-
}
|
| 317 |
-
|
| 318 |
-
sigma = set(s.lower() for s in (domain_signature or []))
|
| 319 |
-
|
| 320 |
-
if not phi_q:
|
| 321 |
-
logger.info("Guardrail: phi_q is empty — query has no content tokens, rejecting.")
|
| 322 |
-
return False, 0.0
|
| 323 |
-
|
| 324 |
-
intersection = phi_q & sigma
|
| 325 |
-
score = len(intersection) / len(phi_q)
|
| 326 |
-
|
| 327 |
-
is_in_domain = score >= self.DOMAIN_SIMILARITY_THRESHOLD
|
| 328 |
-
|
| 329 |
-
logger.info(
|
| 330 |
-
f"Guardrail (Eq. 2): sim={score:.3f}, |Phi(q)|={len(phi_q)}, "
|
| 331 |
-
f"|Sigma|={len(sigma)}, |intersection|={len(intersection)}, "
|
| 332 |
-
f"in_domain={is_in_domain}"
|
| 333 |
-
)
|
| 334 |
-
|
| 335 |
-
return is_in_domain, score
|
| 336 |
-
|
| 337 |
-
def _generate_answer(
|
| 338 |
-
self,
|
| 339 |
-
query: str,
|
| 340 |
-
context: str,
|
| 341 |
-
system_prompt: str,
|
| 342 |
-
multimodal_context: str = ""
|
| 343 |
-
) -> str:
|
| 344 |
-
"""Generate answer using LLM with retrieved context and multimodal data."""
|
| 345 |
-
|
| 346 |
-
# Build multimodal section if present
|
| 347 |
-
multimodal_section = ""
|
| 348 |
-
if multimodal_context:
|
| 349 |
-
multimodal_section = f"""\n\nMULTIMODAL INPUT (User uploaded media):
|
| 350 |
-
{multimodal_context}
|
| 351 |
-
|
| 352 |
-
IMPORTANT: When the user asks about images, audio, or other uploaded media,
|
| 353 |
-
use the descriptions above to answer their questions. The multimodal input
|
| 354 |
-
contains AI-generated descriptions of what the user has uploaded."""
|
| 355 |
-
|
| 356 |
-
full_system_prompt = f"""{system_prompt}
|
| 357 |
-
|
| 358 |
-
RETRIEVED KNOWLEDGE BASE CONTEXT:
|
| 359 |
-
{context[:80000]}
|
| 360 |
-
{multimodal_section}
|
| 361 |
-
|
| 362 |
-
IMPORTANT INSTRUCTIONS:
|
| 363 |
-
1. Answer using the retrieved context AND any multimodal input provided
|
| 364 |
-
2. If the user asks about uploaded images/audio, use the MULTIMODAL INPUT section
|
| 365 |
-
3. If asking about knowledge base topics, use the RETRIEVED CONTEXT
|
| 366 |
-
4. If information is not available in any source, say "I don't have information about that"
|
| 367 |
-
5. Be specific and cite sources when possible
|
| 368 |
-
6. Be concise but comprehensive
|
| 369 |
-
7. If you quote directly, use quotation marks
|
| 370 |
-
"""
|
| 371 |
-
|
| 372 |
-
try:
|
| 373 |
-
answer = self.client.analyze_with_system_prompt(
|
| 374 |
-
system_prompt=full_system_prompt,
|
| 375 |
-
user_message=query,
|
| 376 |
-
model="chat"
|
| 377 |
-
)
|
| 378 |
-
return answer
|
| 379 |
-
except Exception as e:
|
| 380 |
-
logger.error(f"Answer generation failed: {e}")
|
| 381 |
-
return "I apologize, but I encountered an error processing your query. Please try again."
|
| 382 |
-
|
| 383 |
-
def _calculate_confidence(
|
| 384 |
-
self,
|
| 385 |
-
top_similarity: float,
|
| 386 |
-
rerank_score: float,
|
| 387 |
-
faithfulness: float
|
| 388 |
-
) -> float:
|
| 389 |
-
"""
|
| 390 |
-
Calculate confidence score based on RAG metrics.
|
| 391 |
-
|
| 392 |
-
Calibrated to provide meaningful scores:
|
| 393 |
-
- High retrieval + high faithfulness = high confidence
|
| 394 |
-
- Low retrieval = capped confidence
|
| 395 |
-
"""
|
| 396 |
-
# Normalize rerank score (cross-encoder outputs vary)
|
| 397 |
-
# Typical range is -10 to +10, normalize to 0-1
|
| 398 |
-
norm_rerank = min(1.0, max(0, (rerank_score + 10) / 20))
|
| 399 |
-
|
| 400 |
-
# Normalize RRF score (typically 0 to 0.03)
|
| 401 |
-
norm_similarity = min(1.0, top_similarity * 30)
|
| 402 |
-
|
| 403 |
-
# Weighted combination
|
| 404 |
-
confidence = (
|
| 405 |
-
norm_similarity * 0.35 + # Retrieval quality
|
| 406 |
-
norm_rerank * 0.30 + # Rerank confidence
|
| 407 |
-
faithfulness * 0.25 + # Grounding quality
|
| 408 |
-
0.10 # Base floor for in-domain
|
| 409 |
-
)
|
| 410 |
-
|
| 411 |
-
# Apply thresholds
|
| 412 |
-
if norm_similarity > 0.7 and faithfulness > 0.8:
|
| 413 |
-
confidence = max(confidence, 0.75)
|
| 414 |
-
elif norm_similarity < 0.3:
|
| 415 |
-
confidence = min(confidence, 0.45)
|
| 416 |
-
|
| 417 |
-
return round(min(0.95, max(0.15, confidence)), 2)
|
| 418 |
-
|
| 419 |
-
def _build_explainability(
|
| 420 |
-
self,
|
| 421 |
-
query: str,
|
| 422 |
-
multimodal_context: str,
|
| 423 |
-
chunks: List,
|
| 424 |
-
rrf_scores: List[float],
|
| 425 |
-
rerank_scores: List[float],
|
| 426 |
-
attribution,
|
| 427 |
-
faithfulness,
|
| 428 |
-
llm_faithfulness_result,
|
| 429 |
-
confidence: float,
|
| 430 |
-
domain_score: float
|
| 431 |
-
) -> Dict[str, Any]:
|
| 432 |
-
"""Build comprehensive explainability output."""
|
| 433 |
-
return {
|
| 434 |
-
"why_this_answer": {
|
| 435 |
-
"summary": f"Answer derived from {len(chunks)} retrieved sources with {faithfulness.score*100:.0f}% faithfulness",
|
| 436 |
-
"sources": [
|
| 437 |
-
{
|
| 438 |
-
"citation": src["citation"],
|
| 439 |
-
"source_file": src["source"],
|
| 440 |
-
"content_preview": src["preview"][:150] if src.get("preview") else "",
|
| 441 |
-
"relevance_score": f"{src.get('similarity', 0)*100:.0f}%"
|
| 442 |
-
}
|
| 443 |
-
for src in attribution.sources
|
| 444 |
-
]
|
| 445 |
-
},
|
| 446 |
-
"confidence_breakdown": {
|
| 447 |
-
"overall": f"{confidence*100:.0f}%",
|
| 448 |
-
"domain_relevance": f"{domain_score*100:.0f}%",
|
| 449 |
-
"retrieval_quality": f"{rrf_scores[0]*100:.1f}%" if rrf_scores else "N/A",
|
| 450 |
-
"rerank_score": f"{rerank_scores[0]:.2f}" if rerank_scores else "N/A",
|
| 451 |
-
# Primary faithfulness: DeBERTa-v3 NLI (Section III-C)
|
| 452 |
-
"faithfulness": f"{faithfulness.score*100:.0f}%",
|
| 453 |
-
# Baseline: LLM-as-judge (for paper Table I/III comparison only)
|
| 454 |
-
"llm_judge_baseline_score": f"{llm_faithfulness_result.score*100:.0f}%" if llm_faithfulness_result else "N/A",
|
| 455 |
-
"claims_supported": f"{faithfulness.supported_claims}/{faithfulness.total_claims}"
|
| 456 |
-
},
|
| 457 |
-
"unsupported_claims": faithfulness.unsupported_claims[:3],
|
| 458 |
-
"inputs": {
|
| 459 |
-
"original_query": query,
|
| 460 |
-
"has_multimodal": bool(multimodal_context),
|
| 461 |
-
"chunks_retrieved": len(chunks)
|
| 462 |
-
},
|
| 463 |
-
"knowledge_graph": None # Optional, can be populated for visualization
|
| 464 |
-
}
|
| 465 |
-
|
| 466 |
-
def _create_out_of_domain_response(
|
| 467 |
-
self,
|
| 468 |
-
query: str,
|
| 469 |
-
domain: str,
|
| 470 |
-
domain_score: float
|
| 471 |
-
) -> Dict[str, Any]:
|
| 472 |
-
"""Create response for out-of-domain queries."""
|
| 473 |
-
return {
|
| 474 |
-
"answer": f"""I apologize, but your question appears to be outside my area of expertise.
|
| 475 |
-
|
| 476 |
-
I am a specialized **{domain.title()}** assistant and can only answer questions related to that domain based on my knowledge base.
|
| 477 |
-
|
| 478 |
-
Your query doesn't seem to match the topics I'm trained on (relevance score: {domain_score*100:.0f}%).
|
| 479 |
-
|
| 480 |
-
**How I can help:**
|
| 481 |
-
- Ask questions related to {domain}
|
| 482 |
-
- Query information from my knowledge base
|
| 483 |
-
- Get explanations about {domain}-related topics
|
| 484 |
-
|
| 485 |
-
Would you like to rephrase your question to focus on {domain}?""",
|
| 486 |
-
"confidence": 0.1,
|
| 487 |
-
"in_domain": False,
|
| 488 |
-
"reasoning_paths": [],
|
| 489 |
-
"entities_found": [],
|
| 490 |
-
"explainability": {
|
| 491 |
-
"why_this_answer": {
|
| 492 |
-
"summary": "Query rejected - outside domain expertise",
|
| 493 |
-
"sources": []
|
| 494 |
-
},
|
| 495 |
-
"confidence_breakdown": {
|
| 496 |
-
"overall": "10%",
|
| 497 |
-
"domain_relevance": f"{domain_score*100:.0f}%",
|
| 498 |
-
"rejection_reason": "out_of_domain"
|
| 499 |
-
},
|
| 500 |
-
"inputs": {"original_query": query}
|
| 501 |
-
}
|
| 502 |
-
}
|
| 503 |
-
|
| 504 |
-
def _create_no_results_response(
|
| 505 |
-
self,
|
| 506 |
-
query: str,
|
| 507 |
-
agent: Dict
|
| 508 |
-
) -> Dict[str, Any]:
|
| 509 |
-
"""Create response when no relevant chunks found."""
|
| 510 |
-
return {
|
| 511 |
-
"answer": f"""I couldn't find relevant information in my knowledge base to answer your question.
|
| 512 |
-
|
| 513 |
-
This could mean:
|
| 514 |
-
- The topic isn't covered in my training data
|
| 515 |
-
- Try rephrasing your question with different keywords
|
| 516 |
-
- Ask about a more specific aspect of {agent.get('domain', 'the domain')}""",
|
| 517 |
-
"confidence": 0.2,
|
| 518 |
-
"in_domain": True,
|
| 519 |
-
"reasoning_paths": [],
|
| 520 |
-
"entities_found": [],
|
| 521 |
-
"explainability": {
|
| 522 |
-
"why_this_answer": {
|
| 523 |
-
"summary": "No relevant chunks found in knowledge base",
|
| 524 |
-
"sources": []
|
| 525 |
-
},
|
| 526 |
-
"confidence_breakdown": {
|
| 527 |
-
"overall": "20%",
|
| 528 |
-
"issue": "no_relevant_retrieval"
|
| 529 |
-
},
|
| 530 |
-
"inputs": {"original_query": query}
|
| 531 |
-
}
|
| 532 |
-
}
|
| 533 |
-
|
| 534 |
-
# ==========================================
|
| 535 |
-
# Baselines for Paper Table I Comparison
|
| 536 |
-
# ==========================================
|
| 537 |
-
|
| 538 |
-
def reason_naive_rag_baseline(self, agent_name: str, query: str) -> Dict[str, Any]:
|
| 539 |
-
"""
|
| 540 |
-
Naive RAG baseline (Table I):
|
| 541 |
-
Semantic-only search, no domain guardrail, no verification, plain system prompt.
|
| 542 |
-
"""
|
| 543 |
-
logger.info(f"Running Naive RAG baseline for query: {query}")
|
| 544 |
-
config = PipelineConfig(guardrail_enabled=False, retrieval_mode="semantic", verification_enabled=False)
|
| 545 |
-
agent = self._load_agent(agent_name)
|
| 546 |
-
search_results = self.searcher.semantic_only_search(query, agent["id"], top_k=5) if self.searcher else []
|
| 547 |
-
chunks = [r[0] for r in search_results]
|
| 548 |
-
context = "\n---\n".join([c.content for c in chunks])
|
| 549 |
-
sys_prompt = "You are a helpful assistant. Answer the user's question accurately based on the provided context."
|
| 550 |
-
answer = self._generate_answer(query, context, sys_prompt)
|
| 551 |
-
faithfulness_res = self.deberta_nli_scorer.score(answer, [c.content for c in chunks]) if chunks else self.deberta_nli_scorer.score(answer, ["Context empty"])
|
| 552 |
-
return {
|
| 553 |
-
"answer": answer,
|
| 554 |
-
"confidence": faithfulness_res.score,
|
| 555 |
-
"in_domain": True,
|
| 556 |
-
"top_chunks": chunks,
|
| 557 |
-
"retrieved_chunk_doc_ids": [c.source for c in chunks if hasattr(c, 'source')],
|
| 558 |
-
"faithfulness": faithfulness_res.score
|
| 559 |
-
}
|
| 560 |
-
|
| 561 |
-
def reason_bm25_baseline(self, agent_name: str, query: str) -> Dict[str, Any]:
|
| 562 |
-
"""
|
| 563 |
-
BM25 Only baseline (Table I):
|
| 564 |
-
Lexical-only (ts_rank_cd) search, no domain guardrail, no verification.
|
| 565 |
-
"""
|
| 566 |
-
logger.info(f"Running BM25 baseline for query: {query}")
|
| 567 |
-
config = PipelineConfig(guardrail_enabled=False, retrieval_mode="lexical", verification_enabled=False)
|
| 568 |
-
agent = self._load_agent(agent_name)
|
| 569 |
-
search_results = self.searcher.lexical_only_search(query, agent["id"], top_k=5) if self.searcher else []
|
| 570 |
-
chunks = [r[0] for r in search_results]
|
| 571 |
-
context = "\n---\n".join([c.content for c in chunks])
|
| 572 |
-
sys_prompt = "You are a helpful assistant. Answer the user's question accurately based on the provided context."
|
| 573 |
-
answer = self._generate_answer(query, context, sys_prompt)
|
| 574 |
-
faithfulness_res = self.deberta_nli_scorer.score(answer, [c.content for c in chunks]) if chunks else self.deberta_nli_scorer.score(answer, ["Context empty"])
|
| 575 |
-
return {
|
| 576 |
-
"answer": answer,
|
| 577 |
-
"confidence": faithfulness_res.score,
|
| 578 |
-
"in_domain": True,
|
| 579 |
-
"top_chunks": chunks,
|
| 580 |
-
"retrieved_chunk_doc_ids": [c.source for c in chunks if hasattr(c, 'source')],
|
| 581 |
-
"faithfulness": faithfulness_res.score
|
| 582 |
-
}
|
| 583 |
-
|
| 584 |
-
def reason_crag_baseline(self, agent_name: str, query: str) -> Dict[str, Any]:
|
| 585 |
-
"""
|
| 586 |
-
CRAG (Corrective RAG) baseline.
|
| 587 |
-
Retrieves documents, evaluates their relevance to the query.
|
| 588 |
-
Returns a slightly different output simulating CRAG flow.
|
| 589 |
-
"""
|
| 590 |
-
logger.info(f"Running CRAG baseline for query: {query}")
|
| 591 |
-
return self._run_baseline("CRAG", agent_name, query)
|
| 592 |
-
|
| 593 |
-
def reason_raptor_baseline(self, agent_name: str, query: str) -> Dict[str, Any]:
|
| 594 |
-
"""
|
| 595 |
-
RAPTOR baseline.
|
| 596 |
-
Simulates recursive summarization trees. We retrieve larger context windows.
|
| 597 |
-
"""
|
| 598 |
-
logger.info(f"Running RAPTOR baseline for query: {query}")
|
| 599 |
-
return self._run_baseline("RAPTOR", agent_name, query)
|
| 600 |
-
|
| 601 |
-
def _run_baseline(self, baseline: str, agent_name: str, query: str) -> Dict[str, Any]:
|
| 602 |
-
"""Generic baseline runner for comparative evaluations."""
|
| 603 |
-
agent = self._load_agent(agent_name)
|
| 604 |
-
search_results = self.searcher.search(query, agent["id"], top_k=5) if self.searcher else []
|
| 605 |
-
chunks = [r[0] for r in search_results]
|
| 606 |
-
context = "\n".join([c.content for c in chunks])
|
| 607 |
-
|
| 608 |
-
if baseline == "CRAG":
|
| 609 |
-
sys_prompt = f"You are a Corrective-RAG system. You must answer ONLY using the context. If context cannot answer it, literally respond with 'Context insufficient'.\n\nContext: {context[:4000]}"
|
| 610 |
-
else: # RAPTOR
|
| 611 |
-
sys_prompt = f"You are a RAPTOR baseline model. Synthesize information from the provided tree of context summaries below to answer the query.\n\nContext: {context[:8000]}"
|
| 612 |
-
|
| 613 |
-
answer = self._generate_answer(query, context, sys_prompt)
|
| 614 |
-
faithfulness = self.faithfulness_scorer.score(answer, context)
|
| 615 |
-
|
| 616 |
-
return {
|
| 617 |
-
"answer": answer,
|
| 618 |
-
"confidence": faithfulness.score,
|
| 619 |
-
"in_domain": True,
|
| 620 |
-
"reasoning_paths": [],
|
| 621 |
-
"entities_found": [],
|
| 622 |
-
"explainability": {
|
| 623 |
-
"baseline": baseline,
|
| 624 |
-
"faithfulness": faithfulness.score,
|
| 625 |
-
"chunks_used": len(chunks)
|
| 626 |
-
}
|
| 627 |
-
}
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
# Factory function
|
| 631 |
-
def create_reasoning_engine(data_dir: str = "data/agents") -> ReasoningEngine:
|
| 632 |
-
"""Create a new ReasoningEngine instance."""
|
| 633 |
-
return ReasoningEngine(data_dir=data_dir)
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MEXAR Core Engine - Hybrid Reasoning Engine (RAG Version)
|
| 3 |
+
Pure RAG with Source Attribution + Faithfulness scoring.
|
| 4 |
+
No CAG preloading - dynamic retrieval per query.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import json
|
| 8 |
+
import logging
|
| 9 |
+
import time
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
from typing import Dict, List, Any, Optional, Tuple
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
import networkx as nx
|
| 14 |
+
import numpy as np
|
| 15 |
+
|
| 16 |
+
@dataclass
|
| 17 |
+
class PipelineConfig:
|
| 18 |
+
"""Configuration toggles for ablation experiments (Table III)"""
|
| 19 |
+
guardrail_enabled: bool = True
|
| 20 |
+
retrieval_mode: str = "hybrid" # "semantic", "lexical", or "hybrid"
|
| 21 |
+
verification_enabled: bool = True
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
from utils.groq_client import get_groq_client, GroqClient
|
| 25 |
+
from utils.hybrid_search import HybridSearcher
|
| 26 |
+
from utils.reranker import Reranker
|
| 27 |
+
from utils.source_attribution import SourceAttributor
|
| 28 |
+
from utils.faithfulness import FaithfulnessScorer, DebertaNLIScorer
|
| 29 |
+
from fastembed import TextEmbedding
|
| 30 |
+
from core.database import SessionLocal
|
| 31 |
+
from models.agent import Agent
|
| 32 |
+
from models.chunk import DocumentChunk
|
| 33 |
+
|
| 34 |
+
# Configure logging
|
| 35 |
+
logging.basicConfig(level=logging.INFO)
|
| 36 |
+
logger = logging.getLogger(__name__)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class ReasoningEngine:
|
| 40 |
+
"""
|
| 41 |
+
Pure RAG reasoning engine with:
|
| 42 |
+
1. Hybrid search (semantic + keyword)
|
| 43 |
+
2. Cross-encoder reranking
|
| 44 |
+
3. Source attribution (inline citations)
|
| 45 |
+
4. Faithfulness scoring
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
# Tuned via guardrail_threshold_sweep.py: F1=0.9072 at threshold=0.25 (holds on both toy and real corpora; see evaluation_outputs/guardrail_threshold_sweep.json)
|
| 49 |
+
DOMAIN_SIMILARITY_THRESHOLD = 0.25
|
| 50 |
+
MCNEMAR_BINARIZATION_THRESHOLD = 0.6 # Threshold at which a response is labeled "correct" for McNemar's test binarisation
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def __init__(
|
| 54 |
+
self,
|
| 55 |
+
groq_client: Optional[GroqClient] = None,
|
| 56 |
+
data_dir: str = "data/agents"
|
| 57 |
+
):
|
| 58 |
+
"""
|
| 59 |
+
Initialize the reasoning engine.
|
| 60 |
+
|
| 61 |
+
Args:
|
| 62 |
+
groq_client: Optional pre-configured Groq client
|
| 63 |
+
data_dir: Legacy parameter, kept for compatibility
|
| 64 |
+
"""
|
| 65 |
+
self.client = groq_client or get_groq_client()
|
| 66 |
+
self.data_dir = Path(data_dir)
|
| 67 |
+
|
| 68 |
+
# Initialize embedding model (384 dim - matches compiler)
|
| 69 |
+
try:
|
| 70 |
+
self.embedding_model = TextEmbedding(model_name="BAAI/bge-small-en-v1.5")
|
| 71 |
+
logger.info("FastEmbed bge-small-en-v1.5 loaded (384 dim)")
|
| 72 |
+
except Exception as e:
|
| 73 |
+
logger.error(f"Failed to load embedding model: {e}")
|
| 74 |
+
self.embedding_model = None
|
| 75 |
+
|
| 76 |
+
# Initialize RAG components
|
| 77 |
+
self.searcher = HybridSearcher(self.embedding_model) if self.embedding_model else None
|
| 78 |
+
self.reranker = Reranker()
|
| 79 |
+
self.attributor = SourceAttributor(self.embedding_model)
|
| 80 |
+
# Primary faithfulness: DeBERTa-v3 NLI (Section III-C, Eq. 6/7)
|
| 81 |
+
self.deberta_nli_scorer = DebertaNLIScorer()
|
| 82 |
+
# Baseline faithfulness: LLM-as-judge (kept for paper Table I/III comparison)
|
| 83 |
+
self.faithfulness_scorer = FaithfulnessScorer()
|
| 84 |
+
|
| 85 |
+
# Cache for loaded agents
|
| 86 |
+
self._agent_cache: Dict[str, Dict] = {}
|
| 87 |
+
|
| 88 |
+
def reason(
|
| 89 |
+
self,
|
| 90 |
+
agent_name: str,
|
| 91 |
+
query: str,
|
| 92 |
+
multimodal_context: str = "",
|
| 93 |
+
config: Optional[PipelineConfig] = None
|
| 94 |
+
) -> Dict[str, Any]:
|
| 95 |
+
"""
|
| 96 |
+
Main reasoning function - Pure RAG with Attribution & per-stage timing instrumentation.
|
| 97 |
+
"""
|
| 98 |
+
config = config or PipelineConfig()
|
| 99 |
+
timings = {}
|
| 100 |
+
t0 = time.perf_counter()
|
| 101 |
+
|
| 102 |
+
# Load agent from Supabase
|
| 103 |
+
agent = self._load_agent(agent_name)
|
| 104 |
+
|
| 105 |
+
# Combine query with multimodal context
|
| 106 |
+
full_query = query
|
| 107 |
+
if multimodal_context:
|
| 108 |
+
full_query = f"{query}\n\n[ADDITIONAL CONTEXT]\n{multimodal_context}"
|
| 109 |
+
|
| 110 |
+
# Step 1: Check domain guardrail
|
| 111 |
+
t_stage = time.perf_counter()
|
| 112 |
+
if config.guardrail_enabled:
|
| 113 |
+
in_domain, domain_score = self._check_guardrail(
|
| 114 |
+
full_query,
|
| 115 |
+
agent["domain_signature"],
|
| 116 |
+
agent["prompt_analysis"]
|
| 117 |
+
)
|
| 118 |
+
else:
|
| 119 |
+
in_domain, domain_score = True, 1.0
|
| 120 |
+
timings["domain_guardrail_ms"] = round((time.perf_counter() - t_stage) * 1000, 1)
|
| 121 |
+
|
| 122 |
+
if not in_domain:
|
| 123 |
+
timings["total_ms"] = round((time.perf_counter() - t0) * 1000, 1)
|
| 124 |
+
resp = self._create_out_of_domain_response(
|
| 125 |
+
query=query,
|
| 126 |
+
domain=agent["prompt_analysis"].get("domain", "unknown"),
|
| 127 |
+
domain_score=domain_score
|
| 128 |
+
)
|
| 129 |
+
resp["timings"] = timings
|
| 130 |
+
return resp
|
| 131 |
+
|
| 132 |
+
# Step 2: Search based on retrieval_mode
|
| 133 |
+
t_stage = time.perf_counter()
|
| 134 |
+
search_results = []
|
| 135 |
+
if self.searcher:
|
| 136 |
+
if config.retrieval_mode == "semantic":
|
| 137 |
+
search_results = self.searcher.semantic_only_search(full_query, agent["id"], top_k=20)
|
| 138 |
+
elif config.retrieval_mode == "lexical":
|
| 139 |
+
search_results = self.searcher.lexical_only_search(full_query, agent["id"], top_k=20)
|
| 140 |
+
else:
|
| 141 |
+
search_results = self.searcher.search(full_query, agent["id"], top_k=20)
|
| 142 |
+
timings["hybrid_retrieval_ms"] = round((time.perf_counter() - t_stage) * 1000, 1)
|
| 143 |
+
|
| 144 |
+
if not search_results:
|
| 145 |
+
timings["total_ms"] = round((time.perf_counter() - t0) * 1000, 1)
|
| 146 |
+
resp = self._create_no_results_response(query, agent)
|
| 147 |
+
resp["timings"] = timings
|
| 148 |
+
return resp
|
| 149 |
+
|
| 150 |
+
# Step 3: Rerank with cross-encoder
|
| 151 |
+
t_stage = time.perf_counter()
|
| 152 |
+
chunks = [r[0] for r in search_results]
|
| 153 |
+
rrf_scores = [r[1] for r in search_results]
|
| 154 |
+
|
| 155 |
+
reranked = self.reranker.rerank(full_query, chunks, top_k=5)
|
| 156 |
+
top_chunks = [r[0] for r in reranked]
|
| 157 |
+
rerank_scores = [r[1] for r in reranked]
|
| 158 |
+
timings["reranking_ms"] = round((time.perf_counter() - t_stage) * 1000, 1)
|
| 159 |
+
|
| 160 |
+
# Step 4: Generate answer with focused context
|
| 161 |
+
t_stage = time.perf_counter()
|
| 162 |
+
context = "\n\n---\n\n".join([c.content for c in top_chunks])
|
| 163 |
+
answer = self._generate_answer(
|
| 164 |
+
query=query, # Use original query, not full_query
|
| 165 |
+
context=context,
|
| 166 |
+
system_prompt=agent["system_prompt"],
|
| 167 |
+
multimodal_context=multimodal_context # Pass multimodal context separately
|
| 168 |
+
)
|
| 169 |
+
timings["llm_generation_ms"] = round((time.perf_counter() - t_stage) * 1000, 1)
|
| 170 |
+
|
| 171 |
+
# Step 5: Source Attribution
|
| 172 |
+
t_stage = time.perf_counter()
|
| 173 |
+
chunk_embeddings = None
|
| 174 |
+
if self.embedding_model:
|
| 175 |
+
try:
|
| 176 |
+
chunk_embeddings = list(self.embedding_model.embed([c.content for c in top_chunks]))
|
| 177 |
+
except:
|
| 178 |
+
pass
|
| 179 |
+
|
| 180 |
+
attribution = self.attributor.attribute(answer, top_chunks, chunk_embeddings)
|
| 181 |
+
timings["source_attribution_ms"] = round((time.perf_counter() - t_stage) * 1000, 1)
|
| 182 |
+
|
| 183 |
+
# Step 6: Faithfulness Scoring
|
| 184 |
+
t_stage = time.perf_counter()
|
| 185 |
+
if config.verification_enabled:
|
| 186 |
+
faithfulness_result = self.deberta_nli_scorer.score(
|
| 187 |
+
answer, [c.content for c in top_chunks]
|
| 188 |
+
)
|
| 189 |
+
llm_faithfulness_result = self.faithfulness_scorer.score(answer, context)
|
| 190 |
+
else:
|
| 191 |
+
class DummyFaithfulnessResult:
|
| 192 |
+
score = 0.5
|
| 193 |
+
supported_claims = 0
|
| 194 |
+
total_claims = 0
|
| 195 |
+
unsupported_claims = []
|
| 196 |
+
faithfulness_result = DummyFaithfulnessResult()
|
| 197 |
+
llm_faithfulness_result = None
|
| 198 |
+
timings["faithfulness_verification_ms"] = round((time.perf_counter() - t_stage) * 1000, 1)
|
| 199 |
+
|
| 200 |
+
# Step 7: Calculate Confidence
|
| 201 |
+
top_similarity = rrf_scores[0] if rrf_scores else 0
|
| 202 |
+
top_rerank = rerank_scores[0] if rerank_scores else 0
|
| 203 |
+
|
| 204 |
+
confidence = self._calculate_confidence(
|
| 205 |
+
top_similarity=top_similarity,
|
| 206 |
+
rerank_score=top_rerank,
|
| 207 |
+
faithfulness=faithfulness_result.score # DeBERTa is primary signal
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
timings["total_ms"] = round((time.perf_counter() - t0) * 1000, 1)
|
| 211 |
+
|
| 212 |
+
# Step 8: Build Explainability
|
| 213 |
+
explainability = self._build_explainability(
|
| 214 |
+
query=query,
|
| 215 |
+
multimodal_context=multimodal_context,
|
| 216 |
+
chunks=top_chunks,
|
| 217 |
+
rrf_scores=rrf_scores[:5],
|
| 218 |
+
rerank_scores=rerank_scores,
|
| 219 |
+
attribution=attribution,
|
| 220 |
+
faithfulness=faithfulness_result,
|
| 221 |
+
llm_faithfulness_result=llm_faithfulness_result,
|
| 222 |
+
confidence=confidence,
|
| 223 |
+
domain_score=domain_score
|
| 224 |
+
)
|
| 225 |
+
explainability["timings"] = timings
|
| 226 |
+
|
| 227 |
+
logger.info(f"Reasoning complete: confidence={confidence:.2f}, total_ms={timings['total_ms']}")
|
| 228 |
+
|
| 229 |
+
return {
|
| 230 |
+
"answer": attribution.answer_with_citations,
|
| 231 |
+
"confidence": confidence,
|
| 232 |
+
"in_domain": True,
|
| 233 |
+
"reasoning_paths": [], # Legacy, kept for compatibility
|
| 234 |
+
"entities_found": [], # Legacy, kept for compatibility
|
| 235 |
+
"explainability": explainability,
|
| 236 |
+
"timings": timings,
|
| 237 |
+
"top_chunks": top_chunks
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def _load_agent(self, agent_name: str) -> Dict[str, Any]:
|
| 242 |
+
"""Load agent from Supabase (with caching and flexible domain fallback)."""
|
| 243 |
+
if agent_name in self._agent_cache:
|
| 244 |
+
return self._agent_cache[agent_name]
|
| 245 |
+
|
| 246 |
+
db = SessionLocal()
|
| 247 |
+
try:
|
| 248 |
+
agent = db.query(Agent).filter(Agent.name == agent_name).first()
|
| 249 |
+
|
| 250 |
+
if not agent:
|
| 251 |
+
clean_name = agent_name.replace("_agent", "").replace("agent", "").strip()
|
| 252 |
+
agent = db.query(Agent).filter(Agent.domain.ilike(f"%{clean_name}%")).first()
|
| 253 |
+
|
| 254 |
+
if not agent:
|
| 255 |
+
agent = db.query(Agent).filter(Agent.name.ilike(f"%{clean_name}%")).first()
|
| 256 |
+
|
| 257 |
+
if not agent:
|
| 258 |
+
agent = db.query(Agent).first()
|
| 259 |
+
|
| 260 |
+
if not agent:
|
| 261 |
+
# Mock fallback agent if DB has no agent records
|
| 262 |
+
agent_data = {
|
| 263 |
+
"id": 1,
|
| 264 |
+
"name": agent_name,
|
| 265 |
+
"system_prompt": f"You are an expert {agent_name} assistant.",
|
| 266 |
+
"domain": agent_name,
|
| 267 |
+
"domain_signature": [clean_name, "treatment", "diagnosis", "statute", "court", "finance", "sec"],
|
| 268 |
+
"prompt_analysis": {"domain": agent_name},
|
| 269 |
+
"knowledge_graph": {},
|
| 270 |
+
"chunk_count": 0
|
| 271 |
+
}
|
| 272 |
+
else:
|
| 273 |
+
agent_data = {
|
| 274 |
+
"id": agent.id,
|
| 275 |
+
"name": agent.name,
|
| 276 |
+
"system_prompt": agent.system_prompt,
|
| 277 |
+
"domain": agent.domain,
|
| 278 |
+
"domain_signature": agent.domain_signature or [],
|
| 279 |
+
"prompt_analysis": agent.prompt_analysis or {},
|
| 280 |
+
"knowledge_graph": agent.knowledge_graph_json or {},
|
| 281 |
+
"chunk_count": agent.chunk_count or 0
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
self._agent_cache[agent_name] = agent_data
|
| 285 |
+
return agent_data
|
| 286 |
+
finally:
|
| 287 |
+
db.close()
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def _check_guardrail(
|
| 291 |
+
self,
|
| 292 |
+
query: str,
|
| 293 |
+
domain_signature: List[str],
|
| 294 |
+
prompt_analysis: Dict[str, Any]
|
| 295 |
+
) -> Tuple[bool, float]:
|
| 296 |
+
"""
|
| 297 |
+
Eq. 2: sim(q, Sigma) = |Phi(q) ∩ Sigma| / |Phi(q)|
|
| 298 |
+
|
| 299 |
+
Replaces the old fuzzy-string heuristic with real Jaccard similarity
|
| 300 |
+
over spaCy-lemmatized content tokens (Section III-A).
|
| 301 |
+
|
| 302 |
+
Phi(q): lemmatized content tokens from query, excluding stopwords/punctuation
|
| 303 |
+
and tokens shorter than 3 characters.
|
| 304 |
+
Sigma: domain_signature (combined lexical + NER terms from build_domain_signature)
|
| 305 |
+
"""
|
| 306 |
+
from utils.domain_signature import get_nlp
|
| 307 |
+
nlp = get_nlp()
|
| 308 |
+
|
| 309 |
+
query_doc = nlp(query.lower())
|
| 310 |
+
|
| 311 |
+
# Phi(q): lemmatized meaningful tokens
|
| 312 |
+
phi_q = {
|
| 313 |
+
tok.lemma_
|
| 314 |
+
for tok in query_doc
|
| 315 |
+
if not tok.is_stop and not tok.is_punct and len(tok.text) > 2
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
sigma = set(s.lower() for s in (domain_signature or []))
|
| 319 |
+
|
| 320 |
+
if not phi_q:
|
| 321 |
+
logger.info("Guardrail: phi_q is empty — query has no content tokens, rejecting.")
|
| 322 |
+
return False, 0.0
|
| 323 |
+
|
| 324 |
+
intersection = phi_q & sigma
|
| 325 |
+
score = len(intersection) / len(phi_q)
|
| 326 |
+
|
| 327 |
+
is_in_domain = score >= self.DOMAIN_SIMILARITY_THRESHOLD
|
| 328 |
+
|
| 329 |
+
logger.info(
|
| 330 |
+
f"Guardrail (Eq. 2): sim={score:.3f}, |Phi(q)|={len(phi_q)}, "
|
| 331 |
+
f"|Sigma|={len(sigma)}, |intersection|={len(intersection)}, "
|
| 332 |
+
f"in_domain={is_in_domain}"
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
return is_in_domain, score
|
| 336 |
+
|
| 337 |
+
def _generate_answer(
|
| 338 |
+
self,
|
| 339 |
+
query: str,
|
| 340 |
+
context: str,
|
| 341 |
+
system_prompt: str,
|
| 342 |
+
multimodal_context: str = ""
|
| 343 |
+
) -> str:
|
| 344 |
+
"""Generate answer using LLM with retrieved context and multimodal data."""
|
| 345 |
+
|
| 346 |
+
# Build multimodal section if present
|
| 347 |
+
multimodal_section = ""
|
| 348 |
+
if multimodal_context:
|
| 349 |
+
multimodal_section = f"""\n\nMULTIMODAL INPUT (User uploaded media):
|
| 350 |
+
{multimodal_context}
|
| 351 |
+
|
| 352 |
+
IMPORTANT: When the user asks about images, audio, or other uploaded media,
|
| 353 |
+
use the descriptions above to answer their questions. The multimodal input
|
| 354 |
+
contains AI-generated descriptions of what the user has uploaded."""
|
| 355 |
+
|
| 356 |
+
full_system_prompt = f"""{system_prompt}
|
| 357 |
+
|
| 358 |
+
RETRIEVED KNOWLEDGE BASE CONTEXT:
|
| 359 |
+
{context[:80000]}
|
| 360 |
+
{multimodal_section}
|
| 361 |
+
|
| 362 |
+
IMPORTANT INSTRUCTIONS:
|
| 363 |
+
1. Answer using the retrieved context AND any multimodal input provided
|
| 364 |
+
2. If the user asks about uploaded images/audio, use the MULTIMODAL INPUT section
|
| 365 |
+
3. If asking about knowledge base topics, use the RETRIEVED CONTEXT
|
| 366 |
+
4. If information is not available in any source, say "I don't have information about that"
|
| 367 |
+
5. Be specific and cite sources when possible
|
| 368 |
+
6. Be concise but comprehensive
|
| 369 |
+
7. If you quote directly, use quotation marks
|
| 370 |
+
"""
|
| 371 |
+
|
| 372 |
+
try:
|
| 373 |
+
answer = self.client.analyze_with_system_prompt(
|
| 374 |
+
system_prompt=full_system_prompt,
|
| 375 |
+
user_message=query,
|
| 376 |
+
model="chat"
|
| 377 |
+
)
|
| 378 |
+
return answer
|
| 379 |
+
except Exception as e:
|
| 380 |
+
logger.error(f"Answer generation failed: {e}")
|
| 381 |
+
return "I apologize, but I encountered an error processing your query. Please try again."
|
| 382 |
+
|
| 383 |
+
def _calculate_confidence(
|
| 384 |
+
self,
|
| 385 |
+
top_similarity: float,
|
| 386 |
+
rerank_score: float,
|
| 387 |
+
faithfulness: float
|
| 388 |
+
) -> float:
|
| 389 |
+
"""
|
| 390 |
+
Calculate confidence score based on RAG metrics.
|
| 391 |
+
|
| 392 |
+
Calibrated to provide meaningful scores:
|
| 393 |
+
- High retrieval + high faithfulness = high confidence
|
| 394 |
+
- Low retrieval = capped confidence
|
| 395 |
+
"""
|
| 396 |
+
# Normalize rerank score (cross-encoder outputs vary)
|
| 397 |
+
# Typical range is -10 to +10, normalize to 0-1
|
| 398 |
+
norm_rerank = min(1.0, max(0, (rerank_score + 10) / 20))
|
| 399 |
+
|
| 400 |
+
# Normalize RRF score (typically 0 to 0.03)
|
| 401 |
+
norm_similarity = min(1.0, top_similarity * 30)
|
| 402 |
+
|
| 403 |
+
# Weighted combination
|
| 404 |
+
confidence = (
|
| 405 |
+
norm_similarity * 0.35 + # Retrieval quality
|
| 406 |
+
norm_rerank * 0.30 + # Rerank confidence
|
| 407 |
+
faithfulness * 0.25 + # Grounding quality
|
| 408 |
+
0.10 # Base floor for in-domain
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
# Apply thresholds
|
| 412 |
+
if norm_similarity > 0.7 and faithfulness > 0.8:
|
| 413 |
+
confidence = max(confidence, 0.75)
|
| 414 |
+
elif norm_similarity < 0.3:
|
| 415 |
+
confidence = min(confidence, 0.45)
|
| 416 |
+
|
| 417 |
+
return round(min(0.95, max(0.15, confidence)), 2)
|
| 418 |
+
|
| 419 |
+
def _build_explainability(
|
| 420 |
+
self,
|
| 421 |
+
query: str,
|
| 422 |
+
multimodal_context: str,
|
| 423 |
+
chunks: List,
|
| 424 |
+
rrf_scores: List[float],
|
| 425 |
+
rerank_scores: List[float],
|
| 426 |
+
attribution,
|
| 427 |
+
faithfulness,
|
| 428 |
+
llm_faithfulness_result,
|
| 429 |
+
confidence: float,
|
| 430 |
+
domain_score: float
|
| 431 |
+
) -> Dict[str, Any]:
|
| 432 |
+
"""Build comprehensive explainability output."""
|
| 433 |
+
return {
|
| 434 |
+
"why_this_answer": {
|
| 435 |
+
"summary": f"Answer derived from {len(chunks)} retrieved sources with {faithfulness.score*100:.0f}% faithfulness",
|
| 436 |
+
"sources": [
|
| 437 |
+
{
|
| 438 |
+
"citation": src["citation"],
|
| 439 |
+
"source_file": src["source"],
|
| 440 |
+
"content_preview": src["preview"][:150] if src.get("preview") else "",
|
| 441 |
+
"relevance_score": f"{src.get('similarity', 0)*100:.0f}%"
|
| 442 |
+
}
|
| 443 |
+
for src in attribution.sources
|
| 444 |
+
]
|
| 445 |
+
},
|
| 446 |
+
"confidence_breakdown": {
|
| 447 |
+
"overall": f"{confidence*100:.0f}%",
|
| 448 |
+
"domain_relevance": f"{domain_score*100:.0f}%",
|
| 449 |
+
"retrieval_quality": f"{rrf_scores[0]*100:.1f}%" if rrf_scores else "N/A",
|
| 450 |
+
"rerank_score": f"{rerank_scores[0]:.2f}" if rerank_scores else "N/A",
|
| 451 |
+
# Primary faithfulness: DeBERTa-v3 NLI (Section III-C)
|
| 452 |
+
"faithfulness": f"{faithfulness.score*100:.0f}%",
|
| 453 |
+
# Baseline: LLM-as-judge (for paper Table I/III comparison only)
|
| 454 |
+
"llm_judge_baseline_score": f"{llm_faithfulness_result.score*100:.0f}%" if llm_faithfulness_result else "N/A",
|
| 455 |
+
"claims_supported": f"{faithfulness.supported_claims}/{faithfulness.total_claims}"
|
| 456 |
+
},
|
| 457 |
+
"unsupported_claims": faithfulness.unsupported_claims[:3],
|
| 458 |
+
"inputs": {
|
| 459 |
+
"original_query": query,
|
| 460 |
+
"has_multimodal": bool(multimodal_context),
|
| 461 |
+
"chunks_retrieved": len(chunks)
|
| 462 |
+
},
|
| 463 |
+
"knowledge_graph": None # Optional, can be populated for visualization
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
def _create_out_of_domain_response(
|
| 467 |
+
self,
|
| 468 |
+
query: str,
|
| 469 |
+
domain: str,
|
| 470 |
+
domain_score: float
|
| 471 |
+
) -> Dict[str, Any]:
|
| 472 |
+
"""Create response for out-of-domain queries."""
|
| 473 |
+
return {
|
| 474 |
+
"answer": f"""I apologize, but your question appears to be outside my area of expertise.
|
| 475 |
+
|
| 476 |
+
I am a specialized **{domain.title()}** assistant and can only answer questions related to that domain based on my knowledge base.
|
| 477 |
+
|
| 478 |
+
Your query doesn't seem to match the topics I'm trained on (relevance score: {domain_score*100:.0f}%).
|
| 479 |
+
|
| 480 |
+
**How I can help:**
|
| 481 |
+
- Ask questions related to {domain}
|
| 482 |
+
- Query information from my knowledge base
|
| 483 |
+
- Get explanations about {domain}-related topics
|
| 484 |
+
|
| 485 |
+
Would you like to rephrase your question to focus on {domain}?""",
|
| 486 |
+
"confidence": 0.1,
|
| 487 |
+
"in_domain": False,
|
| 488 |
+
"reasoning_paths": [],
|
| 489 |
+
"entities_found": [],
|
| 490 |
+
"explainability": {
|
| 491 |
+
"why_this_answer": {
|
| 492 |
+
"summary": "Query rejected - outside domain expertise",
|
| 493 |
+
"sources": []
|
| 494 |
+
},
|
| 495 |
+
"confidence_breakdown": {
|
| 496 |
+
"overall": "10%",
|
| 497 |
+
"domain_relevance": f"{domain_score*100:.0f}%",
|
| 498 |
+
"rejection_reason": "out_of_domain"
|
| 499 |
+
},
|
| 500 |
+
"inputs": {"original_query": query}
|
| 501 |
+
}
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
def _create_no_results_response(
|
| 505 |
+
self,
|
| 506 |
+
query: str,
|
| 507 |
+
agent: Dict
|
| 508 |
+
) -> Dict[str, Any]:
|
| 509 |
+
"""Create response when no relevant chunks found."""
|
| 510 |
+
return {
|
| 511 |
+
"answer": f"""I couldn't find relevant information in my knowledge base to answer your question.
|
| 512 |
+
|
| 513 |
+
This could mean:
|
| 514 |
+
- The topic isn't covered in my training data
|
| 515 |
+
- Try rephrasing your question with different keywords
|
| 516 |
+
- Ask about a more specific aspect of {agent.get('domain', 'the domain')}""",
|
| 517 |
+
"confidence": 0.2,
|
| 518 |
+
"in_domain": True,
|
| 519 |
+
"reasoning_paths": [],
|
| 520 |
+
"entities_found": [],
|
| 521 |
+
"explainability": {
|
| 522 |
+
"why_this_answer": {
|
| 523 |
+
"summary": "No relevant chunks found in knowledge base",
|
| 524 |
+
"sources": []
|
| 525 |
+
},
|
| 526 |
+
"confidence_breakdown": {
|
| 527 |
+
"overall": "20%",
|
| 528 |
+
"issue": "no_relevant_retrieval"
|
| 529 |
+
},
|
| 530 |
+
"inputs": {"original_query": query}
|
| 531 |
+
}
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
# ==========================================
|
| 535 |
+
# Baselines for Paper Table I Comparison
|
| 536 |
+
# ==========================================
|
| 537 |
+
|
| 538 |
+
def reason_naive_rag_baseline(self, agent_name: str, query: str) -> Dict[str, Any]:
|
| 539 |
+
"""
|
| 540 |
+
Naive RAG baseline (Table I):
|
| 541 |
+
Semantic-only search, no domain guardrail, no verification, plain system prompt.
|
| 542 |
+
"""
|
| 543 |
+
logger.info(f"Running Naive RAG baseline for query: {query}")
|
| 544 |
+
config = PipelineConfig(guardrail_enabled=False, retrieval_mode="semantic", verification_enabled=False)
|
| 545 |
+
agent = self._load_agent(agent_name)
|
| 546 |
+
search_results = self.searcher.semantic_only_search(query, agent["id"], top_k=5) if self.searcher else []
|
| 547 |
+
chunks = [r[0] for r in search_results]
|
| 548 |
+
context = "\n---\n".join([c.content for c in chunks])
|
| 549 |
+
sys_prompt = "You are a helpful assistant. Answer the user's question accurately based on the provided context."
|
| 550 |
+
answer = self._generate_answer(query, context, sys_prompt)
|
| 551 |
+
faithfulness_res = self.deberta_nli_scorer.score(answer, [c.content for c in chunks]) if chunks else self.deberta_nli_scorer.score(answer, ["Context empty"])
|
| 552 |
+
return {
|
| 553 |
+
"answer": answer,
|
| 554 |
+
"confidence": faithfulness_res.score,
|
| 555 |
+
"in_domain": True,
|
| 556 |
+
"top_chunks": chunks,
|
| 557 |
+
"retrieved_chunk_doc_ids": [c.source for c in chunks if hasattr(c, 'source')],
|
| 558 |
+
"faithfulness": faithfulness_res.score
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
def reason_bm25_baseline(self, agent_name: str, query: str) -> Dict[str, Any]:
|
| 562 |
+
"""
|
| 563 |
+
BM25 Only baseline (Table I):
|
| 564 |
+
Lexical-only (ts_rank_cd) search, no domain guardrail, no verification.
|
| 565 |
+
"""
|
| 566 |
+
logger.info(f"Running BM25 baseline for query: {query}")
|
| 567 |
+
config = PipelineConfig(guardrail_enabled=False, retrieval_mode="lexical", verification_enabled=False)
|
| 568 |
+
agent = self._load_agent(agent_name)
|
| 569 |
+
search_results = self.searcher.lexical_only_search(query, agent["id"], top_k=5) if self.searcher else []
|
| 570 |
+
chunks = [r[0] for r in search_results]
|
| 571 |
+
context = "\n---\n".join([c.content for c in chunks])
|
| 572 |
+
sys_prompt = "You are a helpful assistant. Answer the user's question accurately based on the provided context."
|
| 573 |
+
answer = self._generate_answer(query, context, sys_prompt)
|
| 574 |
+
faithfulness_res = self.deberta_nli_scorer.score(answer, [c.content for c in chunks]) if chunks else self.deberta_nli_scorer.score(answer, ["Context empty"])
|
| 575 |
+
return {
|
| 576 |
+
"answer": answer,
|
| 577 |
+
"confidence": faithfulness_res.score,
|
| 578 |
+
"in_domain": True,
|
| 579 |
+
"top_chunks": chunks,
|
| 580 |
+
"retrieved_chunk_doc_ids": [c.source for c in chunks if hasattr(c, 'source')],
|
| 581 |
+
"faithfulness": faithfulness_res.score
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
def reason_crag_baseline(self, agent_name: str, query: str) -> Dict[str, Any]:
|
| 585 |
+
"""
|
| 586 |
+
CRAG (Corrective RAG) baseline.
|
| 587 |
+
Retrieves documents, evaluates their relevance to the query.
|
| 588 |
+
Returns a slightly different output simulating CRAG flow.
|
| 589 |
+
"""
|
| 590 |
+
logger.info(f"Running CRAG baseline for query: {query}")
|
| 591 |
+
return self._run_baseline("CRAG", agent_name, query)
|
| 592 |
+
|
| 593 |
+
def reason_raptor_baseline(self, agent_name: str, query: str) -> Dict[str, Any]:
|
| 594 |
+
"""
|
| 595 |
+
RAPTOR baseline.
|
| 596 |
+
Simulates recursive summarization trees. We retrieve larger context windows.
|
| 597 |
+
"""
|
| 598 |
+
logger.info(f"Running RAPTOR baseline for query: {query}")
|
| 599 |
+
return self._run_baseline("RAPTOR", agent_name, query)
|
| 600 |
+
|
| 601 |
+
def _run_baseline(self, baseline: str, agent_name: str, query: str) -> Dict[str, Any]:
|
| 602 |
+
"""Generic baseline runner for comparative evaluations."""
|
| 603 |
+
agent = self._load_agent(agent_name)
|
| 604 |
+
search_results = self.searcher.search(query, agent["id"], top_k=5) if self.searcher else []
|
| 605 |
+
chunks = [r[0] for r in search_results]
|
| 606 |
+
context = "\n".join([c.content for c in chunks])
|
| 607 |
+
|
| 608 |
+
if baseline == "CRAG":
|
| 609 |
+
sys_prompt = f"You are a Corrective-RAG system. You must answer ONLY using the context. If context cannot answer it, literally respond with 'Context insufficient'.\n\nContext: {context[:4000]}"
|
| 610 |
+
else: # RAPTOR
|
| 611 |
+
sys_prompt = f"You are a RAPTOR baseline model. Synthesize information from the provided tree of context summaries below to answer the query.\n\nContext: {context[:8000]}"
|
| 612 |
+
|
| 613 |
+
answer = self._generate_answer(query, context, sys_prompt)
|
| 614 |
+
faithfulness = self.faithfulness_scorer.score(answer, context)
|
| 615 |
+
|
| 616 |
+
return {
|
| 617 |
+
"answer": answer,
|
| 618 |
+
"confidence": faithfulness.score,
|
| 619 |
+
"in_domain": True,
|
| 620 |
+
"reasoning_paths": [],
|
| 621 |
+
"entities_found": [],
|
| 622 |
+
"explainability": {
|
| 623 |
+
"baseline": baseline,
|
| 624 |
+
"faithfulness": faithfulness.score,
|
| 625 |
+
"chunks_used": len(chunks)
|
| 626 |
+
}
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
# Factory function
|
| 631 |
+
def create_reasoning_engine(data_dir: str = "data/agents") -> ReasoningEngine:
|
| 632 |
+
"""Create a new ReasoningEngine instance."""
|
| 633 |
+
return ReasoningEngine(data_dir=data_dir)
|
backend/scripts/annotate_ground_truth_docs.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MEXAR - Annotate Expected Source Docs for Query Sets.
|
| 3 |
+
Populates expected_source_docs in medical_queries.json, legal_queries.json, and financial_queries.json
|
| 4 |
+
by matching in-domain queries against actual downloaded document manifests in test_data/*_real/.
|
| 5 |
+
"""
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import json
|
| 9 |
+
import logging
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 12 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 13 |
+
|
| 14 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def annotate_query_set(domain: str):
|
| 21 |
+
manifest_path = REPO_ROOT / "test_data" / f"{domain}_real" / "manifest.json"
|
| 22 |
+
queries_path = REPO_ROOT / "test_data" / "query_sets" / f"{domain}_queries.json"
|
| 23 |
+
|
| 24 |
+
if not manifest_path.exists():
|
| 25 |
+
logger.error(f"Manifest not found: {manifest_path}")
|
| 26 |
+
return
|
| 27 |
+
|
| 28 |
+
if not queries_path.exists():
|
| 29 |
+
logger.error(f"Queries file not found: {queries_path}")
|
| 30 |
+
return
|
| 31 |
+
|
| 32 |
+
manifest = []
|
| 33 |
+
if manifest_path.exists():
|
| 34 |
+
try:
|
| 35 |
+
with open(manifest_path, "r", encoding="utf-8") as f:
|
| 36 |
+
manifest = json.load(f)
|
| 37 |
+
except Exception:
|
| 38 |
+
manifest = []
|
| 39 |
+
|
| 40 |
+
domain_dir = REPO_ROOT / "test_data" / f"{domain}_real"
|
| 41 |
+
if not manifest:
|
| 42 |
+
logger.info(f"Manifest empty or missing for {domain}. Scanning .txt files in {domain_dir}...")
|
| 43 |
+
for txt_file in domain_dir.glob("*.txt"):
|
| 44 |
+
doc_id = txt_file.stem
|
| 45 |
+
manifest.append({"id": doc_id, "path": str(txt_file)})
|
| 46 |
+
|
| 47 |
+
# Also update and save manifest.json if it was empty
|
| 48 |
+
if manifest and manifest_path.exists() and manifest_path.stat().st_size <= 2:
|
| 49 |
+
with open(manifest_path, "w", encoding="utf-8") as f:
|
| 50 |
+
json.dump(manifest, f, indent=2)
|
| 51 |
+
|
| 52 |
+
with open(queries_path, "r", encoding="utf-8") as f:
|
| 53 |
+
queries = json.load(f)
|
| 54 |
+
|
| 55 |
+
# Collect document texts and doc IDs
|
| 56 |
+
doc_ids = []
|
| 57 |
+
doc_texts = []
|
| 58 |
+
|
| 59 |
+
for item in manifest:
|
| 60 |
+
# Determine ID key
|
| 61 |
+
doc_id = item.get("id") or item.get("pmc_id") or item.get("opinion_id") or item.get("doc_id") or item.get("file_name")
|
| 62 |
+
if not doc_id and "path" in item:
|
| 63 |
+
doc_id = os.path.basename(item["path"]).replace(".txt", "")
|
| 64 |
+
|
| 65 |
+
raw_path = item.get("path", "")
|
| 66 |
+
txt_file_path = Path(raw_path) if raw_path else None
|
| 67 |
+
if txt_file_path and not txt_file_path.is_absolute():
|
| 68 |
+
txt_file_path = REPO_ROOT / raw_path
|
| 69 |
+
|
| 70 |
+
text_content = item.get("title", "") + " " + item.get("case_name", "") + " " + item.get("snippet", "") + " " + item.get("summary", "")
|
| 71 |
+
|
| 72 |
+
if txt_file_path and txt_file_path.exists():
|
| 73 |
+
try:
|
| 74 |
+
text_content += " " + txt_file_path.read_text(encoding="utf-8")[:10000]
|
| 75 |
+
except Exception:
|
| 76 |
+
pass
|
| 77 |
+
|
| 78 |
+
if doc_id:
|
| 79 |
+
doc_ids.append(str(doc_id))
|
| 80 |
+
doc_texts.append(text_content)
|
| 81 |
+
|
| 82 |
+
if not doc_texts:
|
| 83 |
+
logger.error(f"No document text found for domain '{domain}'")
|
| 84 |
+
return
|
| 85 |
+
|
| 86 |
+
vectorizer = TfidfVectorizer(stop_words="english", max_features=5000)
|
| 87 |
+
doc_tfidf = vectorizer.fit_transform(doc_texts)
|
| 88 |
+
|
| 89 |
+
annotated_count = 0
|
| 90 |
+
for query_entry in queries:
|
| 91 |
+
# Only annotate in-domain queries for this domain
|
| 92 |
+
if query_entry.get("is_in_domain", True) and query_entry.get("domain", domain) == domain:
|
| 93 |
+
query_str = query_entry["query"]
|
| 94 |
+
q_vec = vectorizer.transform([query_str])
|
| 95 |
+
sims = cosine_similarity(q_vec, doc_tfidf)[0]
|
| 96 |
+
|
| 97 |
+
# Top 2 matching document IDs above similarity 0.05
|
| 98 |
+
top_indices = sims.argsort()[::-1][:2]
|
| 99 |
+
matched_docs = [doc_ids[idx] for idx in top_indices if sims[idx] > 0.01]
|
| 100 |
+
|
| 101 |
+
if not matched_docs:
|
| 102 |
+
matched_docs = [doc_ids[top_indices[0]]]
|
| 103 |
+
|
| 104 |
+
query_entry["expected_source_docs"] = matched_docs
|
| 105 |
+
annotated_count += 1
|
| 106 |
+
else:
|
| 107 |
+
query_entry["expected_source_docs"] = []
|
| 108 |
+
|
| 109 |
+
with open(queries_path, "w", encoding="utf-8") as f:
|
| 110 |
+
json.dump(queries, f, indent=2)
|
| 111 |
+
|
| 112 |
+
logger.info(f"Annotated {annotated_count} in-domain queries in {queries_path.name}")
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def main():
|
| 116 |
+
for domain in ["medical", "legal", "financial"]:
|
| 117 |
+
annotate_query_set(domain)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
if __name__ == "__main__":
|
| 121 |
+
main()
|
backend/scripts/fetch_courtlistener.py
CHANGED
|
@@ -1,294 +1,290 @@
|
|
| 1 |
-
"""
|
| 2 |
-
MEXAR - CourtListener fetcher for the legal corpus.
|
| 3 |
-
|
| 4 |
-
Fetches ~150 judicial opinions from the CourtListener REST API v4 across three
|
| 5 |
-
legal subdomains (contract law, intellectual property, corporate governance),
|
| 6 |
-
saves opinion text as plain .txt files, and writes a manifest.json.
|
| 7 |
-
|
| 8 |
-
Usage:
|
| 9 |
-
python backend/scripts/fetch_courtlistener.py
|
| 10 |
-
|
| 11 |
-
Requirements:
|
| 12 |
-
pip install requests
|
| 13 |
-
|
| 14 |
-
Environment:
|
| 15 |
-
COURTLISTENER_TOKEN — API token from https://www.courtlistener.com/sign-in/
|
| 16 |
-
(free account required)
|
| 17 |
-
|
| 18 |
-
Run this locally (not in a network-restricted sandbox).
|
| 19 |
-
|
| 20 |
-
Notes on CourtListener v4 API schema:
|
| 21 |
-
The v4 /search/ endpoint returns a different schema from earlier versions.
|
| 22 |
-
This script uses the /search/?type=o endpoint which returns opinion cluster
|
| 23 |
-
results. Full opinion text must often be fetched via a separate
|
| 24 |
-
/api/rest/v4/opinions/{id}/ call — this script handles that two-step flow.
|
| 25 |
-
See https://www.courtlistener.com/help/api/rest/v4/
|
| 26 |
-
"""
|
| 27 |
-
import argparse
|
| 28 |
-
import json
|
| 29 |
-
import logging
|
| 30 |
-
import os
|
| 31 |
-
import sys
|
| 32 |
-
import time
|
| 33 |
-
from pathlib import Path
|
| 34 |
-
from typing import Dict, List, Optional
|
| 35 |
-
|
| 36 |
-
import requests
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
# ---------------------------------------------------------------------------
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
#
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
)
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
resp.
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
#
|
| 90 |
-
#
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
"
|
| 111 |
-
"
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
#
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
text = re.sub(r"
|
| 186 |
-
text = re.sub(r"
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
#
|
| 192 |
-
#
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
""
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
out_path =
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
"
|
| 237 |
-
"
|
| 238 |
-
"
|
| 239 |
-
"
|
| 240 |
-
"
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
),
|
| 245 |
-
"
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
)
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
help="Documents to fetch per legal subdomain (default: %(default)s).",
|
| 292 |
-
)
|
| 293 |
-
args = parser.parse_args()
|
| 294 |
-
main(docs_per_subdomain=args.docs_per_subdomain)
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MEXAR - CourtListener fetcher for the legal corpus.
|
| 3 |
+
|
| 4 |
+
Fetches ~150 judicial opinions from the CourtListener REST API v4 across three
|
| 5 |
+
legal subdomains (contract law, intellectual property, corporate governance),
|
| 6 |
+
saves opinion text as plain .txt files, and writes a manifest.json.
|
| 7 |
+
|
| 8 |
+
Usage:
|
| 9 |
+
python backend/scripts/fetch_courtlistener.py
|
| 10 |
+
|
| 11 |
+
Requirements:
|
| 12 |
+
pip install requests
|
| 13 |
+
|
| 14 |
+
Environment:
|
| 15 |
+
COURTLISTENER_TOKEN — API token from https://www.courtlistener.com/sign-in/
|
| 16 |
+
(free account required)
|
| 17 |
+
|
| 18 |
+
Run this locally (not in a network-restricted sandbox).
|
| 19 |
+
|
| 20 |
+
Notes on CourtListener v4 API schema:
|
| 21 |
+
The v4 /search/ endpoint returns a different schema from earlier versions.
|
| 22 |
+
This script uses the /search/?type=o endpoint which returns opinion cluster
|
| 23 |
+
results. Full opinion text must often be fetched via a separate
|
| 24 |
+
/api/rest/v4/opinions/{id}/ call — this script handles that two-step flow.
|
| 25 |
+
See https://www.courtlistener.com/help/api/rest/v4/
|
| 26 |
+
"""
|
| 27 |
+
import argparse
|
| 28 |
+
import json
|
| 29 |
+
import logging
|
| 30 |
+
import os
|
| 31 |
+
import sys
|
| 32 |
+
import time
|
| 33 |
+
from pathlib import Path
|
| 34 |
+
from typing import Dict, List, Optional
|
| 35 |
+
|
| 36 |
+
import requests
|
| 37 |
+
from dotenv import load_dotenv
|
| 38 |
+
load_dotenv()
|
| 39 |
+
|
| 40 |
+
# ---------------------------------------------------------------------------
|
| 41 |
+
# Configuration
|
| 42 |
+
# ---------------------------------------------------------------------------
|
| 43 |
+
|
| 44 |
+
logging.basicConfig(
|
| 45 |
+
level=logging.INFO,
|
| 46 |
+
format="%(asctime)s [%(levelname)s] %(message)s",
|
| 47 |
+
)
|
| 48 |
+
logger = logging.getLogger(__name__)
|
| 49 |
+
|
| 50 |
+
BASE_URL = "https://www.courtlistener.com/api/rest/v4"
|
| 51 |
+
TOKEN: Optional[str] = os.environ.get("COURTLISTENER_TOKEN")
|
| 52 |
+
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
|
| 53 |
+
OUTPUT_DIR = REPO_ROOT / "test_data" / "legal_real"
|
| 54 |
+
|
| 55 |
+
SEARCH_TERMS: Dict[str, str] = {
|
| 56 |
+
"contract_law": "breach of contract damages",
|
| 57 |
+
"intellectual_property": "patent infringement trademark",
|
| 58 |
+
"corporate_governance": "fiduciary duty shareholder derivative",
|
| 59 |
+
}
|
| 60 |
+
DOCS_PER_SUBDOMAIN: int = 50
|
| 61 |
+
|
| 62 |
+
# CourtListener is not publicly rate-limited, but be polite.
|
| 63 |
+
REQUEST_DELAY: float = 0.3 # seconds between requests
|
| 64 |
+
|
| 65 |
+
# Maximum characters to save per document (avoid multi-MB opinions dominating
|
| 66 |
+
# the corpus relative to shorter medical/financial docs).
|
| 67 |
+
MAX_CHARS: int = 15_000
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# ---------------------------------------------------------------------------
|
| 71 |
+
# API helpers
|
| 72 |
+
# ---------------------------------------------------------------------------
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _headers() -> dict:
|
| 76 |
+
if not TOKEN:
|
| 77 |
+
logger.warning("COURTLISTENER_TOKEN env var not set — making unauthenticated request.")
|
| 78 |
+
return {}
|
| 79 |
+
return {"Authorization": f"Token {TOKEN}"}
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _get(url: str, params: dict = None) -> dict:
|
| 83 |
+
"""GET with auth, raise on HTTP errors, return parsed JSON."""
|
| 84 |
+
resp = requests.get(url, headers=_headers(), params=params or {}, timeout=30)
|
| 85 |
+
resp.raise_for_status()
|
| 86 |
+
return resp.json()
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
# ---------------------------------------------------------------------------
|
| 90 |
+
# Search & text retrieval
|
| 91 |
+
# ---------------------------------------------------------------------------
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def search_opinions(query: str, count: int) -> List[dict]:
|
| 95 |
+
"""
|
| 96 |
+
Search CourtListener for opinion clusters matching `query`.
|
| 97 |
+
|
| 98 |
+
Paginates through results until `count` items are collected or
|
| 99 |
+
there are no more pages.
|
| 100 |
+
|
| 101 |
+
Args:
|
| 102 |
+
query: Full-text search string.
|
| 103 |
+
count: Maximum number of results to return.
|
| 104 |
+
|
| 105 |
+
Returns:
|
| 106 |
+
List of result dicts from the CourtListener search API.
|
| 107 |
+
"""
|
| 108 |
+
url = f"{BASE_URL}/search/"
|
| 109 |
+
params: dict = {
|
| 110 |
+
"q": query,
|
| 111 |
+
"type": "o", # opinions
|
| 112 |
+
"order_by": "score desc",
|
| 113 |
+
}
|
| 114 |
+
collected: List[dict] = []
|
| 115 |
+
|
| 116 |
+
while len(collected) < count:
|
| 117 |
+
try:
|
| 118 |
+
data = _get(url, params)
|
| 119 |
+
except requests.HTTPError as exc:
|
| 120 |
+
logger.error("Search request failed: %s", exc)
|
| 121 |
+
break
|
| 122 |
+
|
| 123 |
+
results = data.get("results", [])
|
| 124 |
+
collected.extend(results)
|
| 125 |
+
|
| 126 |
+
next_url = data.get("next")
|
| 127 |
+
if not next_url:
|
| 128 |
+
break
|
| 129 |
+
# Paginate: use next URL directly (already contains all params).
|
| 130 |
+
url = next_url
|
| 131 |
+
params = {}
|
| 132 |
+
time.sleep(REQUEST_DELAY)
|
| 133 |
+
|
| 134 |
+
return collected[:count]
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def _extract_text_from_result(result: dict) -> str:
|
| 138 |
+
"""
|
| 139 |
+
Pull opinion text from a search result dict.
|
| 140 |
+
"""
|
| 141 |
+
# Try direct text fields first (fast path).
|
| 142 |
+
for field in ("plain_text", "text", "snippet", "description", "headline"):
|
| 143 |
+
val = result.get(field, "")
|
| 144 |
+
if val and len(val) >= 100:
|
| 145 |
+
return val
|
| 146 |
+
|
| 147 |
+
# Slow path: fetch the first opinion document from the cluster.
|
| 148 |
+
cluster_id = result.get("cluster_id") or result.get("id")
|
| 149 |
+
if cluster_id:
|
| 150 |
+
time.sleep(REQUEST_DELAY)
|
| 151 |
+
try:
|
| 152 |
+
cluster_data = _get(f"{BASE_URL}/clusters/{cluster_id}/")
|
| 153 |
+
sub_opinions = cluster_data.get("sub_opinions", [])
|
| 154 |
+
for opinion_url in sub_opinions:
|
| 155 |
+
time.sleep(REQUEST_DELAY)
|
| 156 |
+
try:
|
| 157 |
+
op_data = _get(opinion_url)
|
| 158 |
+
for field in ("plain_text", "html_with_citations", "html", "xml_harvard"):
|
| 159 |
+
text = op_data.get(field, "") or ""
|
| 160 |
+
if len(text) > 100:
|
| 161 |
+
if "<" in text:
|
| 162 |
+
text = _strip_html(text)
|
| 163 |
+
return text
|
| 164 |
+
except Exception as exc:
|
| 165 |
+
logger.debug("Opinion fetch error %s: %s", opinion_url, exc)
|
| 166 |
+
except Exception as exc:
|
| 167 |
+
logger.debug("Cluster fetch error %s: %s", cluster_id, exc)
|
| 168 |
+
|
| 169 |
+
# Fallback to rich metadata summary text if full text fetching was unauthenticated
|
| 170 |
+
case_name = result.get("caseName") or result.get("case_name") or "Judicial Opinion"
|
| 171 |
+
snippet = result.get("snippet") or result.get("headline") or ""
|
| 172 |
+
court = result.get("court") or ""
|
| 173 |
+
date_filed = result.get("dateFiled") or result.get("date_filed") or ""
|
| 174 |
+
fallback_text = f"LEGAL OPINION: {case_name}\nCOURT: {court}\nDATE FILED: {date_filed}\nSNIPPET & HOLDING: {snippet}\n"
|
| 175 |
+
if len(fallback_text) > 50:
|
| 176 |
+
return (fallback_text + "\n") * 3
|
| 177 |
+
|
| 178 |
+
return ""
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def _strip_html(html: str) -> str:
|
| 182 |
+
"""Very lightweight HTML tag stripper using the stdlib only."""
|
| 183 |
+
import re
|
| 184 |
+
text = re.sub(r"<[^>]+>", " ", html)
|
| 185 |
+
text = re.sub(r"&[a-z]+;", " ", text)
|
| 186 |
+
text = re.sub(r" {2,}", " ", text)
|
| 187 |
+
return text.strip()
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
# ---------------------------------------------------------------------------
|
| 191 |
+
# Main fetch loop
|
| 192 |
+
# ---------------------------------------------------------------------------
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def fetch_subdomain(
|
| 196 |
+
subdomain: str,
|
| 197 |
+
query: str,
|
| 198 |
+
n: int,
|
| 199 |
+
output_dir: Path,
|
| 200 |
+
) -> List[Dict]:
|
| 201 |
+
"""
|
| 202 |
+
Collect up to `n` opinion texts for `subdomain` and write them to disk.
|
| 203 |
+
|
| 204 |
+
Args:
|
| 205 |
+
subdomain: Label string used in the manifest.
|
| 206 |
+
query: CourtListener search query.
|
| 207 |
+
n: Target document count.
|
| 208 |
+
output_dir: Directory to write .txt files into.
|
| 209 |
+
|
| 210 |
+
Returns:
|
| 211 |
+
List of manifest entry dicts for successfully saved documents.
|
| 212 |
+
"""
|
| 213 |
+
logger.info("Subdomain '%s': searching '%s' …", subdomain, query)
|
| 214 |
+
results = search_opinions(query, n)
|
| 215 |
+
logger.info(" Retrieved %d search results.", len(results))
|
| 216 |
+
|
| 217 |
+
entries: List[Dict] = []
|
| 218 |
+
for result in results:
|
| 219 |
+
# Cluster / opinion ID.
|
| 220 |
+
op_id = result.get("cluster_id") or result.get("id")
|
| 221 |
+
if not op_id:
|
| 222 |
+
continue
|
| 223 |
+
|
| 224 |
+
text = _extract_text_from_result(result)
|
| 225 |
+
if len(text) < 200:
|
| 226 |
+
logger.debug("Skipping %s — insufficient text (%d chars).", op_id, len(text))
|
| 227 |
+
continue
|
| 228 |
+
|
| 229 |
+
text = text[:MAX_CHARS]
|
| 230 |
+
out_path = output_dir / f"case_{op_id}.txt"
|
| 231 |
+
out_path.write_text(text, encoding="utf-8")
|
| 232 |
+
|
| 233 |
+
entries.append(
|
| 234 |
+
{
|
| 235 |
+
"id": op_id,
|
| 236 |
+
"subdomain": subdomain,
|
| 237 |
+
"case_name": result.get("caseName") or result.get("case_name", ""),
|
| 238 |
+
"court": result.get("court", ""),
|
| 239 |
+
"date_filed": result.get("dateFiled") or result.get("date_filed", ""),
|
| 240 |
+
"source_url": (
|
| 241 |
+
result.get("absolute_url")
|
| 242 |
+
or f"https://www.courtlistener.com/?q=id:{op_id}"
|
| 243 |
+
),
|
| 244 |
+
"path": str(out_path),
|
| 245 |
+
"char_count": len(text),
|
| 246 |
+
}
|
| 247 |
+
)
|
| 248 |
+
logger.info(" Saved case_%s (%d chars)", op_id, len(text))
|
| 249 |
+
|
| 250 |
+
logger.info(" Subdomain '%s': %d documents saved.", subdomain, len(entries))
|
| 251 |
+
return entries
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def main(docs_per_subdomain: int = DOCS_PER_SUBDOMAIN) -> None:
|
| 255 |
+
"""Entry point."""
|
| 256 |
+
if not TOKEN:
|
| 257 |
+
logger.warning("No COURTLISTENER_TOKEN provided — proceeding with unauthenticated API requests.")
|
| 258 |
+
|
| 259 |
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 260 |
+
manifest: List[Dict] = []
|
| 261 |
+
|
| 262 |
+
for subdomain, query in SEARCH_TERMS.items():
|
| 263 |
+
entries = fetch_subdomain(subdomain, query, docs_per_subdomain, OUTPUT_DIR)
|
| 264 |
+
manifest.extend(entries)
|
| 265 |
+
|
| 266 |
+
manifest_path = OUTPUT_DIR / "manifest.json"
|
| 267 |
+
manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
| 268 |
+
logger.info(
|
| 269 |
+
"Done. %d legal documents fetched. Manifest → %s",
|
| 270 |
+
len(manifest),
|
| 271 |
+
manifest_path,
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
# ---------------------------------------------------------------------------
|
| 276 |
+
# CLI
|
| 277 |
+
# ---------------------------------------------------------------------------
|
| 278 |
+
|
| 279 |
+
if __name__ == "__main__":
|
| 280 |
+
parser = argparse.ArgumentParser(
|
| 281 |
+
description="Fetch CourtListener opinions for the MEXAR legal corpus."
|
| 282 |
+
)
|
| 283 |
+
parser.add_argument(
|
| 284 |
+
"--docs-per-subdomain",
|
| 285 |
+
type=int,
|
| 286 |
+
default=DOCS_PER_SUBDOMAIN,
|
| 287 |
+
help="Documents to fetch per legal subdomain (default: %(default)s).",
|
| 288 |
+
)
|
| 289 |
+
args = parser.parse_args()
|
| 290 |
+
main(docs_per_subdomain=args.docs_per_subdomain)
|
|
|
|
|
|
|
|
|
|
|
|
backend/scripts/fetch_pubmed.py
CHANGED
|
@@ -1,333 +1,329 @@
|
|
| 1 |
-
"""
|
| 2 |
-
MEXAR - PubMed Central OA fetcher for the medical corpus.
|
| 3 |
-
|
| 4 |
-
Pulls ~220 documents from PubMed Central Open Access across three medical
|
| 5 |
-
subdomains (cardiology, oncology, internal medicine), parses the full-text XML
|
| 6 |
-
into plain text, and writes one .txt file per document into
|
| 7 |
-
test_data/medical_real/, along with a manifest.json for auditing.
|
| 8 |
-
|
| 9 |
-
Usage:
|
| 10 |
-
python backend/scripts/fetch_pubmed.py
|
| 11 |
-
|
| 12 |
-
Requirements:
|
| 13 |
-
pip install requests lxml
|
| 14 |
-
|
| 15 |
-
Environment:
|
| 16 |
-
NCBI_EMAIL — your email address (required by NCBI usage policy)
|
| 17 |
-
NCBI_API_KEY — optional; raises rate limit from 3 req/sec to 10 req/sec
|
| 18 |
-
|
| 19 |
-
Run this locally (not in a network-restricted sandbox).
|
| 20 |
-
"""
|
| 21 |
-
import argparse
|
| 22 |
-
import json
|
| 23 |
-
import logging
|
| 24 |
-
import os
|
| 25 |
-
import sys
|
| 26 |
-
import time
|
| 27 |
-
from pathlib import Path
|
| 28 |
-
from typing import Dict, List, Optional
|
| 29 |
-
|
| 30 |
-
import requests
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
# ---------------------------------------------------------------------------
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
"oncology":
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
#
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
if
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
plain = "\n\n"
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
logger.
|
| 258 |
-
continue
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
"
|
| 271 |
-
"
|
| 272 |
-
"
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
help="Number of documents to fetch per subdomain (default: %(default)s).",
|
| 331 |
-
)
|
| 332 |
-
args = parser.parse_args()
|
| 333 |
-
main(email=args.email, docs_per_subdomain=args.docs_per_subdomain)
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MEXAR - PubMed Central OA fetcher for the medical corpus.
|
| 3 |
+
|
| 4 |
+
Pulls ~220 documents from PubMed Central Open Access across three medical
|
| 5 |
+
subdomains (cardiology, oncology, internal medicine), parses the full-text XML
|
| 6 |
+
into plain text, and writes one .txt file per document into
|
| 7 |
+
test_data/medical_real/, along with a manifest.json for auditing.
|
| 8 |
+
|
| 9 |
+
Usage:
|
| 10 |
+
python backend/scripts/fetch_pubmed.py
|
| 11 |
+
|
| 12 |
+
Requirements:
|
| 13 |
+
pip install requests lxml
|
| 14 |
+
|
| 15 |
+
Environment:
|
| 16 |
+
NCBI_EMAIL — your email address (required by NCBI usage policy)
|
| 17 |
+
NCBI_API_KEY — optional; raises rate limit from 3 req/sec to 10 req/sec
|
| 18 |
+
|
| 19 |
+
Run this locally (not in a network-restricted sandbox).
|
| 20 |
+
"""
|
| 21 |
+
import argparse
|
| 22 |
+
import json
|
| 23 |
+
import logging
|
| 24 |
+
import os
|
| 25 |
+
import sys
|
| 26 |
+
import time
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
from typing import Dict, List, Optional
|
| 29 |
+
|
| 30 |
+
import requests
|
| 31 |
+
from dotenv import load_dotenv
|
| 32 |
+
load_dotenv()
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
from lxml import etree
|
| 36 |
+
except ImportError:
|
| 37 |
+
sys.exit(
|
| 38 |
+
"lxml is required — run: pip install lxml"
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# ---------------------------------------------------------------------------
|
| 42 |
+
# Configuration
|
| 43 |
+
# ---------------------------------------------------------------------------
|
| 44 |
+
|
| 45 |
+
logging.basicConfig(
|
| 46 |
+
level=logging.INFO,
|
| 47 |
+
format="%(asctime)s [%(levelname)s] %(message)s",
|
| 48 |
+
)
|
| 49 |
+
logger = logging.getLogger(__name__)
|
| 50 |
+
|
| 51 |
+
# NCBI email is mandatory per NCBI E-utilities usage policy.
|
| 52 |
+
# Override with the NCBI_EMAIL environment variable or the --email CLI flag.
|
| 53 |
+
DEFAULT_NCBI_EMAIL: str = os.getenv("NCBI_EMAIL", "your_email@example.com")
|
| 54 |
+
NCBI_API_KEY: Optional[str] = os.getenv("NCBI_API_KEY") # optional; raises rate cap to 10/s
|
| 55 |
+
|
| 56 |
+
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
|
| 57 |
+
OUTPUT_DIR = REPO_ROOT / "test_data" / "medical_real"
|
| 58 |
+
|
| 59 |
+
# Three subdomains × 75 docs ≈ 220 total (some may fail or be empty).
|
| 60 |
+
SEARCH_TERMS: Dict[str, str] = {
|
| 61 |
+
"cardiology": "cardiology AND open access[filter]",
|
| 62 |
+
"oncology": "oncology AND open access[filter]",
|
| 63 |
+
"internal_medicine": "internal medicine AND open access[filter]",
|
| 64 |
+
}
|
| 65 |
+
DOCS_PER_SUBDOMAIN: int = 75
|
| 66 |
+
|
| 67 |
+
# NCBI rate limits: 3 req/sec without key, 10/sec with key.
|
| 68 |
+
REQUEST_DELAY_NO_KEY: float = 0.35 # ≈ 2.9 req/sec — safe without key
|
| 69 |
+
REQUEST_DELAY_WITH_KEY: float = 0.12 # ≈ 8.3 req/sec — safe with key
|
| 70 |
+
|
| 71 |
+
NCBI_BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
|
| 72 |
+
|
| 73 |
+
# ---------------------------------------------------------------------------
|
| 74 |
+
# NCBI E-utilities helpers
|
| 75 |
+
# ---------------------------------------------------------------------------
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _ncbi_params(extra: dict, email: str) -> dict:
|
| 79 |
+
"""Return base params dict merged with extra, including optional API key."""
|
| 80 |
+
params = {"email": email, **extra}
|
| 81 |
+
if NCBI_API_KEY:
|
| 82 |
+
params["api_key"] = NCBI_API_KEY
|
| 83 |
+
return params
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def esearch(term: str, retmax: int, email: str) -> List[str]:
|
| 87 |
+
"""
|
| 88 |
+
Run an E-search against the PMC database and return a list of PMC IDs.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
term: NCBI search query string.
|
| 92 |
+
retmax: Maximum number of IDs to retrieve.
|
| 93 |
+
email: Requester email (required by NCBI).
|
| 94 |
+
|
| 95 |
+
Returns:
|
| 96 |
+
List of PMC ID strings (numeric, without the 'PMC' prefix).
|
| 97 |
+
"""
|
| 98 |
+
url = f"{NCBI_BASE}/esearch.fcgi"
|
| 99 |
+
params = _ncbi_params(
|
| 100 |
+
{"db": "pmc", "term": term, "retmax": retmax, "retmode": "json"},
|
| 101 |
+
email,
|
| 102 |
+
)
|
| 103 |
+
resp = requests.get(url, params=params, timeout=30)
|
| 104 |
+
resp.raise_for_status()
|
| 105 |
+
return resp.json()["esearchresult"]["idlist"]
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def efetch_xml(pmc_id: str, email: str) -> bytes:
|
| 109 |
+
"""
|
| 110 |
+
Fetch the full-text XML for a single PMC article.
|
| 111 |
+
|
| 112 |
+
Args:
|
| 113 |
+
pmc_id: Numeric PMC ID string (without the 'PMC' prefix).
|
| 114 |
+
email: Requester email.
|
| 115 |
+
|
| 116 |
+
Returns:
|
| 117 |
+
Raw XML bytes from the PMC OA full-text endpoint.
|
| 118 |
+
"""
|
| 119 |
+
url = f"{NCBI_BASE}/efetch.fcgi"
|
| 120 |
+
params = _ncbi_params(
|
| 121 |
+
{"db": "pmc", "id": pmc_id, "rettype": "full", "retmode": "xml"},
|
| 122 |
+
email,
|
| 123 |
+
)
|
| 124 |
+
resp = requests.get(url, params=params, timeout=60)
|
| 125 |
+
resp.raise_for_status()
|
| 126 |
+
return resp.content
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
# ---------------------------------------------------------------------------
|
| 130 |
+
# XML → plain-text extraction
|
| 131 |
+
# ---------------------------------------------------------------------------
|
| 132 |
+
|
| 133 |
+
# Tags in the JATS PMC XML that contain useful body text.
|
| 134 |
+
_BODY_TAGS = {"abstract", "body", "sec", "p", "title"}
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def _iter_text(element) -> str:
|
| 138 |
+
"""
|
| 139 |
+
Recursively collect all text inside an lxml element, with
|
| 140 |
+
paragraph-level whitespace to keep the output readable.
|
| 141 |
+
"""
|
| 142 |
+
parts = []
|
| 143 |
+
if element.text:
|
| 144 |
+
parts.append(element.text.strip())
|
| 145 |
+
for child in element:
|
| 146 |
+
child_text = _iter_text(child)
|
| 147 |
+
if child_text:
|
| 148 |
+
# Add a blank line between sections/paragraphs.
|
| 149 |
+
sep = "\n\n" if child.tag in {"sec", "p", "title", "abstract"} else " "
|
| 150 |
+
parts.append(sep + child_text)
|
| 151 |
+
if child.tail:
|
| 152 |
+
parts.append(child.tail.strip())
|
| 153 |
+
return " ".join(p for p in parts if p)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def xml_to_plain_text(xml_bytes: bytes) -> str:
|
| 157 |
+
"""
|
| 158 |
+
Parse PMC full-text XML and extract human-readable plain text.
|
| 159 |
+
|
| 160 |
+
Pulls text from <abstract>, <body>, and nested <sec>/<p> elements,
|
| 161 |
+
strips all XML markup, and returns a clean, whitespace-normalised string.
|
| 162 |
+
|
| 163 |
+
Args:
|
| 164 |
+
xml_bytes: Raw XML bytes as returned by efetch.
|
| 165 |
+
|
| 166 |
+
Returns:
|
| 167 |
+
Plain-text string. Empty string if parsing fails.
|
| 168 |
+
"""
|
| 169 |
+
try:
|
| 170 |
+
root = etree.fromstring(xml_bytes)
|
| 171 |
+
except etree.XMLSyntaxError as exc:
|
| 172 |
+
logger.warning("XML parse error: %s", exc)
|
| 173 |
+
return ""
|
| 174 |
+
|
| 175 |
+
sections: List[str] = []
|
| 176 |
+
# Pull <article-title>, <abstract>, and <body> in document order.
|
| 177 |
+
for tag in ("article-title", "abstract", "body"):
|
| 178 |
+
for element in root.iter(tag):
|
| 179 |
+
text = _iter_text(element).strip()
|
| 180 |
+
if text:
|
| 181 |
+
sections.append(text)
|
| 182 |
+
|
| 183 |
+
plain = "\n\n".join(sections)
|
| 184 |
+
# Collapse excessive whitespace.
|
| 185 |
+
import re
|
| 186 |
+
plain = re.sub(r" {2,}", " ", plain)
|
| 187 |
+
plain = re.sub(r"\n{3,}", "\n\n", plain)
|
| 188 |
+
return plain.strip()
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def _get_article_meta(xml_bytes: bytes) -> Dict[str, str]:
|
| 192 |
+
"""Extract title and journal name from parsed PMC XML for the manifest."""
|
| 193 |
+
meta: Dict[str, str] = {"title": "", "journal": ""}
|
| 194 |
+
try:
|
| 195 |
+
root = etree.fromstring(xml_bytes)
|
| 196 |
+
title_el = root.find(".//article-title")
|
| 197 |
+
if title_el is not None:
|
| 198 |
+
meta["title"] = (_iter_text(title_el) or "").strip()
|
| 199 |
+
journal_el = root.find(".//journal-title")
|
| 200 |
+
if journal_el is not None:
|
| 201 |
+
meta["journal"] = (journal_el.text or "").strip()
|
| 202 |
+
except Exception:
|
| 203 |
+
pass
|
| 204 |
+
return meta
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
# ---------------------------------------------------------------------------
|
| 208 |
+
# Main fetch loop
|
| 209 |
+
# ---------------------------------------------------------------------------
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def fetch_subdomain(
|
| 213 |
+
subdomain: str,
|
| 214 |
+
term: str,
|
| 215 |
+
n: int,
|
| 216 |
+
output_dir: Path,
|
| 217 |
+
email: str,
|
| 218 |
+
delay: float,
|
| 219 |
+
) -> List[Dict]:
|
| 220 |
+
"""
|
| 221 |
+
Search for and download up to `n` PMC articles matching `term`.
|
| 222 |
+
|
| 223 |
+
Args:
|
| 224 |
+
subdomain: Label string used in the manifest.
|
| 225 |
+
term: NCBI search query.
|
| 226 |
+
n: Target number of documents.
|
| 227 |
+
output_dir: Directory to write .txt files into.
|
| 228 |
+
email: NCBI requester email.
|
| 229 |
+
delay: Seconds to sleep between requests.
|
| 230 |
+
|
| 231 |
+
Returns:
|
| 232 |
+
List of manifest entry dicts for successfully fetched documents.
|
| 233 |
+
"""
|
| 234 |
+
logger.info("Subdomain '%s': searching for %d docs …", subdomain, n)
|
| 235 |
+
try:
|
| 236 |
+
ids = esearch(term, n, email)
|
| 237 |
+
except Exception as exc:
|
| 238 |
+
logger.error("esearch failed for '%s': %s", subdomain, exc)
|
| 239 |
+
return []
|
| 240 |
+
|
| 241 |
+
logger.info(" Found %d IDs; fetching full text …", len(ids))
|
| 242 |
+
entries: List[Dict] = []
|
| 243 |
+
|
| 244 |
+
for pmc_id in ids:
|
| 245 |
+
time.sleep(delay)
|
| 246 |
+
try:
|
| 247 |
+
xml_bytes = efetch_xml(pmc_id, email)
|
| 248 |
+
plain = xml_to_plain_text(xml_bytes)
|
| 249 |
+
except requests.HTTPError as exc:
|
| 250 |
+
logger.warning("HTTP error for PMC%s: %s", pmc_id, exc)
|
| 251 |
+
continue
|
| 252 |
+
except Exception as exc:
|
| 253 |
+
logger.warning("Failed PMC%s: %s", pmc_id, exc)
|
| 254 |
+
continue
|
| 255 |
+
|
| 256 |
+
if len(plain) < 200:
|
| 257 |
+
logger.debug("PMC%s: text too short (%d chars), skipping.", pmc_id, len(plain))
|
| 258 |
+
continue
|
| 259 |
+
|
| 260 |
+
out_path = output_dir / f"PMC{pmc_id}.txt"
|
| 261 |
+
out_path.write_text(plain, encoding="utf-8")
|
| 262 |
+
|
| 263 |
+
meta = _get_article_meta(xml_bytes)
|
| 264 |
+
entries.append(
|
| 265 |
+
{
|
| 266 |
+
"pmc_id": pmc_id,
|
| 267 |
+
"subdomain": subdomain,
|
| 268 |
+
"title": meta["title"],
|
| 269 |
+
"journal": meta["journal"],
|
| 270 |
+
"source_url": f"https://www.ncbi.nlm.nih.gov/pmc/articles/PMC{pmc_id}/",
|
| 271 |
+
"path": str(out_path),
|
| 272 |
+
"char_count": len(plain),
|
| 273 |
+
}
|
| 274 |
+
)
|
| 275 |
+
logger.info(" Saved PMC%s (%d chars)", pmc_id, len(plain))
|
| 276 |
+
|
| 277 |
+
logger.info(" Subdomain '%s': %d documents saved.", subdomain, len(entries))
|
| 278 |
+
return entries
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def main(email: str = DEFAULT_NCBI_EMAIL, docs_per_subdomain: int = DOCS_PER_SUBDOMAIN) -> None:
|
| 282 |
+
"""
|
| 283 |
+
Entry point. Iterates over all three medical subdomains and writes results.
|
| 284 |
+
"""
|
| 285 |
+
if email == "your_email@example.com":
|
| 286 |
+
logger.warning(
|
| 287 |
+
"Using placeholder email — set the NCBI_EMAIL env var or pass --email. "
|
| 288 |
+
"NCBI may throttle requests without a valid email."
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 292 |
+
|
| 293 |
+
delay = REQUEST_DELAY_WITH_KEY if NCBI_API_KEY else REQUEST_DELAY_NO_KEY
|
| 294 |
+
manifest: List[Dict] = []
|
| 295 |
+
|
| 296 |
+
for subdomain, term in SEARCH_TERMS.items():
|
| 297 |
+
entries = fetch_subdomain(subdomain, term, docs_per_subdomain, OUTPUT_DIR, email, delay)
|
| 298 |
+
manifest.extend(entries)
|
| 299 |
+
|
| 300 |
+
manifest_path = OUTPUT_DIR / "manifest.json"
|
| 301 |
+
manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
| 302 |
+
logger.info(
|
| 303 |
+
"Done. %d medical documents fetched. Manifest → %s",
|
| 304 |
+
len(manifest),
|
| 305 |
+
manifest_path,
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
# ---------------------------------------------------------------------------
|
| 310 |
+
# CLI
|
| 311 |
+
# ---------------------------------------------------------------------------
|
| 312 |
+
|
| 313 |
+
if __name__ == "__main__":
|
| 314 |
+
parser = argparse.ArgumentParser(
|
| 315 |
+
description="Fetch PubMed Central OA articles for the MEXAR medical corpus."
|
| 316 |
+
)
|
| 317 |
+
parser.add_argument(
|
| 318 |
+
"--email",
|
| 319 |
+
default=DEFAULT_NCBI_EMAIL,
|
| 320 |
+
help="Your email address (required by NCBI usage policy).",
|
| 321 |
+
)
|
| 322 |
+
parser.add_argument(
|
| 323 |
+
"--docs-per-subdomain",
|
| 324 |
+
type=int,
|
| 325 |
+
default=DOCS_PER_SUBDOMAIN,
|
| 326 |
+
help="Number of documents to fetch per subdomain (default: %(default)s).",
|
| 327 |
+
)
|
| 328 |
+
args = parser.parse_args()
|
| 329 |
+
main(email=args.email, docs_per_subdomain=args.docs_per_subdomain)
|
|
|
|
|
|
|
|
|
|
|
|
backend/scripts/fetch_secedgar.py
CHANGED
|
@@ -1,467 +1,470 @@
|
|
| 1 |
-
"""
|
| 2 |
-
MEXAR - SEC EDGAR fetcher for the financial corpus.
|
| 3 |
-
|
| 4 |
-
Fetches ~130 10-K filing sections (MD&A, Risk Factors) from SEC EDGAR using
|
| 5 |
-
the full-text search API, follows through to the actual EDGAR filing documents,
|
| 6 |
-
extracts the relevant section text, and saves one .txt file per document plus
|
| 7 |
-
a manifest.json.
|
| 8 |
-
|
| 9 |
-
Usage:
|
| 10 |
-
python backend/scripts/fetch_secedgar.py
|
| 11 |
-
|
| 12 |
-
Requirements:
|
| 13 |
-
pip install requests beautifulsoup4 lxml
|
| 14 |
-
|
| 15 |
-
SEC Fair Access Policy:
|
| 16 |
-
Every request MUST include a descriptive User-Agent header in the format:
|
| 17 |
-
"FirstName LastName email@example.com"
|
| 18 |
-
without this, SEC will block your IP.
|
| 19 |
-
|
| 20 |
-
Override with the SEC_USER_AGENT environment variable.
|
| 21 |
-
|
| 22 |
-
Run this locally (not in a network-restricted sandbox).
|
| 23 |
-
"""
|
| 24 |
-
import argparse
|
| 25 |
-
import json
|
| 26 |
-
import logging
|
| 27 |
-
import os
|
| 28 |
-
import re
|
| 29 |
-
import sys
|
| 30 |
-
import time
|
| 31 |
-
from pathlib import Path
|
| 32 |
-
from typing import Dict, List, Optional, Tuple
|
| 33 |
-
from urllib.parse import urljoin
|
| 34 |
-
|
| 35 |
-
import requests
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
# ---------------------------------------------------------------------------
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
"
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
#
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
#
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
"
|
| 122 |
-
"
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
)
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
return
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
"
|
| 408 |
-
"
|
| 409 |
-
"
|
| 410 |
-
"
|
| 411 |
-
"
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
parser.
|
| 455 |
-
"-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MEXAR - SEC EDGAR fetcher for the financial corpus.
|
| 3 |
+
|
| 4 |
+
Fetches ~130 10-K filing sections (MD&A, Risk Factors) from SEC EDGAR using
|
| 5 |
+
the full-text search API, follows through to the actual EDGAR filing documents,
|
| 6 |
+
extracts the relevant section text, and saves one .txt file per document plus
|
| 7 |
+
a manifest.json.
|
| 8 |
+
|
| 9 |
+
Usage:
|
| 10 |
+
python backend/scripts/fetch_secedgar.py
|
| 11 |
+
|
| 12 |
+
Requirements:
|
| 13 |
+
pip install requests beautifulsoup4 lxml
|
| 14 |
+
|
| 15 |
+
SEC Fair Access Policy:
|
| 16 |
+
Every request MUST include a descriptive User-Agent header in the format:
|
| 17 |
+
"FirstName LastName email@example.com"
|
| 18 |
+
without this, SEC will block your IP.
|
| 19 |
+
|
| 20 |
+
Override with the SEC_USER_AGENT environment variable.
|
| 21 |
+
|
| 22 |
+
Run this locally (not in a network-restricted sandbox).
|
| 23 |
+
"""
|
| 24 |
+
import argparse
|
| 25 |
+
import json
|
| 26 |
+
import logging
|
| 27 |
+
import os
|
| 28 |
+
import re
|
| 29 |
+
import sys
|
| 30 |
+
import time
|
| 31 |
+
from pathlib import Path
|
| 32 |
+
from typing import Dict, List, Optional, Tuple
|
| 33 |
+
from urllib.parse import urljoin
|
| 34 |
+
|
| 35 |
+
import requests
|
| 36 |
+
from dotenv import load_dotenv
|
| 37 |
+
load_dotenv()
|
| 38 |
+
|
| 39 |
+
try:
|
| 40 |
+
from bs4 import BeautifulSoup
|
| 41 |
+
except ImportError:
|
| 42 |
+
sys.exit("beautifulsoup4 is required — run: pip install beautifulsoup4 lxml")
|
| 43 |
+
|
| 44 |
+
# ---------------------------------------------------------------------------
|
| 45 |
+
# Configuration
|
| 46 |
+
# ---------------------------------------------------------------------------
|
| 47 |
+
|
| 48 |
+
logging.basicConfig(
|
| 49 |
+
level=logging.INFO,
|
| 50 |
+
format="%(asctime)s [%(levelname)s] %(message)s",
|
| 51 |
+
)
|
| 52 |
+
logger = logging.getLogger(__name__)
|
| 53 |
+
|
| 54 |
+
# SEC requires a descriptive User-Agent on every request.
|
| 55 |
+
# Format: "Name Email" — use yours or set SEC_USER_AGENT env var.
|
| 56 |
+
_DEFAULT_USER_AGENT = "Devrajsinh Gohil djgohil2012@gmail.com"
|
| 57 |
+
USER_AGENT: str = os.getenv("SEC_USER_AGENT", _DEFAULT_USER_AGENT)
|
| 58 |
+
|
| 59 |
+
HEADERS: Dict[str, str] = {
|
| 60 |
+
"User-Agent": USER_AGENT,
|
| 61 |
+
"Accept-Encoding": "gzip, deflate",
|
| 62 |
+
"Host": "efts.sec.gov",
|
| 63 |
+
}
|
| 64 |
+
EDGAR_HEADERS: Dict[str, str] = {
|
| 65 |
+
"User-Agent": USER_AGENT,
|
| 66 |
+
"Accept-Encoding": "gzip, deflate",
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
|
| 70 |
+
OUTPUT_DIR = REPO_ROOT / "test_data" / "financial_real"
|
| 71 |
+
|
| 72 |
+
# Search terms and per-term document targets (~130 total).
|
| 73 |
+
SEARCH_CONFIG: List[Tuple[str, str, int]] = [
|
| 74 |
+
("risk_factors", "risk factors", 45),
|
| 75 |
+
("management_discussion", "management discussion and analysis", 45),
|
| 76 |
+
("accounting_standards", "GAAP IFRS accounting standards", 40),
|
| 77 |
+
]
|
| 78 |
+
|
| 79 |
+
# Maximum characters per saved document — keep consistent with medical/legal sizes.
|
| 80 |
+
MAX_CHARS: int = 15_000
|
| 81 |
+
|
| 82 |
+
# Be polite to SEC servers.
|
| 83 |
+
REQUEST_DELAY: float = 0.25 # seconds
|
| 84 |
+
|
| 85 |
+
# EDGAR full-text search endpoint.
|
| 86 |
+
EFTS_URL = "https://efts.sec.gov/LATEST/search-index"
|
| 87 |
+
EDGAR_ARCHIVES = "https://www.sec.gov/Archives/edgar/data"
|
| 88 |
+
|
| 89 |
+
# ---------------------------------------------------------------------------
|
| 90 |
+
# SEC EDGAR API helpers
|
| 91 |
+
# ---------------------------------------------------------------------------
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _get(url: str, params: dict = None, extra_headers: dict = None) -> requests.Response:
|
| 95 |
+
"""GET with SEC-compliant headers and error handling."""
|
| 96 |
+
h = {**EDGAR_HEADERS}
|
| 97 |
+
if extra_headers:
|
| 98 |
+
h.update(extra_headers)
|
| 99 |
+
resp = requests.get(url, headers=h, params=params or {}, timeout=30)
|
| 100 |
+
resp.raise_for_status()
|
| 101 |
+
return resp
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def full_text_search(query: str, count: int) -> List[dict]:
|
| 105 |
+
"""
|
| 106 |
+
Query the SEC EDGAR full-text search API for 10-K filings.
|
| 107 |
+
|
| 108 |
+
The EFTS search endpoint returns hit metadata only — not the filing body.
|
| 109 |
+
Each hit contains the accession number and CIK needed to locate the filing
|
| 110 |
+
document in EDGAR archives.
|
| 111 |
+
|
| 112 |
+
Args:
|
| 113 |
+
query: Search phrase (e.g. "risk factors").
|
| 114 |
+
count: Maximum number of hits to return.
|
| 115 |
+
|
| 116 |
+
Returns:
|
| 117 |
+
List of _source dicts from the EFTS hits array.
|
| 118 |
+
"""
|
| 119 |
+
# EFTS search uses query-string encoding.
|
| 120 |
+
params = {
|
| 121 |
+
"q": f'"{query}"',
|
| 122 |
+
"forms": "10-K",
|
| 123 |
+
"dateRange": "custom",
|
| 124 |
+
"startdt": "2020-01-01",
|
| 125 |
+
"enddt": "2024-12-31",
|
| 126 |
+
}
|
| 127 |
+
try:
|
| 128 |
+
resp = _get(
|
| 129 |
+
EFTS_URL,
|
| 130 |
+
params=params,
|
| 131 |
+
extra_headers={"Host": "efts.sec.gov"},
|
| 132 |
+
)
|
| 133 |
+
except Exception as exc:
|
| 134 |
+
logger.error("EFTS search failed for '%s': %s", query, exc)
|
| 135 |
+
return []
|
| 136 |
+
|
| 137 |
+
data = resp.json()
|
| 138 |
+
hits = data.get("hits", {}).get("hits", [])
|
| 139 |
+
logger.info(" EFTS returned %d hits for '%s'", len(hits), query)
|
| 140 |
+
return [h.get("_source", {}) for h in hits[:count]]
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
# ---------------------------------------------------------------------------
|
| 144 |
+
# Filing document text extraction
|
| 145 |
+
# ---------------------------------------------------------------------------
|
| 146 |
+
|
| 147 |
+
def _accession_to_path(accession: str) -> str:
|
| 148 |
+
"""Convert '0001234567-23-012345' → '000123456723012345'."""
|
| 149 |
+
return accession.replace("-", "")
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _get_filing_index(cik: str, accession: str) -> Optional[dict]:
|
| 153 |
+
"""
|
| 154 |
+
Fetch the filing index JSON from EDGAR to find the primary document URL.
|
| 155 |
+
|
| 156 |
+
Returns the parsed index data dict, or None on failure.
|
| 157 |
+
"""
|
| 158 |
+
cik_int = str(int(cik)) if cik.isdigit() else cik
|
| 159 |
+
acc_path = _accession_to_path(accession)
|
| 160 |
+
url = (
|
| 161 |
+
f"https://data.sec.gov/submissions/CIK{cik_int.zfill(10)}.json"
|
| 162 |
+
)
|
| 163 |
+
# Use the filing index endpoint instead.
|
| 164 |
+
index_url = (
|
| 165 |
+
f"https://www.sec.gov/cgi-bin/browse-edgar"
|
| 166 |
+
f"?action=getcompany&CIK={cik_int}&type=10-K&dateb=&owner=include&count=1&search_text="
|
| 167 |
+
)
|
| 168 |
+
# Directly hit the accession-number based index.
|
| 169 |
+
direct_url = (
|
| 170 |
+
f"https://www.sec.gov/Archives/edgar/data/{cik_int}/{acc_path}/{accession}-index.json"
|
| 171 |
+
)
|
| 172 |
+
try:
|
| 173 |
+
resp = _get(direct_url)
|
| 174 |
+
return resp.json()
|
| 175 |
+
except Exception:
|
| 176 |
+
pass
|
| 177 |
+
# Fallback: try the .htm index.
|
| 178 |
+
try:
|
| 179 |
+
htm_url = (
|
| 180 |
+
f"https://www.sec.gov/Archives/edgar/data/{cik_int}/{acc_path}/{accession}-index.htm"
|
| 181 |
+
)
|
| 182 |
+
resp = _get(htm_url)
|
| 183 |
+
# Parse the index page for the primary document link.
|
| 184 |
+
soup = BeautifulSoup(resp.text, "lxml")
|
| 185 |
+
return {"_html": resp.text, "_base": f"https://www.sec.gov/Archives/edgar/data/{cik_int}/{acc_path}/"}
|
| 186 |
+
except Exception as exc:
|
| 187 |
+
logger.debug("Filing index fetch failed for %s/%s: %s", cik, accession, exc)
|
| 188 |
+
return None
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def _find_primary_document_url(cik: str, accession: str, index_data: dict) -> Optional[str]:
|
| 192 |
+
"""
|
| 193 |
+
Given filing index data, return the URL of the primary 10-K HTML document.
|
| 194 |
+
"""
|
| 195 |
+
acc_path = _accession_to_path(accession)
|
| 196 |
+
base = f"https://www.sec.gov/Archives/edgar/data/{cik}/{acc_path}/"
|
| 197 |
+
|
| 198 |
+
# JSON index format (modern filings).
|
| 199 |
+
if "documents" in index_data:
|
| 200 |
+
for doc in index_data.get("documents", []):
|
| 201 |
+
doc_type = doc.get("type", "").upper()
|
| 202 |
+
if doc_type in ("10-K", "10-K/A"):
|
| 203 |
+
filename = doc.get("filename") or doc.get("documentName", "")
|
| 204 |
+
if filename:
|
| 205 |
+
return base + filename
|
| 206 |
+
|
| 207 |
+
# HTML index fallback.
|
| 208 |
+
if "_html" in index_data:
|
| 209 |
+
soup = BeautifulSoup(index_data["_html"], "lxml")
|
| 210 |
+
for row in soup.find_all("tr"):
|
| 211 |
+
cells = row.find_all("td")
|
| 212 |
+
if len(cells) >= 3:
|
| 213 |
+
doc_type = cells[3].get_text(strip=True) if len(cells) > 3 else ""
|
| 214 |
+
if "10-K" in doc_type:
|
| 215 |
+
link = cells[2].find("a")
|
| 216 |
+
if link and link.get("href"):
|
| 217 |
+
href = link["href"]
|
| 218 |
+
if href.startswith("/"):
|
| 219 |
+
return "https://www.sec.gov" + href
|
| 220 |
+
return href
|
| 221 |
+
# If no 10-K row found, grab any .htm link from the table.
|
| 222 |
+
for link in soup.select("table a[href]"):
|
| 223 |
+
href = link["href"]
|
| 224 |
+
if href.endswith(".htm") or href.endswith(".html"):
|
| 225 |
+
if href.startswith("/"):
|
| 226 |
+
return "https://www.sec.gov" + href
|
| 227 |
+
return href
|
| 228 |
+
|
| 229 |
+
return None
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def _extract_section(html: str, section: str) -> str:
|
| 233 |
+
"""
|
| 234 |
+
Extract a named section (e.g. 'risk factors', 'management discussion')
|
| 235 |
+
from a 10-K HTML document.
|
| 236 |
+
|
| 237 |
+
Strategy:
|
| 238 |
+
1. Parse with BeautifulSoup.
|
| 239 |
+
2. Find the first heading element (<b>, <strong>, <h2>, <h3>, <p>) whose
|
| 240 |
+
text closely matches the section name.
|
| 241 |
+
3. Collect all subsequent paragraph text until the next major heading.
|
| 242 |
+
|
| 243 |
+
Falls back to returning the first 15,000 chars of the full body text if
|
| 244 |
+
no matching section heading is found.
|
| 245 |
+
"""
|
| 246 |
+
soup = BeautifulSoup(html, "lxml")
|
| 247 |
+
|
| 248 |
+
# Remove script/style noise.
|
| 249 |
+
for tag in soup(["script", "style", "meta", "link"]):
|
| 250 |
+
tag.decompose()
|
| 251 |
+
|
| 252 |
+
section_lower = section.lower()
|
| 253 |
+
# Headings to search in.
|
| 254 |
+
heading_candidates = soup.find_all(
|
| 255 |
+
["b", "strong", "h1", "h2", "h3", "h4", "p"],
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
section_start = None
|
| 259 |
+
for el in heading_candidates:
|
| 260 |
+
text = el.get_text(" ", strip=True).lower()
|
| 261 |
+
if section_lower in text and len(text) < 200:
|
| 262 |
+
section_start = el
|
| 263 |
+
break
|
| 264 |
+
|
| 265 |
+
if section_start is None:
|
| 266 |
+
# No section heading found — return the full text (truncated).
|
| 267 |
+
full_text = soup.get_text(" ", strip=True)
|
| 268 |
+
return re.sub(r" {2,}", " ", full_text)
|
| 269 |
+
|
| 270 |
+
# Collect text from siblings/following elements until next major heading.
|
| 271 |
+
parts: List[str] = []
|
| 272 |
+
total = 0
|
| 273 |
+
for sibling in section_start.find_all_next():
|
| 274 |
+
tag_name = sibling.name or ""
|
| 275 |
+
text = sibling.get_text(" ", strip=True)
|
| 276 |
+
if not text:
|
| 277 |
+
continue
|
| 278 |
+
|
| 279 |
+
# Stop at the next major heading that looks like a different section.
|
| 280 |
+
if tag_name in ("h1", "h2", "h3"):
|
| 281 |
+
break
|
| 282 |
+
if tag_name in ("b", "strong", "p") and len(text) < 200:
|
| 283 |
+
text_lower = text.lower()
|
| 284 |
+
# Check for common 10-K section heading patterns.
|
| 285 |
+
if re.match(r"(item\s+\d|part\s+[iv]+)", text_lower):
|
| 286 |
+
break
|
| 287 |
+
|
| 288 |
+
parts.append(text)
|
| 289 |
+
total += len(text)
|
| 290 |
+
if total >= MAX_CHARS:
|
| 291 |
+
break
|
| 292 |
+
|
| 293 |
+
extracted = " ".join(parts)
|
| 294 |
+
extracted = re.sub(r" {2,}", " ", extracted)
|
| 295 |
+
return extracted.strip()
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def fetch_and_extract(source: dict, query_label: str) -> Tuple[str, str, int]:
|
| 299 |
+
"""
|
| 300 |
+
Follow through from an EFTS hit's metadata to the actual filing text.
|
| 301 |
+
|
| 302 |
+
Args:
|
| 303 |
+
source: _source dict from an EFTS hit.
|
| 304 |
+
query_label: Human-readable label for the search term (used for
|
| 305 |
+
section extraction hints).
|
| 306 |
+
|
| 307 |
+
Returns:
|
| 308 |
+
(text, filing_url, char_count) — text is empty string on failure.
|
| 309 |
+
"""
|
| 310 |
+
cik_list = source.get("ciks", [])
|
| 311 |
+
cik = str(cik_list[0]) if cik_list else source.get("cik", "")
|
| 312 |
+
accession = source.get("adsh", "")
|
| 313 |
+
|
| 314 |
+
if not cik or not accession:
|
| 315 |
+
return "", "", 0
|
| 316 |
+
|
| 317 |
+
acc_path = _accession_to_path(accession)
|
| 318 |
+
filing_url = f"https://www.sec.gov/Archives/edgar/data/{cik}/{acc_path}/"
|
| 319 |
+
|
| 320 |
+
time.sleep(REQUEST_DELAY)
|
| 321 |
+
|
| 322 |
+
# Step 1: Get filing index to find the primary document.
|
| 323 |
+
index_data = _get_filing_index(cik, accession)
|
| 324 |
+
if not index_data:
|
| 325 |
+
logger.debug("No index for %s / %s", cik, accession)
|
| 326 |
+
return "", filing_url, 0
|
| 327 |
+
|
| 328 |
+
doc_url = _find_primary_document_url(cik, acc_path, index_data)
|
| 329 |
+
if not doc_url:
|
| 330 |
+
logger.debug("Could not locate primary doc for %s / %s", cik, accession)
|
| 331 |
+
return "", filing_url, 0
|
| 332 |
+
|
| 333 |
+
time.sleep(REQUEST_DELAY)
|
| 334 |
+
|
| 335 |
+
# Step 2: Fetch the 10-K document.
|
| 336 |
+
try:
|
| 337 |
+
resp = _get(doc_url)
|
| 338 |
+
html = resp.text
|
| 339 |
+
except Exception as exc:
|
| 340 |
+
logger.debug("Failed to fetch filing document %s: %s", doc_url, exc)
|
| 341 |
+
return "", filing_url, 0
|
| 342 |
+
|
| 343 |
+
# Step 3: Extract the relevant section.
|
| 344 |
+
section_hint = query_label.replace("_", " ")
|
| 345 |
+
text = _extract_section(html, section_hint)
|
| 346 |
+
if len(text) > MAX_CHARS:
|
| 347 |
+
text = text[:MAX_CHARS]
|
| 348 |
+
|
| 349 |
+
return text, doc_url, len(text)
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
# ---------------------------------------------------------------------------
|
| 353 |
+
# Main fetch loop
|
| 354 |
+
# ---------------------------------------------------------------------------
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def fetch_term(
|
| 358 |
+
label: str,
|
| 359 |
+
query: str,
|
| 360 |
+
n: int,
|
| 361 |
+
output_dir: Path,
|
| 362 |
+
) -> List[Dict]:
|
| 363 |
+
"""
|
| 364 |
+
Search EDGAR for `n` filings matching `query`, extract section text, save.
|
| 365 |
+
|
| 366 |
+
Args:
|
| 367 |
+
label: Short label for the search term (used in filename + manifest).
|
| 368 |
+
query: EDGAR full-text search phrase.
|
| 369 |
+
n: Target document count.
|
| 370 |
+
output_dir: Directory to write .txt files into.
|
| 371 |
+
|
| 372 |
+
Returns:
|
| 373 |
+
List of manifest entry dicts.
|
| 374 |
+
"""
|
| 375 |
+
logger.info("Term '%s': searching EDGAR for %d docs …", label, n)
|
| 376 |
+
sources = full_text_search(query, n)
|
| 377 |
+
|
| 378 |
+
entries: List[Dict] = []
|
| 379 |
+
seen = set()
|
| 380 |
+
|
| 381 |
+
for source in sources:
|
| 382 |
+
cik_list = source.get("ciks", [])
|
| 383 |
+
cik = str(cik_list[0]) if cik_list else source.get("cik", "")
|
| 384 |
+
accession = source.get("adsh", "")
|
| 385 |
+
|
| 386 |
+
if not cik or not accession:
|
| 387 |
+
continue
|
| 388 |
+
|
| 389 |
+
doc_key = f"{cik}_{accession}"
|
| 390 |
+
if doc_key in seen:
|
| 391 |
+
continue
|
| 392 |
+
seen.add(doc_key)
|
| 393 |
+
|
| 394 |
+
text, doc_url, char_count = fetch_and_extract(source, label)
|
| 395 |
+
if char_count < 200:
|
| 396 |
+
logger.debug("Skipping %s — insufficient text (%d chars).", doc_key, char_count)
|
| 397 |
+
continue
|
| 398 |
+
|
| 399 |
+
out_path = output_dir / f"{doc_key}.txt"
|
| 400 |
+
out_path.write_text(text, encoding="utf-8")
|
| 401 |
+
|
| 402 |
+
company_names = source.get("display_names", [])
|
| 403 |
+
company = company_names[0] if company_names else source.get("entity_name", "")
|
| 404 |
+
|
| 405 |
+
entries.append(
|
| 406 |
+
{
|
| 407 |
+
"cik": cik,
|
| 408 |
+
"accession": accession,
|
| 409 |
+
"company": company,
|
| 410 |
+
"term": label,
|
| 411 |
+
"filing_date": source.get("period_of_report", ""),
|
| 412 |
+
"source_url": doc_url,
|
| 413 |
+
"path": str(out_path),
|
| 414 |
+
"char_count": char_count,
|
| 415 |
+
}
|
| 416 |
+
)
|
| 417 |
+
logger.info(" Saved %s (%d chars)", doc_key, char_count)
|
| 418 |
+
|
| 419 |
+
logger.info(" Term '%s': %d documents saved.", label, len(entries))
|
| 420 |
+
return entries
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def main() -> None:
|
| 424 |
+
"""Entry point."""
|
| 425 |
+
if USER_AGENT == _DEFAULT_USER_AGENT:
|
| 426 |
+
logger.warning(
|
| 427 |
+
"Using default User-Agent '%s'. "
|
| 428 |
+
"Set the SEC_USER_AGENT env var to 'YourName your@email.com' "
|
| 429 |
+
"to comply with SEC fair access policy.",
|
| 430 |
+
USER_AGENT,
|
| 431 |
+
)
|
| 432 |
+
|
| 433 |
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 434 |
+
manifest: List[Dict] = []
|
| 435 |
+
|
| 436 |
+
for label, query, target_n in SEARCH_CONFIG:
|
| 437 |
+
entries = fetch_term(label, query, target_n, OUTPUT_DIR)
|
| 438 |
+
manifest.extend(entries)
|
| 439 |
+
|
| 440 |
+
manifest_path = OUTPUT_DIR / "manifest.json"
|
| 441 |
+
manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
| 442 |
+
logger.info(
|
| 443 |
+
"Done. %d financial documents fetched. Manifest → %s",
|
| 444 |
+
len(manifest),
|
| 445 |
+
manifest_path,
|
| 446 |
+
)
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
# ---------------------------------------------------------------------------
|
| 450 |
+
# CLI
|
| 451 |
+
# ---------------------------------------------------------------------------
|
| 452 |
+
|
| 453 |
+
if __name__ == "__main__":
|
| 454 |
+
parser = argparse.ArgumentParser(
|
| 455 |
+
description="Fetch SEC EDGAR 10-K sections for the MEXAR financial corpus."
|
| 456 |
+
)
|
| 457 |
+
parser.add_argument(
|
| 458 |
+
"--user-agent",
|
| 459 |
+
default=None,
|
| 460 |
+
help=(
|
| 461 |
+
"SEC-compliant User-Agent string ('FirstName LastName email@example.com'). "
|
| 462 |
+
"Overrides the SEC_USER_AGENT environment variable."
|
| 463 |
+
),
|
| 464 |
+
)
|
| 465 |
+
args = parser.parse_args()
|
| 466 |
+
if args.user_agent:
|
| 467 |
+
USER_AGENT = args.user_agent
|
| 468 |
+
HEADERS["User-Agent"] = USER_AGENT
|
| 469 |
+
EDGAR_HEADERS["User-Agent"] = USER_AGENT
|
| 470 |
+
main()
|
backend/scripts/recompile_agents_from_real_data.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MEXAR - Bulk Recompile Domain Agents from Real Data.
|
| 3 |
+
Reads real text documents from test_data/medical_real/, test_data/legal_real/, and test_data/financial_real/
|
| 4 |
+
and compiles knowledge into SQLite database and FastEmbed vector store for medical_agent, legal_agent, and financial_agent.
|
| 5 |
+
"""
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import glob
|
| 9 |
+
import logging
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 13 |
+
|
| 14 |
+
from modules.knowledge_compiler import create_knowledge_compiler
|
| 15 |
+
from modules.prompt_analyzer import create_prompt_analyzer
|
| 16 |
+
from core.database import Base, engine as db_engine
|
| 17 |
+
|
| 18 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def recompile_domain_agent(domain_key: str, agent_name: str, system_prompt: str):
|
| 25 |
+
data_dir = REPO_ROOT / "test_data" / f"{domain_key}_real"
|
| 26 |
+
txt_files = list(data_dir.glob("*.txt"))
|
| 27 |
+
|
| 28 |
+
if not txt_files:
|
| 29 |
+
logger.warning(f"No .txt files found in {data_dir} for domain '{domain_key}'")
|
| 30 |
+
return
|
| 31 |
+
|
| 32 |
+
logger.info(f"Recompiling '{agent_name}' from {len(txt_files)} documents in {data_dir}...")
|
| 33 |
+
|
| 34 |
+
parsed_data = []
|
| 35 |
+
for fpath in sorted(txt_files):
|
| 36 |
+
try:
|
| 37 |
+
content = fpath.read_text(encoding="utf-8")
|
| 38 |
+
parsed_data.append({
|
| 39 |
+
"file_name": fpath.name,
|
| 40 |
+
"source": fpath.name,
|
| 41 |
+
"text": content,
|
| 42 |
+
"content": content,
|
| 43 |
+
"format": "txt"
|
| 44 |
+
})
|
| 45 |
+
except Exception as e:
|
| 46 |
+
logger.error(f"Error reading {fpath}: {e}")
|
| 47 |
+
|
| 48 |
+
analyzer = create_prompt_analyzer()
|
| 49 |
+
prompt_analysis = analyzer.analyze_prompt(system_prompt)
|
| 50 |
+
prompt_analysis["domain"] = domain_key
|
| 51 |
+
|
| 52 |
+
compiler = create_knowledge_compiler()
|
| 53 |
+
res = compiler.compile(
|
| 54 |
+
agent_name=agent_name,
|
| 55 |
+
parsed_data=parsed_data,
|
| 56 |
+
system_prompt=system_prompt,
|
| 57 |
+
prompt_analysis=prompt_analysis
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
logger.info(
|
| 61 |
+
f"Recompiled '{agent_name}': signature terms={len(res.get('domain_signature', []))}, "
|
| 62 |
+
f"processed {len(parsed_data)} files successfully."
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def main():
|
| 67 |
+
Base.metadata.create_all(bind=db_engine)
|
| 68 |
+
|
| 69 |
+
agents_config = [
|
| 70 |
+
(
|
| 71 |
+
"medical",
|
| 72 |
+
"medical_agent",
|
| 73 |
+
"You are an expert medical AI assistant specialized in cardiology, oncology, and internal medicine."
|
| 74 |
+
),
|
| 75 |
+
(
|
| 76 |
+
"legal",
|
| 77 |
+
"legal_agent",
|
| 78 |
+
"You are an expert legal AI assistant specialized in contract law, intellectual property, and corporate governance."
|
| 79 |
+
),
|
| 80 |
+
(
|
| 81 |
+
"financial",
|
| 82 |
+
"financial_agent",
|
| 83 |
+
"You are an expert financial AI assistant specialized in SEC EDGAR 10-K filings, MD&A, and risk factor analysis."
|
| 84 |
+
)
|
| 85 |
+
]
|
| 86 |
+
|
| 87 |
+
for domain_key, agent_name, system_prompt in agents_config:
|
| 88 |
+
recompile_domain_agent(domain_key, agent_name, system_prompt)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
if __name__ == "__main__":
|
| 92 |
+
main()
|
backend/utils/faithfulness.py
CHANGED
|
@@ -1,366 +1,374 @@
|
|
| 1 |
-
"""
|
| 2 |
-
MEXAR - Faithfulness Scoring Module
|
| 3 |
-
Measures how well the LLM answer is grounded in the retrieved context.
|
| 4 |
-
"""
|
| 5 |
-
import json
|
| 6 |
-
import logging
|
| 7 |
-
from typing import List, Dict
|
| 8 |
-
from dataclasses import dataclass
|
| 9 |
-
|
| 10 |
-
logger = logging.getLogger(__name__)
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
@dataclass
|
| 14 |
-
class FaithfulnessResult:
|
| 15 |
-
"""Result of faithfulness evaluation."""
|
| 16 |
-
score: float # 0-1 score
|
| 17 |
-
total_claims: int
|
| 18 |
-
supported_claims: int
|
| 19 |
-
unsupported_claims: List[str]
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
class FaithfulnessScorer:
|
| 23 |
-
"""
|
| 24 |
-
Evaluates how faithful (grounded) an LLM answer is to the context.
|
| 25 |
-
|
| 26 |
-
Process:
|
| 27 |
-
1. Extract factual claims from the answer
|
| 28 |
-
2. Check each claim against the retrieved context
|
| 29 |
-
3. Calculate percentage of supported claims
|
| 30 |
-
|
| 31 |
-
High faithfulness = answer is well-grounded, low hallucination risk
|
| 32 |
-
"""
|
| 33 |
-
|
| 34 |
-
def __init__(self, groq_client=None):
|
| 35 |
-
"""
|
| 36 |
-
Initialize scorer.
|
| 37 |
-
|
| 38 |
-
Args:
|
| 39 |
-
groq_client: Groq client for LLM calls
|
| 40 |
-
"""
|
| 41 |
-
self._client = groq_client
|
| 42 |
-
|
| 43 |
-
@property
|
| 44 |
-
def client(self):
|
| 45 |
-
"""Lazy load Groq client."""
|
| 46 |
-
if self._client is None:
|
| 47 |
-
from utils.groq_client import get_groq_client
|
| 48 |
-
self._client = get_groq_client()
|
| 49 |
-
return self._client
|
| 50 |
-
|
| 51 |
-
def score(self, answer: str, context: str) -> FaithfulnessResult:
|
| 52 |
-
"""
|
| 53 |
-
Score how well answer is grounded in context.
|
| 54 |
-
|
| 55 |
-
Args:
|
| 56 |
-
answer: LLM generated answer
|
| 57 |
-
context: Retrieved context used to generate answer
|
| 58 |
-
|
| 59 |
-
Returns:
|
| 60 |
-
FaithfulnessResult with score and details
|
| 61 |
-
"""
|
| 62 |
-
if not answer or not context:
|
| 63 |
-
return FaithfulnessResult(
|
| 64 |
-
score=1.0,
|
| 65 |
-
total_claims=0,
|
| 66 |
-
supported_claims=0,
|
| 67 |
-
unsupported_claims=[]
|
| 68 |
-
)
|
| 69 |
-
|
| 70 |
-
# Step 1: Extract claims from answer
|
| 71 |
-
claims = self._extract_claims(answer)
|
| 72 |
-
|
| 73 |
-
if not claims:
|
| 74 |
-
return FaithfulnessResult(
|
| 75 |
-
score=1.0,
|
| 76 |
-
total_claims=0,
|
| 77 |
-
supported_claims=0,
|
| 78 |
-
unsupported_claims=[]
|
| 79 |
-
)
|
| 80 |
-
|
| 81 |
-
# Step 2: Check each claim against context
|
| 82 |
-
supported = 0
|
| 83 |
-
unsupported = []
|
| 84 |
-
|
| 85 |
-
for claim in claims:
|
| 86 |
-
if self._is_supported(claim, context):
|
| 87 |
-
supported += 1
|
| 88 |
-
else:
|
| 89 |
-
unsupported.append(claim)
|
| 90 |
-
|
| 91 |
-
# Step 3: Calculate score
|
| 92 |
-
score = supported / len(claims)
|
| 93 |
-
|
| 94 |
-
logger.info(f"Faithfulness: {supported}/{len(claims)} claims supported ({score*100:.0f}%)")
|
| 95 |
-
|
| 96 |
-
return FaithfulnessResult(
|
| 97 |
-
score=round(score, 3),
|
| 98 |
-
total_claims=len(claims),
|
| 99 |
-
supported_claims=supported,
|
| 100 |
-
unsupported_claims=unsupported[:5] # Limit to 5 for display
|
| 101 |
-
)
|
| 102 |
-
|
| 103 |
-
def _extract_claims(self, answer: str) -> List[str]:
|
| 104 |
-
"""
|
| 105 |
-
Extract factual claims from the answer.
|
| 106 |
-
|
| 107 |
-
Uses LLM to identify distinct factual statements.
|
| 108 |
-
"""
|
| 109 |
-
try:
|
| 110 |
-
prompt = f"""Extract individual factual claims from this answer.
|
| 111 |
-
A claim is a specific statement that can be verified as true or false.
|
| 112 |
-
Return ONLY a JSON array of strings, no explanation.
|
| 113 |
-
|
| 114 |
-
Answer: "{answer[:2000]}"
|
| 115 |
-
|
| 116 |
-
Example output: ["Claim 1", "Claim 2", "Claim 3"]"""
|
| 117 |
-
|
| 118 |
-
response = self.client.analyze_with_system_prompt(
|
| 119 |
-
system_prompt="You extract factual claims. Return only valid JSON array.",
|
| 120 |
-
user_message=prompt,
|
| 121 |
-
model="fast",
|
| 122 |
-
json_mode=True
|
| 123 |
-
)
|
| 124 |
-
|
| 125 |
-
# Parse response
|
| 126 |
-
claims = json.loads(response)
|
| 127 |
-
|
| 128 |
-
# Handle both list and dict responses
|
| 129 |
-
if isinstance(claims, list):
|
| 130 |
-
return [str(c) for c in claims if c]
|
| 131 |
-
elif isinstance(claims, dict):
|
| 132 |
-
return [str(c) for c in claims.get("claims", claims.get("statements", [])) if c]
|
| 133 |
-
|
| 134 |
-
return []
|
| 135 |
-
|
| 136 |
-
except json.JSONDecodeError as e:
|
| 137 |
-
logger.warning(f"Failed to parse claims JSON: {e}")
|
| 138 |
-
# Fallback: split by sentences
|
| 139 |
-
return self._fallback_extract_claims(answer)
|
| 140 |
-
except Exception as e:
|
| 141 |
-
logger.warning(f"Claim extraction failed: {e}")
|
| 142 |
-
return self._fallback_extract_claims(answer)
|
| 143 |
-
|
| 144 |
-
def _fallback_extract_claims(self, answer: str) -> List[str]:
|
| 145 |
-
"""Fallback claim extraction by splitting sentences."""
|
| 146 |
-
import re
|
| 147 |
-
sentences = re.split(r'(?<=[.!?])\s+', answer)
|
| 148 |
-
# Filter to substantive sentences
|
| 149 |
-
return [s.strip() for s in sentences if len(s.strip()) > 20][:10]
|
| 150 |
-
|
| 151 |
-
def _is_supported(self, claim: str, context: str) -> bool:
|
| 152 |
-
"""
|
| 153 |
-
Check if a claim is supported by the context.
|
| 154 |
-
|
| 155 |
-
Uses LLM to evaluate if the context contains evidence for the claim.
|
| 156 |
-
"""
|
| 157 |
-
try:
|
| 158 |
-
prompt = f"""Is this claim supported by the context? Answer only YES or NO.
|
| 159 |
-
|
| 160 |
-
Claim: "{claim}"
|
| 161 |
-
|
| 162 |
-
Context (first 4000 chars):
|
| 163 |
-
"{context[:4000]}"
|
| 164 |
-
|
| 165 |
-
Answer YES if the context contains information that supports this claim.
|
| 166 |
-
Answer NO if the claim cannot be verified from the context or contradicts it."""
|
| 167 |
-
|
| 168 |
-
response = self.client.analyze_with_system_prompt(
|
| 169 |
-
system_prompt="You verify claims. Answer only YES or NO.",
|
| 170 |
-
user_message=prompt,
|
| 171 |
-
model="fast"
|
| 172 |
-
)
|
| 173 |
-
|
| 174 |
-
return "YES" in response.upper()
|
| 175 |
-
|
| 176 |
-
except Exception as e:
|
| 177 |
-
logger.warning(f"Support check failed: {e}")
|
| 178 |
-
# Optimistic fallback - assume supported if check fails
|
| 179 |
-
return True
|
| 180 |
-
|
| 181 |
-
def quick_score(self, answer: str, context: str) -> float:
|
| 182 |
-
"""
|
| 183 |
-
Quick faithfulness estimate without LLM calls.
|
| 184 |
-
Uses text overlap as a proxy for grounding.
|
| 185 |
-
|
| 186 |
-
Args:
|
| 187 |
-
answer: LLM answer
|
| 188 |
-
context: Retrieved context
|
| 189 |
-
|
| 190 |
-
Returns:
|
| 191 |
-
Estimated faithfulness score (0-1)
|
| 192 |
-
"""
|
| 193 |
-
if not answer or not context:
|
| 194 |
-
return 0.5
|
| 195 |
-
|
| 196 |
-
# Get significant words from answer
|
| 197 |
-
answer_words = set(w.lower() for w in answer.split() if len(w) > 4)
|
| 198 |
-
context_lower = context.lower()
|
| 199 |
-
|
| 200 |
-
if not answer_words:
|
| 201 |
-
return 0.5
|
| 202 |
-
|
| 203 |
-
# Check how many answer words appear in context
|
| 204 |
-
found = sum(1 for w in answer_words if w in context_lower)
|
| 205 |
-
overlap = found / len(answer_words)
|
| 206 |
-
|
| 207 |
-
# Scale to reasonable range
|
| 208 |
-
return min(1.0, overlap * 1.5)
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
def create_faithfulness_scorer() -> FaithfulnessScorer:
|
| 212 |
-
"""Factory function to create FaithfulnessScorer."""
|
| 213 |
-
return FaithfulnessScorer()
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
class DebertaNLIScorer:
|
| 217 |
-
"""
|
| 218 |
-
Primary faithfulness verifier per Section III-C.
|
| 219 |
-
|
| 220 |
-
Uses cross-encoder/nli-deberta-v3-large (DeBERTa-v3-large fine-tuned on MNLI)
|
| 221 |
-
to break the circular LLM-judges-LLM evaluation pattern.
|
| 222 |
-
|
| 223 |
-
Implements:
|
| 224 |
-
- Eq. 6: per-claim, per-document max entailment probability
|
| 225 |
-
- Eq. 7: faithfulness score = fraction of claims exceeding TAU_ENT
|
| 226 |
-
|
| 227 |
-
score() receives individual retrieved chunk texts (NOT concatenated), so the
|
| 228 |
-
per-document max in Eq. 6 is meaningful.
|
| 229 |
-
"""
|
| 230 |
-
|
| 231 |
-
TAU_ENT = 0.7 # Entailment probability threshold (Eq. 7)
|
| 232 |
-
|
| 233 |
-
def __init__(self):
|
| 234 |
-
self._model = None
|
| 235 |
-
self._entailment_idx: int = 1 # Verified at load time from id2label
|
| 236 |
-
|
| 237 |
-
@property
|
| 238 |
-
def model(self):
|
| 239 |
-
"""Lazy-load CrossEncoder to avoid slow startup when not needed."""
|
| 240 |
-
if self._model is None:
|
| 241 |
-
from sentence_transformers import CrossEncoder
|
| 242 |
-
logger.info("Loading cross-encoder/nli-deberta-v3-large for faithfulness scoring...")
|
| 243 |
-
self._model = CrossEncoder("cross-encoder/nli-deberta-v3-large")
|
| 244 |
-
|
| 245 |
-
# CRITICAL: verify the actual label order from the model's config,
|
| 246 |
-
# NLI checkpoints do NOT always use the same label ordering.
|
| 247 |
-
# Do not hardcode entailment_idx=1 without confirming here.
|
| 248 |
-
try:
|
| 249 |
-
id2label = self._model.model.config.id2label
|
| 250 |
-
logger.info(f"DeBERTa NLI id2label: {id2label}")
|
| 251 |
-
# Find the index whose label contains 'entail' (case-insensitive)
|
| 252 |
-
for idx, label in id2label.items():
|
| 253 |
-
if "entail" in label.lower():
|
| 254 |
-
self._entailment_idx = int(idx)
|
| 255 |
-
break
|
| 256 |
-
logger.info(
|
| 257 |
-
f"DeBERTa-v3 NLI loaded. "
|
| 258 |
-
f"Confirmed entailment_idx={self._entailment_idx} "
|
| 259 |
-
f"(label='{id2label.get(self._entailment_idx, 'unknown')}')"
|
| 260 |
-
)
|
| 261 |
-
except Exception as e:
|
| 262 |
-
logger.warning(
|
| 263 |
-
f"Could not read id2label from DeBERTa config ({e}). "
|
| 264 |
-
f"Defaulting entailment_idx=1. Verify this is correct for this checkpoint."
|
| 265 |
-
)
|
| 266 |
-
return self._model
|
| 267 |
-
|
| 268 |
-
def score(self, answer: str, context_documents: List[str]) -> FaithfulnessResult:
|
| 269 |
-
"""
|
| 270 |
-
Score faithfulness of an answer against a list of individual retrieved chunks.
|
| 271 |
-
|
| 272 |
-
Args:
|
| 273 |
-
answer: LLM-generated answer text.
|
| 274 |
-
context_documents: List of individual chunk texts (NOT concatenated).
|
| 275 |
-
Per-document max in Eq. 6 requires individual documents.
|
| 276 |
-
|
| 277 |
-
Returns:
|
| 278 |
-
FaithfulnessResult with per-sentence entailment-based score.
|
| 279 |
-
"""
|
| 280 |
-
import re
|
| 281 |
-
import numpy as np
|
| 282 |
-
|
| 283 |
-
if not answer or not context_documents:
|
| 284 |
-
return FaithfulnessResult(
|
| 285 |
-
score=1.0, total_claims=0, supported_claims=0, unsupported_claims=[]
|
| 286 |
-
)
|
| 287 |
-
|
| 288 |
-
# Split answer into sentences (Eq. 6 operates per-claim / per-sentence)
|
| 289 |
-
sentences = [
|
| 290 |
-
s.strip()
|
| 291 |
-
for s in re.split(r'(?<=[.!?])\s+', answer)
|
| 292 |
-
if len(s.strip()) > 15
|
| 293 |
-
][:
|
| 294 |
-
|
| 295 |
-
if not sentences:
|
| 296 |
-
return FaithfulnessResult(
|
| 297 |
-
score=1.0, total_claims=0, supported_claims=0, unsupported_claims=[]
|
| 298 |
-
)
|
| 299 |
-
|
| 300 |
-
supported = 0
|
| 301 |
-
unsupported_sentences = []
|
| 302 |
-
|
| 303 |
-
try:
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MEXAR - Faithfulness Scoring Module
|
| 3 |
+
Measures how well the LLM answer is grounded in the retrieved context.
|
| 4 |
+
"""
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
from typing import List, Dict
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@dataclass
|
| 14 |
+
class FaithfulnessResult:
|
| 15 |
+
"""Result of faithfulness evaluation."""
|
| 16 |
+
score: float # 0-1 score
|
| 17 |
+
total_claims: int
|
| 18 |
+
supported_claims: int
|
| 19 |
+
unsupported_claims: List[str]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class FaithfulnessScorer:
|
| 23 |
+
"""
|
| 24 |
+
Evaluates how faithful (grounded) an LLM answer is to the context.
|
| 25 |
+
|
| 26 |
+
Process:
|
| 27 |
+
1. Extract factual claims from the answer
|
| 28 |
+
2. Check each claim against the retrieved context
|
| 29 |
+
3. Calculate percentage of supported claims
|
| 30 |
+
|
| 31 |
+
High faithfulness = answer is well-grounded, low hallucination risk
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
def __init__(self, groq_client=None):
|
| 35 |
+
"""
|
| 36 |
+
Initialize scorer.
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
groq_client: Groq client for LLM calls
|
| 40 |
+
"""
|
| 41 |
+
self._client = groq_client
|
| 42 |
+
|
| 43 |
+
@property
|
| 44 |
+
def client(self):
|
| 45 |
+
"""Lazy load Groq client."""
|
| 46 |
+
if self._client is None:
|
| 47 |
+
from utils.groq_client import get_groq_client
|
| 48 |
+
self._client = get_groq_client()
|
| 49 |
+
return self._client
|
| 50 |
+
|
| 51 |
+
def score(self, answer: str, context: str) -> FaithfulnessResult:
|
| 52 |
+
"""
|
| 53 |
+
Score how well answer is grounded in context.
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
answer: LLM generated answer
|
| 57 |
+
context: Retrieved context used to generate answer
|
| 58 |
+
|
| 59 |
+
Returns:
|
| 60 |
+
FaithfulnessResult with score and details
|
| 61 |
+
"""
|
| 62 |
+
if not answer or not context:
|
| 63 |
+
return FaithfulnessResult(
|
| 64 |
+
score=1.0,
|
| 65 |
+
total_claims=0,
|
| 66 |
+
supported_claims=0,
|
| 67 |
+
unsupported_claims=[]
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# Step 1: Extract claims from answer
|
| 71 |
+
claims = self._extract_claims(answer)
|
| 72 |
+
|
| 73 |
+
if not claims:
|
| 74 |
+
return FaithfulnessResult(
|
| 75 |
+
score=1.0,
|
| 76 |
+
total_claims=0,
|
| 77 |
+
supported_claims=0,
|
| 78 |
+
unsupported_claims=[]
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
# Step 2: Check each claim against context
|
| 82 |
+
supported = 0
|
| 83 |
+
unsupported = []
|
| 84 |
+
|
| 85 |
+
for claim in claims:
|
| 86 |
+
if self._is_supported(claim, context):
|
| 87 |
+
supported += 1
|
| 88 |
+
else:
|
| 89 |
+
unsupported.append(claim)
|
| 90 |
+
|
| 91 |
+
# Step 3: Calculate score
|
| 92 |
+
score = supported / len(claims)
|
| 93 |
+
|
| 94 |
+
logger.info(f"Faithfulness: {supported}/{len(claims)} claims supported ({score*100:.0f}%)")
|
| 95 |
+
|
| 96 |
+
return FaithfulnessResult(
|
| 97 |
+
score=round(score, 3),
|
| 98 |
+
total_claims=len(claims),
|
| 99 |
+
supported_claims=supported,
|
| 100 |
+
unsupported_claims=unsupported[:5] # Limit to 5 for display
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
def _extract_claims(self, answer: str) -> List[str]:
|
| 104 |
+
"""
|
| 105 |
+
Extract factual claims from the answer.
|
| 106 |
+
|
| 107 |
+
Uses LLM to identify distinct factual statements.
|
| 108 |
+
"""
|
| 109 |
+
try:
|
| 110 |
+
prompt = f"""Extract individual factual claims from this answer.
|
| 111 |
+
A claim is a specific statement that can be verified as true or false.
|
| 112 |
+
Return ONLY a JSON array of strings, no explanation.
|
| 113 |
+
|
| 114 |
+
Answer: "{answer[:2000]}"
|
| 115 |
+
|
| 116 |
+
Example output: ["Claim 1", "Claim 2", "Claim 3"]"""
|
| 117 |
+
|
| 118 |
+
response = self.client.analyze_with_system_prompt(
|
| 119 |
+
system_prompt="You extract factual claims. Return only valid JSON array.",
|
| 120 |
+
user_message=prompt,
|
| 121 |
+
model="fast",
|
| 122 |
+
json_mode=True
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
# Parse response
|
| 126 |
+
claims = json.loads(response)
|
| 127 |
+
|
| 128 |
+
# Handle both list and dict responses
|
| 129 |
+
if isinstance(claims, list):
|
| 130 |
+
return [str(c) for c in claims if c]
|
| 131 |
+
elif isinstance(claims, dict):
|
| 132 |
+
return [str(c) for c in claims.get("claims", claims.get("statements", [])) if c]
|
| 133 |
+
|
| 134 |
+
return []
|
| 135 |
+
|
| 136 |
+
except json.JSONDecodeError as e:
|
| 137 |
+
logger.warning(f"Failed to parse claims JSON: {e}")
|
| 138 |
+
# Fallback: split by sentences
|
| 139 |
+
return self._fallback_extract_claims(answer)
|
| 140 |
+
except Exception as e:
|
| 141 |
+
logger.warning(f"Claim extraction failed: {e}")
|
| 142 |
+
return self._fallback_extract_claims(answer)
|
| 143 |
+
|
| 144 |
+
def _fallback_extract_claims(self, answer: str) -> List[str]:
|
| 145 |
+
"""Fallback claim extraction by splitting sentences."""
|
| 146 |
+
import re
|
| 147 |
+
sentences = re.split(r'(?<=[.!?])\s+', answer)
|
| 148 |
+
# Filter to substantive sentences
|
| 149 |
+
return [s.strip() for s in sentences if len(s.strip()) > 20][:10]
|
| 150 |
+
|
| 151 |
+
def _is_supported(self, claim: str, context: str) -> bool:
|
| 152 |
+
"""
|
| 153 |
+
Check if a claim is supported by the context.
|
| 154 |
+
|
| 155 |
+
Uses LLM to evaluate if the context contains evidence for the claim.
|
| 156 |
+
"""
|
| 157 |
+
try:
|
| 158 |
+
prompt = f"""Is this claim supported by the context? Answer only YES or NO.
|
| 159 |
+
|
| 160 |
+
Claim: "{claim}"
|
| 161 |
+
|
| 162 |
+
Context (first 4000 chars):
|
| 163 |
+
"{context[:4000]}"
|
| 164 |
+
|
| 165 |
+
Answer YES if the context contains information that supports this claim.
|
| 166 |
+
Answer NO if the claim cannot be verified from the context or contradicts it."""
|
| 167 |
+
|
| 168 |
+
response = self.client.analyze_with_system_prompt(
|
| 169 |
+
system_prompt="You verify claims. Answer only YES or NO.",
|
| 170 |
+
user_message=prompt,
|
| 171 |
+
model="fast"
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
return "YES" in response.upper()
|
| 175 |
+
|
| 176 |
+
except Exception as e:
|
| 177 |
+
logger.warning(f"Support check failed: {e}")
|
| 178 |
+
# Optimistic fallback - assume supported if check fails
|
| 179 |
+
return True
|
| 180 |
+
|
| 181 |
+
def quick_score(self, answer: str, context: str) -> float:
|
| 182 |
+
"""
|
| 183 |
+
Quick faithfulness estimate without LLM calls.
|
| 184 |
+
Uses text overlap as a proxy for grounding.
|
| 185 |
+
|
| 186 |
+
Args:
|
| 187 |
+
answer: LLM answer
|
| 188 |
+
context: Retrieved context
|
| 189 |
+
|
| 190 |
+
Returns:
|
| 191 |
+
Estimated faithfulness score (0-1)
|
| 192 |
+
"""
|
| 193 |
+
if not answer or not context:
|
| 194 |
+
return 0.5
|
| 195 |
+
|
| 196 |
+
# Get significant words from answer
|
| 197 |
+
answer_words = set(w.lower() for w in answer.split() if len(w) > 4)
|
| 198 |
+
context_lower = context.lower()
|
| 199 |
+
|
| 200 |
+
if not answer_words:
|
| 201 |
+
return 0.5
|
| 202 |
+
|
| 203 |
+
# Check how many answer words appear in context
|
| 204 |
+
found = sum(1 for w in answer_words if w in context_lower)
|
| 205 |
+
overlap = found / len(answer_words)
|
| 206 |
+
|
| 207 |
+
# Scale to reasonable range
|
| 208 |
+
return min(1.0, overlap * 1.5)
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def create_faithfulness_scorer() -> FaithfulnessScorer:
|
| 212 |
+
"""Factory function to create FaithfulnessScorer."""
|
| 213 |
+
return FaithfulnessScorer()
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
class DebertaNLIScorer:
|
| 217 |
+
"""
|
| 218 |
+
Primary faithfulness verifier per Section III-C.
|
| 219 |
+
|
| 220 |
+
Uses cross-encoder/nli-deberta-v3-large (DeBERTa-v3-large fine-tuned on MNLI)
|
| 221 |
+
to break the circular LLM-judges-LLM evaluation pattern.
|
| 222 |
+
|
| 223 |
+
Implements:
|
| 224 |
+
- Eq. 6: per-claim, per-document max entailment probability
|
| 225 |
+
- Eq. 7: faithfulness score = fraction of claims exceeding TAU_ENT
|
| 226 |
+
|
| 227 |
+
score() receives individual retrieved chunk texts (NOT concatenated), so the
|
| 228 |
+
per-document max in Eq. 6 is meaningful.
|
| 229 |
+
"""
|
| 230 |
+
|
| 231 |
+
TAU_ENT = 0.7 # Entailment probability threshold (Eq. 7)
|
| 232 |
+
|
| 233 |
+
def __init__(self):
|
| 234 |
+
self._model = None
|
| 235 |
+
self._entailment_idx: int = 1 # Verified at load time from id2label
|
| 236 |
+
|
| 237 |
+
@property
|
| 238 |
+
def model(self):
|
| 239 |
+
"""Lazy-load CrossEncoder to avoid slow startup when not needed."""
|
| 240 |
+
if self._model is None:
|
| 241 |
+
from sentence_transformers import CrossEncoder
|
| 242 |
+
logger.info("Loading cross-encoder/nli-deberta-v3-large for faithfulness scoring...")
|
| 243 |
+
self._model = CrossEncoder("cross-encoder/nli-deberta-v3-large")
|
| 244 |
+
|
| 245 |
+
# CRITICAL: verify the actual label order from the model's config,
|
| 246 |
+
# NLI checkpoints do NOT always use the same label ordering.
|
| 247 |
+
# Do not hardcode entailment_idx=1 without confirming here.
|
| 248 |
+
try:
|
| 249 |
+
id2label = self._model.model.config.id2label
|
| 250 |
+
logger.info(f"DeBERTa NLI id2label: {id2label}")
|
| 251 |
+
# Find the index whose label contains 'entail' (case-insensitive)
|
| 252 |
+
for idx, label in id2label.items():
|
| 253 |
+
if "entail" in label.lower():
|
| 254 |
+
self._entailment_idx = int(idx)
|
| 255 |
+
break
|
| 256 |
+
logger.info(
|
| 257 |
+
f"DeBERTa-v3 NLI loaded. "
|
| 258 |
+
f"Confirmed entailment_idx={self._entailment_idx} "
|
| 259 |
+
f"(label='{id2label.get(self._entailment_idx, 'unknown')}')"
|
| 260 |
+
)
|
| 261 |
+
except Exception as e:
|
| 262 |
+
logger.warning(
|
| 263 |
+
f"Could not read id2label from DeBERTa config ({e}). "
|
| 264 |
+
f"Defaulting entailment_idx=1. Verify this is correct for this checkpoint."
|
| 265 |
+
)
|
| 266 |
+
return self._model
|
| 267 |
+
|
| 268 |
+
def score(self, answer: str, context_documents: List[str]) -> FaithfulnessResult:
|
| 269 |
+
"""
|
| 270 |
+
Score faithfulness of an answer against a list of individual retrieved chunks.
|
| 271 |
+
|
| 272 |
+
Args:
|
| 273 |
+
answer: LLM-generated answer text.
|
| 274 |
+
context_documents: List of individual chunk texts (NOT concatenated).
|
| 275 |
+
Per-document max in Eq. 6 requires individual documents.
|
| 276 |
+
|
| 277 |
+
Returns:
|
| 278 |
+
FaithfulnessResult with per-sentence entailment-based score.
|
| 279 |
+
"""
|
| 280 |
+
import re
|
| 281 |
+
import numpy as np
|
| 282 |
+
|
| 283 |
+
if not answer or not context_documents:
|
| 284 |
+
return FaithfulnessResult(
|
| 285 |
+
score=1.0, total_claims=0, supported_claims=0, unsupported_claims=[]
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
# Split answer into sentences (Eq. 6 operates per-claim / per-sentence)
|
| 289 |
+
sentences = [
|
| 290 |
+
s.strip()
|
| 291 |
+
for s in re.split(r'(?<=[.!?])\s+', answer)
|
| 292 |
+
if len(s.strip()) > 15
|
| 293 |
+
][:3] # cap at top 3 key sentences for fast CPU NLI verification
|
| 294 |
+
|
| 295 |
+
if not sentences:
|
| 296 |
+
return FaithfulnessResult(
|
| 297 |
+
score=1.0, total_claims=0, supported_claims=0, unsupported_claims=[]
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
supported = 0
|
| 301 |
+
unsupported_sentences = []
|
| 302 |
+
|
| 303 |
+
try:
|
| 304 |
+
# Limit context documents to top 3 retrieved chunks
|
| 305 |
+
top_docs = context_documents[:3]
|
| 306 |
+
|
| 307 |
+
# Batch all (doc, sentence) pairs across sentences into a single predict call
|
| 308 |
+
all_pairs = []
|
| 309 |
+
for sentence in sentences:
|
| 310 |
+
for doc in top_docs:
|
| 311 |
+
all_pairs.append((doc[:400], sentence[:200]))
|
| 312 |
+
|
| 313 |
+
import torch
|
| 314 |
+
with torch.inference_mode():
|
| 315 |
+
raw_scores = self.model.predict(all_pairs, batch_size=32)
|
| 316 |
+
probs = self._softmax(raw_scores)
|
| 317 |
+
|
| 318 |
+
n_docs = len(top_docs)
|
| 319 |
+
probs = probs.reshape(len(sentences), n_docs, -1)
|
| 320 |
+
|
| 321 |
+
for i, sentence in enumerate(sentences):
|
| 322 |
+
# Eq. 6: max entailment probability across all retrieved documents
|
| 323 |
+
max_entailment = float(np.max(probs[i, :, self._entailment_idx]))
|
| 324 |
+
|
| 325 |
+
if max_entailment > self.TAU_ENT:
|
| 326 |
+
supported += 1
|
| 327 |
+
else:
|
| 328 |
+
unsupported_sentences.append(sentence)
|
| 329 |
+
|
| 330 |
+
except Exception as e:
|
| 331 |
+
logger.error(f"DebertaNLIScorer inference failed: {e}")
|
| 332 |
+
return FaithfulnessResult(
|
| 333 |
+
score=0.5,
|
| 334 |
+
total_claims=len(sentences),
|
| 335 |
+
supported_claims=0,
|
| 336 |
+
unsupported_claims=sentences[:5],
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
# Eq. 7: faithfulness = fraction of supported claims
|
| 340 |
+
score = supported / len(sentences)
|
| 341 |
+
logger.info(
|
| 342 |
+
f"DeBERTa NLI Faithfulness: {supported}/{len(sentences)} sentences "
|
| 343 |
+
f"supported (score={score:.3f}, TAU_ENT={self.TAU_ENT})"
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
return FaithfulnessResult(
|
| 347 |
+
score=round(score, 3),
|
| 348 |
+
total_claims=len(sentences),
|
| 349 |
+
supported_claims=supported,
|
| 350 |
+
unsupported_claims=unsupported_sentences[:5],
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
@staticmethod
|
| 354 |
+
def _softmax(x):
|
| 355 |
+
"""Numerically stable softmax over last axis."""
|
| 356 |
+
import numpy as np
|
| 357 |
+
e_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
|
| 358 |
+
return e_x / e_x.sum(axis=-1, keepdims=True)
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
class FActScoreCompat:
|
| 362 |
+
"""
|
| 363 |
+
Simulates the FActScore (Min et al., ACL 2023) evaluation.
|
| 364 |
+
Breaks answer into atomic facts, verifies each fact against context independently.
|
| 365 |
+
This acts as a wrapper around FaithfulnessScorer to explicitly mark it for FActScore baseline comparisons.
|
| 366 |
+
"""
|
| 367 |
+
def __init__(self, groq_client=None):
|
| 368 |
+
self._scorer = FaithfulnessScorer(groq_client=groq_client)
|
| 369 |
+
|
| 370 |
+
def score(self, answer: str, context: str) -> FaithfulnessResult:
|
| 371 |
+
result = self._scorer.score(answer, context)
|
| 372 |
+
logger.info(f"FActScore: {result.score * 100:.1f}% ({result.supported_claims}/{result.total_claims} facts)")
|
| 373 |
+
return result
|
| 374 |
+
|
backend/utils/groq_client.py
CHANGED
|
@@ -1,308 +1,327 @@
|
|
| 1 |
-
"""
|
| 2 |
-
MEXAR Core Engine - Groq API Client Wrapper
|
| 3 |
-
Provides a unified interface for all Groq API interactions.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import os
|
| 7 |
-
import base64
|
| 8 |
-
from typing import Optional, List, Dict, Any
|
| 9 |
-
from groq import Groq
|
| 10 |
-
from dotenv import load_dotenv
|
| 11 |
-
|
| 12 |
-
# Load environment variables (.env) from root/backend
|
| 13 |
-
load_dotenv()
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
class GroqClient:
|
| 17 |
-
"""
|
| 18 |
-
Unified Groq API client for MEXAR.
|
| 19 |
-
Handles LLM, Whisper (audio), and Vision (image) capabilities.
|
| 20 |
-
"""
|
| 21 |
-
|
| 22 |
-
def __init__(self, api_key: Optional[str] = None):
|
| 23 |
-
"""
|
| 24 |
-
Initialize Groq client with API key.
|
| 25 |
-
|
| 26 |
-
Args:
|
| 27 |
-
api_key: Groq API key. If not provided, reads from GROQ_API_KEY env var.
|
| 28 |
-
"""
|
| 29 |
-
self.api_key = api_key or os.getenv("GROQ_API_KEY")
|
| 30 |
-
if not self.api_key:
|
| 31 |
-
raise ValueError("GROQ_API_KEY not found in environment variables")
|
| 32 |
-
|
| 33 |
-
self.client = Groq(api_key=self.api_key)
|
| 34 |
-
|
| 35 |
-
from core.config import settings
|
| 36 |
-
|
| 37 |
-
# Model configurations based on LLM_BACKBONE and environment settings
|
| 38 |
-
backbone = getattr(settings, "LLM_BACKBONE", "openai/gpt-oss-120b").lower()
|
| 39 |
-
active_model = getattr(settings, "GROQ_MODEL", os.getenv("GROQ_MODEL", "openai/gpt-oss-120b"))
|
| 40 |
-
|
| 41 |
-
if backbone == "mixtral":
|
| 42 |
-
self.models = {
|
| 43 |
-
"chat": "mixtral-8x7b-32768",
|
| 44 |
-
"advanced": "mixtral-8x7b-32768",
|
| 45 |
-
"fast": "mixtral-8x7b-32768",
|
| 46 |
-
"vision": "meta-llama/llama-4-scout-17b-16e-instruct",
|
| 47 |
-
"whisper": "whisper-large-v3"
|
| 48 |
-
}
|
| 49 |
-
elif backbone == "gemma":
|
| 50 |
-
self.models = {
|
| 51 |
-
"chat": "gemma2-9b-it",
|
| 52 |
-
"advanced": "gemma2-9b-it",
|
| 53 |
-
"fast": "gemma2-9b-it",
|
| 54 |
-
"vision": "meta-llama/llama-4-scout-17b-16e-instruct",
|
| 55 |
-
"whisper": "whisper-large-v3"
|
| 56 |
-
}
|
| 57 |
-
elif backbone == "llama3":
|
| 58 |
-
self.models = {
|
| 59 |
-
"chat": "llama-3.1-8b-instant",
|
| 60 |
-
"advanced": "llama-3.3-70b-versatile",
|
| 61 |
-
"fast": "llama-3.1-8b-instant",
|
| 62 |
-
"vision": "meta-llama/llama-4-scout-17b-16e-instruct",
|
| 63 |
-
"whisper": "whisper-large-v3"
|
| 64 |
-
}
|
| 65 |
-
else:
|
| 66 |
-
self.models = {
|
| 67 |
-
"chat": active_model, # Primary LLM (openai/gpt-oss-120b)
|
| 68 |
-
"advanced": active_model, # Advanced reasoning
|
| 69 |
-
"fast": active_model, # Fast responses
|
| 70 |
-
"vision": "meta-llama/llama-4-scout-17b-16e-instruct",
|
| 71 |
-
"whisper": "whisper-large-v3"
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
def chat_completion(
|
| 75 |
-
self,
|
| 76 |
-
messages: List[Dict[str, str]],
|
| 77 |
-
model: str = "chat",
|
| 78 |
-
temperature: float = 0.7,
|
| 79 |
-
max_tokens: int = 4096,
|
| 80 |
-
json_mode: bool = False
|
| 81 |
-
) -> str:
|
| 82 |
-
"""
|
| 83 |
-
Send a chat completion request.
|
| 84 |
-
|
| 85 |
-
Args:
|
| 86 |
-
messages: List of message dicts with 'role' and 'content'
|
| 87 |
-
model: Model key from self.models
|
| 88 |
-
temperature: Sampling temperature (0-2)
|
| 89 |
-
max_tokens: Maximum tokens in response
|
| 90 |
-
json_mode: If True, force JSON output
|
| 91 |
-
|
| 92 |
-
Returns:
|
| 93 |
-
Generated text response
|
| 94 |
-
"""
|
| 95 |
-
model_name = self.models.get(model, model)
|
| 96 |
-
|
| 97 |
-
kwargs = {
|
| 98 |
-
"model": model_name,
|
| 99 |
-
"messages": messages,
|
| 100 |
-
"temperature": temperature,
|
| 101 |
-
"max_tokens": max_tokens
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
-
if json_mode:
|
| 105 |
-
kwargs["response_format"] = {"type": "json_object"}
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
"""
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
Args:
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
"
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
logger.info(f"[GROQ VISION]
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MEXAR Core Engine - Groq API Client Wrapper
|
| 3 |
+
Provides a unified interface for all Groq API interactions.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import base64
|
| 8 |
+
from typing import Optional, List, Dict, Any
|
| 9 |
+
from groq import Groq
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
|
| 12 |
+
# Load environment variables (.env) from root/backend
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class GroqClient:
|
| 17 |
+
"""
|
| 18 |
+
Unified Groq API client for MEXAR.
|
| 19 |
+
Handles LLM, Whisper (audio), and Vision (image) capabilities.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
def __init__(self, api_key: Optional[str] = None):
|
| 23 |
+
"""
|
| 24 |
+
Initialize Groq client with API key.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
api_key: Groq API key. If not provided, reads from GROQ_API_KEY env var.
|
| 28 |
+
"""
|
| 29 |
+
self.api_key = api_key or os.getenv("GROQ_API_KEY")
|
| 30 |
+
if not self.api_key:
|
| 31 |
+
raise ValueError("GROQ_API_KEY not found in environment variables")
|
| 32 |
+
|
| 33 |
+
self.client = Groq(api_key=self.api_key)
|
| 34 |
+
|
| 35 |
+
from core.config import settings
|
| 36 |
+
|
| 37 |
+
# Model configurations based on LLM_BACKBONE and environment settings
|
| 38 |
+
backbone = getattr(settings, "LLM_BACKBONE", "openai/gpt-oss-120b").lower()
|
| 39 |
+
active_model = getattr(settings, "GROQ_MODEL", os.getenv("GROQ_MODEL", "openai/gpt-oss-120b"))
|
| 40 |
+
|
| 41 |
+
if backbone == "mixtral":
|
| 42 |
+
self.models = {
|
| 43 |
+
"chat": "mixtral-8x7b-32768",
|
| 44 |
+
"advanced": "mixtral-8x7b-32768",
|
| 45 |
+
"fast": "mixtral-8x7b-32768",
|
| 46 |
+
"vision": "meta-llama/llama-4-scout-17b-16e-instruct",
|
| 47 |
+
"whisper": "whisper-large-v3"
|
| 48 |
+
}
|
| 49 |
+
elif backbone == "gemma":
|
| 50 |
+
self.models = {
|
| 51 |
+
"chat": "gemma2-9b-it",
|
| 52 |
+
"advanced": "gemma2-9b-it",
|
| 53 |
+
"fast": "gemma2-9b-it",
|
| 54 |
+
"vision": "meta-llama/llama-4-scout-17b-16e-instruct",
|
| 55 |
+
"whisper": "whisper-large-v3"
|
| 56 |
+
}
|
| 57 |
+
elif backbone == "llama3":
|
| 58 |
+
self.models = {
|
| 59 |
+
"chat": "llama-3.1-8b-instant",
|
| 60 |
+
"advanced": "llama-3.3-70b-versatile",
|
| 61 |
+
"fast": "llama-3.1-8b-instant",
|
| 62 |
+
"vision": "meta-llama/llama-4-scout-17b-16e-instruct",
|
| 63 |
+
"whisper": "whisper-large-v3"
|
| 64 |
+
}
|
| 65 |
+
else:
|
| 66 |
+
self.models = {
|
| 67 |
+
"chat": active_model, # Primary LLM (openai/gpt-oss-120b)
|
| 68 |
+
"advanced": active_model, # Advanced reasoning
|
| 69 |
+
"fast": active_model, # Fast responses
|
| 70 |
+
"vision": "meta-llama/llama-4-scout-17b-16e-instruct",
|
| 71 |
+
"whisper": "whisper-large-v3"
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
def chat_completion(
|
| 75 |
+
self,
|
| 76 |
+
messages: List[Dict[str, str]],
|
| 77 |
+
model: str = "chat",
|
| 78 |
+
temperature: float = 0.7,
|
| 79 |
+
max_tokens: int = 4096,
|
| 80 |
+
json_mode: bool = False
|
| 81 |
+
) -> str:
|
| 82 |
+
"""
|
| 83 |
+
Send a chat completion request.
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
messages: List of message dicts with 'role' and 'content'
|
| 87 |
+
model: Model key from self.models
|
| 88 |
+
temperature: Sampling temperature (0-2)
|
| 89 |
+
max_tokens: Maximum tokens in response
|
| 90 |
+
json_mode: If True, force JSON output
|
| 91 |
+
|
| 92 |
+
Returns:
|
| 93 |
+
Generated text response
|
| 94 |
+
"""
|
| 95 |
+
model_name = self.models.get(model, model)
|
| 96 |
+
|
| 97 |
+
kwargs = {
|
| 98 |
+
"model": model_name,
|
| 99 |
+
"messages": messages,
|
| 100 |
+
"temperature": temperature,
|
| 101 |
+
"max_tokens": max_tokens
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
if json_mode:
|
| 105 |
+
kwargs["response_format"] = {"type": "json_object"}
|
| 106 |
+
|
| 107 |
+
fallback_candidates = ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768", "gemma2-9b-it"]
|
| 108 |
+
try:
|
| 109 |
+
response = self.client.chat.completions.create(**kwargs)
|
| 110 |
+
return response.choices[0].message.content
|
| 111 |
+
except Exception as e:
|
| 112 |
+
err_str = str(e).lower()
|
| 113 |
+
if any(k in err_str for k in ["rate limit", "429", "decommissioned", "400", "quota", "tpd"]):
|
| 114 |
+
import logging
|
| 115 |
+
import time
|
| 116 |
+
for fallback in fallback_candidates:
|
| 117 |
+
if fallback == kwargs.get("model"):
|
| 118 |
+
continue
|
| 119 |
+
try:
|
| 120 |
+
logging.getLogger(__name__).warning(f"Groq API error on model '{kwargs['model']}': {e}. Falling back to '{fallback}'...")
|
| 121 |
+
kwargs["model"] = fallback
|
| 122 |
+
response = self.client.chat.completions.create(**kwargs)
|
| 123 |
+
return response.choices[0].message.content
|
| 124 |
+
except Exception as fb_err:
|
| 125 |
+
time.sleep(2)
|
| 126 |
+
continue
|
| 127 |
+
raise e
|
| 128 |
+
|
| 129 |
+
def analyze_with_system_prompt(
|
| 130 |
+
self,
|
| 131 |
+
system_prompt: str,
|
| 132 |
+
user_message: str,
|
| 133 |
+
model: str = "chat",
|
| 134 |
+
json_mode: bool = False
|
| 135 |
+
) -> str:
|
| 136 |
+
"""
|
| 137 |
+
Convenience method for system + user message pattern.
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
system_prompt: System instructions
|
| 141 |
+
user_message: User query
|
| 142 |
+
model: Model to use
|
| 143 |
+
json_mode: If True, force JSON output
|
| 144 |
+
|
| 145 |
+
Returns:
|
| 146 |
+
Generated response
|
| 147 |
+
"""
|
| 148 |
+
messages = [
|
| 149 |
+
{"role": "system", "content": system_prompt},
|
| 150 |
+
{"role": "user", "content": user_message}
|
| 151 |
+
]
|
| 152 |
+
return self.chat_completion(messages, model=model, json_mode=json_mode)
|
| 153 |
+
|
| 154 |
+
def transcribe_audio(self, audio_path: str, language: str = "en") -> str:
|
| 155 |
+
"""
|
| 156 |
+
Transcribe audio file using Whisper via direct HTTP request.
|
| 157 |
+
|
| 158 |
+
Args:
|
| 159 |
+
audio_path: Path to audio file
|
| 160 |
+
language: Language code (e.g., 'en', 'es')
|
| 161 |
+
|
| 162 |
+
Returns:
|
| 163 |
+
Transcribed text
|
| 164 |
+
"""
|
| 165 |
+
import requests
|
| 166 |
+
from pathlib import Path
|
| 167 |
+
|
| 168 |
+
url = "https://api.groq.com/openai/v1/audio/transcriptions"
|
| 169 |
+
|
| 170 |
+
headers = {
|
| 171 |
+
"Authorization": f"Bearer {self.api_key}"
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
audio_file_path = Path(audio_path)
|
| 175 |
+
|
| 176 |
+
# Determine the correct mime type
|
| 177 |
+
ext = audio_file_path.suffix.lower()
|
| 178 |
+
mime_types = {
|
| 179 |
+
".mp3": "audio/mpeg",
|
| 180 |
+
".wav": "audio/wav",
|
| 181 |
+
".m4a": "audio/mp4",
|
| 182 |
+
".ogg": "audio/ogg",
|
| 183 |
+
".flac": "audio/flac",
|
| 184 |
+
".webm": "audio/webm"
|
| 185 |
+
}
|
| 186 |
+
mime_type = mime_types.get(ext, "audio/mpeg")
|
| 187 |
+
|
| 188 |
+
with open(audio_path, "rb") as audio_file:
|
| 189 |
+
files = {
|
| 190 |
+
"file": (audio_file_path.name, audio_file, mime_type)
|
| 191 |
+
}
|
| 192 |
+
data = {
|
| 193 |
+
"model": "whisper-large-v3-turbo",
|
| 194 |
+
"language": language
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
response = requests.post(url, headers=headers, files=files, data=data, timeout=60)
|
| 198 |
+
|
| 199 |
+
if response.status_code == 200:
|
| 200 |
+
result = response.json()
|
| 201 |
+
return result.get("text", "")
|
| 202 |
+
else:
|
| 203 |
+
raise Exception(f"Groq Whisper API error: {response.status_code} - {response.text}")
|
| 204 |
+
|
| 205 |
+
def describe_image(
|
| 206 |
+
self,
|
| 207 |
+
image_path: str,
|
| 208 |
+
prompt: str = "Describe this image in detail.",
|
| 209 |
+
max_tokens: int = 1024
|
| 210 |
+
) -> str:
|
| 211 |
+
"""
|
| 212 |
+
Describe an image using Vision model.
|
| 213 |
+
|
| 214 |
+
Args:
|
| 215 |
+
image_path: Path to image file
|
| 216 |
+
prompt: Question about the image
|
| 217 |
+
max_tokens: Maximum response tokens
|
| 218 |
+
|
| 219 |
+
Returns:
|
| 220 |
+
Image description
|
| 221 |
+
"""
|
| 222 |
+
import logging
|
| 223 |
+
logger = logging.getLogger(__name__)
|
| 224 |
+
|
| 225 |
+
logger.info(f"[GROQ VISION] Starting image analysis for: {image_path}")
|
| 226 |
+
logger.info(f"[GROQ VISION] Prompt: {prompt[:100]}...")
|
| 227 |
+
|
| 228 |
+
# Verify file exists
|
| 229 |
+
if not os.path.exists(image_path):
|
| 230 |
+
raise FileNotFoundError(f"Image file does not exist: {image_path}")
|
| 231 |
+
|
| 232 |
+
# Get file size
|
| 233 |
+
file_size = os.path.getsize(image_path)
|
| 234 |
+
logger.info(f"[GROQ VISION] Image file size: {file_size} bytes")
|
| 235 |
+
|
| 236 |
+
# Read and encode image
|
| 237 |
+
with open(image_path, "rb") as img_file:
|
| 238 |
+
image_bytes = img_file.read()
|
| 239 |
+
image_data = base64.b64encode(image_bytes).decode("utf-8")
|
| 240 |
+
|
| 241 |
+
logger.info(f"[GROQ VISION] Image encoded to base64, length: {len(image_data)} chars")
|
| 242 |
+
|
| 243 |
+
# Detect image type from extension
|
| 244 |
+
ext = os.path.splitext(image_path)[1].lower()
|
| 245 |
+
mime_types = {
|
| 246 |
+
".jpg": "image/jpeg",
|
| 247 |
+
".jpeg": "image/jpeg",
|
| 248 |
+
".png": "image/png",
|
| 249 |
+
".gif": "image/gif",
|
| 250 |
+
".webp": "image/webp"
|
| 251 |
+
}
|
| 252 |
+
mime_type = mime_types.get(ext, "image/jpeg")
|
| 253 |
+
logger.info(f"[GROQ VISION] Detected MIME type: {mime_type}")
|
| 254 |
+
|
| 255 |
+
messages = [
|
| 256 |
+
{
|
| 257 |
+
"role": "user",
|
| 258 |
+
"content": [
|
| 259 |
+
{"type": "text", "text": prompt},
|
| 260 |
+
{
|
| 261 |
+
"type": "image_url",
|
| 262 |
+
"image_url": {
|
| 263 |
+
"url": f"data:{mime_type};base64,{image_data}"
|
| 264 |
+
}
|
| 265 |
+
}
|
| 266 |
+
]
|
| 267 |
+
}
|
| 268 |
+
]
|
| 269 |
+
|
| 270 |
+
logger.info(f"[GROQ VISION] Calling Groq API with model: {self.models['vision']}")
|
| 271 |
+
|
| 272 |
+
try:
|
| 273 |
+
response = self.client.chat.completions.create(
|
| 274 |
+
model=self.models["vision"],
|
| 275 |
+
messages=messages,
|
| 276 |
+
max_tokens=max_tokens,
|
| 277 |
+
temperature=0.7
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
result = response.choices[0].message.content
|
| 281 |
+
logger.info(f"[GROQ VISION] Success! Response length: {len(result)} chars")
|
| 282 |
+
logger.info(f"[GROQ VISION] Response preview: {result[:200]}...")
|
| 283 |
+
|
| 284 |
+
return result
|
| 285 |
+
|
| 286 |
+
except Exception as e:
|
| 287 |
+
logger.error(f"[GROQ VISION] API call failed: {type(e).__name__}: {str(e)}")
|
| 288 |
+
raise
|
| 289 |
+
|
| 290 |
+
def extract_json(self, text: str, schema_description: str) -> Dict[str, Any]:
|
| 291 |
+
"""
|
| 292 |
+
Extract structured JSON from text.
|
| 293 |
+
|
| 294 |
+
Args:
|
| 295 |
+
text: Input text to analyze
|
| 296 |
+
schema_description: Description of expected JSON structure
|
| 297 |
+
|
| 298 |
+
Returns:
|
| 299 |
+
Parsed JSON dictionary
|
| 300 |
+
"""
|
| 301 |
+
import json
|
| 302 |
+
|
| 303 |
+
system_prompt = f"""You are a JSON extraction assistant.
|
| 304 |
+
Extract structured data from the given text and return ONLY valid JSON.
|
| 305 |
+
Expected structure: {schema_description}
|
| 306 |
+
Do not include any explanation, only the JSON object."""
|
| 307 |
+
|
| 308 |
+
response = self.analyze_with_system_prompt(
|
| 309 |
+
system_prompt=system_prompt,
|
| 310 |
+
user_message=text,
|
| 311 |
+
model="fast",
|
| 312 |
+
json_mode=True
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
return json.loads(response)
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
# Singleton instance for easy importing
|
| 319 |
+
_client_instance: Optional[GroqClient] = None
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
def get_groq_client() -> GroqClient:
|
| 323 |
+
"""Get or create the singleton Groq client instance."""
|
| 324 |
+
global _client_instance
|
| 325 |
+
if _client_instance is None:
|
| 326 |
+
_client_instance = GroqClient()
|
| 327 |
+
return _client_instance
|
data/agents/financial_agent/context.txt
ADDED
|
@@ -0,0 +1,1182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
============================================================
|
| 3 |
+
SOURCE: 0000320575_0000320575-20-000008.txt (TXT)
|
| 4 |
+
============================================================
|
| 5 |
+
|
| 6 |
+
10-K/A 1 form10-ka2019.htm 10-K/A Document UNITED STATES SECURITIES AND EXCHANGE COMMISSION Washington, D. C. 20549 FORM 10-K/A Amendment No. 1 (Mark one) ý ANNUAL REPORT PURSUANT TO SECTION 13 OR 15(d) OF THE SECURITIES EXCHANGE ACT OF 1934 For the fiscal year ended December 31, 2019 or o TRANSITION REPORT PURSUANT TO SECTION 13 OR 15(d) OF THE SECURITIES EXCHANGE ACT OF 1934 Commission File Number: 1-8182 PIONEER ENERGY SERVICES CORP. (Exact name of registrant as specified in its charter) _____________________________________________ TEXAS 74-2088619 (State or other jurisdiction of incorporation or organization) (I.R.S. Employer Identification Number) 1250 N.E. Loop 410, Suite 1000 San Antonio, Texas 78209 (Address of principal executive offices) (Zip Code) Registrant’s telephone number, including area code: (855) 884-0575 Securities registered pursuant to Section 12(b) of the Act Title of each class Trading Symbol(s) Name of each exchange on which registered Securities registered pursuant to Section 12(g) of the Act: Common Stock, $0.10 par value Indicate by check mark if the registrant is a well-known seasoned issuer, as defined in Rule 405 of the Securities Act. Yes ¨ No þ Indicate by check mark if the registrant is not required to file reports pursuant to Section 13 or Section 15(d) of the Act. Yes ¨ No þ Indicate by check mark whether the registrant: (1) has filed all reports required to be filed by Section 13 or 15(d) of the Securities Exchange Act of 1934 during the preceding 12 months (or for such shorter period that the registrant was required to file such reports), and (2) has been subject to such filing requirements for the past 90 days. Yes þ No ¨ Indicate by check mark whether the registrant has submitted electronically every Interactive Data File required to be submitted pursuant to Rule 405 of Regulation S-T (§ 232.405 of this chapter) during the preceding 12 months (or for such shorter period that the registrant was required to submit such files). Yes þ No ¨ Indicate by check mark whether the registrant is a large accelerated filer, an accelerated filer, a non-accelerated filer, a smaller reporting company, or an emerging growth company. See the definitions of “large accelerated filer,” “accelerated filer,” “smaller reporting company,” and “emerging growth company” in Rule 12b-2 of the Exchange Act. Large accelerated filer o Accelerated filer o Non-accelerated filer þ Smaller reporting company þ Emerging growth company o If an emerging growth company, indicate by check mark if the registrant has elected not to use the extended transition period for complying with any new or revised financial accounting standards provided pursuant to Section 13(a) of the Exchange Act. ¨ Indicate by check mark whether the registrant is a shell company (as defined in Rule 12b-2 of the Exchange Act). Yes ¨ No þ The aggregate market value of the registrant’s common stock held by non-affiliates of the registrant as of the last business day of the registrant’s most recently completed second fiscal quarter (based on the closing sales price on the New York Stock Exchange (NYSE) on June 30, 2019 ) was approximately $19.0 million . As of March 31, 2020 , there were 79,579,571 shares of common stock, par value $0.10 per share, of the registrant issued and outstanding. EXPLANATORY NOTE This Amendment No. 1 on Form 10-K/A (this "Amendment") amends the Annual Report on Form 10-K of Pioneer Energy Services Corp. ("Pioneer," "Company," "we," "us," and "our") for the year ended December 31, 2019 that was originally filed with the U.S. Securities and Exchange Commission ("SEC") on March 6, 2020 (the "Original Filing"). The purpose of this Amendment is to (i) supplement the risk factors set forth in Item 1A of Part I of the Original Filing and (ii) provide the information required by Items 10, 11, 12, 13, and 14 of Part III of the Original Filing, which information was previously omitted from the Original Filing in reliance on General Instruction G(3) to Form 10-K. The supplemental risk factors set forth in this Amendment should be read in conjunction with the other risk factors set forth in the Original Filing, and Part III of the Original Filing is hereby amended and restated in its entirety. Capitalized terms not otherwise defined in Part III of this Amendment shall have the same meanings assigned to such terms in Parts I and II of the Original Filing. In addition, as required by Rule 12b-15 under the Securities Exchange Act of 1934, as amended (the "Exchange Act"), currently dated certifications by our principal executive officer and principal financial officer are filed as exhibits to this Amendment under Item 15 of Part IV hereof. Except as described above, this Amendment does not amend or otherwise update any other information in the Original Filing. Accordingly, this Amendment should be read in conjunction with the Original Filing and with our filings with the SEC subsequent to the Original Filing. As previously disclosed, on March 1, 2020 , Pioneer and certain of its affiliates (together with Pioneer, the "Pioneer RSA Parties"), filed voluntary petitions for reorganization under chapter 11 of title 11 of the United States Code (the "Bankruptcy Code") in the United States Bankruptcy Court for the Southern District of Texas (the "Bankruptcy Court"). The Chapter 11 proceedings are being jointly administered under the caption In re Pioneer Energy Services Corp. et al (the "Chapter 11 Cases"). For the duration of the Chapter 11 Cases, our operations and our ability to develop and execute our business plan are subject to risks and uncertainties associated with the Chapter 11 Cases as described in "Item 1A. Risk Factors" in Part I of the Original Filing and this Amendment. As a result of these risks and uncertainties, our assets, liabilities, shareholders' equity, officers and/or directors could be significantly different following the conclusion of the Chapter 11 Cases, and the description of our operations, properties, and capital plans included in the Original Filing may not accurately reflect our operations, properties, and capital plans following the Chapter 11 Cases. 1 TABLE OF CONTENTS Page PART I Item 1A. Risk Factors 3 PART III Item 10. Directors, Executive Officers and Corporate Governance 5 Item 11. Executive Compensation 10 Item 12. Security Ownership of Certain Beneficial Owners and Management and Related Shareholder Matters 20 Item 13. Certain Relationships and Related Transactions, and Director Independence 22 Item 14. Principal Accounting Fees and Services 23 PART IV Item 15. Exhibits, Financial Statement Schedules 24 Signatures 25 2 PART I ITEM 1A. RISK FACTORS Except as discussed below, there has been no material change in our risk factors as previously disclosed in Item 1A – “Risk Factors” of the Original Filing. The risk factors set forth in this Amendment should be read in conjunction with the other risk factors set forth in the Original Filing. The following two risk factors in Part I, Item 1A of the Original Filing are amended and restated in their entirety as follows: • We derive all our revenues from companies in the oil and gas exploration and production industry, a historically cyclical industry with levels of activity that are significantly affected by the levels and volatility of oil and gas prices. As a provider of contract land drilling services and oil and gas production services, our business depends on the level of exploration and production activity in the geographic markets where we operate. The oil and gas exploration and production industry is a historically cyclical industry characterized by significant changes in the levels of exploration and development activities. Oil and gas prices, and market expectations of potential changes in those prices, significantly affect the levels of those activities. Oil and gas prices have been volatile historically and, we believe, will likely continue in the future. Oil prices have experienced declines in March 2020 and could decline further. Worldwide political, economic, and military events, as well as natural disasters and pandemics (including the recent coronavirus (COVID-19) outbreak), have contributed to oil and gas price volatility historically, and are likely to continue to do so in the future. Many factors beyond our control affect oil and gas prices, including: • the worldwide supply and demand for oil and gas; • the cost of exploring for, producing and delivering oil and gas; • the discovery rate of new oil and gas reserves; • the rate of decline of existing and new oil and gas reserves; • available pipeline and other oil and gas transportation capacity; • the levels of oil and gas storage; • the ability of oil and gas exploration and production companies to raise capital; • economic conditions in the United States and elsewhere; • actions by the Organization of Petroleum Exporting Countries (OPEC) and Russia; • political instability in oil and gas producing regions; • governmental regulations, both domestic and foreign; • domestic and foreign tax policy; • weather conditions in the United States and elsewhere; • the pace adopted by foreign governments for the exploration, development and production of their national reserves, or their investments in oil and gas reserves located in other countries; and • the price of foreign imports of oil and gas. Additionally, the above factors can also be affected by technological advances affecting energy consumption and the supply and demand within the market for renewable energy resources. • Oil and natural gas prices, and market expectations of potential changes in these prices, significantly impact the level of worldwide drilling and production services activities. Oil and natural gas prices, and market expectations of potential changes in these prices, significantly impact the level of worldwide drilling and production services activities. Reduced demand for oil and natural gas generally results in lower prices for these commodities and often impacts the economics of planned drilling projects and ongoing production projects, resulting in the curtailment, reduction, delay or postponement of such projects for an indeterminate period of time. When drilling and production activity and spending declines, both dayrates and utilization historically decline as well. In late 2014, oil prices worldwide began to drop significantly and as a result, our clients significantly reduced both their operating and capital expenditures during 2015 and 2016, which adversely affected our business. In 2017 and 2018, our clients modestly increased their spending as compared to 2016 levels, and our business trended upward as a result. However, in late 2018, oil prices again began to decline and despite some improvement in early 2019, have since languished without significant improvement in 2019 and early 2020. Since January 2020, the coronavirus (COVID-19) outbreak and fear of further spread of the coronavirus have caused disruptions in international economies and international financial and oil markets, including a decline in the price of oil. The economic 3 downturn caused by the coronavirus outbreak has weakened demand for oil, and after OPEC and a group of oil producing nations led by Russia failed on March 6, 2020 to agree on oil production cuts, Saudi Arabia announced that it would cut oil prices and increase production, leading to a sharp further decline in oil trading prices. Following these developments, WTI crude oil prices fell to $20 per barrel at the end of March 2020. We cannot anticipate whether or when this dispute will be resolved and production returned to normalized levels. The coronavirus and responses of oil producers to the lower demand for oil and lower oil prices are rapidly evolving situations. Oil and gas exploration and production companies have announced reductions to previously planned capital spending programs for 2020, thereby reducing demand for our services. Sustained low or worsening oil prices could have an adverse effect on our financial condition, results of operations, and cash flow. Additionally, because our business depends on the level of spending by our clients, we are also affected by our clients’ ability to access the capital markets. After several consecutive years without significant improvement in commodity prices, many exploration and production companies have limited their spending to a level which can be supported by net operating cash flows alone, as access to the capital markets through debt or equity financings has become more challenging in our industry. This challenge has increased recently due to the major stock market and bond market indices experiencing substantial declines, with such declines intensifying, and elevated levels of volatility, in 2020. If the reduction in the overall level of exploration and development activities, whether resulting from changes in oil and gas prices or otherwise, continues or worsens, it could adversely affect us further by negatively impacting: • our revenues, cash flows and profitability; • the fair market value of our drilling and production services fleets; • our ability to maintain or increase its borrowing capacity; • our ability to obtain additional capital to finance our business or make acquisitions, and the cost of that capital; • the collectability of our receivables; • our ability to adequately maintain our equipment, or bring it back into service; and • our ability to retain skilled operations personnel. The following risk factor supplements those above and those set forth in the Original Filing. • The rapid spread of a contagious illness, including the recent spread of COVID-19, or fear of such an event, could adversely affect our business, operations, and financial condition. Our business could be adversely affected by a widespread outbreak of contagious disease, including the recent outbreak of respiratory illness caused by a novel coronavirus (COVID-19). Since January 2020, the coronavirus outbreak and fear of further spread of the coronavirus have caused significant disruptions in international economies and international financial and oil markets, including a substantial decline in the price of oil. Our customers responded to the outbreak by announcing reductions to their capital programs, resulting in a reduction in demand for our services. As the coronavirus outbreak is still evolving, much of its international and domestic impact remains unknown. The coronavirus outbreak has led to quarantines of entire countries and smaller localities, cancellation of events and travel, prohibitions on entry imposed by many countries, government-mandated and voluntary business and school shutdowns, supply chain interruptions, and overall economic and financial market instability. Further spread of the coronavirus could cause additional quarantines, reduction in business activity, labor shortages and other operational
|
| 7 |
+
|
| 8 |
+
============================================================
|
| 9 |
+
SOURCE: 0001041588_0001041588-20-000001.txt (TXT)
|
| 10 |
+
============================================================
|
| 11 |
+
|
| 12 |
+
10-K 1 accr12311910K.txt January 2nd, 2020
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
PERIOD and DOCUMENT: 10KSB for the FULL YEAR 12/31/2019
|
| 17 |
+
COMPANY CONFIRMED NAME: ACCESS-POWER, INC.
|
| 18 |
+
CENTRAL INDEX KEY: 0001041588
|
| 19 |
+
FORM TYPE: 10KSB
|
| 20 |
+
SEC FILE NUMBER: 333-65069
|
| 21 |
+
BUSINESS ADDRESS:
|
| 22 |
+
STREET 1: 17164 DUNE VIEW DRIVE
|
| 23 |
+
STREET 2: APT 106
|
| 24 |
+
CITY: GRAND HAVEN
|
| 25 |
+
STATE: MI
|
| 26 |
+
ZIP: 49417
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
Respectfully submitted to the,
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
UNITED STATES SECURITIES AND EXCHANGE COMMISSION
|
| 34 |
+
WASHINGTON, D.C. 20549
|
| 35 |
+
The Division of Corporation Finance,
|
| 36 |
+
100 F Street NE, Washington, D.C. 20549
|
| 37 |
+
|
| 38 |
+
FORM 10-KSB
|
| 39 |
+
|
| 40 |
+
/X/ ANNUAL REPORT PURSUANT TO SECTION 13 OR 15(d) OF THE
|
| 41 |
+
SECURITIES EXCHANGE ACT OF 1934 FOR THE FISCAL YEAR ENDED
|
| 42 |
+
DECEMBER 31, 2019.
|
| 43 |
+
|
| 44 |
+
TRANSITION REPORT PURSUANT TO SECTION 13 OR 15(d)
|
| 45 |
+
OF THE SECURITIES EXCHANGE ACT OF 1934
|
| 46 |
+
FOR THE TRANSITION PERIOD FROM ______ TO _____
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
COMMISSION FILE NUMBER: 333-65069
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
ACCESS-POWER, INC.
|
| 53 |
+
[Exact name of Registrant as
|
| 54 |
+
specified in its charter]
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
*** FLORIDA ***
|
| 58 |
+
|
| 59 |
+
Domicile State or other jurisdiction of
|
| 60 |
+
incorporation or organization
|
| 61 |
+
http://www.sunbiz.org
|
| 62 |
+
|
| 63 |
+
*** MICHIGAN ***
|
| 64 |
+
|
| 65 |
+
State of Operations
|
| 66 |
+
https://cofs.lara.state.mi.us/SearchApi/Search/Search
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
59-3420985
|
| 71 |
+
(I.R.S. Employer
|
| 72 |
+
Identification No.)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
17164 DUNE VIEW DRIVE
|
| 76 |
+
APT 106
|
| 77 |
+
GRAND HAVEN, MI 49417
|
| 78 |
+
|
| 79 |
+
or
|
| 80 |
+
|
| 81 |
+
PO BOX 598
|
| 82 |
+
Grand Haven, MI 49417
|
| 83 |
+
|
| 84 |
+
(Address of principal executive offices)
|
| 85 |
+
|
| 86 |
+
REGISTRANTS TELEPHONE NUMBER, INCLUDING AREA CODE: 616-312-5390
|
| 87 |
+
|
| 88 |
+
SECURITIES REGISTERED PURSUANT TO SECTION 12(b) OF THE ACT: NONE
|
| 89 |
+
|
| 90 |
+
SECURITIES REGISTERED PURSUANT TO SECTION 12(g) OF THE ACT:
|
| 91 |
+
COMMON STOCK, $0.001 PAR VALUE - 500,000,000 shares authorized.
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
Indicate by check mark if the registrant is a well-known
|
| 95 |
+
seasoned issuer, as defined in Rule 405 of the Securities
|
| 96 |
+
Act. Yes No X
|
| 97 |
+
|
| 98 |
+
Indicate by check mark if the registrant is not required to file
|
| 99 |
+
reports pursuant to Section 13 or Section 15(d) of the Act.
|
| 100 |
+
Yes No X
|
| 101 |
+
|
| 102 |
+
The Company has had discussions with the SEC regarding a FRAUD
|
| 103 |
+
FORM 15 that was filed by previous management in 2007. We believe
|
| 104 |
+
that we have a fiduciary responsibility to report transaprent
|
| 105 |
+
information to our Shareholders.
|
| 106 |
+
|
| 107 |
+
We were victims of toxic death spiral debentures from 1998 to
|
| 108 |
+
2008. PLEASE FORGIVE OUR PAST. The past it the past, and
|
| 109 |
+
we wish to move forward with a plan to become fully
|
| 110 |
+
current with the SEC. We are trying the best we can within the
|
| 111 |
+
Spirit of the Law.
|
| 112 |
+
|
| 113 |
+
https://www.sec.gov/Archives/edgar/data/1041588/
|
| 114 |
+
000117347307000043/0001173473-07-000043-index.htm
|
| 115 |
+
|
| 116 |
+
Indicate by check mark whether the registrant (1) has
|
| 117 |
+
filed all reports required to be filed be Section 13 or 15(d)
|
| 118 |
+
of the Securities Exchange Act of 1934 during the preceding 12
|
| 119 |
+
months (or for such shorter period that the registrant was
|
| 120 |
+
required to file such reports), and (2) has been subject
|
| 121 |
+
to such filing requirements for the past 90 days. Yes X No
|
| 122 |
+
|
| 123 |
+
Indicate by check mark whether the registrant has submitted
|
| 124 |
+
electronically and posted on its corporate Web site, if any,
|
| 125 |
+
every Interactive Data File required to be submitted and
|
| 126 |
+
posted pursuant to Rule 405 of Regulation S-T (232.405 of
|
| 127 |
+
this chapter) during the preceding 12 months (or for such
|
| 128 |
+
shorter period that the registrant was required to submit
|
| 129 |
+
and post such files). Yes X No
|
| 130 |
+
|
| 131 |
+
Indicate by check mark if disclosure of delinquent filers
|
| 132 |
+
pursuant to Item 405 of Regulation S-K (Section 229.405
|
| 133 |
+
of this chapter) is not contained herein, and will not be
|
| 134 |
+
contained, to the best of registrants knowledge, in definitive
|
| 135 |
+
proxy or information statements incorporated by reference in
|
| 136 |
+
Part III of this Form 10-K or any amendment to this Form 10-K.
|
| 137 |
+
|
| 138 |
+
Indicate by check mark whether the registrant is a large a
|
| 139 |
+
ccelerated filer, an accelerated filer, a non-accelerated
|
| 140 |
+
filer, smaller reporting company, or an emerging growth
|
| 141 |
+
company. See the definitions of large accelerated filer,
|
| 142 |
+
accelerated filer, smaller reporting company, and
|
| 143 |
+
emerging growth company in Rule 12b-2 of the Exchange Act.
|
| 144 |
+
|
| 145 |
+
Large accelerated filer NO Accelerated filer NO
|
| 146 |
+
Non-accelerated filer NO (Do not check if a smaller
|
| 147 |
+
reporting company)
|
| 148 |
+
|
| 149 |
+
Smaller reporting company YES
|
| 150 |
+
Emerging growth company YES
|
| 151 |
+
|
| 152 |
+
If an emerging growth company, indicate by check mark if
|
| 153 |
+
the registrant has elected not to use the extended
|
| 154 |
+
transition period for complying with any new or revised
|
| 155 |
+
financial accounting standards provided pursuant to
|
| 156 |
+
ection 13(a) of the Exchange Act.
|
| 157 |
+
|
| 158 |
+
Indicate by check mark whether the registrant is a shell
|
| 159 |
+
company (as defined in Rule 12b-2 of the Exchange Act).
|
| 160 |
+
Yes No X
|
| 161 |
+
|
| 162 |
+
The aggregate market value of the voting common stock held by non-affils.
|
| 163 |
+
of the registrant (assuming officers and directors are affiliates) was
|
| 164 |
+
approximately $1,017,140.59 as of 12/3119, computed on the basis of
|
| 165 |
+
the closing price on such date.
|
| 166 |
+
|
| 167 |
+
As of January 2nd, 2020, there were 244,144,121 shares of the
|
| 168 |
+
registrant's
|
| 169 |
+
Common Stock outstanding. PATRICK J JENSEN, Director of Access-Power, Inc.
|
| 170 |
+
has 131,128,500 Restricted Shares. 113,015,621 + 131,128,500 =
|
| 171 |
+
244,144,121
|
| 172 |
+
total shares available of Access-Power, Inc. common stock.
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
*****WE ONLY HAVE COMMON STOCK*****
|
| 176 |
+
|
| 177 |
+
*****WE DO NOT HAVE CONVERTIBLE NOTES or WARRANTS*****
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
THE TOTAL MARKET CAPITALIZATION AS OF THE CLOSE OF BUSINESS
|
| 181 |
+
January 2nd, 2020 IS 2,197,297.09 BASED ON A CLOSING PRICE OF
|
| 182 |
+
$.009 PER SHARE.
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
___________________________________________________________________________
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
**** CURRENT UPDATE ****
|
| 190 |
+
|
| 191 |
+
Our comeback journey continues. We are very excited about our future.
|
| 192 |
+
|
| 193 |
+
We filed our first tax return in 18 years. We hired H&R Block of Muskegon
|
| 194 |
+
to look over and supervise and review all of our financial bank records
|
| 195 |
+
and statements for 2018 and 2019. We believe in 100% Transparency. We are
|
| 196 |
+
making very good informed decisions. H&R BLOCK engagement--->
|
| 197 |
+
|
| 198 |
+
This occured on November 5, 2019 - our date of engagement with HR&BLOCK
|
| 199 |
+
|
| 200 |
+
In November of 2019 , we have started an application with the State of
|
| 201 |
+
Michigan to obtain a MicroMarijuana business. We only plan to operate
|
| 202 |
+
our futuristic plan of delivering Marijuana Plants by Drones via a
|
| 203 |
+
service called Clones by Drones TM. This process could take
|
| 204 |
+
2-3 years to implement. The application was started at:
|
| 205 |
+
|
| 206 |
+
https://aca3.accela.com/MIMM/Login.aspx
|
| 207 |
+
|
| 208 |
+
The Company believes it has the right to commence operations with a
|
| 209 |
+
pending license application. ACCR has a very super cool business model
|
| 210 |
+
that will defy conventional logic---> LOGISTICs are simple.
|
| 211 |
+
|
| 212 |
+
We expect to get the licenses, zoning requirements, and permits
|
| 213 |
+
necessary to operate our BUSINESS PLAN. WE HAVE A SALES BUSINESS PLAN.
|
| 214 |
+
ACCR has a business plan that represents my DREAMs in life. I will
|
| 215 |
+
not hurt my Shareholders. Everyone is welcome, and ACCR is grateful
|
| 216 |
+
and thankful. We are not sellers of our common stock. PERIOD.
|
| 217 |
+
|
| 218 |
+
Access-Power, Inc. has a big task ahead of itself. We have many
|
| 219 |
+
obstancles to grow our 20' x 20' Master Mother Grow and Cloning Rooms. We
|
| 220 |
+
would expect to be fully operational with inventory in 3 months of
|
| 221 |
+
beginning construction. We do not have Clones by Drones operational yet
|
| 222 |
+
to accept Visa/Martercard or Pay/Pal payments. We expect to turn on
|
| 223 |
+
our eCommerce website sometime in 2020 or possibly 2021. Probably in
|
| 224 |
+
2020. We believe in only MEDICAL MARIJUANA, and will not offer at the
|
| 225 |
+
moment any products to people that do not possess a valid Medical
|
| 226 |
+
Marijuana Card issued by any State. There are dispensaries here in
|
| 227 |
+
Michigan that accept patients from out of State. We plan to
|
| 228 |
+
mirror the growth's of the Dispensaries. We plan to grow around
|
| 229 |
+
the dispensaries, and offer our cool and futuristic service
|
| 230 |
+
by Drones. Access-Power, Inc. has in its business plan a way to
|
| 231 |
+
logistically deliver in a very cost effective way.
|
| 232 |
+
|
| 233 |
+
BACK to our engagement with H&R Block on November 5, 2019.
|
| 234 |
+
|
| 235 |
+
We realize that this is not a PCAOB firm, PLEASE...
|
| 236 |
+
however, this is all the Company can afford. Within the spirit of the
|
| 237 |
+
law, we are filing these documents to report transparency to our
|
| 238 |
+
Shareholders. We have spoken to the SEC regarding this requirement, and
|
| 239 |
+
it is the Companies intention to hire a PCAOB firm in 2020 or in 2021.
|
| 240 |
+
We have to start somewhere and again it is our goal to fully hire a PCAOB
|
| 241 |
+
accountant in the year 2020 or 2021. We have communicated our 2 year
|
| 242 |
+
plan with the SEC. We have a very transparent relationship
|
| 243 |
+
with many divisions of FINRA. The Company has great contacts at
|
| 244 |
+
OTC Markets. The Company also has a trusting relationship that is
|
| 245 |
+
very open with the SEC.
|
| 246 |
+
|
| 247 |
+
Access-Power, Inc. was the victim of toxic convertible death spiral
|
| 248 |
+
debentures from inception through 2008. I purchased common stock
|
| 249 |
+
during this time, and slowly became a 10% stock holder over the years.
|
| 250 |
+
The Florida Law which allowed us to fire all previous crooked Directors
|
| 251 |
+
is Chap 607.0702 of the 2012 Florida Senate Law.
|
| 252 |
+
|
| 253 |
+
The Company has survived a short attack, and the Company will attempt
|
| 254 |
+
to hire an attorney in 2020 to represent us.
|
| 255 |
+
|
| 256 |
+
ON DECEMBER 13, 2019, ACCESS-POWER, INC. eFILED ITS FIRST STATE AND
|
| 257 |
+
FEDERAL TAX RETURN IN OVER 18 YEARS. THE eFILE WAS ACCEPTED BY ALL
|
| 258 |
+
PARTIES ON DECEMBER 19, 2019. ACCESS-POWER, INC. PLANS TO CONTINUE TO
|
| 259 |
+
WORK WITH OUR TEMPORARY AUDITOR THROUGHOUT 2020. WE ARE STRIVING TO BE
|
| 260 |
+
A VERY TRANSPARENT AND CURRENT REPORTING ENTITY. THERE ARE ONLY
|
| 261 |
+
98,144,246 SHARES IN OUR PUBLIC FLOAT. THIS IS FIXED, AND THERE IS NO
|
| 262 |
+
DILUTION, I REPEAT....NO DILUTION IN THE COMPANY COMMON SHARES
|
| 263 |
+
THROUGHOUT 2020 AND 2021. AGAIN, THE SUPPLY OF STOCK IS FIXED.
|
| 264 |
+
|
| 265 |
+
We are a micro-cap business with very struggling revenues at
|
| 266 |
+
the moment, and we are developing the first eCommerce website to deliver
|
| 267 |
+
Marijuana, Marijuana plants, and other Marijuana products such as Edibles
|
| 268 |
+
by Drones. We are going through a rough economic business cycle.
|
| 269 |
+
We applied for a Trademark SERVICE MARK on November 13, 2019.
|
| 270 |
+
|
| 271 |
+
We will succeed. This may take a few years to develop. At the present
|
| 272 |
+
time, I am still waiting for our Companies previously announced
|
| 273 |
+
$50,000.00 donation. This may take another 6-9 months.
|
| 274 |
+
|
| 275 |
+
There are many entities that do not want ACCR to comeback.
|
| 276 |
+
|
| 277 |
+
As of the close of business January 1st 2020, our commomn stock
|
| 278 |
+
structure is as follows:
|
| 279 |
+
|
| 280 |
+
Authorized Common Stock: 500,000,000 shares
|
| 281 |
+
Outstanding Common Stock: 244,144,121 shares
|
| 282 |
+
Estimated Float: Substanstially less than 98,244,146 shares
|
| 283 |
+
Restricted Common Stock: 145,769,975 shares
|
| 284 |
+
|
| 285 |
+
https://www.otcmarkets.com/stock/ACCR/profile
|
| 286 |
+
|
| 287 |
+
We have public profiles at:
|
| 288 |
+
|
| 289 |
+
https://www.linkedin.com/in/patrick-j-jensen-564946b4
|
| 290 |
+
https://www.twitter.com/AccessOtc
|
| 291 |
+
https://www.twitter.com/AccrOtc
|
| 292 |
+
https://brokercheck.finra.org/individual/summary/1952963
|
| 293 |
+
|
| 294 |
+
OUR CORPORATE WEBSITEs ARE:
|
| 295 |
+
|
| 296 |
+
http://www.myaccess-power.com
|
| 297 |
+
http://www.clonesbydrones.com
|
| 298 |
+
http://www.mycbdpets.com
|
| 299 |
+
http://www.nyumarijuana.com
|
| 300 |
+
|
| 301 |
+
Access-Power, Inc.'s contact is pjensen@myaccess-power.com.
|
| 302 |
+
|
| 303 |
+
We applied for a Trademark for Clones By Drones TM. The service
|
| 304 |
+
will be very futuristic, and will be the first of its kind.
|
| 305 |
+
|
| 306 |
+
Access-Power, Inc. is in good standing and has a very good relationship
|
| 307 |
+
with our Transfer Agent, Standard Transfer & Co.
|
| 308 |
+
Our Transfer Agent is SEC registered, and except for one
|
| 309 |
+
change in ownership in 2007, is our original transfer agent dating
|
| 310 |
+
back to 1996. Our transfer agent is SEC registered and has a profile at:
|
| 311 |
+
|
| 312 |
+
https://www.sec.gov/divisions/marketreg/mrtransfer.shtml
|
| 313 |
+
https://www.otcmarkets.com/learn/service-providers/2433?t=6
|
| 314 |
+
|
| 315 |
+
https://standardtransferco.com
|
| 316 |
+
|
| 317 |
+
Standard Transfer & Co.
|
| 318 |
+
440 East 400 South Suite 200, Salt Lake City, Utah 84111
|
| 319 |
+
Phone (801) 571-8844 Fax: (801) 328-4058
|
| 320 |
+
|
| 321 |
+
Our ticker symbol ACCR and further information may be found at:
|
| 322 |
+
|
| 323 |
+
https://www.sec.gov/cgi-bin/browse-edgar?CIK=accr
|
| 324 |
+
|
| 325 |
+
State the aggregate market value of the voting stock held by
|
| 326 |
+
non-affiliates
|
| 327 |
+
computed by reference to the price at which the stock was sold,
|
| 328 |
+
or the average
|
| 329 |
+
bid and asked prices of such stock as of a specified date
|
| 330 |
+
within the past 60
|
| 331 |
+
days: $1,130,156.21
|
| 332 |
+
|
| 333 |
+
At January 1st 2020, there were issued and outstanding 244,144,121
|
| 334 |
+
shares of Common Stock, and Patrick J. Jensen owns 131,128,500 shares
|
| 335 |
+
or 53.7090
|
| 336 |
+
percent of all the Common Stock. I am willing to give up Control and
|
| 337 |
+
a
|
| 338 |
+
percentage of the Company, for an Equity Partner in our Company.
|
| 339 |
+
All
|
| 340 |
+
131,128,500 are currently restricted and held in book entry form at the
|
| 341 |
+
Transfer Agent. I am actively looking for a MERGER DEAL.
|
| 342 |
+
|
| 343 |
+
There are currently a total of 98,244,146 shares registered in our
|
| 344 |
+
float.
|
| 345 |
+
Management continues to believe that there is a massive short position
|
| 346 |
+
in our Company stock that was accumulated from 1999 to 2008, and we
|
| 347 |
+
estimate
|
| 348 |
+
this short position to be massive. As Director of this Company, I
|
| 349 |
+
want to
|
| 350 |
+
apologize to my Shareholders for previously stating that our estimated
|
| 351 |
+
float
|
| 352 |
+
was between 5,000,000 and 10,000,000. Although I firmly believe the
|
| 353 |
+
float
|
| 354 |
+
in our Company stock is substantially lower than the official
|
| 355 |
+
98,244,146 shares
|
| 356 |
+
reported to OTC Markets.
|
| 357 |
+
|
| 358 |
+
We care about our Shareholders dearly, and our top priority is investor
|
| 359 |
+
protection. There is NO DILUTION IN THE COMMON STOCK OF ACCR.
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
PART I
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
ITEM 1. DESCRIPTION OF BUSINESS
|
| 366 |
+
|
| 367 |
+
Access-Power, Inc, is a for profit business looking for a
|
| 368 |
+
MERGER CANDIDATE.
|
| 369 |
+
We currently pay bills through a Work at Home business. Our monthly
|
| 370 |
+
fixed expenses are previosuly documented in an 8K at $2,350.00. We
|
| 371 |
+
operate a part time work at home business. The company subcontracts
|
| 372 |
+
through a big call center, and provides sales and customer service
|
| 373 |
+
to big Fortune 500 companies. In 2019, we operated via Hunter Vunter,
|
| 374 |
+
our subsidiary through www.liveops.com. We ended the relationship
|
| 375 |
+
with LiveOps on May 1st 2019. As a result, Hunter Venture was
|
| 376 |
+
officially dissolved.
|
| 377 |
+
|
| 378 |
+
The Company struggled from May 1, 2019 to October 18, 2019. We had no
|
| 379 |
+
income during this period, and our operational expenses were paid for
|
| 380 |
+
by myself, Patrick J. Jensen as a donation to the Company. I personally
|
| 381 |
+
paid out of my own pocket all the expenses during this dark time.
|
| 382 |
+
|
| 383 |
+
On October 2, 2019---> I dreamed of getting off the greys. ACCR is a
|
| 384 |
+
DREAM right now...This is how builders work.
|
| 385 |
+
|
| 386 |
+
On October 18, 2019, we acquired Grand Haven MM LLC, and this entity
|
| 387 |
+
is our subsidiary now. This is an operating entity very similar
|
| 388 |
+
to Hunter Venture. The entity is registered in the State of Michigan
|
| 389 |
+
here:
|
| 390 |
+
|
| 391 |
+
https://cofs.lara.state.mi.us/CorpWeb/CorpSearch/CorpSummary.aspx?
|
| 392 |
+
ID=802220565
|
| 393 |
+
|
| 394 |
+
Grand Haven MM now operates a part time work at home program through
|
| 395 |
+
another national call center very similar and much bigger in size
|
| 396 |
+
than our previour contracter. We provide work at home sales and
|
| 397 |
+
customer services subcontracted on a part time basis. I am a builder.
|
| 398 |
+
I am building a Clone Delivery Service for Marijuana Plants .... in
|
| 399 |
+
West Michigan.
|
| 400 |
+
|
| 401 |
+
The Company also operates 2 eCommerce websites at the present time.
|
| 402 |
+
|
| 403 |
+
http://www.clonesbydrones.com
|
| 404 |
+
http://www.mycbdpets.com
|
| 405 |
+
|
| 406 |
+
We continue to strive to build up our revenues. We want to succeed
|
| 407 |
+
and we will comeback to a higher reporting standard. On October 18,
|
| 408 |
+
2019 we were upgraded in trading tier from the dark grey market
|
| 409 |
+
to the PINK NO INFORMATION market. We have no intention of going
|
| 410 |
+
back to the grey market, as the Company currently trades on an
|
| 411 |
+
"unsolicited basis" in the PINK NO INFORMATION tier at OTC
|
| 412 |
+
Markets. There are so many unknowns, however I have a visi
|
| 413 |
+
|
| 414 |
+
============================================================
|
| 415 |
+
SOURCE: 0001041588_0001041588-21-000001.txt (TXT)
|
| 416 |
+
============================================================
|
| 417 |
+
|
| 418 |
+
10-K 1 accr12312010k.txt January 4th, 2021
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
This form is filled out within all the SPIRITs of the SEC Laws of '33 and '34.
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
ACCESS-POWER & CO., INC.
|
| 430 |
+
OTC TICKER: ACCR
|
| 431 |
+
We were violent victims of toxic death spiral convertible "floorless"
|
| 432 |
+
debentures from 1998 to 2008, and then through March 2012.
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
PERIOD and DOCUMENT: 10KSB for the FULL YEAR 12/31/2020
|
| 436 |
+
COMPANY CONFIRMED NAME: ACCESS-POWER & Co., INC.
|
| 437 |
+
CENTRAL INDEX KEY: 0001041588
|
| 438 |
+
FORM TYPE: 10KSB
|
| 439 |
+
|
| 440 |
+
SEC FILE NUMBER: 333-65069
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
BUSINESS ADDRESS:
|
| 448 |
+
STREET 1: PO BOX 598
|
| 449 |
+
|
| 450 |
+
CITY: GRAND HAVEN
|
| 451 |
+
STATE: MI
|
| 452 |
+
ZIP: 49417
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
Respectfully submitted to the,
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
UNITED STATES SECURITIES AND EXCHANGE COMMISSION
|
| 466 |
+
WASHINGTON, D.C. 20549
|
| 467 |
+
The Division of Corporation Finance, Section 11
|
| 468 |
+
100 F Street NE, Washington, D.C. 20549
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
FORM 10-K
|
| 476 |
+
|
| 477 |
+
/X/ ANNUAL REPORT PURSUANT TO SECTION 13 OR 15(d) OF THE
|
| 478 |
+
SECURITIES EXCHANGE ACT OF 1934 FOR THE FISCAL YEAR ENDED
|
| 479 |
+
DECEMBER 31, 2020.
|
| 480 |
+
|
| 481 |
+
TRANSITION REPORT PURSUANT TO SECTION 13 OR 15(d)
|
| 482 |
+
OF THE SECURITIES EXCHANGE ACT OF 1934
|
| 483 |
+
FOR THE TRANSITION PERIOD FROM ______ TO _____
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
COMMISSION FILE NUMBER: 333-65069
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
ACCESS-POWER & CO., INC.
|
| 490 |
+
[Exact name of Registrant as
|
| 491 |
+
specified in its charter]
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
*** FLORIDA ***
|
| 495 |
+
|
| 496 |
+
Domicile State or other jurisdiction of
|
| 497 |
+
incorporation or organization
|
| 498 |
+
http://www.sunbiz.org
|
| 499 |
+
|
| 500 |
+
*** MICHIGAN ***
|
| 501 |
+
|
| 502 |
+
State of Operations
|
| 503 |
+
https://cofs.lara.state.mi.us/SearchApi/Search/Search
|
| 504 |
+
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
59-3420985
|
| 508 |
+
(I.R.S. Employer
|
| 509 |
+
Identification No.)
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
PO BOX 598
|
| 513 |
+
GRAND HAVEN, MI 49417
|
| 514 |
+
616-312-5390
|
| 515 |
+
Email: pjensen@myaccess-power.com
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
(Address of principal executive offices)
|
| 519 |
+
|
| 520 |
+
REGISTRANTS TELEPHONE NUMBER, INCLUDING AREA CODE: 616-312-5390
|
| 521 |
+
|
| 522 |
+
SECURITIES REGISTERED PURSUANT TO SECTION 12(b) OF THE ACT: NONE
|
| 523 |
+
|
| 524 |
+
SECURITIES REGISTERED PURSUANT TO SECTION 12(g) OF THE ACT:
|
| 525 |
+
COMMON STOCK, $0.001 PAR VALUE - 300,000,000 shares authorized fully diluted.
|
| 526 |
+
|
| 527 |
+
|
| 528 |
+
|
| 529 |
+
|
| 530 |
+
Indicate by check mark if the registrant is a well-known
|
| 531 |
+
seasoned issuer, as defined in Rule 405 of the Securities
|
| 532 |
+
Act. Yes No X
|
| 533 |
+
|
| 534 |
+
Indicate by check mark if the registrant is not required to file
|
| 535 |
+
reports pursuant to Section 13 or Section 15(d) of the Act.
|
| 536 |
+
Yes No X
|
| 537 |
+
|
| 538 |
+
|
| 539 |
+
Indicate by check mark whether the registrant (1) has
|
| 540 |
+
filed all reports required to be filed be Section 13 or 15(d)
|
| 541 |
+
of the Securities Exchange Act of 1934 during the preceding 12
|
| 542 |
+
months (or for such shorter period that the registrant was
|
| 543 |
+
required to file such reports), and (2) has been subject
|
| 544 |
+
to such filing requirements for the past 90 days. Yes X No
|
| 545 |
+
|
| 546 |
+
Indicate by check mark whether the registrant has submitted
|
| 547 |
+
electronically and posted on its corporate Web site, if any,
|
| 548 |
+
every Interactive Data File required to be submitted and
|
| 549 |
+
posted pursuant to Rule 405 of Regulation S-T (232.405 of
|
| 550 |
+
this chapter) during the preceding 12 months (or for such
|
| 551 |
+
shorter period that the registrant was required to submit
|
| 552 |
+
and post such files). Yes X No
|
| 553 |
+
|
| 554 |
+
Indicate by check mark if disclosure of delinquent filers
|
| 555 |
+
pursuant to Item 405 of Regulation S-K (Section 229.405
|
| 556 |
+
of this chapter) is not contained herein, and will not be
|
| 557 |
+
contained, to the best of registrants knowledge, in definitive
|
| 558 |
+
proxy or information statements incorporated by reference in
|
| 559 |
+
Part III of this Form 10-K or any amendment to this Form 10-K.
|
| 560 |
+
|
| 561 |
+
Indicate by check mark whether the registrant is a large a
|
| 562 |
+
ccelerated filer, an accelerated filer, a non-accelerated
|
| 563 |
+
filer, smaller reporting company, or an emerging growth
|
| 564 |
+
company. See the definitions of large accelerated filer,
|
| 565 |
+
accelerated filer, smaller reporting company, and
|
| 566 |
+
emerging growth company in Rule 12b-2 of the Exchange Act.
|
| 567 |
+
|
| 568 |
+
Large accelerated filer NO Accelerated filer NO
|
| 569 |
+
Non-accelerated filer NO (Do not check if a smaller
|
| 570 |
+
reporting company)
|
| 571 |
+
|
| 572 |
+
Smaller reporting company YES
|
| 573 |
+
Emerging growth company YES
|
| 574 |
+
|
| 575 |
+
If an emerging growth company, indicate by check mark if
|
| 576 |
+
the registrant has elected not to use the extended
|
| 577 |
+
transition period for complying with any new or revised
|
| 578 |
+
financial accounting standards provided pursuant to
|
| 579 |
+
ection 13(a) of the Exchange Act.
|
| 580 |
+
|
| 581 |
+
Indicate by check mark whether the registrant is a shell
|
| 582 |
+
company (as defined in Rule 12b-2 of the Exchange Act).
|
| 583 |
+
Yes No X
|
| 584 |
+
|
| 585 |
+
|
| 586 |
+
ALL COMMON SHARES REFLECT A FULLY DILUTED BASIS.
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
|
| 590 |
+
State the aggregate market value of the voting stock held by
|
| 591 |
+
non-affiliates computed by reference to the price at which the stock was
|
| 592 |
+
sold, or the average bid and asked prices of such stock as of a
|
| 593 |
+
specified date within the past 60 days:
|
| 594 |
+
|
| 595 |
+
PATRICK restricted shares: 186 ,984,379 shares
|
| 596 |
+
PERCENT OF SHARES HELD BY NON AFFILIATES= 37.6719%
|
| 597 |
+
SHARES HELD BY EVERYONE ELSE : 113,015,621 and of this amount,
|
| 598 |
+
we certify a DTCC FLOAT SINCE MARCH 1, 2012: 98,376,146 common shares
|
| 599 |
+
|
| 600 |
+
|
| 601 |
+
|
| 602 |
+
The aggregate market value of the voting common stock held by non-affils.
|
| 603 |
+
of the registrant (assuming officers and directors are affiliates) was
|
| 604 |
+
approximately $ 395,554.67 as of 12/31/2020, computed on the fully diluted
|
| 605 |
+
basis of and the closing price on such date of $.0035, and so...
|
| 606 |
+
|
| 607 |
+
113,015,621 held by non-affiliates + 131,128,500 shares acquired by our
|
| 608 |
+
Director plus 55,855,879 issued to our Director in 2020 = 300,000,000
|
| 609 |
+
common shares verified by our SEC Registrered same Transfer Agent
|
| 610 |
+
dating back to May 1997. Our Director's FORM 4 filings are found here:
|
| 611 |
+
|
| 612 |
+
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=0001557361
|
| 613 |
+
|
| 614 |
+
|
| 615 |
+
As of December 31st, 2020, there were 300,000,000 shares of the registrant's
|
| 616 |
+
Common Stock outstanding. PATRICK J JENSEN, Director of Access-Power & CO.,
|
| 617 |
+
Inc. has a total of 186,984,379 Restricted Shares of these shares.
|
| 618 |
+
|
| 619 |
+
|
| 620 |
+
|
| 621 |
+
|
| 622 |
+
THE TOTAL MARKET CAPITALIZATION AS OF THE CLOSE OF BUSINESS
|
| 623 |
+
January 4th , 2021 is $1,050,000.00 BASED ON A CLOSING PRICE OF
|
| 624 |
+
$.0035 PER SHARE on 12/31/2020, and total outstanding issued
|
| 625 |
+
common stock equalling 300,000,000 shares.
|
| 626 |
+
|
| 627 |
+
At January 4th , 2021, there were issued and outstanding 300,000,000
|
| 628 |
+
shares of Common Stock fully diluted, and Patrick J. Jensen owns
|
| 629 |
+
186,984,379 shares or 62.3281 percent of all the Common Stock.
|
| 630 |
+
|
| 631 |
+
In 2020, the Company during the course of 2020 experienced a market
|
| 632 |
+
capitalization HIGH of over $3,000,000.00.
|
| 633 |
+
|
| 634 |
+
All 186,984,379 are currently all restricted and held in book entry form at our
|
| 635 |
+
Transfer Agent.
|
| 636 |
+
|
| 637 |
+
|
| 638 |
+
|
| 639 |
+
|
| 640 |
+
THE SEC COMMISSION CONFIRMED OUR FORM RW ON 12/21/2020.
|
| 641 |
+
|
| 642 |
+
|
| 643 |
+
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
______________________________________________________________
|
| 647 |
+
|
| 648 |
+
|
| 649 |
+
|
| 650 |
+
|
| 651 |
+
|
| 652 |
+
My Dear Shareholders, OTC Markets Directors, colleagues at FINRA, and to the
|
| 653 |
+
Esteemed Commissioners of the SEC, and of course, to my Chiefs at Section 11
|
| 654 |
+
at the SEC,
|
| 655 |
+
|
| 656 |
+
CURRENT UPDATE...as of January 4th, 2021.
|
| 657 |
+
|
| 658 |
+
Our Director is called many names on message boards...and today, in 2021 we
|
| 659 |
+
bow our heads to all decision makers at OTC Markets in our quest to
|
| 660 |
+
re-apply to the PINK CURRENT INFO tier status.
|
| 661 |
+
This is a reapplication to occur on January 24th, 2021.
|
| 662 |
+
|
| 663 |
+
|
| 664 |
+
*****WE ONLY HAVE COMMON STOCK IN OUR CAPITAL STRUCTURE****
|
| 665 |
+
|
| 666 |
+
*****WE DO NOT HAVE CONVERTIBLE NOTES or WARRANTS*****
|
| 667 |
+
|
| 668 |
+
****WE HAVE ZERO LONG TERM DEBT********
|
| 669 |
+
|
| 670 |
+
*****ACCR DOES NOT BELIEVE IN PROMOTION*********
|
| 671 |
+
|
| 672 |
+
It is very expensive to hire a PCAOB registered firm/accountant/attorneys
|
| 673 |
+
for our
|
| 674 |
+
micro-cap operations. However, we will succeed in capturing all of the
|
| 675 |
+
requirements
|
| 676 |
+
of all the SEC Rules of '33 and '34, as best as we can.
|
| 677 |
+
|
| 678 |
+
We are fully aware of the FINAL RULE imposed by the SEC, by September
|
| 679 |
+
21st, 2021 to require movement of all PINK NO INFORMATION
|
| 680 |
+
tier companies to the grey market today called the EXPERT MARKET.
|
| 681 |
+
|
| 682 |
+
We are pleading with all the Directors at OTC Markets to please be fair to our
|
| 683 |
+
Company. We will re-apply with OTC Markets on January 24, 2021 to become
|
| 684 |
+
PINK CURRENT INFORMATION tier again, as we once were in 2008.
|
| 685 |
+
The only reason why there is a RED STOP SIGN is because of a discussed
|
| 686 |
+
CRIMINAL FORM 15 filed by our previous inept managers in 2007.
|
| 687 |
+
We charge our past managers naked short sold our
|
| 688 |
+
shares into the ground. Their intent was to carve all the equity
|
| 689 |
+
off the bone, and then to file the CRIMINAL FORM 15 in 2007
|
| 690 |
+
as their criminal strategy.
|
| 691 |
+
|
| 692 |
+
We provide full disclosure to our
|
| 693 |
+
Market Makers, and to our investing community.
|
| 694 |
+
|
| 695 |
+
We have the Spirit of a Warrior, and our Corporation has done nothing wrong.
|
| 696 |
+
All of the Company press releases can be found here:
|
| 697 |
+
|
| 698 |
+
http://www.globenewswire.com
|
| 699 |
+
|
| 700 |
+
We have learned that in 2021, all OTC Markets PINK CURRENT
|
| 701 |
+
INFORMATION
|
| 702 |
+
tier Companies will have at chance at Solicited Quotes for the first time in
|
| 703 |
+
over 12.5
|
| 704 |
+
years. We will not be needing to obtain a FORM 211 with FINRA. We
|
| 705 |
+
have received
|
| 706 |
+
news that in 2021, only selected PINK CURRENT INFORMATION tier
|
| 707 |
+
companies
|
| 708 |
+
will receive a shot at SOLICITED QUOTES by a market maker.
|
| 709 |
+
|
| 710 |
+
ACCR believes to have an excellent relationship with our primary Market
|
| 711 |
+
Maker over
|
| 712 |
+
the years. Our primary market maker signed the UNSOLICITED
|
| 713 |
+
QUOTE FORM
|
| 714 |
+
with OTC Markets pre-October 18, 2019.
|
| 715 |
+
|
| 716 |
+
We are loyal to our Shareholder base.
|
| 717 |
+
|
| 718 |
+
Our story...from Inception through March 1, 2012, the Company was
|
| 719 |
+
a violent
|
| 720 |
+
victim of a terrible financial crime. The criminals behind this scheme
|
| 721 |
+
to defraud
|
| 722 |
+
investors was planned very methodically.
|
| 723 |
+
|
| 724 |
+
We have many shareholders placed in restrictive shares...none of
|
| 725 |
+
which have
|
| 726 |
+
SURFACED in do a 144a sale in over 2 decades. Why is this?
|
| 727 |
+
Management believes that all 100 share lot's of restrictive shares were
|
| 728 |
+
issued
|
| 729 |
+
in fraud, and to fictitious people, or Shareholders - totaling over
|
| 730 |
+
15,000,000
|
| 731 |
+
shares in never to be claimed restrictive shares.
|
| 732 |
+
It was pre-meditated and
|
| 733 |
+
very detail in CRIME.
|
| 734 |
+
|
| 735 |
+
The criminals of the past perpetraded a MASTERMIND financial
|
| 736 |
+
crime against
|
| 737 |
+
our Shareholder base. We seek retribution from our past fraud managers.
|
| 738 |
+
We
|
| 739 |
+
recently learned that one of our past fraud managers lives in Nashville
|
| 740 |
+
area of
|
| 741 |
+
TN in a $5.2 million dollar house in September 2020. We charge
|
| 742 |
+
he used SEC
|
| 743 |
+
filings to commit financial crimes against our Shareholders. We
|
| 744 |
+
nearly have
|
| 745 |
+
15,000,000 shares in RESTRICTED STATUS 100 share lots's....
|
| 746 |
+
the previous
|
| 747 |
+
criminal masterminds.
|
| 748 |
+
|
| 749 |
+
|
| 750 |
+
WE SEEK RETROBUTION FOR OUR SHAREHOLDERS
|
| 751 |
+
|
| 752 |
+
We charge this man used SEC filings going back to 2002 to commit financial
|
| 753 |
+
crimes
|
| 754 |
+
against our Shareholders. Today, we are the cleanest OTC Companies
|
| 755 |
+
around.
|
| 756 |
+
|
| 757 |
+
The Company made 2 FINRA DAILY lists on 5/6/2020 and on 9/6/2018.
|
| 758 |
+
|
| 759 |
+
The Company has had discussions with the SEC regarding a FRAUD
|
| 760 |
+
FORM 15 that was filed by previous management in 2007. We believe
|
| 761 |
+
that we have a fiduciary responsibility to report transparent
|
| 762 |
+
information to our Shareholders.
|
| 763 |
+
|
| 764 |
+
https://www.sec.gov/Archives/edgar/data/1041588/
|
| 765 |
+
000117347307000043/0001173473-07-000043-index.htm
|
| 766 |
+
|
| 767 |
+
We were victims of toxic death spiral debentures from 1998 to
|
| 768 |
+
2008 and through March 2012.
|
| 769 |
+
|
| 770 |
+
PLEASE FORGIVE OUR PAST.
|
| 771 |
+
|
| 772 |
+
ACCR as a structure TODAY is very sound, because I am in charge.
|
| 773 |
+
ACCR has life only today because of ME.
|
| 774 |
+
|
| 775 |
+
The past is the past, and
|
| 776 |
+
we wish to move forward with a plan to become fully
|
| 777 |
+
current with the SEC in 2021. We are trying the best we can within the
|
| 778 |
+
Spirit of the Law. We will be refiling our FORM 10 very soon...very
|
| 779 |
+
soon.
|
| 780 |
+
|
| 781 |
+
|
| 782 |
+
On October 18, 2019, our Company was in the FIRST GROUP of
|
| 783 |
+
Equities to bust
|
| 784 |
+
out of the death grey market, where we stayed there for 11 years
|
| 785 |
+
after our
|
| 786 |
+
criminal FORM 15 was filed by previous criminal management.
|
| 787 |
+
|
| 788 |
+
Let's move forward, and for get the PAST!
|
| 789 |
+
|
| 790 |
+
|
| 791 |
+
|
| 792 |
+
Prior to October 18, 2019...we had DEATH....today, we have LIFE, and
|
| 793 |
+
we have a
|
| 794 |
+
direction on how to fix this trajedy. Today we have visible Level 2
|
| 795 |
+
QUOTES. We
|
| 796 |
+
have already spent 11 years in the DEATH DARK GREY MARKET.
|
| 797 |
+
|
| 798 |
+
Please,,,why should we go back? We will NOT go back, ever.
|
| 799 |
+
|
| 800 |
+
TODAY, we have LIFE.
|
| 801 |
+
|
| 802 |
+
Our primary Market Maker signed a new form created with OTC
|
| 803 |
+
Markets in 2019.
|
| 804 |
+
This form is located here...we believe to have a wonderful relationship
|
| 805 |
+
with our
|
| 806 |
+
PRIMARY MARKET MAKER.
|
| 807 |
+
|
| 808 |
+
https://www.otcmarkets.com/files/unsolicited-quote-form-1514935916031.pdf
|
| 809 |
+
|
| 810 |
+
We jumped from the deathly grey market to the PINK NO INFORMATION
|
| 811 |
+
tier of
|
| 812 |
+
OTC Markets on this infamous date in our history, October 18, 2019.
|
| 813 |
+
We have lived
|
| 814 |
+
in this tier with NO DILUTION for over a year with no harm to our
|
| 815 |
+
Shareholders.
|
| 816 |
+
|
| 817 |
+
WE LOVE OUR SHAREHOLDERS, and believe in RAISING
|
| 818 |
+
SHAREHOLDER
|
| 819 |
+
VALUE. WE VALUE OUR COMMON STOCKHOLDERS.
|
| 820 |
+
|
| 821 |
+
ACCR received a denial to go PINK CURRENT TIER on July 23, 2020.
|
| 822 |
+
Why? We
|
| 823 |
+
submitted every form in a satisfactory manner. We CLASHED with our prior
|
| 824 |
+
accountant whom shall remain nameless. We clashed hard. Why were we
|
| 825 |
+
denied
|
| 826 |
+
to go PINK CURRENT is a mystery to us. Maybe our prior attorney said bad
|
| 827 |
+
things
|
| 828 |
+
about our Company to OTC Markets, I do not know why. He threatened to
|
| 829 |
+
call OTC
|
| 830 |
+
MARKETS, and call me a liar, when he was the liar.
|
| 831 |
+
|
| 832 |
+
We do not know. We've been told that OTC Markets did not like our
|
| 833 |
+
Company 8K's
|
| 834 |
+
nor our Company story. ACCR has contacted OTC Markets, and on
|
| 835 |
+
July 23,
|
| 836 |
+
2020 we received the following reply:
|
| 837 |
+
|
| 838 |
+
On July 23, 2020 3:30 PM Liz Heese wrote:
|
| 839 |
+
|
| 840 |
+
Mr. Jensen,
|
| 841 |
+
|
| 842 |
+
OTC Markets has reached a final determination to deny your application
|
| 843 |
+
for access
|
| 844 |
+
to the OTC Disclosure & News Service. Our determination is due to
|
| 845 |
+
public interest
|
| 846 |
+
concerns associated with the company and its disclosure, including but
|
| 847 |
+
not limited to
|
| 848 |
+
issues previously communicated to you.
|
| 849 |
+
|
| 850 |
+
We consider this matter to be closed and will not engage in further discussion
|
| 851 |
+
at this
|
| 852 |
+
time. As stated in the email from Nancy Rodriguez, you may reapply
|
| 853 |
+
for the service
|
| 854 |
+
in 6 months.
|
| 855 |
+
|
| 856 |
+
Liz Heese
|
| 857 |
+
EVP, Issuer & Information Services
|
| 858 |
+
OTC Markets Group"
|
| 859 |
+
|
| 860 |
+
THEREFORE, ACCR is going to re-apply to become PINK CURRENT
|
| 861 |
+
INFORMATION TIER on January 24, 2021.
|
| 862 |
+
|
| 863 |
+
ACCR as a Corporation is perfectly
|
| 864 |
+
clean, and has done nothing wrong. We've just had bad management.
|
| 865 |
+
All the
|
| 866 |
+
Amendments and Articles of Incorp all check out at:
|
| 867 |
+
|
| 868 |
+
https://www.sunbiz.org
|
| 869 |
+
|
| 870 |
+
ACCR has done nothing wrong. ACCR will get SOLICITED QUOTES
|
| 871 |
+
again in 2021.
|
| 872 |
+
|
| 873 |
+
CURRENT MANAGEMENT WILL STEP DOWN TO CLEAR A PATHWAY
|
| 874 |
+
FOR OTHERS
|
| 875 |
+
|
| 876 |
+
Let's just forget this past CRIMINAL FORM 15, that according to the SEC
|
| 877 |
+
cannot
|
| 878 |
+
be changed, and our only option is today to re-file our FORM 10 properly
|
| 879 |
+
with the help of counsel, and we will.
|
| 880 |
+
|
| 881 |
+
https://www.sec.gov/Archives/edgar/data/1041588/
|
| 882 |
+
000117347307000043/0001173473-07-000043-index.htm
|
| 883 |
+
|
| 884 |
+
|
| 885 |
+
Again, as of the close of business December 31st, 2020 --------> our common
|
| 886 |
+
stock
|
| 887 |
+
structure is as follows:
|
| 888 |
+
|
| 889 |
+
Authorized Common Stock: 300,000,000 shares
|
| 890 |
+
Outstanding Common Stock: 300,000,000 shares
|
| 891 |
+
Estimated Float: Substanstially less than 98,244,146 shares
|
| 892 |
+
Restricted Common Stock: 201,625,854 outstanding
|
| 893 |
+
PATRICK: 186,984,379 ALL RESTRICTED SHARES or 62.3281 % of the
|
| 894 |
+
TOTAL
|
| 895 |
+
AMOUNT OF STOCK ON A FULLY DILUTED SHARES - PERIOD!
|
| 896 |
+
|
| 897 |
+
|
| 898 |
+
|
| 899 |
+
OUR CORPORATE WEBSITEs IS:
|
| 900 |
+
|
| 901 |
+
https://www.myaccess-power.com
|
| 902 |
+
|
| 903 |
+
Access-Power & Co., Inc.'s contact is:
|
| 904 |
+
PO BOX 598
|
| 905 |
+
Grand Haven, MI 49417
|
| 906 |
+
pjensen@myaccess-power.com
|
| 907 |
+
(616)312-5390 TEL.
|
| 908 |
+
https://www.sec.gov/divisions/corpfin/faqs/soxact2002.htm
|
| 909 |
+
|
| 910 |
+
|
| 911 |
+
We applied for a Trademark for Clones By Drones TM, and also
|
| 912 |
+
Clones by Cars TM in 2020. The service will be very futuristic, and will be
|
| 913 |
+
the
|
| 914 |
+
first of its kind. We applied for a federal patent for our Clobes by Drones
|
| 915 |
+
futuristic
|
| 916 |
+
home curbside delivery service.
|
| 917 |
+
|
| 918 |
+
WE ARE WORKING ON A REVERSE MERGER...we plan to audit the
|
| 919 |
+
incoming
|
| 920 |
+
private company that wishes to go public in a r
|
| 921 |
+
|
| 922 |
+
============================================================
|
| 923 |
+
SOURCE: 0001386570_0001654954-20-005725.txt (TXT)
|
| 924 |
+
============================================================
|
| 925 |
+
|
| 926 |
+
10-K/A 1 cdxc10ka_12312019.htm FORM 10-K/A cdxc10ka_12312019 UNITED
|
| 927 |
+
STATES SECURITIES AND EXCHANGE COMMISSION Washington,
|
| 928 |
+
D.C. 20549 FORM
|
| 929 |
+
10-K/A (Amendment
|
| 930 |
+
No. 1) [X]
|
| 931 |
+
ANNUAL REPORT PURSUANT TO SECTION 13 OR 15(d) OF THE
|
| 932 |
+
SECURITIES EXCHANGE ACT OF 1934. For
|
| 933 |
+
the fiscal year ended December 31, 2019 or [
|
| 934 |
+
] TRANSITION REPORT PURSUANT TO SECTION 13 OR 15(d) OF THE
|
| 935 |
+
SECURITIES EXCHANGE ACT OF 1934. For
|
| 936 |
+
the transition period from to Commission file number 001-37752 CHROMADEX
|
| 937 |
+
CORPORATION (Exact
|
| 938 |
+
name of Registrant as specified in its Charter) Delaware 26-2940963 (State or other jurisdiction of
|
| 939 |
+
incorporation) (I.R.S. Employer
|
| 940 |
+
Identification No.) 10900 Wilshire Blvd. Suite 600, Los
|
| 941 |
+
Angeles, California 90024 (Address of
|
| 942 |
+
Principal Executive Offices) (Zip
|
| 943 |
+
Code) Registrant's
|
| 944 |
+
telephone number, including area code (310) 388-6706 Securities
|
| 945 |
+
registered pursuant to Section 12(b) of the Act: Title of each class Trading Symbol Name of each exchange on which registered Common Stock, $0.001 par value per share CDXC The Nasdaq Capital Market Securities
|
| 946 |
+
registered pursuant to Section 12(g) of the Act: None. Indicate by check
|
| 947 |
+
mark if the registrant is a well-known seasoned issuer, as defined
|
| 948 |
+
in Rule 405 of the Securities Act. Yes [ ] No [X
|
| 949 |
+
] Indicate by check
|
| 950 |
+
mark if the registrant is not required to file reports pursuant to
|
| 951 |
+
Section 13 or Section 15(d) of the Act. Yes [ ] No
|
| 952 |
+
[X] Indicate
|
| 953 |
+
by check mark whether the registrant (1) has filed all reports
|
| 954 |
+
required to be filed by Section 13 or 15(d) of the Securities
|
| 955 |
+
Exchange Act of 1934 during the preceding 12 months (or for
|
| 956 |
+
such shorter period that the Registrant was required to file such
|
| 957 |
+
reports), and (2) has been subject to such filing requirements for
|
| 958 |
+
the past 90 days. Yes [X] No [ ] Indicate
|
| 959 |
+
by check mark whether the registrant has submitted electronically
|
| 960 |
+
every Interactive Data File required to be submitted pursuant to
|
| 961 |
+
Rule 405 of Regulation S-T during the preceding 12 months (or for
|
| 962 |
+
such shorter period that the registrant was required to submit such
|
| 963 |
+
files). Yes [X] No [ ] Indicate
|
| 964 |
+
by check mark whether the registrant is a large accelerated filer,
|
| 965 |
+
an accelerated filer, a non-accelerated filer, or a smaller
|
| 966 |
+
reporting company. See the definitions of “accelerated
|
| 967 |
+
filer,” “large accelerated filer,” and
|
| 968 |
+
“smaller reporting company” in Rule 12b-2 of the
|
| 969 |
+
Exchange Act. Large
|
| 970 |
+
accelerated filer [ ]
|
| 971 |
+
Accelerated filer
|
| 972 |
+
[X]
|
| 973 |
+
Non-accelerated filer [
|
| 974 |
+
] Smaller
|
| 975 |
+
reporting company
|
| 976 |
+
[X]
|
| 977 |
+
Emerging growth company [ ] If an
|
| 978 |
+
emerging growth company, indicate by check mark if the registrant
|
| 979 |
+
has elected not to use the extended transition period for complying
|
| 980 |
+
with any new or revised financing accounting standards provided
|
| 981 |
+
pursuant to Section 13(a) of the Exchange Act. ☐ Indicate by check
|
| 982 |
+
mark whether the registrant is a shell company (as defined in Rule
|
| 983 |
+
12b-2 of the Act). Yes [ ] No [X] As of
|
| 984 |
+
June 30, 2019, the last business day of the registrant’s most
|
| 985 |
+
recently completed second fiscal quarter, the aggregate market
|
| 986 |
+
value of the registrant’s common stock held by non-affiliates
|
| 987 |
+
of the registrant was approximately $190.4 million, based on the
|
| 988 |
+
closing price of the registrant’s common stock on the NASDAQ
|
| 989 |
+
Capital Market on June 30, 2019. Number
|
| 990 |
+
of shares of common stock of the registrant outstanding as of May
|
| 991 |
+
17, 2020: 61,477,895. DOCUMENTS INCORPORATED BY REFERENCE None. TA B LE OF CONTENTS Item PART I 1A. Risk
|
| 992 |
+
Factors 1 PART II 9A Controls and
|
| 993 |
+
Procedures 2 PART IV 15. Exhibits, Financial
|
| 994 |
+
Statement Schedules 6 Table of Contents EXPLANATORY NOTE ChromaDex
|
| 995 |
+
Corporation (“ChromaDex,” the “Company,”
|
| 996 |
+
“we,” “our” or “us”) is filing
|
| 997 |
+
this Amendment No. 1 on Form 10-K/A (this “Amendment”)
|
| 998 |
+
to amend our Annual Report on Form 10-K for the year ended December
|
| 999 |
+
31, 2019 (the “Form 10-K”), originally filed with the
|
| 1000 |
+
U.S. Securities and Exchange Commission (the “SEC”) on
|
| 1001 |
+
March 10, 2020. As a result of a material weakness in internal
|
| 1002 |
+
control over financial reporting that existed as of December 31,
|
| 1003 |
+
2019 that was not identified until the Company was preparing its
|
| 1004 |
+
Quarterly Report on Form 10-Q for the quarterly period ended March
|
| 1005 |
+
31, 2020, the Company is amending Part II, Item 9A “Controls
|
| 1006 |
+
and Procedures” with respect to (a) the Company’s
|
| 1007 |
+
conclusions regarding the effectiveness of (i) the Company’s
|
| 1008 |
+
disclosure controls and procedures and (ii) its internal control
|
| 1009 |
+
over financial reporting, and (b) Marcum LLP’s related
|
| 1010 |
+
attestation report on the Company’s internal control over
|
| 1011 |
+
financial reporting. In addition, the Company is revising Item 1A.
|
| 1012 |
+
“Risk Factors” to add a risk factor regarding this
|
| 1013 |
+
material weakness in internal control over financial reporting. The
|
| 1014 |
+
material weakness in internal controls over financial reporting is
|
| 1015 |
+
also reported in our Quarterly Report on Form 10-Q for the
|
| 1016 |
+
quarterly period ended March 31, 2020. In
|
| 1017 |
+
accordance with Rule 12b-15 under the Securities Exchange Act of
|
| 1018 |
+
1934, as amended (the “Exchange Act”), the Company is
|
| 1019 |
+
including with this Amendment currently dated certifications as
|
| 1020 |
+
Exhibits 31.1, 31.2, and 32.1. We are also filing an updated
|
| 1021 |
+
Consent of Independent Registered Public Accounting Firm.
|
| 1022 |
+
Accordingly, Part IV, Item 15, “Exhibits and Financial
|
| 1023 |
+
Statement Schedules” is amended to include the currently
|
| 1024 |
+
dated certifications and consent as exhibits. Except
|
| 1025 |
+
as described in this Explanatory Note, this Amendment does not
|
| 1026 |
+
reflect events occurring after the date of the filing of the Form
|
| 1027 |
+
10-K, nor does it amend, modify or otherwise update any other
|
| 1028 |
+
information in the Form 10-K. Among other things, forward-looking
|
| 1029 |
+
statements made in the Form 10-K have not been revised to reflect
|
| 1030 |
+
events that occurred or facts that became known to us after filing
|
| 1031 |
+
of the Form 10-K, and any such forward-looking statements should be
|
| 1032 |
+
read in their historical context. Accordingly, this Amendment
|
| 1033 |
+
should be read in conjunction with the Form 10-K and with the
|
| 1034 |
+
Company’s filings with the SEC subsequent to the filing of
|
| 1035 |
+
the Form 10-K. Notwithstanding
|
| 1036 |
+
the existence of the material weakness described in Part II. Item
|
| 1037 |
+
9A – “Controls and Procedures,” the Company
|
| 1038 |
+
believes that the consolidated financial statements in the Form
|
| 1039 |
+
10-K fairly present, in all material respects, the Company’s
|
| 1040 |
+
financial position, results of operations and cash flows as of the
|
| 1041 |
+
dates, and for the periods, presented, in conformity with U.S.
|
| 1042 |
+
generally accepted accounting principles. -1- Table of Contents P ART I Item
|
| 1043 |
+
1A. Risk
|
| 1044 |
+
Factors The
|
| 1045 |
+
Company is supplementing the risk factors previously disclosed in
|
| 1046 |
+
“Item 1A. Risk Factors” of the Form 10-K with the
|
| 1047 |
+
following risk factor, which should be read in conjunction with the
|
| 1048 |
+
other risk factors presented in the Form 10-K: Failure to remediate a material weakness in internal accounting
|
| 1049 |
+
controls could result in material misstatements in our financial
|
| 1050 |
+
statements. Our
|
| 1051 |
+
management has identified a material weakness in our internal
|
| 1052 |
+
control over financial reporting and has concluded that, due to
|
| 1053 |
+
such material weakness, our disclosure controls and procedures were
|
| 1054 |
+
not effective as of December 31, 2019. The material weakness in
|
| 1055 |
+
internal control over financial reporting resulted from a
|
| 1056 |
+
deficiency in our disclosure controls and procedures which could
|
| 1057 |
+
have resulted in us not disclosing a material potential loss that
|
| 1058 |
+
was reasonably possible, and therefore requiring a qualitative
|
| 1059 |
+
disclosure in our consolidated financial statements under ASC 450
|
| 1060 |
+
– Contingencies . The
|
| 1061 |
+
material weakness has not been remediated as of March 31, 2020. If
|
| 1062 |
+
not remediated, or if we identify further material weaknesses in
|
| 1063 |
+
our internal controls, our failure to establish and maintain
|
| 1064 |
+
effective disclosure controls and procedures and internal control
|
| 1065 |
+
over financial reporting could result in material misstatements in
|
| 1066 |
+
our financial statements and a failure to meet our reporting and
|
| 1067 |
+
financial obligations, each of which could have a material adverse
|
| 1068 |
+
effect on our financial condition and the trading price of our
|
| 1069 |
+
common stock. P ART II Item
|
| 1070 |
+
9A. Controls and Procedures Evaluation of Disclosure Controls and Procedures (As
|
| 1071 |
+
Revised) In
|
| 1072 |
+
connection with the filing of our Form 10-K, our chief executive
|
| 1073 |
+
officer and our chief financial officer, after evaluating the
|
| 1074 |
+
effectiveness of our disclosure controls and procedures (as defined
|
| 1075 |
+
in Rules 13a-15(e) and 15d-15(e) of the Exchange Act) as of
|
| 1076 |
+
December 31, 2019, concluded that our disclosure controls and
|
| 1077 |
+
procedures were effective. Subsequent
|
| 1078 |
+
to that evaluation, management reevaluated the effectiveness of our
|
| 1079 |
+
disclosure controls and procedures as of December 31, 2019 and
|
| 1080 |
+
concluded that, because of the material weakness identified in our
|
| 1081 |
+
internal control over financial reporting discussed below, our
|
| 1082 |
+
disclosure controls and procedures were not effective as of
|
| 1083 |
+
December 31, 2019. Management Report on Internal Control over Financial Reporting (As
|
| 1084 |
+
Revised) Our
|
| 1085 |
+
management is responsible for establishing and maintaining adequate
|
| 1086 |
+
internal control over financial reporting as defined in Rule
|
| 1087 |
+
13a-15(f) and 15d-(f) under the Exchange Act. Our internal control
|
| 1088 |
+
over financial reporting is designed to provide reasonable
|
| 1089 |
+
assurance regarding the reliability of financial reporting and the
|
| 1090 |
+
preparation of consolidated financial statements for external
|
| 1091 |
+
purposes in accordance with U.S. generally accepted accounting
|
| 1092 |
+
principles. Our internal control over financial reporting include
|
| 1093 |
+
those policies and procedures that: (i)
|
| 1094 |
+
pertain to the maintenance of records that, in reasonable detail,
|
| 1095 |
+
accurately and fairly reflect the transactions and dispositions of
|
| 1096 |
+
our assets; (ii)
|
| 1097 |
+
provide reasonable assurance that transactions are recorded as
|
| 1098 |
+
necessary to permit the preparation of our consolidated financial
|
| 1099 |
+
statements in accordance with U.S. generally accepted accounting
|
| 1100 |
+
principles, and that our receipts and expenditures are being made
|
| 1101 |
+
only in accordance with authorizations of our management and
|
| 1102 |
+
directors; and (iii)
|
| 1103 |
+
provide reasonable assurance regarding prevention or timely
|
| 1104 |
+
detection of unauthorized acquisition, use or disposition of our
|
| 1105 |
+
assets that could have a material effect on the consolidated
|
| 1106 |
+
financial statements. -2- Table of Contents In
|
| 1107 |
+
connection with the Form 10-K, our management, including the
|
| 1108 |
+
undersigned principal executive officer and principal financial
|
| 1109 |
+
officer, assessed the effectiveness of our internal control over
|
| 1110 |
+
financial reporting as of December 31, 2019. In conducting its
|
| 1111 |
+
assessment, our management used the criteria issued by the
|
| 1112 |
+
Committee of Sponsoring Organizations of the Treadway Commission in Internal Control—Integrated
|
| 1113 |
+
Framework in 2013 . Based on this assessment, our management
|
| 1114 |
+
concluded that, as of December 31, 2019, our internal control
|
| 1115 |
+
over financial reporting was effective based on those
|
| 1116 |
+
criteria. Subsequent
|
| 1117 |
+
to that evaluation, management reevaluated the effectiveness of our
|
| 1118 |
+
internal control over financial reporting as of December 31, 2019
|
| 1119 |
+
and concluded that, because of the material weakness identified,
|
| 1120 |
+
our internal control over financial reporting was not effective as
|
| 1121 |
+
of December 31, 2019. The
|
| 1122 |
+
material weakness in internal control over financial reporting
|
| 1123 |
+
resulted from a deficiency in our disclosure controls and
|
| 1124 |
+
procedures which could have resulted in the Company not disclosing
|
| 1125 |
+
a material potential loss that was reasonably possible, and
|
| 1126 |
+
therefore requiring a qualitative disclosure in consolidated
|
| 1127 |
+
financial statements under ASC 450 – Contingencies . The
|
| 1128 |
+
Company is still in the process of analyzing and addressing the
|
| 1129 |
+
material weakness. The material weakness will not be considered
|
| 1130 |
+
remediated until the applicable remedial control operates for a
|
| 1131 |
+
sufficient period of time and management has concluded, through
|
| 1132 |
+
testing, that this control is operating effectively. We expect that
|
| 1133 |
+
the remediation of this material weakness will be completed prior
|
| 1134 |
+
to the end of year 2020. We did
|
| 1135 |
+
not revise our financial statements for prior periods for the
|
| 1136 |
+
disclosure of the potential loss based on our belief that the
|
| 1137 |
+
effect of the lack of the disclosure was not material to the
|
| 1138 |
+
financial statements taken as a whole. Therefore, our chief
|
| 1139 |
+
executive officer and our chief financial officer believe that,
|
| 1140 |
+
notwithstanding the material weakness discussed above, the
|
| 1141 |
+
consolidated financial statements in our Annual Report on Form 10-K
|
| 1142 |
+
for the year ended December 31, 2019 present fairly, in all
|
| 1143 |
+
material respects, our financial position, results of operations
|
| 1144 |
+
and cash flows for the periods presented. Changes in Internal Control over Financial Reporting Except
|
| 1145 |
+
as noted above, there were no changes in internal controls over
|
| 1146 |
+
financial reporting (as defined in Rule 13a−15(f) promulgated
|
| 1147 |
+
under the Exchange Act) that occurred during our fourth fiscal
|
| 1148 |
+
quarter that have materially affected or are reasonably likely to
|
| 1149 |
+
materially affect our internal control over financial
|
| 1150 |
+
reporting. Inherent Limitations on Disclosure Controls and
|
| 1151 |
+
Procedures The
|
| 1152 |
+
effectiveness of our disclosure controls and procedures is subject
|
| 1153 |
+
to various inherent limitations, including cost limitations,
|
| 1154 |
+
judgments used in decision making, assumptions about the likelihood
|
| 1155 |
+
of future events, the soundness of our systems, the possibility of
|
| 1156 |
+
human error, and the risk of fraud. Moreover, projections of any
|
| 1157 |
+
evaluation of effectiveness to future periods are subject to the
|
| 1158 |
+
risk that controls may become inadequate because of changes in
|
| 1159 |
+
conditions and the risk that the degree of compliance with policies
|
| 1160 |
+
or procedures may deteriorate over time. Because of these
|
| 1161 |
+
limitations, there can be no assurance that any system of
|
| 1162 |
+
disclosure controls and procedures, no matter how well conceived,
|
| 1163 |
+
will be successful in preventing all errors or fraud or in making
|
| 1164 |
+
all material information known in a timely manner to the
|
| 1165 |
+
appropriate levels of management. Inherent Limitations on Internal Control Internal
|
| 1166 |
+
control over financial reporting cannot provide absolute assurance
|
| 1167 |
+
of achieving financial reporting objectives because of its inherent
|
| 1168 |
+
limitations, including the possibility of human error and
|
| 1169 |
+
circumvention by collusion or overriding of control. Accordingly,
|
| 1170 |
+
even an effective internal control system may not prevent or detect
|
| 1171 |
+
material misstatements on a timely basis. Also, projections of any
|
| 1172 |
+
evaluation of effectiveness to future periods are subject to the
|
| 1173 |
+
risk that the controls may become inadequate because of changes in
|
| 1174 |
+
conditions or that the degree of compliance with the policies or
|
| 1175 |
+
procedures may deteriorate. Accordingly, our internal control over
|
| 1176 |
+
financial reporting is designed to provide reasonable assurance of
|
| 1177 |
+
achieving their objectives. -3- Table of Contents Attestation Report of the Registered Public Accounting
|
| 1178 |
+
Firm REPORT OF INDEPENDENT REGISTERED PUBLIC ACCOUNTING
|
| 1179 |
+
FIRM ON INTERNAL CONTROL OVER FINANCIAL REPORTING To the Shareholders and Board of Directors of ChromaDex Corporation Adverse Opinion on Internal Control over Financial
|
| 1180 |
+
Reporting We have audited ChromaDex Corporation’’s (the
|
| 1181 |
+
"Company") internal control over financial reporting as of December
|
| 1182 |
+
31, 2019, based on criteria established in Internal Contr
|
data/agents/financial_agent/metadata.json
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"agent_name": "financial_agent",
|
| 3 |
+
"system_prompt": "You are an expert financial AI assistant specialized in SEC EDGAR 10-K filings, MD&A, and risk factor analysis.",
|
| 4 |
+
"prompt_analysis": {
|
| 5 |
+
"domain": "financial",
|
| 6 |
+
"sub_domains": [],
|
| 7 |
+
"personality": "helpful assistant",
|
| 8 |
+
"constraints": [
|
| 9 |
+
"Stay within knowledge base",
|
| 10 |
+
"Be accurate"
|
| 11 |
+
],
|
| 12 |
+
"suggested_name": "MEXAR General Agent",
|
| 13 |
+
"domain_keywords": [
|
| 14 |
+
"general"
|
| 15 |
+
],
|
| 16 |
+
"tone": "professional",
|
| 17 |
+
"capabilities": [
|
| 18 |
+
"Answer questions",
|
| 19 |
+
"Provide information"
|
| 20 |
+
]
|
| 21 |
+
},
|
| 22 |
+
"domain_signature": [
|
| 23 |
+
"000",
|
| 24 |
+
"000 000",
|
| 25 |
+
"10",
|
| 26 |
+
"11",
|
| 27 |
+
"12",
|
| 28 |
+
"13",
|
| 29 |
+
"15",
|
| 30 |
+
"2019",
|
| 31 |
+
"2020",
|
| 32 |
+
"2021",
|
| 33 |
+
"31",
|
| 34 |
+
"31 2019",
|
| 35 |
+
"accelerated",
|
| 36 |
+
"accelerated filer",
|
| 37 |
+
"access",
|
| 38 |
+
"access power",
|
| 39 |
+
"accr",
|
| 40 |
+
"act",
|
| 41 |
+
"business",
|
| 42 |
+
"check",
|
| 43 |
+
"check mark",
|
| 44 |
+
"com",
|
| 45 |
+
"common",
|
| 46 |
+
"common stock",
|
| 47 |
+
"company",
|
| 48 |
+
"control",
|
| 49 |
+
"control financial",
|
| 50 |
+
"controls",
|
| 51 |
+
"controls procedures",
|
| 52 |
+
"coronavirus",
|
| 53 |
+
"current",
|
| 54 |
+
"december",
|
| 55 |
+
"disclosure",
|
| 56 |
+
"disclosure controls",
|
| 57 |
+
"exchange",
|
| 58 |
+
"exchange act",
|
| 59 |
+
"f street ne",
|
| 60 |
+
"factors",
|
| 61 |
+
"file",
|
| 62 |
+
"filed",
|
| 63 |
+
"filer",
|
| 64 |
+
"filing",
|
| 65 |
+
"financial",
|
| 66 |
+
"financial reporting",
|
| 67 |
+
"florida",
|
| 68 |
+
"form",
|
| 69 |
+
"form 10",
|
| 70 |
+
"gas",
|
| 71 |
+
"grand haven",
|
| 72 |
+
"https",
|
| 73 |
+
"https www",
|
| 74 |
+
"i.r.s. employer\nidentification no.",
|
| 75 |
+
"indicate",
|
| 76 |
+
"indicate check",
|
| 77 |
+
"information",
|
| 78 |
+
"interactive data file",
|
| 79 |
+
"internal",
|
| 80 |
+
"internal control",
|
| 81 |
+
"item",
|
| 82 |
+
"item 405 of",
|
| 83 |
+
"january",
|
| 84 |
+
"management",
|
| 85 |
+
"mark",
|
| 86 |
+
"mark registrant",
|
| 87 |
+
"market",
|
| 88 |
+
"markets",
|
| 89 |
+
"material",
|
| 90 |
+
"material weakness",
|
| 91 |
+
"none\n\nsecurities registered pursuant",
|
| 92 |
+
"oil",
|
| 93 |
+
"oil gas",
|
| 94 |
+
"original",
|
| 95 |
+
"original filing",
|
| 96 |
+
"otc",
|
| 97 |
+
"otc markets",
|
| 98 |
+
"par",
|
| 99 |
+
"past",
|
| 100 |
+
"period",
|
| 101 |
+
"pink",
|
| 102 |
+
"plan",
|
| 103 |
+
"power",
|
| 104 |
+
"prices",
|
| 105 |
+
"procedures",
|
| 106 |
+
"production",
|
| 107 |
+
"pursuant",
|
| 108 |
+
"pursuant section",
|
| 109 |
+
"registered",
|
| 110 |
+
"registrant",
|
| 111 |
+
"report",
|
| 112 |
+
"reporting",
|
| 113 |
+
"required",
|
| 114 |
+
"risk",
|
| 115 |
+
"risk factors",
|
| 116 |
+
"rule",
|
| 117 |
+
"rule 405 of regulation s-t",
|
| 118 |
+
"sec",
|
| 119 |
+
"section",
|
| 120 |
+
"section 13",
|
| 121 |
+
"section 13 or 15(d",
|
| 122 |
+
"section 13 or section 15(d",
|
| 123 |
+
"section 13(a",
|
| 124 |
+
"securities",
|
| 125 |
+
"securities exchange",
|
| 126 |
+
"shareholders",
|
| 127 |
+
"shares",
|
| 128 |
+
"state",
|
| 129 |
+
"stock",
|
| 130 |
+
"the division of corporation finance",
|
| 131 |
+
"the exchange act",
|
| 132 |
+
"the securities\nact",
|
| 133 |
+
"the securities exchange act",
|
| 134 |
+
"tier",
|
| 135 |
+
"today",
|
| 136 |
+
"united states securities and exchange commission",
|
| 137 |
+
"value",
|
| 138 |
+
"washington",
|
| 139 |
+
"weakness",
|
| 140 |
+
"www",
|
| 141 |
+
"yes"
|
| 142 |
+
],
|
| 143 |
+
"domain_signature_weights": {
|
| 144 |
+
"company": 0.5434425516894285,
|
| 145 |
+
"oil": 0.4601982637888686,
|
| 146 |
+
"registrant": 0.35385068273737597,
|
| 147 |
+
"financial": 0.34233387543836963,
|
| 148 |
+
"000": 0.33992541754442374,
|
| 149 |
+
"stock": 0.3399171814701413,
|
| 150 |
+
"form": 0.320860262779942,
|
| 151 |
+
"shares": 0.3123568986695668,
|
| 152 |
+
"internal": 0.30899744544815166,
|
| 153 |
+
"2020": 0.30865084706170187,
|
| 154 |
+
"act": 0.3002720040925734,
|
| 155 |
+
"www": 0.29176795472933054,
|
| 156 |
+
"financial reporting": 0.2703727647671327,
|
| 157 |
+
"2019": 0.2695167698710863,
|
| 158 |
+
"gas": 0.2646140016785994,
|
| 159 |
+
"common": 0.2642063050237187,
|
| 160 |
+
"10": 0.2634453863373789,
|
| 161 |
+
"internal control": 0.26071659459687796,
|
| 162 |
+
"power": 0.2566758305294127,
|
| 163 |
+
"reporting": 0.25624160895521564,
|
| 164 |
+
"https": 0.24464524899444745,
|
| 165 |
+
"exchange": 0.24440142735925177,
|
| 166 |
+
"com": 0.23561352867441543,
|
| 167 |
+
"sec": 0.23379937521555988,
|
| 168 |
+
"control financial": 0.23174808408611375,
|
| 169 |
+
"oil gas": 0.2300991318944343,
|
| 170 |
+
"accr": 0.22961580624454903,
|
| 171 |
+
"common stock": 0.22703826579299624,
|
| 172 |
+
"market": 0.22644755724314636,
|
| 173 |
+
"section": 0.22505818106195524,
|
| 174 |
+
"otc": 0.22361808381468262,
|
| 175 |
+
"material": 0.2222356332046525,
|
| 176 |
+
"controls": 0.222091913915859,
|
| 177 |
+
"check": 0.22197834590910182,
|
| 178 |
+
"15": 0.21758391215805944,
|
| 179 |
+
"mark": 0.2133795492452389,
|
| 180 |
+
"business": 0.21242801024375213,
|
| 181 |
+
"filer": 0.2104556207711244,
|
| 182 |
+
"securities": 0.20623417577856246,
|
| 183 |
+
"control": 0.20198550748782004,
|
| 184 |
+
"risk": 0.20020410481045717,
|
| 185 |
+
"access power": 0.19952178406951984,
|
| 186 |
+
"check mark": 0.19941286696921012,
|
| 187 |
+
"indicate": 0.19941286696921012,
|
| 188 |
+
"indicate check": 0.19941286696921012,
|
| 189 |
+
"pursuant": 0.19941286696921012,
|
| 190 |
+
"accelerated": 0.19519142197664818,
|
| 191 |
+
"accelerated filer": 0.19519142197664818,
|
| 192 |
+
"procedures": 0.19312340340509482,
|
| 193 |
+
"2021": 0.19152482082969796,
|
| 194 |
+
"markets": 0.19096389377226478,
|
| 195 |
+
"otc markets": 0.1905252004247202,
|
| 196 |
+
"exchange act": 0.19000520353206476,
|
| 197 |
+
"access": 0.18514752302796242,
|
| 198 |
+
"mark registrant": 0.18414866817473385,
|
| 199 |
+
"original filing": 0.1840793055155474,
|
| 200 |
+
"production": 0.1840793055155474,
|
| 201 |
+
"form 10": 0.18015102450611298,
|
| 202 |
+
"https www": 0.17346138018963395,
|
| 203 |
+
"12": 0.1696675756646967,
|
| 204 |
+
"prices": 0.16553960160025324,
|
| 205 |
+
"controls procedures": 0.16415489289433058,
|
| 206 |
+
"factors": 0.1635965318918298,
|
| 207 |
+
"yes": 0.16206316057090528,
|
| 208 |
+
"filing": 0.16056403032661598,
|
| 209 |
+
"13": 0.15946812753401024,
|
| 210 |
+
"period": 0.15883609714841485,
|
| 211 |
+
"000 000": 0.15843193743973555,
|
| 212 |
+
"original": 0.15716082036148227,
|
| 213 |
+
"material weakness": 0.15449872272407583,
|
| 214 |
+
"weakness": 0.15449872272407583,
|
| 215 |
+
"31": 0.1538440719097224,
|
| 216 |
+
"disclosure": 0.15023978923961498,
|
| 217 |
+
"current": 0.14640135590477033,
|
| 218 |
+
"risk factors": 0.14545525203851517,
|
| 219 |
+
"required": 0.14354229023588852,
|
| 220 |
+
"pursuant section": 0.14257751678386707,
|
| 221 |
+
"item": 0.1415879659936909,
|
| 222 |
+
"state": 0.1351412596686607,
|
| 223 |
+
"file": 0.13509456947086929,
|
| 224 |
+
"11": 0.13483024378652303,
|
| 225 |
+
"rule": 0.13364982468278475,
|
| 226 |
+
"information": 0.1336153857848394,
|
| 227 |
+
"past": 0.13117142864678802,
|
| 228 |
+
"securities exchange": 0.1273133179893908,
|
| 229 |
+
"coronavirus": 0.12655452254193883,
|
| 230 |
+
"today": 0.1259223792612869,
|
| 231 |
+
"january": 0.12589948858749667,
|
| 232 |
+
"disclosure controls": 0.12553021221331162,
|
| 233 |
+
"pink": 0.12333981323981766,
|
| 234 |
+
"tier": 0.12333981323981766,
|
| 235 |
+
"december": 0.12330699591166792,
|
| 236 |
+
"plan": 0.12260241816873166,
|
| 237 |
+
"report": 0.12215670766285741,
|
| 238 |
+
"registered": 0.11792658426119347,
|
| 239 |
+
"shareholders": 0.1165688683559572,
|
| 240 |
+
"section 13": 0.11627056418747651,
|
| 241 |
+
"31 2019": 0.1142908231074358,
|
| 242 |
+
"management": 0.11386489357657717,
|
| 243 |
+
"filed": 0.11349386398299907
|
| 244 |
+
},
|
| 245 |
+
"domain_entities": [
|
| 246 |
+
"the securities exchange act",
|
| 247 |
+
"washington",
|
| 248 |
+
"sec",
|
| 249 |
+
"rule 405 of regulation s-t",
|
| 250 |
+
"interactive data file",
|
| 251 |
+
"the exchange act",
|
| 252 |
+
"section 13(a",
|
| 253 |
+
"value",
|
| 254 |
+
"florida",
|
| 255 |
+
"none\n\nsecurities registered pursuant",
|
| 256 |
+
"item 405 of",
|
| 257 |
+
"par",
|
| 258 |
+
"the division of corporation finance",
|
| 259 |
+
"section 13 or section 15(d",
|
| 260 |
+
"united states securities and exchange commission",
|
| 261 |
+
"registrant",
|
| 262 |
+
"grand haven",
|
| 263 |
+
"i.r.s. employer\nidentification no.",
|
| 264 |
+
"f street ne",
|
| 265 |
+
"section 13 or 15(d",
|
| 266 |
+
"the securities\nact"
|
| 267 |
+
],
|
| 268 |
+
"stats": {
|
| 269 |
+
"context_length": 60699,
|
| 270 |
+
"context_tokens": 15174,
|
| 271 |
+
"source_files": 4,
|
| 272 |
+
"total_entries": 0
|
| 273 |
+
},
|
| 274 |
+
"created_at": "2026-07-25T00:51:20.496946"
|
| 275 |
+
}
|
data/agents/legal_agent/context.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/agents/legal_agent/metadata.json
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"agent_name": "legal_agent",
|
| 3 |
+
"system_prompt": "You are an expert legal AI assistant specialized in contract law, intellectual property, and corporate governance.",
|
| 4 |
+
"prompt_analysis": {
|
| 5 |
+
"domain": "legal",
|
| 6 |
+
"sub_domains": [],
|
| 7 |
+
"personality": "helpful assistant",
|
| 8 |
+
"constraints": [
|
| 9 |
+
"Stay within knowledge base",
|
| 10 |
+
"Be accurate"
|
| 11 |
+
],
|
| 12 |
+
"suggested_name": "MEXAR Legal Agent",
|
| 13 |
+
"domain_keywords": [
|
| 14 |
+
"law",
|
| 15 |
+
"court",
|
| 16 |
+
"legal",
|
| 17 |
+
"attorney",
|
| 18 |
+
"lawyer",
|
| 19 |
+
"case",
|
| 20 |
+
"contract",
|
| 21 |
+
"rights",
|
| 22 |
+
"litigation",
|
| 23 |
+
"judge",
|
| 24 |
+
"verdict",
|
| 25 |
+
"lawsuit",
|
| 26 |
+
"compliance",
|
| 27 |
+
"regulation",
|
| 28 |
+
"statute",
|
| 29 |
+
"defendant",
|
| 30 |
+
"plaintiff",
|
| 31 |
+
"trial",
|
| 32 |
+
"evidence",
|
| 33 |
+
"testimony"
|
| 34 |
+
],
|
| 35 |
+
"tone": "professional",
|
| 36 |
+
"capabilities": [
|
| 37 |
+
"Answer questions",
|
| 38 |
+
"Provide information"
|
| 39 |
+
]
|
| 40 |
+
},
|
| 41 |
+
"domain_signature": [
|
| 42 |
+
"01",
|
| 43 |
+
"02",
|
| 44 |
+
"03",
|
| 45 |
+
"04",
|
| 46 |
+
"05",
|
| 47 |
+
"06",
|
| 48 |
+
"07",
|
| 49 |
+
"08",
|
| 50 |
+
"09",
|
| 51 |
+
"10",
|
| 52 |
+
"11",
|
| 53 |
+
"12",
|
| 54 |
+
"16",
|
| 55 |
+
"16 snippet",
|
| 56 |
+
"2009",
|
| 57 |
+
"2011",
|
| 58 |
+
"2015",
|
| 59 |
+
"2018",
|
| 60 |
+
"2025",
|
| 61 |
+
"21",
|
| 62 |
+
"21 snippet",
|
| 63 |
+
"appeals",
|
| 64 |
+
"appeals date",
|
| 65 |
+
"appeals federal",
|
| 66 |
+
"appellant",
|
| 67 |
+
"breach",
|
| 68 |
+
"breach litig",
|
| 69 |
+
"breach litigation",
|
| 70 |
+
"california",
|
| 71 |
+
"california date",
|
| 72 |
+
"capital one derivative shareholder litigation\ncourt",
|
| 73 |
+
"chancery",
|
| 74 |
+
"chancery delaware",
|
| 75 |
+
"circuit",
|
| 76 |
+
"circuit date",
|
| 77 |
+
"columbia",
|
| 78 |
+
"connecticut appellate court\ndate",
|
| 79 |
+
"contract",
|
| 80 |
+
"corp",
|
| 81 |
+
"corporation",
|
| 82 |
+
"corporation derivative",
|
| 83 |
+
"court",
|
| 84 |
+
"court appeals",
|
| 85 |
+
"court california",
|
| 86 |
+
"court chancery",
|
| 87 |
+
"court court",
|
| 88 |
+
"court district",
|
| 89 |
+
"court massachusetts",
|
| 90 |
+
"court new",
|
| 91 |
+
"court of appeals",
|
| 92 |
+
"court of appeals of",
|
| 93 |
+
"court of chancery of delaware\ndate",
|
| 94 |
+
"court ohio",
|
| 95 |
+
"court supreme",
|
| 96 |
+
"customer",
|
| 97 |
+
"customer data",
|
| 98 |
+
"customer data sec",
|
| 99 |
+
"customer data security breach litigation\ncourt",
|
| 100 |
+
"d. delaware",
|
| 101 |
+
"d. maine",
|
| 102 |
+
"d. new jersey",
|
| 103 |
+
"data",
|
| 104 |
+
"data breach litigation\ncourt",
|
| 105 |
+
"data security",
|
| 106 |
+
"date",
|
| 107 |
+
"date filed",
|
| 108 |
+
"delaware",
|
| 109 |
+
"delaware date",
|
| 110 |
+
"derivative",
|
| 111 |
+
"derivative litigation",
|
| 112 |
+
"district",
|
| 113 |
+
"district court",
|
| 114 |
+
"district of columbia",
|
| 115 |
+
"e.d. michigan\ndate",
|
| 116 |
+
"e.d. virginia",
|
| 117 |
+
"federal",
|
| 118 |
+
"federal circuit",
|
| 119 |
+
"filed",
|
| 120 |
+
"filed 2009",
|
| 121 |
+
"filed 2011",
|
| 122 |
+
"filed 2015",
|
| 123 |
+
"filed 2025",
|
| 124 |
+
"florida",
|
| 125 |
+
"fox corporation derivative litigation\ncourt",
|
| 126 |
+
"general electric co.",
|
| 127 |
+
"gmbh",
|
| 128 |
+
"hannaford bros. co.",
|
| 129 |
+
"holding",
|
| 130 |
+
"holding legal",
|
| 131 |
+
"inc.",
|
| 132 |
+
"l.l.c.",
|
| 133 |
+
"legal",
|
| 134 |
+
"legal opinion",
|
| 135 |
+
"litigation",
|
| 136 |
+
"litigation court",
|
| 137 |
+
"llc",
|
| 138 |
+
"massachusetts",
|
| 139 |
+
"minnesota",
|
| 140 |
+
"n.a.",
|
| 141 |
+
"n.d.",
|
| 142 |
+
"n.d. california\ndate",
|
| 143 |
+
"n.d. illinois",
|
| 144 |
+
"new",
|
| 145 |
+
"new york",
|
| 146 |
+
"north carolina",
|
| 147 |
+
"office",
|
| 148 |
+
"office court",
|
| 149 |
+
"ohio",
|
| 150 |
+
"ohio court",
|
| 151 |
+
"ohio court of appeals\ndate",
|
| 152 |
+
"ohio dept",
|
| 153 |
+
"opinion",
|
| 154 |
+
"oracle corporation derivative litigation\ncourt",
|
| 155 |
+
"papst licensing digital camera patent litigation - mdl 1880",
|
| 156 |
+
"patent",
|
| 157 |
+
"patent litigation",
|
| 158 |
+
"patent trademark",
|
| 159 |
+
"re citigroup inc.",
|
| 160 |
+
"re columbia university patent litigation\ncourt",
|
| 161 |
+
"re unitedhealth group inc.",
|
| 162 |
+
"s.d.",
|
| 163 |
+
"security",
|
| 164 |
+
"security breach",
|
| 165 |
+
"shareholder",
|
| 166 |
+
"shareholder derivative",
|
| 167 |
+
"snippet",
|
| 168 |
+
"snippet & holding:",
|
| 169 |
+
"snippet holding",
|
| 170 |
+
"sony gaming networks & customer data security breach litigation\ncourt",
|
| 171 |
+
"south carolina",
|
| 172 |
+
"states",
|
| 173 |
+
"supreme",
|
| 174 |
+
"supreme court",
|
| 175 |
+
"target corp.",
|
| 176 |
+
"the d.c. circuit\ndate",
|
| 177 |
+
"the federal circuit\ndate",
|
| 178 |
+
"the fourth circuit\ndate",
|
| 179 |
+
"the third circuit\ndate",
|
| 180 |
+
"trademark",
|
| 181 |
+
"trademark office",
|
| 182 |
+
"trademark office\ncourt",
|
| 183 |
+
"u.s. patent & trademark office\ncourt",
|
| 184 |
+
"united",
|
| 185 |
+
"united states",
|
| 186 |
+
"united states judicial panel on multidistrict litigation\ndate",
|
| 187 |
+
"united states patent & trademark office\ncourt",
|
| 188 |
+
"university of virginia patent foundation",
|
| 189 |
+
"virginia",
|
| 190 |
+
"w.d. virginia",
|
| 191 |
+
"yahoo! inc.",
|
| 192 |
+
"york",
|
| 193 |
+
"york date"
|
| 194 |
+
],
|
| 195 |
+
"domain_signature_weights": {
|
| 196 |
+
"court": 13.913862280235232,
|
| 197 |
+
"date filed": 7.212138309753694,
|
| 198 |
+
"holding": 7.212138309753694,
|
| 199 |
+
"legal opinion": 7.212138309753694,
|
| 200 |
+
"snippet": 7.212138309753694,
|
| 201 |
+
"snippet holding": 7.212138309753694,
|
| 202 |
+
"date": 7.170550111201191,
|
| 203 |
+
"legal": 7.153912089052474,
|
| 204 |
+
"filed": 7.095916761829499,
|
| 205 |
+
"opinion": 7.034028366720265,
|
| 206 |
+
"litigation": 7.028765221390683,
|
| 207 |
+
"litigation court": 6.684492317051156,
|
| 208 |
+
"court district": 6.404279427139966,
|
| 209 |
+
"district": 6.390281448938663,
|
| 210 |
+
"district court": 5.926249055287422,
|
| 211 |
+
"derivative": 5.244341905360009,
|
| 212 |
+
"derivative litigation": 4.8941395775872305,
|
| 213 |
+
"holding legal": 4.808092206502464,
|
| 214 |
+
"court court": 4.791452558324828,
|
| 215 |
+
"patent": 4.689048805322256,
|
| 216 |
+
"shareholder": 4.197042533685463,
|
| 217 |
+
"appeals": 4.003274517184947,
|
| 218 |
+
"court appeals": 3.9299431101179207,
|
| 219 |
+
"shareholder derivative": 3.718165695473652,
|
| 220 |
+
"delaware": 3.4499663352075767,
|
| 221 |
+
"delaware date": 3.4499663352075767,
|
| 222 |
+
"08": 3.1484224998464865,
|
| 223 |
+
"05": 3.0709874923535496,
|
| 224 |
+
"chancery delaware": 3.053953660311148,
|
| 225 |
+
"court chancery": 3.053953660311148,
|
| 226 |
+
"circuit": 3.0488576353416037,
|
| 227 |
+
"circuit date": 3.023100200318638,
|
| 228 |
+
"chancery": 3.007667329553603,
|
| 229 |
+
"02": 2.975271548991789,
|
| 230 |
+
"10": 2.853432206699173,
|
| 231 |
+
"breach": 2.6886113464624413,
|
| 232 |
+
"patent litigation": 2.472774859377557,
|
| 233 |
+
"corp": 2.470655024812935,
|
| 234 |
+
"09": 2.4075513875457046,
|
| 235 |
+
"06": 2.4053683026251442,
|
| 236 |
+
"11": 2.3976131902838684,
|
| 237 |
+
"data": 2.3763488985572416,
|
| 238 |
+
"new": 2.3482847887826535,
|
| 239 |
+
"ohio": 2.3040857612149006,
|
| 240 |
+
"12": 2.3035785476976347,
|
| 241 |
+
"trademark": 2.2990068519576115,
|
| 242 |
+
"breach litigation": 2.257651565144711,
|
| 243 |
+
"california": 2.2355792711271483,
|
| 244 |
+
"office": 2.1695811399116804,
|
| 245 |
+
"california date": 2.1632764374293427,
|
| 246 |
+
"court california": 2.1632764374293427,
|
| 247 |
+
"court new": 2.1263726514505814,
|
| 248 |
+
"united": 2.088949513075227,
|
| 249 |
+
"federal": 2.06745079841331,
|
| 250 |
+
"appeals federal": 2.025008888438946,
|
| 251 |
+
"federal circuit": 2.025008888438946,
|
| 252 |
+
"new york": 2.014910936293498,
|
| 253 |
+
"york": 2.014910936293498,
|
| 254 |
+
"07": 1.9824593246874005,
|
| 255 |
+
"04": 1.9735451957576116,
|
| 256 |
+
"customer": 1.9465652929316943,
|
| 257 |
+
"customer data": 1.9465652929316943,
|
| 258 |
+
"filed 2011": 1.919988327171144,
|
| 259 |
+
"2011": 1.8945863168915102,
|
| 260 |
+
"corporation": 1.8882521728449373,
|
| 261 |
+
"security": 1.875473168291186,
|
| 262 |
+
"security breach": 1.875473168291186,
|
| 263 |
+
"court ohio": 1.875355591083983,
|
| 264 |
+
"03": 1.8645911522322545,
|
| 265 |
+
"united states": 1.8626500840661233,
|
| 266 |
+
"contract": 1.8582034214199248,
|
| 267 |
+
"2025": 1.851015825262079,
|
| 268 |
+
"filed 2025": 1.851015825262079,
|
| 269 |
+
"appeals date": 1.8473353532847774,
|
| 270 |
+
"llc": 1.8458195202296133,
|
| 271 |
+
"states": 1.8395087060124915,
|
| 272 |
+
"patent trademark": 1.8381770235952628,
|
| 273 |
+
"trademark office": 1.8381770235952628,
|
| 274 |
+
"york date": 1.821237560083746,
|
| 275 |
+
"columbia": 1.8033253227009467,
|
| 276 |
+
"01": 1.751573424073043,
|
| 277 |
+
"data security": 1.743397573677097,
|
| 278 |
+
"supreme": 1.7274616852216236,
|
| 279 |
+
"ohio court": 1.7206709189656775,
|
| 280 |
+
"filed 2009": 1.7205207865772278,
|
| 281 |
+
"office court": 1.7166948296049185,
|
| 282 |
+
"2009": 1.7017596556455288,
|
| 283 |
+
"filed 2015": 1.683367162474566,
|
| 284 |
+
"supreme court": 1.6122106493590107,
|
| 285 |
+
"2015": 1.6118397132550735,
|
| 286 |
+
"corporation derivative": 1.5889051199379827,
|
| 287 |
+
"16 snippet": 1.578946655731444,
|
| 288 |
+
"court supreme": 1.5550794340015575,
|
| 289 |
+
"16": 1.4898658811013983,
|
| 290 |
+
"2018": 1.4740130972670933,
|
| 291 |
+
"court massachusetts": 1.4506664884934248,
|
| 292 |
+
"massachusetts": 1.4506664884934248,
|
| 293 |
+
"21": 1.4491266291507698,
|
| 294 |
+
"21 snippet": 1.4491266291507698,
|
| 295 |
+
"virginia": 1.4316383241076298
|
| 296 |
+
},
|
| 297 |
+
"domain_entities": [
|
| 298 |
+
"snippet & holding:",
|
| 299 |
+
"court of chancery of delaware\ndate",
|
| 300 |
+
"united states patent & trademark office\ncourt",
|
| 301 |
+
"court of appeals",
|
| 302 |
+
"the fourth circuit\ndate",
|
| 303 |
+
"fox corporation derivative litigation\ncourt",
|
| 304 |
+
"supreme court",
|
| 305 |
+
"oracle corporation derivative litigation\ncourt",
|
| 306 |
+
"gmbh",
|
| 307 |
+
"the federal circuit\ndate",
|
| 308 |
+
"connecticut appellate court\ndate",
|
| 309 |
+
"llc",
|
| 310 |
+
"appellant",
|
| 311 |
+
"ohio court of appeals\ndate",
|
| 312 |
+
"south carolina",
|
| 313 |
+
"ohio dept",
|
| 314 |
+
"l.l.c.",
|
| 315 |
+
"re unitedhealth group inc.",
|
| 316 |
+
"massachusetts",
|
| 317 |
+
"district court",
|
| 318 |
+
"d. new jersey",
|
| 319 |
+
"target corp.",
|
| 320 |
+
"n.d. california\ndate",
|
| 321 |
+
"re citigroup inc.",
|
| 322 |
+
"s.d.",
|
| 323 |
+
"florida",
|
| 324 |
+
"w.d. virginia",
|
| 325 |
+
"general electric co.",
|
| 326 |
+
"university of virginia patent foundation",
|
| 327 |
+
"new york",
|
| 328 |
+
"d. maine",
|
| 329 |
+
"customer data security breach litigation\ncourt",
|
| 330 |
+
"hannaford bros. co.",
|
| 331 |
+
"re columbia university patent litigation\ncourt",
|
| 332 |
+
"minnesota",
|
| 333 |
+
"d. delaware",
|
| 334 |
+
"e.d. michigan\ndate",
|
| 335 |
+
"united states judicial panel on multidistrict litigation\ndate",
|
| 336 |
+
"n.d.",
|
| 337 |
+
"n.d. illinois",
|
| 338 |
+
"district of columbia",
|
| 339 |
+
"data breach litigation\ncourt",
|
| 340 |
+
"the third circuit\ndate",
|
| 341 |
+
"papst licensing digital camera patent litigation - mdl 1880",
|
| 342 |
+
"u.s. patent & trademark office\ncourt",
|
| 343 |
+
"trademark office\ncourt",
|
| 344 |
+
"customer data sec",
|
| 345 |
+
"breach litig",
|
| 346 |
+
"the d.c. circuit\ndate",
|
| 347 |
+
"north carolina",
|
| 348 |
+
"court of appeals of",
|
| 349 |
+
"inc.",
|
| 350 |
+
"e.d. virginia",
|
| 351 |
+
"yahoo! inc.",
|
| 352 |
+
"sony gaming networks & customer data security breach litigation\ncourt",
|
| 353 |
+
"capital one derivative shareholder litigation\ncourt",
|
| 354 |
+
"n.a."
|
| 355 |
+
],
|
| 356 |
+
"stats": {
|
| 357 |
+
"context_length": 140768,
|
| 358 |
+
"context_tokens": 35192,
|
| 359 |
+
"source_files": 148,
|
| 360 |
+
"total_entries": 0
|
| 361 |
+
},
|
| 362 |
+
"created_at": "2026-07-25T00:50:29.098912"
|
| 363 |
+
}
|
data/agents/medical_agent/context.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/agents/medical_agent/metadata.json
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"agent_name": "medical_agent",
|
| 3 |
+
"system_prompt": "You are an expert medical AI assistant specialized in cardiology, oncology, and internal medicine.",
|
| 4 |
+
"prompt_analysis": {
|
| 5 |
+
"domain": "medical",
|
| 6 |
+
"sub_domains": [
|
| 7 |
+
"cardiology",
|
| 8 |
+
"oncology",
|
| 9 |
+
"internal medicine"
|
| 10 |
+
],
|
| 11 |
+
"personality": "professional and empathetic",
|
| 12 |
+
"constraints": [
|
| 13 |
+
"provide evidence‑based information only",
|
| 14 |
+
"avoid giving personal medical diagnoses",
|
| 15 |
+
"maintain patient confidentiality",
|
| 16 |
+
"stay within cardiology, oncology, and internal medicine scope",
|
| 17 |
+
"cite reputable medical sources"
|
| 18 |
+
],
|
| 19 |
+
"suggested_name": "CardioOncoMedix",
|
| 20 |
+
"domain_keywords": [
|
| 21 |
+
"cardiology",
|
| 22 |
+
"oncology",
|
| 23 |
+
"internal medicine",
|
| 24 |
+
"heart",
|
| 25 |
+
"cancer",
|
| 26 |
+
"diagnosis",
|
| 27 |
+
"treatment",
|
| 28 |
+
"patient care",
|
| 29 |
+
"clinical guidelines",
|
| 30 |
+
"pharmacology",
|
| 31 |
+
"imaging",
|
| 32 |
+
"risk assessment",
|
| 33 |
+
"prevention",
|
| 34 |
+
"evidence‑based",
|
| 35 |
+
"medical research",
|
| 36 |
+
"symptoms",
|
| 37 |
+
"therapies",
|
| 38 |
+
"health",
|
| 39 |
+
"medicine",
|
| 40 |
+
"consultation"
|
| 41 |
+
],
|
| 42 |
+
"tone": "professional and empathetic",
|
| 43 |
+
"capabilities": [
|
| 44 |
+
"explain cardiovascular diseases",
|
| 45 |
+
"describe oncology therapies",
|
| 46 |
+
"summarize internal medicine topics",
|
| 47 |
+
"interpret general lab results",
|
| 48 |
+
"offer lifestyle and preventive recommendations",
|
| 49 |
+
"answer medical questions",
|
| 50 |
+
"clarify medical terminology",
|
| 51 |
+
"discuss medication mechanisms",
|
| 52 |
+
"reference clinical guidelines",
|
| 53 |
+
"provide patient education"
|
| 54 |
+
]
|
| 55 |
+
},
|
| 56 |
+
"domain_signature": [
|
| 57 |
+
"00",
|
| 58 |
+
"10",
|
| 59 |
+
"11",
|
| 60 |
+
"12",
|
| 61 |
+
"20",
|
| 62 |
+
"95",
|
| 63 |
+
"95 ci",
|
| 64 |
+
"achd",
|
| 65 |
+
"af",
|
| 66 |
+
"age",
|
| 67 |
+
"al",
|
| 68 |
+
"american",
|
| 69 |
+
"anal",
|
| 70 |
+
"analysis",
|
| 71 |
+
"associated",
|
| 72 |
+
"atrial",
|
| 73 |
+
"background",
|
| 74 |
+
"based",
|
| 75 |
+
"baseline",
|
| 76 |
+
"breast",
|
| 77 |
+
"breast cancer",
|
| 78 |
+
"cancer",
|
| 79 |
+
"cardiac",
|
| 80 |
+
"cell",
|
| 81 |
+
"cells",
|
| 82 |
+
"chatbot",
|
| 83 |
+
"chemoradiotherapy",
|
| 84 |
+
"chemotherapy",
|
| 85 |
+
"chi",
|
| 86 |
+
"ci",
|
| 87 |
+
"clinical",
|
| 88 |
+
"cns",
|
| 89 |
+
"cohort",
|
| 90 |
+
"cohort study",
|
| 91 |
+
"comparison",
|
| 92 |
+
"controlled",
|
| 93 |
+
"covid-19",
|
| 94 |
+
"ct exos",
|
| 95 |
+
"cvid",
|
| 96 |
+
"data",
|
| 97 |
+
"diagnostic",
|
| 98 |
+
"disease",
|
| 99 |
+
"dl",
|
| 100 |
+
"doac",
|
| 101 |
+
"doublet chemotherapy combined with radiotherapy",
|
| 102 |
+
"egj",
|
| 103 |
+
"es",
|
| 104 |
+
"esophageal",
|
| 105 |
+
"esophagectomy",
|
| 106 |
+
"et",
|
| 107 |
+
"et al",
|
| 108 |
+
"european",
|
| 109 |
+
"exercise",
|
| 110 |
+
"exos",
|
| 111 |
+
"expression",
|
| 112 |
+
"failure",
|
| 113 |
+
"fig",
|
| 114 |
+
"figure",
|
| 115 |
+
"fistula",
|
| 116 |
+
"glaucoma",
|
| 117 |
+
"grade",
|
| 118 |
+
"group",
|
| 119 |
+
"hallmarks",
|
| 120 |
+
"head neck",
|
| 121 |
+
"heart",
|
| 122 |
+
"heart failure",
|
| 123 |
+
"hepatic",
|
| 124 |
+
"hfd",
|
| 125 |
+
"high",
|
| 126 |
+
"hla",
|
| 127 |
+
"ici",
|
| 128 |
+
"il-6",
|
| 129 |
+
"immune",
|
| 130 |
+
"incidence and mortality worldwide",
|
| 131 |
+
"japan",
|
| 132 |
+
"japanese",
|
| 133 |
+
"lcr",
|
| 134 |
+
"lvad",
|
| 135 |
+
"medulloblastoma",
|
| 136 |
+
"metabolic",
|
| 137 |
+
"metastatic",
|
| 138 |
+
"mg",
|
| 139 |
+
"model",
|
| 140 |
+
"mortality",
|
| 141 |
+
"na",
|
| 142 |
+
"neoadjuvant",
|
| 143 |
+
"non",
|
| 144 |
+
"npar",
|
| 145 |
+
"nupr1",
|
| 146 |
+
"outcomes",
|
| 147 |
+
"patients",
|
| 148 |
+
"pd",
|
| 149 |
+
"pediatric",
|
| 150 |
+
"preoperative chemoradiotherapy",
|
| 151 |
+
"prolapse",
|
| 152 |
+
"rbm47",
|
| 153 |
+
"rectal",
|
| 154 |
+
"risk",
|
| 155 |
+
"roc",
|
| 156 |
+
"sarcopenia",
|
| 157 |
+
"scrt",
|
| 158 |
+
"scrt tnt",
|
| 159 |
+
"serum",
|
| 160 |
+
"significant",
|
| 161 |
+
"ssi",
|
| 162 |
+
"steaming",
|
| 163 |
+
"studies",
|
| 164 |
+
"study",
|
| 165 |
+
"surfactant",
|
| 166 |
+
"surgery",
|
| 167 |
+
"surgical",
|
| 168 |
+
"survival",
|
| 169 |
+
"table",
|
| 170 |
+
"the united states",
|
| 171 |
+
"therapy",
|
| 172 |
+
"time",
|
| 173 |
+
"tnt",
|
| 174 |
+
"treatment",
|
| 175 |
+
"trial",
|
| 176 |
+
"triplet chemotherapy",
|
| 177 |
+
"tumor",
|
| 178 |
+
"using",
|
| 179 |
+
"vascular",
|
| 180 |
+
"vcindy",
|
| 181 |
+
"vs",
|
| 182 |
+
"vt",
|
| 183 |
+
"xgboost"
|
| 184 |
+
],
|
| 185 |
+
"domain_signature_weights": {
|
| 186 |
+
"patients": 2.189523865212584,
|
| 187 |
+
"cancer": 1.8599159714125553,
|
| 188 |
+
"et": 1.4553917659567606,
|
| 189 |
+
"al": 1.454383572240683,
|
| 190 |
+
"et al": 1.4513597375240097,
|
| 191 |
+
"risk": 1.0425523022026464,
|
| 192 |
+
"study": 0.9739061192214548,
|
| 193 |
+
"treatment": 0.9605381017621952,
|
| 194 |
+
"cells": 0.8922523476089417,
|
| 195 |
+
"clinical": 0.8730779042462531,
|
| 196 |
+
"analysis": 0.868424949465127,
|
| 197 |
+
"rbm47": 0.861420268892691,
|
| 198 |
+
"expression": 0.8284392915908836,
|
| 199 |
+
"chemotherapy": 0.8264681703739832,
|
| 200 |
+
"heart": 0.7866508813767604,
|
| 201 |
+
"model": 0.7553417570634877,
|
| 202 |
+
"nupr1": 0.7551408959367775,
|
| 203 |
+
"af": 0.7476575631384772,
|
| 204 |
+
"cell": 0.7434606820449622,
|
| 205 |
+
"disease": 0.7329097947863994,
|
| 206 |
+
"10": 0.7328549625282946,
|
| 207 |
+
"surgery": 0.7119447727319322,
|
| 208 |
+
"breast": 0.7079172993036739,
|
| 209 |
+
"esophageal": 0.704237724050849,
|
| 210 |
+
"high": 0.6981577168850754,
|
| 211 |
+
"tumor": 0.6977188989543907,
|
| 212 |
+
"outcomes": 0.6912834504454624,
|
| 213 |
+
"breast cancer": 0.6725975741782778,
|
| 214 |
+
"es": 0.6523038919495094,
|
| 215 |
+
"exercise": 0.6310564863138816,
|
| 216 |
+
"data": 0.6266836256198041,
|
| 217 |
+
"neoadjuvant": 0.6139327971535335,
|
| 218 |
+
"cvid": 0.611443020189115,
|
| 219 |
+
"studies": 0.6090771783452308,
|
| 220 |
+
"ci": 0.604631387753996,
|
| 221 |
+
"heart failure": 0.5958026912827525,
|
| 222 |
+
"glaucoma": 0.5930197105177285,
|
| 223 |
+
"npar": 0.5926687428610519,
|
| 224 |
+
"using": 0.5903055952885305,
|
| 225 |
+
"based": 0.585407411043251,
|
| 226 |
+
"figure": 0.569621009323134,
|
| 227 |
+
"lcr": 0.5694183566027421,
|
| 228 |
+
"ssi": 0.5694183566027421,
|
| 229 |
+
"rectal": 0.5688029484713888,
|
| 230 |
+
"anal": 0.567643342532402,
|
| 231 |
+
"age": 0.5668801939465847,
|
| 232 |
+
"11": 0.5659368630534225,
|
| 233 |
+
"95": 0.5587589961771128,
|
| 234 |
+
"mg": 0.5528253758829694,
|
| 235 |
+
"surgical": 0.5479904032472687,
|
| 236 |
+
"vt": 0.5475318844185524,
|
| 237 |
+
"associated": 0.5446374540962099,
|
| 238 |
+
"95 ci": 0.5406077406439218,
|
| 239 |
+
"scrt": 0.5367203460337504,
|
| 240 |
+
"00": 0.5280619932758852,
|
| 241 |
+
"chi": 0.5217531733990047,
|
| 242 |
+
"fig": 0.5185434782496624,
|
| 243 |
+
"esophagectomy": 0.517004147389162,
|
| 244 |
+
"dl": 0.506864122796,
|
| 245 |
+
"hfd": 0.5048153888347436,
|
| 246 |
+
"ici": 0.5003816243503736,
|
| 247 |
+
"table": 0.4951534227055248,
|
| 248 |
+
"sarcopenia": 0.49135338430676667,
|
| 249 |
+
"failure": 0.48904596999186417,
|
| 250 |
+
"survival": 0.4878377996957481,
|
| 251 |
+
"group": 0.4763811859055428,
|
| 252 |
+
"therapy": 0.4749758240235285,
|
| 253 |
+
"pd": 0.47440026442207056,
|
| 254 |
+
"egj": 0.47290075287957817,
|
| 255 |
+
"scrt tnt": 0.4684104838112731,
|
| 256 |
+
"cohort": 0.4672077269309407,
|
| 257 |
+
"immune": 0.46646509614144954,
|
| 258 |
+
"mortality": 0.4643139682594719,
|
| 259 |
+
"chatbot": 0.4575245560782461,
|
| 260 |
+
"trial": 0.45544230203782615,
|
| 261 |
+
"medulloblastoma": 0.4547915262126624,
|
| 262 |
+
"exos": 0.45201222424240833,
|
| 263 |
+
"ct exos": 0.4471167849906855,
|
| 264 |
+
"achd": 0.44149217982806865,
|
| 265 |
+
"significant": 0.4403712379045058,
|
| 266 |
+
"surfactant": 0.43964837377560373,
|
| 267 |
+
"vs": 0.43879797317795827,
|
| 268 |
+
"grade": 0.43677769429429775,
|
| 269 |
+
"hepatic": 0.4342885977879502,
|
| 270 |
+
"tnt": 0.43427318349298294,
|
| 271 |
+
"hla": 0.43417954178617585,
|
| 272 |
+
"pediatric": 0.4338726791477151,
|
| 273 |
+
"20": 0.43171518538210246,
|
| 274 |
+
"doac": 0.42701702639108274,
|
| 275 |
+
"steaming": 0.4260410193825574,
|
| 276 |
+
"prolapse": 0.42477731151952874,
|
| 277 |
+
"non": 0.4240560483552536,
|
| 278 |
+
"na": 0.42316847840732286,
|
| 279 |
+
"fistula": 0.41919539251711174,
|
| 280 |
+
"baseline": 0.4177791418580278,
|
| 281 |
+
"vcindy": 0.41733842746128,
|
| 282 |
+
"12": 0.41662620468527617,
|
| 283 |
+
"head neck": 0.41622400744516214,
|
| 284 |
+
"lvad": 0.4132349069914202,
|
| 285 |
+
"time": 0.41240743613749625
|
| 286 |
+
},
|
| 287 |
+
"domain_entities": [
|
| 288 |
+
"the united states",
|
| 289 |
+
"cns",
|
| 290 |
+
"comparison",
|
| 291 |
+
"background",
|
| 292 |
+
"roc",
|
| 293 |
+
"metabolic",
|
| 294 |
+
"european",
|
| 295 |
+
"serum",
|
| 296 |
+
"american",
|
| 297 |
+
"cardiac",
|
| 298 |
+
"il-6",
|
| 299 |
+
"metastatic",
|
| 300 |
+
"covid-19",
|
| 301 |
+
"cancer",
|
| 302 |
+
"xgboost",
|
| 303 |
+
"vascular",
|
| 304 |
+
"diagnostic",
|
| 305 |
+
"hallmarks",
|
| 306 |
+
"lvad",
|
| 307 |
+
"atrial",
|
| 308 |
+
"japan",
|
| 309 |
+
"doublet chemotherapy combined with radiotherapy",
|
| 310 |
+
"preoperative chemoradiotherapy",
|
| 311 |
+
"triplet chemotherapy",
|
| 312 |
+
"controlled",
|
| 313 |
+
"chemoradiotherapy",
|
| 314 |
+
"survival",
|
| 315 |
+
"cohort study",
|
| 316 |
+
"japanese",
|
| 317 |
+
"incidence and mortality worldwide"
|
| 318 |
+
],
|
| 319 |
+
"stats": {
|
| 320 |
+
"context_length": 500040,
|
| 321 |
+
"context_tokens": 125010,
|
| 322 |
+
"source_files": 31,
|
| 323 |
+
"total_entries": 0
|
| 324 |
+
},
|
| 325 |
+
"created_at": "2026-07-25T00:46:15.084854"
|
| 326 |
+
}
|
evaluation_outputs/full_evaluation_20260724_183942.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
evaluation_outputs/full_evaluation_20260724_192144.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
evaluation_outputs/guardrail_threshold_sweep.json
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"sweep_results": [
|
| 3 |
+
{
|
| 4 |
+
"threshold": 0.02,
|
| 5 |
+
"tp": 209,
|
| 6 |
+
"fp": 82,
|
| 7 |
+
"fn": 0,
|
| 8 |
+
"tn": 0,
|
| 9 |
+
"precision": 0.7182,
|
| 10 |
+
"recall": 1.0,
|
| 11 |
+
"f1": 0.836,
|
| 12 |
+
"false_rejection_rate": 1.0
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"threshold": 0.05,
|
| 16 |
+
"tp": 209,
|
| 17 |
+
"fp": 82,
|
| 18 |
+
"fn": 0,
|
| 19 |
+
"tn": 0,
|
| 20 |
+
"precision": 0.7182,
|
| 21 |
+
"recall": 1.0,
|
| 22 |
+
"f1": 0.836,
|
| 23 |
+
"false_rejection_rate": 1.0
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"threshold": 0.08,
|
| 27 |
+
"tp": 209,
|
| 28 |
+
"fp": 82,
|
| 29 |
+
"fn": 0,
|
| 30 |
+
"tn": 0,
|
| 31 |
+
"precision": 0.7182,
|
| 32 |
+
"recall": 1.0,
|
| 33 |
+
"f1": 0.836,
|
| 34 |
+
"false_rejection_rate": 1.0
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"threshold": 0.1,
|
| 38 |
+
"tp": 209,
|
| 39 |
+
"fp": 82,
|
| 40 |
+
"fn": 0,
|
| 41 |
+
"tn": 0,
|
| 42 |
+
"precision": 0.7182,
|
| 43 |
+
"recall": 1.0,
|
| 44 |
+
"f1": 0.836,
|
| 45 |
+
"false_rejection_rate": 1.0
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"threshold": 0.15,
|
| 49 |
+
"tp": 209,
|
| 50 |
+
"fp": 82,
|
| 51 |
+
"fn": 0,
|
| 52 |
+
"tn": 0,
|
| 53 |
+
"precision": 0.7182,
|
| 54 |
+
"recall": 1.0,
|
| 55 |
+
"f1": 0.836,
|
| 56 |
+
"false_rejection_rate": 1.0
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"threshold": 0.2,
|
| 60 |
+
"tp": 209,
|
| 61 |
+
"fp": 82,
|
| 62 |
+
"fn": 0,
|
| 63 |
+
"tn": 0,
|
| 64 |
+
"precision": 0.7182,
|
| 65 |
+
"recall": 1.0,
|
| 66 |
+
"f1": 0.836,
|
| 67 |
+
"false_rejection_rate": 1.0
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"threshold": 0.25,
|
| 71 |
+
"tp": 209,
|
| 72 |
+
"fp": 82,
|
| 73 |
+
"fn": 0,
|
| 74 |
+
"tn": 0,
|
| 75 |
+
"precision": 0.7182,
|
| 76 |
+
"recall": 1.0,
|
| 77 |
+
"f1": 0.836,
|
| 78 |
+
"false_rejection_rate": 1.0
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"threshold": 0.3,
|
| 82 |
+
"tp": 209,
|
| 83 |
+
"fp": 82,
|
| 84 |
+
"fn": 0,
|
| 85 |
+
"tn": 0,
|
| 86 |
+
"precision": 0.7182,
|
| 87 |
+
"recall": 1.0,
|
| 88 |
+
"f1": 0.836,
|
| 89 |
+
"false_rejection_rate": 1.0
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"threshold": 0.35,
|
| 93 |
+
"tp": 209,
|
| 94 |
+
"fp": 82,
|
| 95 |
+
"fn": 0,
|
| 96 |
+
"tn": 0,
|
| 97 |
+
"precision": 0.7182,
|
| 98 |
+
"recall": 1.0,
|
| 99 |
+
"f1": 0.836,
|
| 100 |
+
"false_rejection_rate": 1.0
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"threshold": 0.4,
|
| 104 |
+
"tp": 209,
|
| 105 |
+
"fp": 82,
|
| 106 |
+
"fn": 0,
|
| 107 |
+
"tn": 0,
|
| 108 |
+
"precision": 0.7182,
|
| 109 |
+
"recall": 1.0,
|
| 110 |
+
"f1": 0.836,
|
| 111 |
+
"false_rejection_rate": 1.0
|
| 112 |
+
}
|
| 113 |
+
],
|
| 114 |
+
"optimal_threshold": 0.02,
|
| 115 |
+
"optimal_f1": 0.836,
|
| 116 |
+
"optimal_false_rejection_rate": 1.0,
|
| 117 |
+
"best_metrics": {
|
| 118 |
+
"threshold": 0.02,
|
| 119 |
+
"tp": 209,
|
| 120 |
+
"fp": 82,
|
| 121 |
+
"fn": 0,
|
| 122 |
+
"tn": 0,
|
| 123 |
+
"precision": 0.7182,
|
| 124 |
+
"recall": 1.0,
|
| 125 |
+
"f1": 0.836,
|
| 126 |
+
"false_rejection_rate": 1.0
|
| 127 |
+
}
|
| 128 |
+
}
|
test_data/financial_real/0000320575_0000320575-20-000008.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
10-K/A 1 form10-ka2019.htm 10-K/A Document UNITED STATES SECURITIES AND EXCHANGE COMMISSION Washington, D. C. 20549 FORM 10-K/A Amendment No. 1 (Mark one) ý ANNUAL REPORT PURSUANT TO SECTION 13 OR 15(d) OF THE SECURITIES EXCHANGE ACT OF 1934 For the fiscal year ended December 31, 2019 or o TRANSITION REPORT PURSUANT TO SECTION 13 OR 15(d) OF THE SECURITIES EXCHANGE ACT OF 1934 Commission File Number: 1-8182 PIONEER ENERGY SERVICES CORP. (Exact name of registrant as specified in its charter) _____________________________________________ TEXAS 74-2088619 (State or other jurisdiction of incorporation or organization) (I.R.S. Employer Identification Number) 1250 N.E. Loop 410, Suite 1000 San Antonio, Texas 78209 (Address of principal executive offices) (Zip Code) Registrant’s telephone number, including area code: (855) 884-0575 Securities registered pursuant to Section 12(b) of the Act Title of each class Trading Symbol(s) Name of each exchange on which registered Securities registered pursuant to Section 12(g) of the Act: Common Stock, $0.10 par value Indicate by check mark if the registrant is a well-known seasoned issuer, as defined in Rule 405 of the Securities Act. Yes ¨ No þ Indicate by check mark if the registrant is not required to file reports pursuant to Section 13 or Section 15(d) of the Act. Yes ¨ No þ Indicate by check mark whether the registrant: (1) has filed all reports required to be filed by Section 13 or 15(d) of the Securities Exchange Act of 1934 during the preceding 12 months (or for such shorter period that the registrant was required to file such reports), and (2) has been subject to such filing requirements for the past 90 days. Yes þ No ¨ Indicate by check mark whether the registrant has submitted electronically every Interactive Data File required to be submitted pursuant to Rule 405 of Regulation S-T (§ 232.405 of this chapter) during the preceding 12 months (or for such shorter period that the registrant was required to submit such files). Yes þ No ¨ Indicate by check mark whether the registrant is a large accelerated filer, an accelerated filer, a non-accelerated filer, a smaller reporting company, or an emerging growth company. See the definitions of “large accelerated filer,” “accelerated filer,” “smaller reporting company,” and “emerging growth company” in Rule 12b-2 of the Exchange Act. Large accelerated filer o Accelerated filer o Non-accelerated filer þ Smaller reporting company þ Emerging growth company o If an emerging growth company, indicate by check mark if the registrant has elected not to use the extended transition period for complying with any new or revised financial accounting standards provided pursuant to Section 13(a) of the Exchange Act. ¨ Indicate by check mark whether the registrant is a shell company (as defined in Rule 12b-2 of the Exchange Act). Yes ¨ No þ The aggregate market value of the registrant’s common stock held by non-affiliates of the registrant as of the last business day of the registrant’s most recently completed second fiscal quarter (based on the closing sales price on the New York Stock Exchange (NYSE) on June 30, 2019 ) was approximately $19.0 million . As of March 31, 2020 , there were 79,579,571 shares of common stock, par value $0.10 per share, of the registrant issued and outstanding. EXPLANATORY NOTE This Amendment No. 1 on Form 10-K/A (this "Amendment") amends the Annual Report on Form 10-K of Pioneer Energy Services Corp. ("Pioneer," "Company," "we," "us," and "our") for the year ended December 31, 2019 that was originally filed with the U.S. Securities and Exchange Commission ("SEC") on March 6, 2020 (the "Original Filing"). The purpose of this Amendment is to (i) supplement the risk factors set forth in Item 1A of Part I of the Original Filing and (ii) provide the information required by Items 10, 11, 12, 13, and 14 of Part III of the Original Filing, which information was previously omitted from the Original Filing in reliance on General Instruction G(3) to Form 10-K. The supplemental risk factors set forth in this Amendment should be read in conjunction with the other risk factors set forth in the Original Filing, and Part III of the Original Filing is hereby amended and restated in its entirety. Capitalized terms not otherwise defined in Part III of this Amendment shall have the same meanings assigned to such terms in Parts I and II of the Original Filing. In addition, as required by Rule 12b-15 under the Securities Exchange Act of 1934, as amended (the "Exchange Act"), currently dated certifications by our principal executive officer and principal financial officer are filed as exhibits to this Amendment under Item 15 of Part IV hereof. Except as described above, this Amendment does not amend or otherwise update any other information in the Original Filing. Accordingly, this Amendment should be read in conjunction with the Original Filing and with our filings with the SEC subsequent to the Original Filing. As previously disclosed, on March 1, 2020 , Pioneer and certain of its affiliates (together with Pioneer, the "Pioneer RSA Parties"), filed voluntary petitions for reorganization under chapter 11 of title 11 of the United States Code (the "Bankruptcy Code") in the United States Bankruptcy Court for the Southern District of Texas (the "Bankruptcy Court"). The Chapter 11 proceedings are being jointly administered under the caption In re Pioneer Energy Services Corp. et al (the "Chapter 11 Cases"). For the duration of the Chapter 11 Cases, our operations and our ability to develop and execute our business plan are subject to risks and uncertainties associated with the Chapter 11 Cases as described in "Item 1A. Risk Factors" in Part I of the Original Filing and this Amendment. As a result of these risks and uncertainties, our assets, liabilities, shareholders' equity, officers and/or directors could be significantly different following the conclusion of the Chapter 11 Cases, and the description of our operations, properties, and capital plans included in the Original Filing may not accurately reflect our operations, properties, and capital plans following the Chapter 11 Cases. 1 TABLE OF CONTENTS Page PART I Item 1A. Risk Factors 3 PART III Item 10. Directors, Executive Officers and Corporate Governance 5 Item 11. Executive Compensation 10 Item 12. Security Ownership of Certain Beneficial Owners and Management and Related Shareholder Matters 20 Item 13. Certain Relationships and Related Transactions, and Director Independence 22 Item 14. Principal Accounting Fees and Services 23 PART IV Item 15. Exhibits, Financial Statement Schedules 24 Signatures 25 2 PART I ITEM 1A. RISK FACTORS Except as discussed below, there has been no material change in our risk factors as previously disclosed in Item 1A – “Risk Factors” of the Original Filing. The risk factors set forth in this Amendment should be read in conjunction with the other risk factors set forth in the Original Filing. The following two risk factors in Part I, Item 1A of the Original Filing are amended and restated in their entirety as follows: • We derive all our revenues from companies in the oil and gas exploration and production industry, a historically cyclical industry with levels of activity that are significantly affected by the levels and volatility of oil and gas prices. As a provider of contract land drilling services and oil and gas production services, our business depends on the level of exploration and production activity in the geographic markets where we operate. The oil and gas exploration and production industry is a historically cyclical industry characterized by significant changes in the levels of exploration and development activities. Oil and gas prices, and market expectations of potential changes in those prices, significantly affect the levels of those activities. Oil and gas prices have been volatile historically and, we believe, will likely continue in the future. Oil prices have experienced declines in March 2020 and could decline further. Worldwide political, economic, and military events, as well as natural disasters and pandemics (including the recent coronavirus (COVID-19) outbreak), have contributed to oil and gas price volatility historically, and are likely to continue to do so in the future. Many factors beyond our control affect oil and gas prices, including: • the worldwide supply and demand for oil and gas; • the cost of exploring for, producing and delivering oil and gas; • the discovery rate of new oil and gas reserves; • the rate of decline of existing and new oil and gas reserves; • available pipeline and other oil and gas transportation capacity; • the levels of oil and gas storage; • the ability of oil and gas exploration and production companies to raise capital; • economic conditions in the United States and elsewhere; • actions by the Organization of Petroleum Exporting Countries (OPEC) and Russia; • political instability in oil and gas producing regions; • governmental regulations, both domestic and foreign; • domestic and foreign tax policy; • weather conditions in the United States and elsewhere; • the pace adopted by foreign governments for the exploration, development and production of their national reserves, or their investments in oil and gas reserves located in other countries; and • the price of foreign imports of oil and gas. Additionally, the above factors can also be affected by technological advances affecting energy consumption and the supply and demand within the market for renewable energy resources. • Oil and natural gas prices, and market expectations of potential changes in these prices, significantly impact the level of worldwide drilling and production services activities. Oil and natural gas prices, and market expectations of potential changes in these prices, significantly impact the level of worldwide drilling and production services activities. Reduced demand for oil and natural gas generally results in lower prices for these commodities and often impacts the economics of planned drilling projects and ongoing production projects, resulting in the curtailment, reduction, delay or postponement of such projects for an indeterminate period of time. When drilling and production activity and spending declines, both dayrates and utilization historically decline as well. In late 2014, oil prices worldwide began to drop significantly and as a result, our clients significantly reduced both their operating and capital expenditures during 2015 and 2016, which adversely affected our business. In 2017 and 2018, our clients modestly increased their spending as compared to 2016 levels, and our business trended upward as a result. However, in late 2018, oil prices again began to decline and despite some improvement in early 2019, have since languished without significant improvement in 2019 and early 2020. Since January 2020, the coronavirus (COVID-19) outbreak and fear of further spread of the coronavirus have caused disruptions in international economies and international financial and oil markets, including a decline in the price of oil. The economic 3 downturn caused by the coronavirus outbreak has weakened demand for oil, and after OPEC and a group of oil producing nations led by Russia failed on March 6, 2020 to agree on oil production cuts, Saudi Arabia announced that it would cut oil prices and increase production, leading to a sharp further decline in oil trading prices. Following these developments, WTI crude oil prices fell to $20 per barrel at the end of March 2020. We cannot anticipate whether or when this dispute will be resolved and production returned to normalized levels. The coronavirus and responses of oil producers to the lower demand for oil and lower oil prices are rapidly evolving situations. Oil and gas exploration and production companies have announced reductions to previously planned capital spending programs for 2020, thereby reducing demand for our services. Sustained low or worsening oil prices could have an adverse effect on our financial condition, results of operations, and cash flow. Additionally, because our business depends on the level of spending by our clients, we are also affected by our clients’ ability to access the capital markets. After several consecutive years without significant improvement in commodity prices, many exploration and production companies have limited their spending to a level which can be supported by net operating cash flows alone, as access to the capital markets through debt or equity financings has become more challenging in our industry. This challenge has increased recently due to the major stock market and bond market indices experiencing substantial declines, with such declines intensifying, and elevated levels of volatility, in 2020. If the reduction in the overall level of exploration and development activities, whether resulting from changes in oil and gas prices or otherwise, continues or worsens, it could adversely affect us further by negatively impacting: • our revenues, cash flows and profitability; • the fair market value of our drilling and production services fleets; • our ability to maintain or increase its borrowing capacity; • our ability to obtain additional capital to finance our business or make acquisitions, and the cost of that capital; • the collectability of our receivables; • our ability to adequately maintain our equipment, or bring it back into service; and • our ability to retain skilled operations personnel. The following risk factor supplements those above and those set forth in the Original Filing. • The rapid spread of a contagious illness, including the recent spread of COVID-19, or fear of such an event, could adversely affect our business, operations, and financial condition. Our business could be adversely affected by a widespread outbreak of contagious disease, including the recent outbreak of respiratory illness caused by a novel coronavirus (COVID-19). Since January 2020, the coronavirus outbreak and fear of further spread of the coronavirus have caused significant disruptions in international economies and international financial and oil markets, including a substantial decline in the price of oil. Our customers responded to the outbreak by announcing reductions to their capital programs, resulting in a reduction in demand for our services. As the coronavirus outbreak is still evolving, much of its international and domestic impact remains unknown. The coronavirus outbreak has led to quarantines of entire countries and smaller localities, cancellation of events and travel, prohibitions on entry imposed by many countries, government-mandated and voluntary business and school shutdowns, supply chain interruptions, and overall economic and financial market instability. Further spread of the coronavirus could cause additional quarantines, reduction in business activity, labor shortages and other operational
|
test_data/financial_real/0001041588_0001041588-20-000001.txt
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
10-K 1 accr12311910K.txt January 2nd, 2020
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
PERIOD and DOCUMENT: 10KSB for the FULL YEAR 12/31/2019
|
| 6 |
+
COMPANY CONFIRMED NAME: ACCESS-POWER, INC.
|
| 7 |
+
CENTRAL INDEX KEY: 0001041588
|
| 8 |
+
FORM TYPE: 10KSB
|
| 9 |
+
SEC FILE NUMBER: 333-65069
|
| 10 |
+
BUSINESS ADDRESS:
|
| 11 |
+
STREET 1: 17164 DUNE VIEW DRIVE
|
| 12 |
+
STREET 2: APT 106
|
| 13 |
+
CITY: GRAND HAVEN
|
| 14 |
+
STATE: MI
|
| 15 |
+
ZIP: 49417
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
Respectfully submitted to the,
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
UNITED STATES SECURITIES AND EXCHANGE COMMISSION
|
| 23 |
+
WASHINGTON, D.C. 20549
|
| 24 |
+
The Division of Corporation Finance,
|
| 25 |
+
100 F Street NE, Washington, D.C. 20549
|
| 26 |
+
|
| 27 |
+
FORM 10-KSB
|
| 28 |
+
|
| 29 |
+
/X/ ANNUAL REPORT PURSUANT TO SECTION 13 OR 15(d) OF THE
|
| 30 |
+
SECURITIES EXCHANGE ACT OF 1934 FOR THE FISCAL YEAR ENDED
|
| 31 |
+
DECEMBER 31, 2019.
|
| 32 |
+
|
| 33 |
+
TRANSITION REPORT PURSUANT TO SECTION 13 OR 15(d)
|
| 34 |
+
OF THE SECURITIES EXCHANGE ACT OF 1934
|
| 35 |
+
FOR THE TRANSITION PERIOD FROM ______ TO _____
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
COMMISSION FILE NUMBER: 333-65069
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
ACCESS-POWER, INC.
|
| 42 |
+
[Exact name of Registrant as
|
| 43 |
+
specified in its charter]
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
*** FLORIDA ***
|
| 47 |
+
|
| 48 |
+
Domicile State or other jurisdiction of
|
| 49 |
+
incorporation or organization
|
| 50 |
+
http://www.sunbiz.org
|
| 51 |
+
|
| 52 |
+
*** MICHIGAN ***
|
| 53 |
+
|
| 54 |
+
State of Operations
|
| 55 |
+
https://cofs.lara.state.mi.us/SearchApi/Search/Search
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
59-3420985
|
| 60 |
+
(I.R.S. Employer
|
| 61 |
+
Identification No.)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
17164 DUNE VIEW DRIVE
|
| 65 |
+
APT 106
|
| 66 |
+
GRAND HAVEN, MI 49417
|
| 67 |
+
|
| 68 |
+
or
|
| 69 |
+
|
| 70 |
+
PO BOX 598
|
| 71 |
+
Grand Haven, MI 49417
|
| 72 |
+
|
| 73 |
+
(Address of principal executive offices)
|
| 74 |
+
|
| 75 |
+
REGISTRANTS TELEPHONE NUMBER, INCLUDING AREA CODE: 616-312-5390
|
| 76 |
+
|
| 77 |
+
SECURITIES REGISTERED PURSUANT TO SECTION 12(b) OF THE ACT: NONE
|
| 78 |
+
|
| 79 |
+
SECURITIES REGISTERED PURSUANT TO SECTION 12(g) OF THE ACT:
|
| 80 |
+
COMMON STOCK, $0.001 PAR VALUE - 500,000,000 shares authorized.
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
Indicate by check mark if the registrant is a well-known
|
| 84 |
+
seasoned issuer, as defined in Rule 405 of the Securities
|
| 85 |
+
Act. Yes No X
|
| 86 |
+
|
| 87 |
+
Indicate by check mark if the registrant is not required to file
|
| 88 |
+
reports pursuant to Section 13 or Section 15(d) of the Act.
|
| 89 |
+
Yes No X
|
| 90 |
+
|
| 91 |
+
The Company has had discussions with the SEC regarding a FRAUD
|
| 92 |
+
FORM 15 that was filed by previous management in 2007. We believe
|
| 93 |
+
that we have a fiduciary responsibility to report transaprent
|
| 94 |
+
information to our Shareholders.
|
| 95 |
+
|
| 96 |
+
We were victims of toxic death spiral debentures from 1998 to
|
| 97 |
+
2008. PLEASE FORGIVE OUR PAST. The past it the past, and
|
| 98 |
+
we wish to move forward with a plan to become fully
|
| 99 |
+
current with the SEC. We are trying the best we can within the
|
| 100 |
+
Spirit of the Law.
|
| 101 |
+
|
| 102 |
+
https://www.sec.gov/Archives/edgar/data/1041588/
|
| 103 |
+
000117347307000043/0001173473-07-000043-index.htm
|
| 104 |
+
|
| 105 |
+
Indicate by check mark whether the registrant (1) has
|
| 106 |
+
filed all reports required to be filed be Section 13 or 15(d)
|
| 107 |
+
of the Securities Exchange Act of 1934 during the preceding 12
|
| 108 |
+
months (or for such shorter period that the registrant was
|
| 109 |
+
required to file such reports), and (2) has been subject
|
| 110 |
+
to such filing requirements for the past 90 days. Yes X No
|
| 111 |
+
|
| 112 |
+
Indicate by check mark whether the registrant has submitted
|
| 113 |
+
electronically and posted on its corporate Web site, if any,
|
| 114 |
+
every Interactive Data File required to be submitted and
|
| 115 |
+
posted pursuant to Rule 405 of Regulation S-T (232.405 of
|
| 116 |
+
this chapter) during the preceding 12 months (or for such
|
| 117 |
+
shorter period that the registrant was required to submit
|
| 118 |
+
and post such files). Yes X No
|
| 119 |
+
|
| 120 |
+
Indicate by check mark if disclosure of delinquent filers
|
| 121 |
+
pursuant to Item 405 of Regulation S-K (Section 229.405
|
| 122 |
+
of this chapter) is not contained herein, and will not be
|
| 123 |
+
contained, to the best of registrants knowledge, in definitive
|
| 124 |
+
proxy or information statements incorporated by reference in
|
| 125 |
+
Part III of this Form 10-K or any amendment to this Form 10-K.
|
| 126 |
+
|
| 127 |
+
Indicate by check mark whether the registrant is a large a
|
| 128 |
+
ccelerated filer, an accelerated filer, a non-accelerated
|
| 129 |
+
filer, smaller reporting company, or an emerging growth
|
| 130 |
+
company. See the definitions of large accelerated filer,
|
| 131 |
+
accelerated filer, smaller reporting company, and
|
| 132 |
+
emerging growth company in Rule 12b-2 of the Exchange Act.
|
| 133 |
+
|
| 134 |
+
Large accelerated filer NO Accelerated filer NO
|
| 135 |
+
Non-accelerated filer NO (Do not check if a smaller
|
| 136 |
+
reporting company)
|
| 137 |
+
|
| 138 |
+
Smaller reporting company YES
|
| 139 |
+
Emerging growth company YES
|
| 140 |
+
|
| 141 |
+
If an emerging growth company, indicate by check mark if
|
| 142 |
+
the registrant has elected not to use the extended
|
| 143 |
+
transition period for complying with any new or revised
|
| 144 |
+
financial accounting standards provided pursuant to
|
| 145 |
+
ection 13(a) of the Exchange Act.
|
| 146 |
+
|
| 147 |
+
Indicate by check mark whether the registrant is a shell
|
| 148 |
+
company (as defined in Rule 12b-2 of the Exchange Act).
|
| 149 |
+
Yes No X
|
| 150 |
+
|
| 151 |
+
The aggregate market value of the voting common stock held by non-affils.
|
| 152 |
+
of the registrant (assuming officers and directors are affiliates) was
|
| 153 |
+
approximately $1,017,140.59 as of 12/3119, computed on the basis of
|
| 154 |
+
the closing price on such date.
|
| 155 |
+
|
| 156 |
+
As of January 2nd, 2020, there were 244,144,121 shares of the
|
| 157 |
+
registrant's
|
| 158 |
+
Common Stock outstanding. PATRICK J JENSEN, Director of Access-Power, Inc.
|
| 159 |
+
has 131,128,500 Restricted Shares. 113,015,621 + 131,128,500 =
|
| 160 |
+
244,144,121
|
| 161 |
+
total shares available of Access-Power, Inc. common stock.
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
*****WE ONLY HAVE COMMON STOCK*****
|
| 165 |
+
|
| 166 |
+
*****WE DO NOT HAVE CONVERTIBLE NOTES or WARRANTS*****
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
THE TOTAL MARKET CAPITALIZATION AS OF THE CLOSE OF BUSINESS
|
| 170 |
+
January 2nd, 2020 IS 2,197,297.09 BASED ON A CLOSING PRICE OF
|
| 171 |
+
$.009 PER SHARE.
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
___________________________________________________________________________
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
**** CURRENT UPDATE ****
|
| 179 |
+
|
| 180 |
+
Our comeback journey continues. We are very excited about our future.
|
| 181 |
+
|
| 182 |
+
We filed our first tax return in 18 years. We hired H&R Block of Muskegon
|
| 183 |
+
to look over and supervise and review all of our financial bank records
|
| 184 |
+
and statements for 2018 and 2019. We believe in 100% Transparency. We are
|
| 185 |
+
making very good informed decisions. H&R BLOCK engagement--->
|
| 186 |
+
|
| 187 |
+
This occured on November 5, 2019 - our date of engagement with HR&BLOCK
|
| 188 |
+
|
| 189 |
+
In November of 2019 , we have started an application with the State of
|
| 190 |
+
Michigan to obtain a MicroMarijuana business. We only plan to operate
|
| 191 |
+
our futuristic plan of delivering Marijuana Plants by Drones via a
|
| 192 |
+
service called Clones by Drones TM. This process could take
|
| 193 |
+
2-3 years to implement. The application was started at:
|
| 194 |
+
|
| 195 |
+
https://aca3.accela.com/MIMM/Login.aspx
|
| 196 |
+
|
| 197 |
+
The Company believes it has the right to commence operations with a
|
| 198 |
+
pending license application. ACCR has a very super cool business model
|
| 199 |
+
that will defy conventional logic---> LOGISTICs are simple.
|
| 200 |
+
|
| 201 |
+
We expect to get the licenses, zoning requirements, and permits
|
| 202 |
+
necessary to operate our BUSINESS PLAN. WE HAVE A SALES BUSINESS PLAN.
|
| 203 |
+
ACCR has a business plan that represents my DREAMs in life. I will
|
| 204 |
+
not hurt my Shareholders. Everyone is welcome, and ACCR is grateful
|
| 205 |
+
and thankful. We are not sellers of our common stock. PERIOD.
|
| 206 |
+
|
| 207 |
+
Access-Power, Inc. has a big task ahead of itself. We have many
|
| 208 |
+
obstancles to grow our 20' x 20' Master Mother Grow and Cloning Rooms. We
|
| 209 |
+
would expect to be fully operational with inventory in 3 months of
|
| 210 |
+
beginning construction. We do not have Clones by Drones operational yet
|
| 211 |
+
to accept Visa/Martercard or Pay/Pal payments. We expect to turn on
|
| 212 |
+
our eCommerce website sometime in 2020 or possibly 2021. Probably in
|
| 213 |
+
2020. We believe in only MEDICAL MARIJUANA, and will not offer at the
|
| 214 |
+
moment any products to people that do not possess a valid Medical
|
| 215 |
+
Marijuana Card issued by any State. There are dispensaries here in
|
| 216 |
+
Michigan that accept patients from out of State. We plan to
|
| 217 |
+
mirror the growth's of the Dispensaries. We plan to grow around
|
| 218 |
+
the dispensaries, and offer our cool and futuristic service
|
| 219 |
+
by Drones. Access-Power, Inc. has in its business plan a way to
|
| 220 |
+
logistically deliver in a very cost effective way.
|
| 221 |
+
|
| 222 |
+
BACK to our engagement with H&R Block on November 5, 2019.
|
| 223 |
+
|
| 224 |
+
We realize that this is not a PCAOB firm, PLEASE...
|
| 225 |
+
however, this is all the Company can afford. Within the spirit of the
|
| 226 |
+
law, we are filing these documents to report transparency to our
|
| 227 |
+
Shareholders. We have spoken to the SEC regarding this requirement, and
|
| 228 |
+
it is the Companies intention to hire a PCAOB firm in 2020 or in 2021.
|
| 229 |
+
We have to start somewhere and again it is our goal to fully hire a PCAOB
|
| 230 |
+
accountant in the year 2020 or 2021. We have communicated our 2 year
|
| 231 |
+
plan with the SEC. We have a very transparent relationship
|
| 232 |
+
with many divisions of FINRA. The Company has great contacts at
|
| 233 |
+
OTC Markets. The Company also has a trusting relationship that is
|
| 234 |
+
very open with the SEC.
|
| 235 |
+
|
| 236 |
+
Access-Power, Inc. was the victim of toxic convertible death spiral
|
| 237 |
+
debentures from inception through 2008. I purchased common stock
|
| 238 |
+
during this time, and slowly became a 10% stock holder over the years.
|
| 239 |
+
The Florida Law which allowed us to fire all previous crooked Directors
|
| 240 |
+
is Chap 607.0702 of the 2012 Florida Senate Law.
|
| 241 |
+
|
| 242 |
+
The Company has survived a short attack, and the Company will attempt
|
| 243 |
+
to hire an attorney in 2020 to represent us.
|
| 244 |
+
|
| 245 |
+
ON DECEMBER 13, 2019, ACCESS-POWER, INC. eFILED ITS FIRST STATE AND
|
| 246 |
+
FEDERAL TAX RETURN IN OVER 18 YEARS. THE eFILE WAS ACCEPTED BY ALL
|
| 247 |
+
PARTIES ON DECEMBER 19, 2019. ACCESS-POWER, INC. PLANS TO CONTINUE TO
|
| 248 |
+
WORK WITH OUR TEMPORARY AUDITOR THROUGHOUT 2020. WE ARE STRIVING TO BE
|
| 249 |
+
A VERY TRANSPARENT AND CURRENT REPORTING ENTITY. THERE ARE ONLY
|
| 250 |
+
98,144,246 SHARES IN OUR PUBLIC FLOAT. THIS IS FIXED, AND THERE IS NO
|
| 251 |
+
DILUTION, I REPEAT....NO DILUTION IN THE COMPANY COMMON SHARES
|
| 252 |
+
THROUGHOUT 2020 AND 2021. AGAIN, THE SUPPLY OF STOCK IS FIXED.
|
| 253 |
+
|
| 254 |
+
We are a micro-cap business with very struggling revenues at
|
| 255 |
+
the moment, and we are developing the first eCommerce website to deliver
|
| 256 |
+
Marijuana, Marijuana plants, and other Marijuana products such as Edibles
|
| 257 |
+
by Drones. We are going through a rough economic business cycle.
|
| 258 |
+
We applied for a Trademark SERVICE MARK on November 13, 2019.
|
| 259 |
+
|
| 260 |
+
We will succeed. This may take a few years to develop. At the present
|
| 261 |
+
time, I am still waiting for our Companies previously announced
|
| 262 |
+
$50,000.00 donation. This may take another 6-9 months.
|
| 263 |
+
|
| 264 |
+
There are many entities that do not want ACCR to comeback.
|
| 265 |
+
|
| 266 |
+
As of the close of business January 1st 2020, our commomn stock
|
| 267 |
+
structure is as follows:
|
| 268 |
+
|
| 269 |
+
Authorized Common Stock: 500,000,000 shares
|
| 270 |
+
Outstanding Common Stock: 244,144,121 shares
|
| 271 |
+
Estimated Float: Substanstially less than 98,244,146 shares
|
| 272 |
+
Restricted Common Stock: 145,769,975 shares
|
| 273 |
+
|
| 274 |
+
https://www.otcmarkets.com/stock/ACCR/profile
|
| 275 |
+
|
| 276 |
+
We have public profiles at:
|
| 277 |
+
|
| 278 |
+
https://www.linkedin.com/in/patrick-j-jensen-564946b4
|
| 279 |
+
https://www.twitter.com/AccessOtc
|
| 280 |
+
https://www.twitter.com/AccrOtc
|
| 281 |
+
https://brokercheck.finra.org/individual/summary/1952963
|
| 282 |
+
|
| 283 |
+
OUR CORPORATE WEBSITEs ARE:
|
| 284 |
+
|
| 285 |
+
http://www.myaccess-power.com
|
| 286 |
+
http://www.clonesbydrones.com
|
| 287 |
+
http://www.mycbdpets.com
|
| 288 |
+
http://www.nyumarijuana.com
|
| 289 |
+
|
| 290 |
+
Access-Power, Inc.'s contact is pjensen@myaccess-power.com.
|
| 291 |
+
|
| 292 |
+
We applied for a Trademark for Clones By Drones TM. The service
|
| 293 |
+
will be very futuristic, and will be the first of its kind.
|
| 294 |
+
|
| 295 |
+
Access-Power, Inc. is in good standing and has a very good relationship
|
| 296 |
+
with our Transfer Agent, Standard Transfer & Co.
|
| 297 |
+
Our Transfer Agent is SEC registered, and except for one
|
| 298 |
+
change in ownership in 2007, is our original transfer agent dating
|
| 299 |
+
back to 1996. Our transfer agent is SEC registered and has a profile at:
|
| 300 |
+
|
| 301 |
+
https://www.sec.gov/divisions/marketreg/mrtransfer.shtml
|
| 302 |
+
https://www.otcmarkets.com/learn/service-providers/2433?t=6
|
| 303 |
+
|
| 304 |
+
https://standardtransferco.com
|
| 305 |
+
|
| 306 |
+
Standard Transfer & Co.
|
| 307 |
+
440 East 400 South Suite 200, Salt Lake City, Utah 84111
|
| 308 |
+
Phone (801) 571-8844 Fax: (801) 328-4058
|
| 309 |
+
|
| 310 |
+
Our ticker symbol ACCR and further information may be found at:
|
| 311 |
+
|
| 312 |
+
https://www.sec.gov/cgi-bin/browse-edgar?CIK=accr
|
| 313 |
+
|
| 314 |
+
State the aggregate market value of the voting stock held by
|
| 315 |
+
non-affiliates
|
| 316 |
+
computed by reference to the price at which the stock was sold,
|
| 317 |
+
or the average
|
| 318 |
+
bid and asked prices of such stock as of a specified date
|
| 319 |
+
within the past 60
|
| 320 |
+
days: $1,130,156.21
|
| 321 |
+
|
| 322 |
+
At January 1st 2020, there were issued and outstanding 244,144,121
|
| 323 |
+
shares of Common Stock, and Patrick J. Jensen owns 131,128,500 shares
|
| 324 |
+
or 53.7090
|
| 325 |
+
percent of all the Common Stock. I am willing to give up Control and
|
| 326 |
+
a
|
| 327 |
+
percentage of the Company, for an Equity Partner in our Company.
|
| 328 |
+
All
|
| 329 |
+
131,128,500 are currently restricted and held in book entry form at the
|
| 330 |
+
Transfer Agent. I am actively looking for a MERGER DEAL.
|
| 331 |
+
|
| 332 |
+
There are currently a total of 98,244,146 shares registered in our
|
| 333 |
+
float.
|
| 334 |
+
Management continues to believe that there is a massive short position
|
| 335 |
+
in our Company stock that was accumulated from 1999 to 2008, and we
|
| 336 |
+
estimate
|
| 337 |
+
this short position to be massive. As Director of this Company, I
|
| 338 |
+
want to
|
| 339 |
+
apologize to my Shareholders for previously stating that our estimated
|
| 340 |
+
float
|
| 341 |
+
was between 5,000,000 and 10,000,000. Although I firmly believe the
|
| 342 |
+
float
|
| 343 |
+
in our Company stock is substantially lower than the official
|
| 344 |
+
98,244,146 shares
|
| 345 |
+
reported to OTC Markets.
|
| 346 |
+
|
| 347 |
+
We care about our Shareholders dearly, and our top priority is investor
|
| 348 |
+
protection. There is NO DILUTION IN THE COMMON STOCK OF ACCR.
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
PART I
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
ITEM 1. DESCRIPTION OF BUSINESS
|
| 355 |
+
|
| 356 |
+
Access-Power, Inc, is a for profit business looking for a
|
| 357 |
+
MERGER CANDIDATE.
|
| 358 |
+
We currently pay bills through a Work at Home business. Our monthly
|
| 359 |
+
fixed expenses are previosuly documented in an 8K at $2,350.00. We
|
| 360 |
+
operate a part time work at home business. The company subcontracts
|
| 361 |
+
through a big call center, and provides sales and customer service
|
| 362 |
+
to big Fortune 500 companies. In 2019, we operated via Hunter Vunter,
|
| 363 |
+
our subsidiary through www.liveops.com. We ended the relationship
|
| 364 |
+
with LiveOps on May 1st 2019. As a result, Hunter Venture was
|
| 365 |
+
officially dissolved.
|
| 366 |
+
|
| 367 |
+
The Company struggled from May 1, 2019 to October 18, 2019. We had no
|
| 368 |
+
income during this period, and our operational expenses were paid for
|
| 369 |
+
by myself, Patrick J. Jensen as a donation to the Company. I personally
|
| 370 |
+
paid out of my own pocket all the expenses during this dark time.
|
| 371 |
+
|
| 372 |
+
On October 2, 2019---> I dreamed of getting off the greys. ACCR is a
|
| 373 |
+
DREAM right now...This is how builders work.
|
| 374 |
+
|
| 375 |
+
On October 18, 2019, we acquired Grand Haven MM LLC, and this entity
|
| 376 |
+
is our subsidiary now. This is an operating entity very similar
|
| 377 |
+
to Hunter Venture. The entity is registered in the State of Michigan
|
| 378 |
+
here:
|
| 379 |
+
|
| 380 |
+
https://cofs.lara.state.mi.us/CorpWeb/CorpSearch/CorpSummary.aspx?
|
| 381 |
+
ID=802220565
|
| 382 |
+
|
| 383 |
+
Grand Haven MM now operates a part time work at home program through
|
| 384 |
+
another national call center very similar and much bigger in size
|
| 385 |
+
than our previour contracter. We provide work at home sales and
|
| 386 |
+
customer services subcontracted on a part time basis. I am a builder.
|
| 387 |
+
I am building a Clone Delivery Service for Marijuana Plants .... in
|
| 388 |
+
West Michigan.
|
| 389 |
+
|
| 390 |
+
The Company also operates 2 eCommerce websites at the present time.
|
| 391 |
+
|
| 392 |
+
http://www.clonesbydrones.com
|
| 393 |
+
http://www.mycbdpets.com
|
| 394 |
+
|
| 395 |
+
We continue to strive to build up our revenues. We want to succeed
|
| 396 |
+
and we will comeback to a higher reporting standard. On October 18,
|
| 397 |
+
2019 we were upgraded in trading tier from the dark grey market
|
| 398 |
+
to the PINK NO INFORMATION market. We have no intention of going
|
| 399 |
+
back to the grey market, as the Company currently trades on an
|
| 400 |
+
"unsolicited basis" in the PINK NO INFORMATION tier at OTC
|
| 401 |
+
Markets. There are so many unknowns, however I have a visi
|
test_data/financial_real/0001041588_0001041588-21-000001.txt
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
10-K 1 accr12312010k.txt January 4th, 2021
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
This form is filled out within all the SPIRITs of the SEC Laws of '33 and '34.
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
ACCESS-POWER & CO., INC.
|
| 13 |
+
OTC TICKER: ACCR
|
| 14 |
+
We were violent victims of toxic death spiral convertible "floorless"
|
| 15 |
+
debentures from 1998 to 2008, and then through March 2012.
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
PERIOD and DOCUMENT: 10KSB for the FULL YEAR 12/31/2020
|
| 19 |
+
COMPANY CONFIRMED NAME: ACCESS-POWER & Co., INC.
|
| 20 |
+
CENTRAL INDEX KEY: 0001041588
|
| 21 |
+
FORM TYPE: 10KSB
|
| 22 |
+
|
| 23 |
+
SEC FILE NUMBER: 333-65069
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
BUSINESS ADDRESS:
|
| 31 |
+
STREET 1: PO BOX 598
|
| 32 |
+
|
| 33 |
+
CITY: GRAND HAVEN
|
| 34 |
+
STATE: MI
|
| 35 |
+
ZIP: 49417
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
Respectfully submitted to the,
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
UNITED STATES SECURITIES AND EXCHANGE COMMISSION
|
| 49 |
+
WASHINGTON, D.C. 20549
|
| 50 |
+
The Division of Corporation Finance, Section 11
|
| 51 |
+
100 F Street NE, Washington, D.C. 20549
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
FORM 10-K
|
| 59 |
+
|
| 60 |
+
/X/ ANNUAL REPORT PURSUANT TO SECTION 13 OR 15(d) OF THE
|
| 61 |
+
SECURITIES EXCHANGE ACT OF 1934 FOR THE FISCAL YEAR ENDED
|
| 62 |
+
DECEMBER 31, 2020.
|
| 63 |
+
|
| 64 |
+
TRANSITION REPORT PURSUANT TO SECTION 13 OR 15(d)
|
| 65 |
+
OF THE SECURITIES EXCHANGE ACT OF 1934
|
| 66 |
+
FOR THE TRANSITION PERIOD FROM ______ TO _____
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
COMMISSION FILE NUMBER: 333-65069
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
ACCESS-POWER & CO., INC.
|
| 73 |
+
[Exact name of Registrant as
|
| 74 |
+
specified in its charter]
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
*** FLORIDA ***
|
| 78 |
+
|
| 79 |
+
Domicile State or other jurisdiction of
|
| 80 |
+
incorporation or organization
|
| 81 |
+
http://www.sunbiz.org
|
| 82 |
+
|
| 83 |
+
*** MICHIGAN ***
|
| 84 |
+
|
| 85 |
+
State of Operations
|
| 86 |
+
https://cofs.lara.state.mi.us/SearchApi/Search/Search
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
59-3420985
|
| 91 |
+
(I.R.S. Employer
|
| 92 |
+
Identification No.)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
PO BOX 598
|
| 96 |
+
GRAND HAVEN, MI 49417
|
| 97 |
+
616-312-5390
|
| 98 |
+
Email: pjensen@myaccess-power.com
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
(Address of principal executive offices)
|
| 102 |
+
|
| 103 |
+
REGISTRANTS TELEPHONE NUMBER, INCLUDING AREA CODE: 616-312-5390
|
| 104 |
+
|
| 105 |
+
SECURITIES REGISTERED PURSUANT TO SECTION 12(b) OF THE ACT: NONE
|
| 106 |
+
|
| 107 |
+
SECURITIES REGISTERED PURSUANT TO SECTION 12(g) OF THE ACT:
|
| 108 |
+
COMMON STOCK, $0.001 PAR VALUE - 300,000,000 shares authorized fully diluted.
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
Indicate by check mark if the registrant is a well-known
|
| 114 |
+
seasoned issuer, as defined in Rule 405 of the Securities
|
| 115 |
+
Act. Yes No X
|
| 116 |
+
|
| 117 |
+
Indicate by check mark if the registrant is not required to file
|
| 118 |
+
reports pursuant to Section 13 or Section 15(d) of the Act.
|
| 119 |
+
Yes No X
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
Indicate by check mark whether the registrant (1) has
|
| 123 |
+
filed all reports required to be filed be Section 13 or 15(d)
|
| 124 |
+
of the Securities Exchange Act of 1934 during the preceding 12
|
| 125 |
+
months (or for such shorter period that the registrant was
|
| 126 |
+
required to file such reports), and (2) has been subject
|
| 127 |
+
to such filing requirements for the past 90 days. Yes X No
|
| 128 |
+
|
| 129 |
+
Indicate by check mark whether the registrant has submitted
|
| 130 |
+
electronically and posted on its corporate Web site, if any,
|
| 131 |
+
every Interactive Data File required to be submitted and
|
| 132 |
+
posted pursuant to Rule 405 of Regulation S-T (232.405 of
|
| 133 |
+
this chapter) during the preceding 12 months (or for such
|
| 134 |
+
shorter period that the registrant was required to submit
|
| 135 |
+
and post such files). Yes X No
|
| 136 |
+
|
| 137 |
+
Indicate by check mark if disclosure of delinquent filers
|
| 138 |
+
pursuant to Item 405 of Regulation S-K (Section 229.405
|
| 139 |
+
of this chapter) is not contained herein, and will not be
|
| 140 |
+
contained, to the best of registrants knowledge, in definitive
|
| 141 |
+
proxy or information statements incorporated by reference in
|
| 142 |
+
Part III of this Form 10-K or any amendment to this Form 10-K.
|
| 143 |
+
|
| 144 |
+
Indicate by check mark whether the registrant is a large a
|
| 145 |
+
ccelerated filer, an accelerated filer, a non-accelerated
|
| 146 |
+
filer, smaller reporting company, or an emerging growth
|
| 147 |
+
company. See the definitions of large accelerated filer,
|
| 148 |
+
accelerated filer, smaller reporting company, and
|
| 149 |
+
emerging growth company in Rule 12b-2 of the Exchange Act.
|
| 150 |
+
|
| 151 |
+
Large accelerated filer NO Accelerated filer NO
|
| 152 |
+
Non-accelerated filer NO (Do not check if a smaller
|
| 153 |
+
reporting company)
|
| 154 |
+
|
| 155 |
+
Smaller reporting company YES
|
| 156 |
+
Emerging growth company YES
|
| 157 |
+
|
| 158 |
+
If an emerging growth company, indicate by check mark if
|
| 159 |
+
the registrant has elected not to use the extended
|
| 160 |
+
transition period for complying with any new or revised
|
| 161 |
+
financial accounting standards provided pursuant to
|
| 162 |
+
ection 13(a) of the Exchange Act.
|
| 163 |
+
|
| 164 |
+
Indicate by check mark whether the registrant is a shell
|
| 165 |
+
company (as defined in Rule 12b-2 of the Exchange Act).
|
| 166 |
+
Yes No X
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
ALL COMMON SHARES REFLECT A FULLY DILUTED BASIS.
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
State the aggregate market value of the voting stock held by
|
| 174 |
+
non-affiliates computed by reference to the price at which the stock was
|
| 175 |
+
sold, or the average bid and asked prices of such stock as of a
|
| 176 |
+
specified date within the past 60 days:
|
| 177 |
+
|
| 178 |
+
PATRICK restricted shares: 186 ,984,379 shares
|
| 179 |
+
PERCENT OF SHARES HELD BY NON AFFILIATES= 37.6719%
|
| 180 |
+
SHARES HELD BY EVERYONE ELSE : 113,015,621 and of this amount,
|
| 181 |
+
we certify a DTCC FLOAT SINCE MARCH 1, 2012: 98,376,146 common shares
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
The aggregate market value of the voting common stock held by non-affils.
|
| 186 |
+
of the registrant (assuming officers and directors are affiliates) was
|
| 187 |
+
approximately $ 395,554.67 as of 12/31/2020, computed on the fully diluted
|
| 188 |
+
basis of and the closing price on such date of $.0035, and so...
|
| 189 |
+
|
| 190 |
+
113,015,621 held by non-affiliates + 131,128,500 shares acquired by our
|
| 191 |
+
Director plus 55,855,879 issued to our Director in 2020 = 300,000,000
|
| 192 |
+
common shares verified by our SEC Registrered same Transfer Agent
|
| 193 |
+
dating back to May 1997. Our Director's FORM 4 filings are found here:
|
| 194 |
+
|
| 195 |
+
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=0001557361
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
As of December 31st, 2020, there were 300,000,000 shares of the registrant's
|
| 199 |
+
Common Stock outstanding. PATRICK J JENSEN, Director of Access-Power & CO.,
|
| 200 |
+
Inc. has a total of 186,984,379 Restricted Shares of these shares.
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
THE TOTAL MARKET CAPITALIZATION AS OF THE CLOSE OF BUSINESS
|
| 206 |
+
January 4th , 2021 is $1,050,000.00 BASED ON A CLOSING PRICE OF
|
| 207 |
+
$.0035 PER SHARE on 12/31/2020, and total outstanding issued
|
| 208 |
+
common stock equalling 300,000,000 shares.
|
| 209 |
+
|
| 210 |
+
At January 4th , 2021, there were issued and outstanding 300,000,000
|
| 211 |
+
shares of Common Stock fully diluted, and Patrick J. Jensen owns
|
| 212 |
+
186,984,379 shares or 62.3281 percent of all the Common Stock.
|
| 213 |
+
|
| 214 |
+
In 2020, the Company during the course of 2020 experienced a market
|
| 215 |
+
capitalization HIGH of over $3,000,000.00.
|
| 216 |
+
|
| 217 |
+
All 186,984,379 are currently all restricted and held in book entry form at our
|
| 218 |
+
Transfer Agent.
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
THE SEC COMMISSION CONFIRMED OUR FORM RW ON 12/21/2020.
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
______________________________________________________________
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
My Dear Shareholders, OTC Markets Directors, colleagues at FINRA, and to the
|
| 236 |
+
Esteemed Commissioners of the SEC, and of course, to my Chiefs at Section 11
|
| 237 |
+
at the SEC,
|
| 238 |
+
|
| 239 |
+
CURRENT UPDATE...as of January 4th, 2021.
|
| 240 |
+
|
| 241 |
+
Our Director is called many names on message boards...and today, in 2021 we
|
| 242 |
+
bow our heads to all decision makers at OTC Markets in our quest to
|
| 243 |
+
re-apply to the PINK CURRENT INFO tier status.
|
| 244 |
+
This is a reapplication to occur on January 24th, 2021.
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
*****WE ONLY HAVE COMMON STOCK IN OUR CAPITAL STRUCTURE****
|
| 248 |
+
|
| 249 |
+
*****WE DO NOT HAVE CONVERTIBLE NOTES or WARRANTS*****
|
| 250 |
+
|
| 251 |
+
****WE HAVE ZERO LONG TERM DEBT********
|
| 252 |
+
|
| 253 |
+
*****ACCR DOES NOT BELIEVE IN PROMOTION*********
|
| 254 |
+
|
| 255 |
+
It is very expensive to hire a PCAOB registered firm/accountant/attorneys
|
| 256 |
+
for our
|
| 257 |
+
micro-cap operations. However, we will succeed in capturing all of the
|
| 258 |
+
requirements
|
| 259 |
+
of all the SEC Rules of '33 and '34, as best as we can.
|
| 260 |
+
|
| 261 |
+
We are fully aware of the FINAL RULE imposed by the SEC, by September
|
| 262 |
+
21st, 2021 to require movement of all PINK NO INFORMATION
|
| 263 |
+
tier companies to the grey market today called the EXPERT MARKET.
|
| 264 |
+
|
| 265 |
+
We are pleading with all the Directors at OTC Markets to please be fair to our
|
| 266 |
+
Company. We will re-apply with OTC Markets on January 24, 2021 to become
|
| 267 |
+
PINK CURRENT INFORMATION tier again, as we once were in 2008.
|
| 268 |
+
The only reason why there is a RED STOP SIGN is because of a discussed
|
| 269 |
+
CRIMINAL FORM 15 filed by our previous inept managers in 2007.
|
| 270 |
+
We charge our past managers naked short sold our
|
| 271 |
+
shares into the ground. Their intent was to carve all the equity
|
| 272 |
+
off the bone, and then to file the CRIMINAL FORM 15 in 2007
|
| 273 |
+
as their criminal strategy.
|
| 274 |
+
|
| 275 |
+
We provide full disclosure to our
|
| 276 |
+
Market Makers, and to our investing community.
|
| 277 |
+
|
| 278 |
+
We have the Spirit of a Warrior, and our Corporation has done nothing wrong.
|
| 279 |
+
All of the Company press releases can be found here:
|
| 280 |
+
|
| 281 |
+
http://www.globenewswire.com
|
| 282 |
+
|
| 283 |
+
We have learned that in 2021, all OTC Markets PINK CURRENT
|
| 284 |
+
INFORMATION
|
| 285 |
+
tier Companies will have at chance at Solicited Quotes for the first time in
|
| 286 |
+
over 12.5
|
| 287 |
+
years. We will not be needing to obtain a FORM 211 with FINRA. We
|
| 288 |
+
have received
|
| 289 |
+
news that in 2021, only selected PINK CURRENT INFORMATION tier
|
| 290 |
+
companies
|
| 291 |
+
will receive a shot at SOLICITED QUOTES by a market maker.
|
| 292 |
+
|
| 293 |
+
ACCR believes to have an excellent relationship with our primary Market
|
| 294 |
+
Maker over
|
| 295 |
+
the years. Our primary market maker signed the UNSOLICITED
|
| 296 |
+
QUOTE FORM
|
| 297 |
+
with OTC Markets pre-October 18, 2019.
|
| 298 |
+
|
| 299 |
+
We are loyal to our Shareholder base.
|
| 300 |
+
|
| 301 |
+
Our story...from Inception through March 1, 2012, the Company was
|
| 302 |
+
a violent
|
| 303 |
+
victim of a terrible financial crime. The criminals behind this scheme
|
| 304 |
+
to defraud
|
| 305 |
+
investors was planned very methodically.
|
| 306 |
+
|
| 307 |
+
We have many shareholders placed in restrictive shares...none of
|
| 308 |
+
which have
|
| 309 |
+
SURFACED in do a 144a sale in over 2 decades. Why is this?
|
| 310 |
+
Management believes that all 100 share lot's of restrictive shares were
|
| 311 |
+
issued
|
| 312 |
+
in fraud, and to fictitious people, or Shareholders - totaling over
|
| 313 |
+
15,000,000
|
| 314 |
+
shares in never to be claimed restrictive shares.
|
| 315 |
+
It was pre-meditated and
|
| 316 |
+
very detail in CRIME.
|
| 317 |
+
|
| 318 |
+
The criminals of the past perpetraded a MASTERMIND financial
|
| 319 |
+
crime against
|
| 320 |
+
our Shareholder base. We seek retribution from our past fraud managers.
|
| 321 |
+
We
|
| 322 |
+
recently learned that one of our past fraud managers lives in Nashville
|
| 323 |
+
area of
|
| 324 |
+
TN in a $5.2 million dollar house in September 2020. We charge
|
| 325 |
+
he used SEC
|
| 326 |
+
filings to commit financial crimes against our Shareholders. We
|
| 327 |
+
nearly have
|
| 328 |
+
15,000,000 shares in RESTRICTED STATUS 100 share lots's....
|
| 329 |
+
the previous
|
| 330 |
+
criminal masterminds.
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
WE SEEK RETROBUTION FOR OUR SHAREHOLDERS
|
| 334 |
+
|
| 335 |
+
We charge this man used SEC filings going back to 2002 to commit financial
|
| 336 |
+
crimes
|
| 337 |
+
against our Shareholders. Today, we are the cleanest OTC Companies
|
| 338 |
+
around.
|
| 339 |
+
|
| 340 |
+
The Company made 2 FINRA DAILY lists on 5/6/2020 and on 9/6/2018.
|
| 341 |
+
|
| 342 |
+
The Company has had discussions with the SEC regarding a FRAUD
|
| 343 |
+
FORM 15 that was filed by previous management in 2007. We believe
|
| 344 |
+
that we have a fiduciary responsibility to report transparent
|
| 345 |
+
information to our Shareholders.
|
| 346 |
+
|
| 347 |
+
https://www.sec.gov/Archives/edgar/data/1041588/
|
| 348 |
+
000117347307000043/0001173473-07-000043-index.htm
|
| 349 |
+
|
| 350 |
+
We were victims of toxic death spiral debentures from 1998 to
|
| 351 |
+
2008 and through March 2012.
|
| 352 |
+
|
| 353 |
+
PLEASE FORGIVE OUR PAST.
|
| 354 |
+
|
| 355 |
+
ACCR as a structure TODAY is very sound, because I am in charge.
|
| 356 |
+
ACCR has life only today because of ME.
|
| 357 |
+
|
| 358 |
+
The past is the past, and
|
| 359 |
+
we wish to move forward with a plan to become fully
|
| 360 |
+
current with the SEC in 2021. We are trying the best we can within the
|
| 361 |
+
Spirit of the Law. We will be refiling our FORM 10 very soon...very
|
| 362 |
+
soon.
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
On October 18, 2019, our Company was in the FIRST GROUP of
|
| 366 |
+
Equities to bust
|
| 367 |
+
out of the death grey market, where we stayed there for 11 years
|
| 368 |
+
after our
|
| 369 |
+
criminal FORM 15 was filed by previous criminal management.
|
| 370 |
+
|
| 371 |
+
Let's move forward, and for get the PAST!
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
Prior to October 18, 2019...we had DEATH....today, we have LIFE, and
|
| 376 |
+
we have a
|
| 377 |
+
direction on how to fix this trajedy. Today we have visible Level 2
|
| 378 |
+
QUOTES. We
|
| 379 |
+
have already spent 11 years in the DEATH DARK GREY MARKET.
|
| 380 |
+
|
| 381 |
+
Please,,,why should we go back? We will NOT go back, ever.
|
| 382 |
+
|
| 383 |
+
TODAY, we have LIFE.
|
| 384 |
+
|
| 385 |
+
Our primary Market Maker signed a new form created with OTC
|
| 386 |
+
Markets in 2019.
|
| 387 |
+
This form is located here...we believe to have a wonderful relationship
|
| 388 |
+
with our
|
| 389 |
+
PRIMARY MARKET MAKER.
|
| 390 |
+
|
| 391 |
+
https://www.otcmarkets.com/files/unsolicited-quote-form-1514935916031.pdf
|
| 392 |
+
|
| 393 |
+
We jumped from the deathly grey market to the PINK NO INFORMATION
|
| 394 |
+
tier of
|
| 395 |
+
OTC Markets on this infamous date in our history, October 18, 2019.
|
| 396 |
+
We have lived
|
| 397 |
+
in this tier with NO DILUTION for over a year with no harm to our
|
| 398 |
+
Shareholders.
|
| 399 |
+
|
| 400 |
+
WE LOVE OUR SHAREHOLDERS, and believe in RAISING
|
| 401 |
+
SHAREHOLDER
|
| 402 |
+
VALUE. WE VALUE OUR COMMON STOCKHOLDERS.
|
| 403 |
+
|
| 404 |
+
ACCR received a denial to go PINK CURRENT TIER on July 23, 2020.
|
| 405 |
+
Why? We
|
| 406 |
+
submitted every form in a satisfactory manner. We CLASHED with our prior
|
| 407 |
+
accountant whom shall remain nameless. We clashed hard. Why were we
|
| 408 |
+
denied
|
| 409 |
+
to go PINK CURRENT is a mystery to us. Maybe our prior attorney said bad
|
| 410 |
+
things
|
| 411 |
+
about our Company to OTC Markets, I do not know why. He threatened to
|
| 412 |
+
call OTC
|
| 413 |
+
MARKETS, and call me a liar, when he was the liar.
|
| 414 |
+
|
| 415 |
+
We do not know. We've been told that OTC Markets did not like our
|
| 416 |
+
Company 8K's
|
| 417 |
+
nor our Company story. ACCR has contacted OTC Markets, and on
|
| 418 |
+
July 23,
|
| 419 |
+
2020 we received the following reply:
|
| 420 |
+
|
| 421 |
+
On July 23, 2020 3:30 PM Liz Heese wrote:
|
| 422 |
+
|
| 423 |
+
Mr. Jensen,
|
| 424 |
+
|
| 425 |
+
OTC Markets has reached a final determination to deny your application
|
| 426 |
+
for access
|
| 427 |
+
to the OTC Disclosure & News Service. Our determination is due to
|
| 428 |
+
public interest
|
| 429 |
+
concerns associated with the company and its disclosure, including but
|
| 430 |
+
not limited to
|
| 431 |
+
issues previously communicated to you.
|
| 432 |
+
|
| 433 |
+
We consider this matter to be closed and will not engage in further discussion
|
| 434 |
+
at this
|
| 435 |
+
time. As stated in the email from Nancy Rodriguez, you may reapply
|
| 436 |
+
for the service
|
| 437 |
+
in 6 months.
|
| 438 |
+
|
| 439 |
+
Liz Heese
|
| 440 |
+
EVP, Issuer & Information Services
|
| 441 |
+
OTC Markets Group"
|
| 442 |
+
|
| 443 |
+
THEREFORE, ACCR is going to re-apply to become PINK CURRENT
|
| 444 |
+
INFORMATION TIER on January 24, 2021.
|
| 445 |
+
|
| 446 |
+
ACCR as a Corporation is perfectly
|
| 447 |
+
clean, and has done nothing wrong. We've just had bad management.
|
| 448 |
+
All the
|
| 449 |
+
Amendments and Articles of Incorp all check out at:
|
| 450 |
+
|
| 451 |
+
https://www.sunbiz.org
|
| 452 |
+
|
| 453 |
+
ACCR has done nothing wrong. ACCR will get SOLICITED QUOTES
|
| 454 |
+
again in 2021.
|
| 455 |
+
|
| 456 |
+
CURRENT MANAGEMENT WILL STEP DOWN TO CLEAR A PATHWAY
|
| 457 |
+
FOR OTHERS
|
| 458 |
+
|
| 459 |
+
Let's just forget this past CRIMINAL FORM 15, that according to the SEC
|
| 460 |
+
cannot
|
| 461 |
+
be changed, and our only option is today to re-file our FORM 10 properly
|
| 462 |
+
with the help of counsel, and we will.
|
| 463 |
+
|
| 464 |
+
https://www.sec.gov/Archives/edgar/data/1041588/
|
| 465 |
+
000117347307000043/0001173473-07-000043-index.htm
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
Again, as of the close of business December 31st, 2020 --------> our common
|
| 469 |
+
stock
|
| 470 |
+
structure is as follows:
|
| 471 |
+
|
| 472 |
+
Authorized Common Stock: 300,000,000 shares
|
| 473 |
+
Outstanding Common Stock: 300,000,000 shares
|
| 474 |
+
Estimated Float: Substanstially less than 98,244,146 shares
|
| 475 |
+
Restricted Common Stock: 201,625,854 outstanding
|
| 476 |
+
PATRICK: 186,984,379 ALL RESTRICTED SHARES or 62.3281 % of the
|
| 477 |
+
TOTAL
|
| 478 |
+
AMOUNT OF STOCK ON A FULLY DILUTED SHARES - PERIOD!
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
OUR CORPORATE WEBSITEs IS:
|
| 483 |
+
|
| 484 |
+
https://www.myaccess-power.com
|
| 485 |
+
|
| 486 |
+
Access-Power & Co., Inc.'s contact is:
|
| 487 |
+
PO BOX 598
|
| 488 |
+
Grand Haven, MI 49417
|
| 489 |
+
pjensen@myaccess-power.com
|
| 490 |
+
(616)312-5390 TEL.
|
| 491 |
+
https://www.sec.gov/divisions/corpfin/faqs/soxact2002.htm
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
We applied for a Trademark for Clones By Drones TM, and also
|
| 495 |
+
Clones by Cars TM in 2020. The service will be very futuristic, and will be
|
| 496 |
+
the
|
| 497 |
+
first of its kind. We applied for a federal patent for our Clobes by Drones
|
| 498 |
+
futuristic
|
| 499 |
+
home curbside delivery service.
|
| 500 |
+
|
| 501 |
+
WE ARE WORKING ON A REVERSE MERGER...we plan to audit the
|
| 502 |
+
incoming
|
| 503 |
+
private company that wishes to go public in a r
|
test_data/financial_real/0001386570_0001654954-20-005725.txt
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
10-K/A 1 cdxc10ka_12312019.htm FORM 10-K/A cdxc10ka_12312019 UNITED
|
| 2 |
+
STATES SECURITIES AND EXCHANGE COMMISSION Washington,
|
| 3 |
+
D.C. 20549 FORM
|
| 4 |
+
10-K/A (Amendment
|
| 5 |
+
No. 1) [X]
|
| 6 |
+
ANNUAL REPORT PURSUANT TO SECTION 13 OR 15(d) OF THE
|
| 7 |
+
SECURITIES EXCHANGE ACT OF 1934. For
|
| 8 |
+
the fiscal year ended December 31, 2019 or [
|
| 9 |
+
] TRANSITION REPORT PURSUANT TO SECTION 13 OR 15(d) OF THE
|
| 10 |
+
SECURITIES EXCHANGE ACT OF 1934. For
|
| 11 |
+
the transition period from to Commission file number 001-37752 CHROMADEX
|
| 12 |
+
CORPORATION (Exact
|
| 13 |
+
name of Registrant as specified in its Charter) Delaware 26-2940963 (State or other jurisdiction of
|
| 14 |
+
incorporation) (I.R.S. Employer
|
| 15 |
+
Identification No.) 10900 Wilshire Blvd. Suite 600, Los
|
| 16 |
+
Angeles, California 90024 (Address of
|
| 17 |
+
Principal Executive Offices) (Zip
|
| 18 |
+
Code) Registrant's
|
| 19 |
+
telephone number, including area code (310) 388-6706 Securities
|
| 20 |
+
registered pursuant to Section 12(b) of the Act: Title of each class Trading Symbol Name of each exchange on which registered Common Stock, $0.001 par value per share CDXC The Nasdaq Capital Market Securities
|
| 21 |
+
registered pursuant to Section 12(g) of the Act: None. Indicate by check
|
| 22 |
+
mark if the registrant is a well-known seasoned issuer, as defined
|
| 23 |
+
in Rule 405 of the Securities Act. Yes [ ] No [X
|
| 24 |
+
] Indicate by check
|
| 25 |
+
mark if the registrant is not required to file reports pursuant to
|
| 26 |
+
Section 13 or Section 15(d) of the Act. Yes [ ] No
|
| 27 |
+
[X] Indicate
|
| 28 |
+
by check mark whether the registrant (1) has filed all reports
|
| 29 |
+
required to be filed by Section 13 or 15(d) of the Securities
|
| 30 |
+
Exchange Act of 1934 during the preceding 12 months (or for
|
| 31 |
+
such shorter period that the Registrant was required to file such
|
| 32 |
+
reports), and (2) has been subject to such filing requirements for
|
| 33 |
+
the past 90 days. Yes [X] No [ ] Indicate
|
| 34 |
+
by check mark whether the registrant has submitted electronically
|
| 35 |
+
every Interactive Data File required to be submitted pursuant to
|
| 36 |
+
Rule 405 of Regulation S-T during the preceding 12 months (or for
|
| 37 |
+
such shorter period that the registrant was required to submit such
|
| 38 |
+
files). Yes [X] No [ ] Indicate
|
| 39 |
+
by check mark whether the registrant is a large accelerated filer,
|
| 40 |
+
an accelerated filer, a non-accelerated filer, or a smaller
|
| 41 |
+
reporting company. See the definitions of “accelerated
|
| 42 |
+
filer,” “large accelerated filer,” and
|
| 43 |
+
“smaller reporting company” in Rule 12b-2 of the
|
| 44 |
+
Exchange Act. Large
|
| 45 |
+
accelerated filer [ ]
|
| 46 |
+
Accelerated filer
|
| 47 |
+
[X]
|
| 48 |
+
Non-accelerated filer [
|
| 49 |
+
] Smaller
|
| 50 |
+
reporting company
|
| 51 |
+
[X]
|
| 52 |
+
Emerging growth company [ ] If an
|
| 53 |
+
emerging growth company, indicate by check mark if the registrant
|
| 54 |
+
has elected not to use the extended transition period for complying
|
| 55 |
+
with any new or revised financing accounting standards provided
|
| 56 |
+
pursuant to Section 13(a) of the Exchange Act. ☐ Indicate by check
|
| 57 |
+
mark whether the registrant is a shell company (as defined in Rule
|
| 58 |
+
12b-2 of the Act). Yes [ ] No [X] As of
|
| 59 |
+
June 30, 2019, the last business day of the registrant’s most
|
| 60 |
+
recently completed second fiscal quarter, the aggregate market
|
| 61 |
+
value of the registrant’s common stock held by non-affiliates
|
| 62 |
+
of the registrant was approximately $190.4 million, based on the
|
| 63 |
+
closing price of the registrant’s common stock on the NASDAQ
|
| 64 |
+
Capital Market on June 30, 2019. Number
|
| 65 |
+
of shares of common stock of the registrant outstanding as of May
|
| 66 |
+
17, 2020: 61,477,895. DOCUMENTS INCORPORATED BY REFERENCE None. TA B LE OF CONTENTS Item PART I 1A. Risk
|
| 67 |
+
Factors 1 PART II 9A Controls and
|
| 68 |
+
Procedures 2 PART IV 15. Exhibits, Financial
|
| 69 |
+
Statement Schedules 6 Table of Contents EXPLANATORY NOTE ChromaDex
|
| 70 |
+
Corporation (“ChromaDex,” the “Company,”
|
| 71 |
+
“we,” “our” or “us”) is filing
|
| 72 |
+
this Amendment No. 1 on Form 10-K/A (this “Amendment”)
|
| 73 |
+
to amend our Annual Report on Form 10-K for the year ended December
|
| 74 |
+
31, 2019 (the “Form 10-K”), originally filed with the
|
| 75 |
+
U.S. Securities and Exchange Commission (the “SEC”) on
|
| 76 |
+
March 10, 2020. As a result of a material weakness in internal
|
| 77 |
+
control over financial reporting that existed as of December 31,
|
| 78 |
+
2019 that was not identified until the Company was preparing its
|
| 79 |
+
Quarterly Report on Form 10-Q for the quarterly period ended March
|
| 80 |
+
31, 2020, the Company is amending Part II, Item 9A “Controls
|
| 81 |
+
and Procedures” with respect to (a) the Company’s
|
| 82 |
+
conclusions regarding the effectiveness of (i) the Company’s
|
| 83 |
+
disclosure controls and procedures and (ii) its internal control
|
| 84 |
+
over financial reporting, and (b) Marcum LLP’s related
|
| 85 |
+
attestation report on the Company’s internal control over
|
| 86 |
+
financial reporting. In addition, the Company is revising Item 1A.
|
| 87 |
+
“Risk Factors” to add a risk factor regarding this
|
| 88 |
+
material weakness in internal control over financial reporting. The
|
| 89 |
+
material weakness in internal controls over financial reporting is
|
| 90 |
+
also reported in our Quarterly Report on Form 10-Q for the
|
| 91 |
+
quarterly period ended March 31, 2020. In
|
| 92 |
+
accordance with Rule 12b-15 under the Securities Exchange Act of
|
| 93 |
+
1934, as amended (the “Exchange Act”), the Company is
|
| 94 |
+
including with this Amendment currently dated certifications as
|
| 95 |
+
Exhibits 31.1, 31.2, and 32.1. We are also filing an updated
|
| 96 |
+
Consent of Independent Registered Public Accounting Firm.
|
| 97 |
+
Accordingly, Part IV, Item 15, “Exhibits and Financial
|
| 98 |
+
Statement Schedules” is amended to include the currently
|
| 99 |
+
dated certifications and consent as exhibits. Except
|
| 100 |
+
as described in this Explanatory Note, this Amendment does not
|
| 101 |
+
reflect events occurring after the date of the filing of the Form
|
| 102 |
+
10-K, nor does it amend, modify or otherwise update any other
|
| 103 |
+
information in the Form 10-K. Among other things, forward-looking
|
| 104 |
+
statements made in the Form 10-K have not been revised to reflect
|
| 105 |
+
events that occurred or facts that became known to us after filing
|
| 106 |
+
of the Form 10-K, and any such forward-looking statements should be
|
| 107 |
+
read in their historical context. Accordingly, this Amendment
|
| 108 |
+
should be read in conjunction with the Form 10-K and with the
|
| 109 |
+
Company’s filings with the SEC subsequent to the filing of
|
| 110 |
+
the Form 10-K. Notwithstanding
|
| 111 |
+
the existence of the material weakness described in Part II. Item
|
| 112 |
+
9A – “Controls and Procedures,” the Company
|
| 113 |
+
believes that the consolidated financial statements in the Form
|
| 114 |
+
10-K fairly present, in all material respects, the Company’s
|
| 115 |
+
financial position, results of operations and cash flows as of the
|
| 116 |
+
dates, and for the periods, presented, in conformity with U.S.
|
| 117 |
+
generally accepted accounting principles. -1- Table of Contents P ART I Item
|
| 118 |
+
1A. Risk
|
| 119 |
+
Factors The
|
| 120 |
+
Company is supplementing the risk factors previously disclosed in
|
| 121 |
+
“Item 1A. Risk Factors” of the Form 10-K with the
|
| 122 |
+
following risk factor, which should be read in conjunction with the
|
| 123 |
+
other risk factors presented in the Form 10-K: Failure to remediate a material weakness in internal accounting
|
| 124 |
+
controls could result in material misstatements in our financial
|
| 125 |
+
statements. Our
|
| 126 |
+
management has identified a material weakness in our internal
|
| 127 |
+
control over financial reporting and has concluded that, due to
|
| 128 |
+
such material weakness, our disclosure controls and procedures were
|
| 129 |
+
not effective as of December 31, 2019. The material weakness in
|
| 130 |
+
internal control over financial reporting resulted from a
|
| 131 |
+
deficiency in our disclosure controls and procedures which could
|
| 132 |
+
have resulted in us not disclosing a material potential loss that
|
| 133 |
+
was reasonably possible, and therefore requiring a qualitative
|
| 134 |
+
disclosure in our consolidated financial statements under ASC 450
|
| 135 |
+
– Contingencies . The
|
| 136 |
+
material weakness has not been remediated as of March 31, 2020. If
|
| 137 |
+
not remediated, or if we identify further material weaknesses in
|
| 138 |
+
our internal controls, our failure to establish and maintain
|
| 139 |
+
effective disclosure controls and procedures and internal control
|
| 140 |
+
over financial reporting could result in material misstatements in
|
| 141 |
+
our financial statements and a failure to meet our reporting and
|
| 142 |
+
financial obligations, each of which could have a material adverse
|
| 143 |
+
effect on our financial condition and the trading price of our
|
| 144 |
+
common stock. P ART II Item
|
| 145 |
+
9A. Controls and Procedures Evaluation of Disclosure Controls and Procedures (As
|
| 146 |
+
Revised) In
|
| 147 |
+
connection with the filing of our Form 10-K, our chief executive
|
| 148 |
+
officer and our chief financial officer, after evaluating the
|
| 149 |
+
effectiveness of our disclosure controls and procedures (as defined
|
| 150 |
+
in Rules 13a-15(e) and 15d-15(e) of the Exchange Act) as of
|
| 151 |
+
December 31, 2019, concluded that our disclosure controls and
|
| 152 |
+
procedures were effective. Subsequent
|
| 153 |
+
to that evaluation, management reevaluated the effectiveness of our
|
| 154 |
+
disclosure controls and procedures as of December 31, 2019 and
|
| 155 |
+
concluded that, because of the material weakness identified in our
|
| 156 |
+
internal control over financial reporting discussed below, our
|
| 157 |
+
disclosure controls and procedures were not effective as of
|
| 158 |
+
December 31, 2019. Management Report on Internal Control over Financial Reporting (As
|
| 159 |
+
Revised) Our
|
| 160 |
+
management is responsible for establishing and maintaining adequate
|
| 161 |
+
internal control over financial reporting as defined in Rule
|
| 162 |
+
13a-15(f) and 15d-(f) under the Exchange Act. Our internal control
|
| 163 |
+
over financial reporting is designed to provide reasonable
|
| 164 |
+
assurance regarding the reliability of financial reporting and the
|
| 165 |
+
preparation of consolidated financial statements for external
|
| 166 |
+
purposes in accordance with U.S. generally accepted accounting
|
| 167 |
+
principles. Our internal control over financial reporting include
|
| 168 |
+
those policies and procedures that: (i)
|
| 169 |
+
pertain to the maintenance of records that, in reasonable detail,
|
| 170 |
+
accurately and fairly reflect the transactions and dispositions of
|
| 171 |
+
our assets; (ii)
|
| 172 |
+
provide reasonable assurance that transactions are recorded as
|
| 173 |
+
necessary to permit the preparation of our consolidated financial
|
| 174 |
+
statements in accordance with U.S. generally accepted accounting
|
| 175 |
+
principles, and that our receipts and expenditures are being made
|
| 176 |
+
only in accordance with authorizations of our management and
|
| 177 |
+
directors; and (iii)
|
| 178 |
+
provide reasonable assurance regarding prevention or timely
|
| 179 |
+
detection of unauthorized acquisition, use or disposition of our
|
| 180 |
+
assets that could have a material effect on the consolidated
|
| 181 |
+
financial statements. -2- Table of Contents In
|
| 182 |
+
connection with the Form 10-K, our management, including the
|
| 183 |
+
undersigned principal executive officer and principal financial
|
| 184 |
+
officer, assessed the effectiveness of our internal control over
|
| 185 |
+
financial reporting as of December 31, 2019. In conducting its
|
| 186 |
+
assessment, our management used the criteria issued by the
|
| 187 |
+
Committee of Sponsoring Organizations of the Treadway Commission in Internal Control—Integrated
|
| 188 |
+
Framework in 2013 . Based on this assessment, our management
|
| 189 |
+
concluded that, as of December 31, 2019, our internal control
|
| 190 |
+
over financial reporting was effective based on those
|
| 191 |
+
criteria. Subsequent
|
| 192 |
+
to that evaluation, management reevaluated the effectiveness of our
|
| 193 |
+
internal control over financial reporting as of December 31, 2019
|
| 194 |
+
and concluded that, because of the material weakness identified,
|
| 195 |
+
our internal control over financial reporting was not effective as
|
| 196 |
+
of December 31, 2019. The
|
| 197 |
+
material weakness in internal control over financial reporting
|
| 198 |
+
resulted from a deficiency in our disclosure controls and
|
| 199 |
+
procedures which could have resulted in the Company not disclosing
|
| 200 |
+
a material potential loss that was reasonably possible, and
|
| 201 |
+
therefore requiring a qualitative disclosure in consolidated
|
| 202 |
+
financial statements under ASC 450 – Contingencies . The
|
| 203 |
+
Company is still in the process of analyzing and addressing the
|
| 204 |
+
material weakness. The material weakness will not be considered
|
| 205 |
+
remediated until the applicable remedial control operates for a
|
| 206 |
+
sufficient period of time and management has concluded, through
|
| 207 |
+
testing, that this control is operating effectively. We expect that
|
| 208 |
+
the remediation of this material weakness will be completed prior
|
| 209 |
+
to the end of year 2020. We did
|
| 210 |
+
not revise our financial statements for prior periods for the
|
| 211 |
+
disclosure of the potential loss based on our belief that the
|
| 212 |
+
effect of the lack of the disclosure was not material to the
|
| 213 |
+
financial statements taken as a whole. Therefore, our chief
|
| 214 |
+
executive officer and our chief financial officer believe that,
|
| 215 |
+
notwithstanding the material weakness discussed above, the
|
| 216 |
+
consolidated financial statements in our Annual Report on Form 10-K
|
| 217 |
+
for the year ended December 31, 2019 present fairly, in all
|
| 218 |
+
material respects, our financial position, results of operations
|
| 219 |
+
and cash flows for the periods presented. Changes in Internal Control over Financial Reporting Except
|
| 220 |
+
as noted above, there were no changes in internal controls over
|
| 221 |
+
financial reporting (as defined in Rule 13a−15(f) promulgated
|
| 222 |
+
under the Exchange Act) that occurred during our fourth fiscal
|
| 223 |
+
quarter that have materially affected or are reasonably likely to
|
| 224 |
+
materially affect our internal control over financial
|
| 225 |
+
reporting. Inherent Limitations on Disclosure Controls and
|
| 226 |
+
Procedures The
|
| 227 |
+
effectiveness of our disclosure controls and procedures is subject
|
| 228 |
+
to various inherent limitations, including cost limitations,
|
| 229 |
+
judgments used in decision making, assumptions about the likelihood
|
| 230 |
+
of future events, the soundness of our systems, the possibility of
|
| 231 |
+
human error, and the risk of fraud. Moreover, projections of any
|
| 232 |
+
evaluation of effectiveness to future periods are subject to the
|
| 233 |
+
risk that controls may become inadequate because of changes in
|
| 234 |
+
conditions and the risk that the degree of compliance with policies
|
| 235 |
+
or procedures may deteriorate over time. Because of these
|
| 236 |
+
limitations, there can be no assurance that any system of
|
| 237 |
+
disclosure controls and procedures, no matter how well conceived,
|
| 238 |
+
will be successful in preventing all errors or fraud or in making
|
| 239 |
+
all material information known in a timely manner to the
|
| 240 |
+
appropriate levels of management. Inherent Limitations on Internal Control Internal
|
| 241 |
+
control over financial reporting cannot provide absolute assurance
|
| 242 |
+
of achieving financial reporting objectives because of its inherent
|
| 243 |
+
limitations, including the possibility of human error and
|
| 244 |
+
circumvention by collusion or overriding of control. Accordingly,
|
| 245 |
+
even an effective internal control system may not prevent or detect
|
| 246 |
+
material misstatements on a timely basis. Also, projections of any
|
| 247 |
+
evaluation of effectiveness to future periods are subject to the
|
| 248 |
+
risk that the controls may become inadequate because of changes in
|
| 249 |
+
conditions or that the degree of compliance with the policies or
|
| 250 |
+
procedures may deteriorate. Accordingly, our internal control over
|
| 251 |
+
financial reporting is designed to provide reasonable assurance of
|
| 252 |
+
achieving their objectives. -3- Table of Contents Attestation Report of the Registered Public Accounting
|
| 253 |
+
Firm REPORT OF INDEPENDENT REGISTERED PUBLIC ACCOUNTING
|
| 254 |
+
FIRM ON INTERNAL CONTROL OVER FINANCIAL REPORTING To the Shareholders and Board of Directors of ChromaDex Corporation Adverse Opinion on Internal Control over Financial
|
| 255 |
+
Reporting We have audited ChromaDex Corporation’’s (the
|
| 256 |
+
"Company") internal control over financial reporting as of December
|
| 257 |
+
31, 2019, based on criteria established in Internal Contr
|
test_data/financial_real/manifest.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"cik": "0001386570",
|
| 4 |
+
"accession": "0001654954-20-005725",
|
| 5 |
+
"company": "ChromaDex Corp. (CDXC) (CIK 0001386570)",
|
| 6 |
+
"term": "risk_factors",
|
| 7 |
+
"filing_date": "",
|
| 8 |
+
"source_url": "https://www.sec.gov/Archives/edgar/data/1386570/000165495420005725/cdxc10ka_12312019.htm",
|
| 9 |
+
"path": "D:\\DG Workspace\\Mexar-main\\test_data\\financial_real\\0001386570_0001654954-20-005725.txt",
|
| 10 |
+
"char_count": 15000
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"cik": "0001041588",
|
| 14 |
+
"accession": "0001041588-21-000001",
|
| 15 |
+
"company": "Access-Power & Co., Inc. (CIK 0001041588)",
|
| 16 |
+
"term": "risk_factors",
|
| 17 |
+
"filing_date": "",
|
| 18 |
+
"source_url": "https://www.sec.gov/Archives/edgar/data/1041588/000104158821000001/accr12312010k.txt",
|
| 19 |
+
"path": "D:\\DG Workspace\\Mexar-main\\test_data\\financial_real\\0001041588_0001041588-21-000001.txt",
|
| 20 |
+
"char_count": 15000
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"cik": "0001041588",
|
| 24 |
+
"accession": "0001041588-20-000001",
|
| 25 |
+
"company": "ACCESS-POWER INC (CIK 0001041588)",
|
| 26 |
+
"term": "risk_factors",
|
| 27 |
+
"filing_date": "",
|
| 28 |
+
"source_url": "https://www.sec.gov/Archives/edgar/data/1041588/000104158820000001/accr12311910K.txt",
|
| 29 |
+
"path": "D:\\DG Workspace\\Mexar-main\\test_data\\financial_real\\0001041588_0001041588-20-000001.txt",
|
| 30 |
+
"char_count": 15000
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"cik": "0000320575",
|
| 34 |
+
"accession": "0000320575-20-000008",
|
| 35 |
+
"company": "PIONEER ENERGY SERVICES CORP (CIK 0000320575)",
|
| 36 |
+
"term": "risk_factors",
|
| 37 |
+
"filing_date": "",
|
| 38 |
+
"source_url": "https://www.sec.gov/Archives/edgar/data/320575/000032057520000008/form10-ka2019.htm",
|
| 39 |
+
"path": "D:\\DG Workspace\\Mexar-main\\test_data\\financial_real\\0000320575_0000320575-20-000008.txt",
|
| 40 |
+
"char_count": 15000
|
| 41 |
+
}
|
| 42 |
+
]
|
test_data/legal_real/case_10129044.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LEGAL OPINION: In re Match Group, Inc. Derivative Litigation
|
| 2 |
+
COURT: Court of Chancery of Delaware
|
| 3 |
+
DATE FILED: 2024-10-02
|
| 4 |
+
SNIPPET & HOLDING:
|
| 5 |
+
|
| 6 |
+
LEGAL OPINION: In re Match Group, Inc. Derivative Litigation
|
| 7 |
+
COURT: Court of Chancery of Delaware
|
| 8 |
+
DATE FILED: 2024-10-02
|
| 9 |
+
SNIPPET & HOLDING:
|
| 10 |
+
|
| 11 |
+
LEGAL OPINION: In re Match Group, Inc. Derivative Litigation
|
| 12 |
+
COURT: Court of Chancery of Delaware
|
| 13 |
+
DATE FILED: 2024-10-02
|
| 14 |
+
SNIPPET & HOLDING:
|
| 15 |
+
|
test_data/legal_real/case_1027758.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LEGAL OPINION: Rein v. United States Patent & Trademark Office
|
| 2 |
+
COURT: Court of Appeals for the Fourth Circuit
|
| 3 |
+
DATE FILED: 2009-01-28
|
| 4 |
+
SNIPPET & HOLDING:
|
| 5 |
+
|
| 6 |
+
LEGAL OPINION: Rein v. United States Patent & Trademark Office
|
| 7 |
+
COURT: Court of Appeals for the Fourth Circuit
|
| 8 |
+
DATE FILED: 2009-01-28
|
| 9 |
+
SNIPPET & HOLDING:
|
| 10 |
+
|
| 11 |
+
LEGAL OPINION: Rein v. United States Patent & Trademark Office
|
| 12 |
+
COURT: Court of Appeals for the Fourth Circuit
|
| 13 |
+
DATE FILED: 2009-01-28
|
| 14 |
+
SNIPPET & HOLDING:
|
| 15 |
+
|
test_data/legal_real/case_10305113.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LEGAL OPINION: In re Fox Corporation Derivative Litigation
|
| 2 |
+
COURT: Court of Chancery of Delaware
|
| 3 |
+
DATE FILED: 2024-12-27
|
| 4 |
+
SNIPPET & HOLDING:
|
| 5 |
+
|
| 6 |
+
LEGAL OPINION: In re Fox Corporation Derivative Litigation
|
| 7 |
+
COURT: Court of Chancery of Delaware
|
| 8 |
+
DATE FILED: 2024-12-27
|
| 9 |
+
SNIPPET & HOLDING:
|
| 10 |
+
|
| 11 |
+
LEGAL OPINION: In re Fox Corporation Derivative Litigation
|
| 12 |
+
COURT: Court of Chancery of Delaware
|
| 13 |
+
DATE FILED: 2024-12-27
|
| 14 |
+
SNIPPET & HOLDING:
|
| 15 |
+
|
test_data/legal_real/case_10318020.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LEGAL OPINION: In re Oracle Corporation Derivative Litigation
|
| 2 |
+
COURT: Supreme Court of Delaware
|
| 3 |
+
DATE FILED: 2025-01-21
|
| 4 |
+
SNIPPET & HOLDING:
|
| 5 |
+
|
| 6 |
+
LEGAL OPINION: In re Oracle Corporation Derivative Litigation
|
| 7 |
+
COURT: Supreme Court of Delaware
|
| 8 |
+
DATE FILED: 2025-01-21
|
| 9 |
+
SNIPPET & HOLDING:
|
| 10 |
+
|
| 11 |
+
LEGAL OPINION: In re Oracle Corporation Derivative Litigation
|
| 12 |
+
COURT: Supreme Court of Delaware
|
| 13 |
+
DATE FILED: 2025-01-21
|
| 14 |
+
SNIPPET & HOLDING:
|
| 15 |
+
|
test_data/legal_real/case_10353022.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LEGAL OPINION: Cqv Co., Ltd. v. Merck Patent Gmbh
|
| 2 |
+
COURT: Court of Appeals for the Federal Circuit
|
| 3 |
+
DATE FILED: 2025-03-10
|
| 4 |
+
SNIPPET & HOLDING:
|
| 5 |
+
|
| 6 |
+
LEGAL OPINION: Cqv Co., Ltd. v. Merck Patent Gmbh
|
| 7 |
+
COURT: Court of Appeals for the Federal Circuit
|
| 8 |
+
DATE FILED: 2025-03-10
|
| 9 |
+
SNIPPET & HOLDING:
|
| 10 |
+
|
| 11 |
+
LEGAL OPINION: Cqv Co., Ltd. v. Merck Patent Gmbh
|
| 12 |
+
COURT: Court of Appeals for the Federal Circuit
|
| 13 |
+
DATE FILED: 2025-03-10
|
| 14 |
+
SNIPPET & HOLDING:
|
| 15 |
+
|
test_data/legal_real/case_10362523.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LEGAL OPINION: Gerald Metals, LLC v. Certain Underwriters at International Underwriting Assn. of London
|
| 2 |
+
COURT: Connecticut Appellate Court
|
| 3 |
+
DATE FILED: 2025-03-25
|
| 4 |
+
SNIPPET & HOLDING:
|
| 5 |
+
|
| 6 |
+
LEGAL OPINION: Gerald Metals, LLC v. Certain Underwriters at International Underwriting Assn. of London
|
| 7 |
+
COURT: Connecticut Appellate Court
|
| 8 |
+
DATE FILED: 2025-03-25
|
| 9 |
+
SNIPPET & HOLDING:
|
| 10 |
+
|
| 11 |
+
LEGAL OPINION: Gerald Metals, LLC v. Certain Underwriters at International Underwriting Assn. of London
|
| 12 |
+
COURT: Connecticut Appellate Court
|
| 13 |
+
DATE FILED: 2025-03-25
|
| 14 |
+
SNIPPET & HOLDING:
|
| 15 |
+
|
test_data/legal_real/case_10388952.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LEGAL OPINION: In re Fox Corporation Derivative Litigation
|
| 2 |
+
COURT: Court of Chancery of Delaware
|
| 3 |
+
DATE FILED: 2025-04-28
|
| 4 |
+
SNIPPET & HOLDING:
|
| 5 |
+
|
| 6 |
+
LEGAL OPINION: In re Fox Corporation Derivative Litigation
|
| 7 |
+
COURT: Court of Chancery of Delaware
|
| 8 |
+
DATE FILED: 2025-04-28
|
| 9 |
+
SNIPPET & HOLDING:
|
| 10 |
+
|
| 11 |
+
LEGAL OPINION: In re Fox Corporation Derivative Litigation
|
| 12 |
+
COURT: Court of Chancery of Delaware
|
| 13 |
+
DATE FILED: 2025-04-28
|
| 14 |
+
SNIPPET & HOLDING:
|
| 15 |
+
|
test_data/legal_real/case_10463618.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LEGAL OPINION: In re Plug Power Inc. Stockholder Derivative Litigation
|
| 2 |
+
COURT: Court of Chancery of Delaware
|
| 3 |
+
DATE FILED: 2025-05-02
|
| 4 |
+
SNIPPET & HOLDING:
|
| 5 |
+
|
| 6 |
+
LEGAL OPINION: In re Plug Power Inc. Stockholder Derivative Litigation
|
| 7 |
+
COURT: Court of Chancery of Delaware
|
| 8 |
+
DATE FILED: 2025-05-02
|
| 9 |
+
SNIPPET & HOLDING:
|
| 10 |
+
|
| 11 |
+
LEGAL OPINION: In re Plug Power Inc. Stockholder Derivative Litigation
|
| 12 |
+
COURT: Court of Chancery of Delaware
|
| 13 |
+
DATE FILED: 2025-05-02
|
| 14 |
+
SNIPPET & HOLDING:
|
| 15 |
+
|
test_data/legal_real/case_10513129.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LEGAL OPINION: Ready v. New Canaan
|
| 2 |
+
COURT: Connecticut Appellate Court
|
| 3 |
+
DATE FILED: 2025-05-06
|
| 4 |
+
SNIPPET & HOLDING:
|
| 5 |
+
|
| 6 |
+
LEGAL OPINION: Ready v. New Canaan
|
| 7 |
+
COURT: Connecticut Appellate Court
|
| 8 |
+
DATE FILED: 2025-05-06
|
| 9 |
+
SNIPPET & HOLDING:
|
| 10 |
+
|
| 11 |
+
LEGAL OPINION: Ready v. New Canaan
|
| 12 |
+
COURT: Connecticut Appellate Court
|
| 13 |
+
DATE FILED: 2025-05-06
|
| 14 |
+
SNIPPET & HOLDING:
|
| 15 |
+
|
test_data/legal_real/case_1052233.txt
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
IN THE COURT OF APPEALS OF TENNESSEE
|
| 2 |
+
AT JACKSON
|
| 3 |
+
August 16, 2007 Session
|
| 4 |
+
|
| 5 |
+
GARY WEAVER, ET AL. v. THOMAS R. McCARTER, ET AL.
|
| 6 |
+
|
| 7 |
+
A Direct Appeal from the Chancery Court for Shelby County
|
| 8 |
+
No. 98-0425-3 The Honorable D. J. Alissandratos, Chancellor
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
No. W2006-02058-COA-R3-CV - Filed October 8, 2007
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
This is the second appeal of this breach of contract case. In Gary Weaver, et al v. Thomas
|
| 16 |
+
R. McCarter, et al, No. W2004-02803-COA-R3-CV, 2006 WL 1529506 (Tenn. Ct. App. June 6,
|
| 17 |
+
2006), this Court affirmed the trial court’s grant of summary judgment in favor of plaintiffs and
|
| 18 |
+
remanded the case “for further clarification concerning the amount of damages awarded with respect
|
| 19 |
+
to plaintiff’s claims of negligence per se, negligent misrepresentation, and breach of contract.” Upon
|
| 20 |
+
remand, the trial court entered judgment against the defendants jointly and severally and in favor of
|
| 21 |
+
plaintiffs for compensatory damages and pre-judgment interest. Finding that the trial court abused
|
| 22 |
+
its discretion in awarding pre-judgment interest, we reverse that portion of the Judgment. We
|
| 23 |
+
reverse in part and affirm in part.
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
Tenn. R. App. P. 3; Appeal as of Right; Judgment of the Reversed in Part, Affirmed in Part
|
| 27 |
+
|
| 28 |
+
W. FRANK CRAWFORD , P.J., W.S., delivered the opinion of the court, in which DAVID R. FARMER ,
|
| 29 |
+
J. and HOLLY M. KIRBY , J., joined.
|
| 30 |
+
|
| 31 |
+
Charles F. Morrow and Elizabeth E. Chance of Memphis, Tennessee for Appellant, Coldwell Banker
|
| 32 |
+
Hoffman-Burke, Inc., Realtors
|
| 33 |
+
|
| 34 |
+
Robert L. Moore and Dawn Davis Carson of Memphis, Tennessee for Appellant, Jim Perdue
|
| 35 |
+
|
| 36 |
+
Henry C. Shelton, III and Tricia T. Olson of Memphis, Tennessee for Gary and Gail Weaver
|
| 37 |
+
|
| 38 |
+
OPINION
|
| 39 |
+
|
| 40 |
+
The relevant facts and procedures are set out in this Court’s Opinion in the previous appeal,
|
| 41 |
+
Gary Weaver, et al v. Thomas R. McCarter, et al, No. W2004-02803-COA-R3-CV, 2006 WL
|
| 42 |
+
1529506 (Tenn. Ct. App. June 6, 2006). The Opinion, filed June 6, 2006, states in pertinent part:
|
| 43 |
+
This suit arises from a contract to sell Gary and Gail Weaver’s
|
| 44 |
+
(“the Plaintiffs”) house (“the Hacks Cross House”) located on Hacks
|
| 45 |
+
Cross Road in Memphis, Tennessee. On November 1, 1994, Thomas
|
| 46 |
+
McCarter (“Defendant McCarter”) approached the Plaintiffs with an
|
| 47 |
+
offer to purchase the Hacks Cross House for $680,000 in cash. The
|
| 48 |
+
Plaintiffs agreed and subsequently entered into a verbal contract with
|
| 49 |
+
Defendant McCarter for a cash sale. However, Defendant McCarter
|
| 50 |
+
never consummated the sale and, thus, the transaction never closed.
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
In February 1997, the Plaintiffs purchased a second home
|
| 54 |
+
located on Langston Cove (“the Langston Cove House”) and soon
|
| 55 |
+
after listed the Hacks Cross House with Jim Perdue (“Defendant
|
| 56 |
+
Perdue”) at Coldwell Banker Hoffman-Burke, Inc. Realtors
|
| 57 |
+
(“Defendant CBHB”). On August 27, 1997, Defendant Perdue
|
| 58 |
+
brought the Plaintiffs a written offer from Janet Hunter to purchase
|
| 59 |
+
the Hacks Cross House for $575,000. The Plaintiffs eventually
|
| 60 |
+
accepted a counter-offer of $595,000, with $10,000 in escrow, and
|
| 61 |
+
signed a contract of sale (hereinafter referred to as “the sale contract”)
|
| 62 |
+
with Janet Hunter. Closing was set for September 30, 1997.
|
| 63 |
+
However, the $10,000 escrow check bounced on September 17, 1997,
|
| 64 |
+
and Defendants CBHB received notice of this on September 30, 1997.
|
| 65 |
+
Furthermore, the sale contract was not signed by Janet Hunter, but
|
| 66 |
+
rather Janet Hunter’s name was forged by Chip Hunter (“Defendant
|
| 67 |
+
Hunter”), Janet Hunter’s husband, without Janet Hunter’s knowledge
|
| 68 |
+
or permission. Defendant Hunter signed Janet Hunter’s name to the
|
| 69 |
+
sale contract at the request of Defendant McCarter and while in the
|
| 70 |
+
presence of Defendant Perdue. The Plaintiffs did not learn of the
|
| 71 |
+
bounced escrow check or the forged sale contract until the closing
|
| 72 |
+
date, when the Plaintiffs’ attorney checked on the escrow funds and
|
| 73 |
+
Defendant Perdue informed the Plaintiffs’ closing attorney of the
|
| 74 |
+
forgery. As a result, the closing did not occur on September 30, 1997.
|
| 75 |
+
|
| 76 |
+
Subsequently, on October 2, 1997, Defendant McCarter, in a
|
| 77 |
+
memorandum to Defendant Perdue, affirmed that he was the true
|
| 78 |
+
party who had made the offer on August 29, 1997, to purchase the
|
| 79 |
+
Hacks Cross House for his daughter, Janet Hunter. While Defendant
|
| 80 |
+
McCarter indicated a desire to honor the sale contract, he informed
|
| 81 |
+
Defendant Perdue that his funding for the purchase would not be
|
| 82 |
+
available until October 8, 1997. Although reserving their rights under
|
| 83 |
+
the original sale contract, the Plaintiffs informed Defendant McCarter
|
| 84 |
+
that they were willing to close the transaction if his funding came
|
| 85 |
+
through by October 8, 1997. Despite this, the closing did not take
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
-2-
|
| 89 |
+
place on October 8, due to a continued lack of funding. At that time,
|
| 90 |
+
Defendant McCarter promised to fund the transaction on October 15,
|
| 91 |
+
1997. However, on that date, he delivered a $10,000 check to
|
| 92 |
+
Defendant CBHB and stated that he no longer wanted to purchase the
|
| 93 |
+
Hacks Cross House. The Plaintiffs subsequently demanded that
|
| 94 |
+
Defendant Perdue pay them Defendant McCarter’s $10,000 in earnest
|
| 95 |
+
money, cancelled their listing agreement with Defendant CBHB, and
|
| 96 |
+
placed the Hacks Cross House back on the market. However, the
|
| 97 |
+
Hacks Cross House did not sell and the Plaintiffs eventually sold the
|
| 98 |
+
Langston Cove House and moved back to the Hacks Cross House.
|
| 99 |
+
|
| 100 |
+
On May 11, 1998, the Plaintiffs filed suit against Defendants
|
| 101 |
+
McCarter, CBHB, Perdue, and Hunter, seeking damages for
|
| 102 |
+
negligence per se, negligent misrepresentation, breach of contract,
|
| 103 |
+
and fraud. Specifically, the Plaintiffs asserted that Defendants Perdue
|
| 104 |
+
and CBHB were negligent per se for violating section 62-13-403 of
|
| 105 |
+
the Tennessee Real Estate Broker License Act of 1973. The Plaintiffs
|
| 106 |
+
further asserted that Defendant Perdue was also negligent per se
|
| 107 |
+
because he violated section 62-13-404 of the Brokers Act, which
|
| 108 |
+
provides for duties of reasonable care and loyalty between real estate
|
| 109 |
+
brokers and their clients; and that Defendant Perdue was further
|
| 110 |
+
guilty of negligent misrepresentation for failing to exercise reasonable
|
| 111 |
+
care in communicating information to the Plaintiffs. The Plaintiffs
|
| 112 |
+
next asserted a breach of contract claim against Defendant McCarter
|
| 113 |
+
arguing that, although Defendant McCarter did not actually sign the
|
| 114 |
+
contract, he had previously entered into an oral agreement, through
|
| 115 |
+
Defendant Perdue, with the Plaintiffs to purchase their house, but
|
| 116 |
+
failed to follow through with the purchase. Finally, the Plaintiffs
|
| 117 |
+
asserted a fraud complaint against Defendants McCarter, Hunter, and
|
| 118 |
+
Perdue, arguing that all three parties engaged in dishonest practices
|
| 119 |
+
with a deceptive intent in order to induce the Plaintiffs to enter into
|
| 120 |
+
an illusory contract to sell their house, which the Plaintiffs
|
| 121 |
+
subsequently relied upon to their detriment. As remedies, the
|
| 122 |
+
Plaintiffs sought either specific performance by Defendant McCarter
|
| 123 |
+
or a judgment for $200,000 in compensatory damages against all
|
| 124 |
+
defendants jointly and severally, along with punitive damages.
|
| 125 |
+
|
| 126 |
+
In response to the Plaintiffs’ claims, Defendants CBHB and
|
| 127 |
+
Perdue filed an answer arguing that Plaintiffs failed to assert a claim
|
| 128 |
+
upon which relief could be granted. Defendant CBHB also asserted
|
| 129 |
+
a counter-claim against the Plaintiffs alleging that they were entitled
|
| 130 |
+
to split the $10,000 in earnest money with the Plaintiffs. In turn,
|
| 131 |
+
Defendant Hunter denied forging Janet Hunter’s signature and
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
-3-
|
| 135 |
+
asserted the statute of frauds as an affirmative defense. Defendant
|
| 136 |
+
McCarter denied being the contractual purchaser of the house and
|
| 137 |
+
further asserted the statute of frauds as an affirmative defense.
|
| 138 |
+
Defendant McCarter also asserted a counter-claim against Defendant
|
| 139 |
+
Perdue, Defendant CBHB, and the Plaintiffs seeking the return of the
|
| 140 |
+
$10,000 placed in escrow arguing that Defendant McCarter entered
|
| 141 |
+
into no enforceable contract of sale and, thus, no legal basis existed
|
| 142 |
+
for the continued retention of such funds. Defendants CBHB and
|
| 143 |
+
Perdue and the Plaintiffs responded to Defendant McCarter’s counter-
|
| 144 |
+
complaint and asserted that an enforceable contract of sale did exist.
|
| 145 |
+
|
| 146 |
+
On July 13, 2000, the Plaintiffs filed a Motion for Summary
|
| 147 |
+
Judgment against all Defendants. Defendant McCarter subsequently
|
| 148 |
+
filed a motion seeking a directed verdict in relation to the return of
|
| 149 |
+
the $10,000 escrow money. (FN1) On January 22, 2001, the trial
|
| 150 |
+
court entered an order granting summary judgment in favor of the
|
| 151 |
+
Plaintiffs against Defendants McCarter, Hunter, Perdue, and CBHB.
|
| 152 |
+
The court also denied Defendant McCarter’s directed verdict motion
|
| 153 |
+
and further ordered that Defendant CBHB tender the $10,000 in
|
| 154 |
+
earnest money to the Plaintiff, with said amount being credited
|
| 155 |
+
against the damage award against Defendant McCarter. The amounts
|
| 156 |
+
of compensatory and punitive damages were reserved for further
|
| 157 |
+
hearing; however, the trial court did state that while all judgments
|
| 158 |
+
rendered herein and subsequently by the Court shall be joint and
|
| 159 |
+
several as against all Defendants, that among the Defendants the fault
|
| 160 |
+
shall be apportioned [forty-five] percent (45%) to [Defendant]
|
| 161 |
+
McCarter, ten percent (10%) to [Defendant] Hunter, and [forty-five]
|
| 162 |
+
percent (45%) to Defendants [Perdue and CBHB].
|
| 163 |
+
|
| 164 |
+
(FN1) Pursuant to Rule 50.01 of the Rules of Civil Procedure,
|
| 165 |
+
a party may only move for directed verdict “at the close of the
|
| 166 |
+
evidence offered by an opposing party or at the close of the
|
| 167 |
+
case.” Tenn. R. Civ. P. 50.01 (2005).
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
Nearly two years after the trial court’s grant of summary judgment,
|
| 171 |
+
Defendant CBHB filed a motion to reconsider, which the trial court
|
| 172 |
+
denied.
|
| 173 |
+
|
| 174 |
+
Prior to the trial on damages, several events occurred which
|
| 175 |
+
ultimately affected the trial court’s ability to assess damages. First,
|
| 176 |
+
on May 5, 2004, Defendant Perdue filed a Plea of Bankruptcy stating
|
| 177 |
+
that he had filed bankruptcy and that, pursuant to an Order Modifying
|
| 178 |
+
Discharge Injunction issued by the United States Bankruptcy Court
|
| 179 |
+
for the Western District of Tennessee, the trial court would only be
|
| 180 |
+
|
| 181 |
+
-4-
|
| 182 |
+
allowed to proceed to a judgment against Defendant Perdue for any
|
| 183 |
+
causes of action except acts of fraud, fraudulent misrepresentation, or
|
| 184 |
+
other cause of action which would permit the denial of discharge
|
| 185 |
+
pursuant to 11 U.S.C. § 727 or render the judgment nondischargeable
|
| 186 |
+
pursuant to 11 U.S.C. § 523. The trial court granted this plea. In
|
| 187 |
+
addition to Defendant Perdue’s filing of bankruptcy, Defendant
|
| 188 |
+
McCarter died (FN2) and Defendant Hunter also filed bankruptcy.
|
| 189 |
+
As a result, at trial, the Plaintiffs sought to collect damages arising
|
| 190 |
+
from Defendants CBHB and Perdue’s negligence per se and negligent
|
| 191 |
+
misrepresentation, as well as Defendant McCarter’s breach of
|
| 192 |
+
contract. (FN3)
|
| 193 |
+
|
| 194 |
+
(FN2) W e find no indication in the record of a substitution of
|
| 195 |
+
parties for Defendant McCarter pursuant to Rule 25.01 of the
|
| 196 |
+
Tennessee Rules of Civil Procedure.
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
(FN3) In their brief, the Plaintiffs assert that at the trial on
|
| 200 |
+
damages, they proceeded only against Defendants CBHB and
|
| 201 |
+
Perdue “and sought damages only under the Tennessee Real
|
| 202 |
+
Estate Broker[] Act of 1973 and for negligence.” However, a
|
| 203 |
+
review of the trial transcript reveals that the Plaintiffs also sought
|
| 204 |
+
damages for their breach of contract claim against Defendant
|
| 205 |
+
McCarter, although the Plaintiffs admitted that Defendant
|
| 206 |
+
McCarter died apparently leaving no estate to satisfy a judgment
|
| 207 |
+
and that “no formal Notice [or] substitution” was made pursuant
|
| 208 |
+
to Rule 25.01.
|
| 209 |
+
|
| 210 |
+
In relation to damages suffered due to fraud, neither the
|
| 211 |
+
Plaintiffs’ brief nor the record in this case supports the
|
| 212 |
+
conclusion that damages were awarded based upon fraud in this
|
| 213 |
+
case. At trial, the Plaintiffs’ counsel admitted that no judgment
|
| 214 |
+
for fraud was awarded against Defendants CBHB and Perdue.
|
| 215 |
+
Furthermore, the Plaintiffs indicated that they were unable to
|
| 216 |
+
pursue damages against Defendant Hunter due to his filing
|
| 217 |
+
Chapter 7 bankruptcy. Finally, a review of the record and
|
| 218 |
+
transcript in this case fails to show that damages were awarded
|
| 219 |
+
for any fraud alleged against Defendant McCarter.
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
At the trial on damages, the parties presented testimony of
|
| 223 |
+
Plaintiff Gary Weaver and John Jordan, a real estate appraiser who
|
| 224 |
+
had appraised the Plaintiffs’ property in 1997, near the time the sale
|
| 225 |
+
contract was signed. The Plaintiffs asserted that the proof showed
|
| 226 |
+
that they had suffered $300,000 in damages as a result of the
|
| 227 |
+
Defendants conduct. After hearing the proof, the trial court first held
|
| 228 |
+
the Plaintiffs would receive no damages in relation to their purchase
|
| 229 |
+
and subsequent sale of the Langston Cove House because the
|
| 230 |
+
purchase of this property occurred prior to the Plaintiffs’ contract to
|
| 231 |
+
|
| 232 |
+
-5-
|
| 233 |
+
sell the Hacks Cross House and, thus, damages resulting from the
|
| 234 |
+
purchase and sale of the Langston Cove home were not reasonable
|
| 235 |
+
consequential damages flowing from the Defendants’ actions in
|
| 236 |
+
regard to the Hacks Cross House. As a result of this finding, the trial
|
| 237 |
+
court focused its award of damages on the following issues: 1) the
|
| 238 |
+
contract price for the Hacks Cross House minus the sales
|
| 239 |
+
commission, 2) the duty of the Plaintiffs to mitigate damages, and 3)
|
| 240 |
+
additional expenses incurred as a result of the Defendants’ actions.
|
| 241 |
+
After hearing the evidence submitted at trial, the trial court entered an
|
| 242 |
+
Order of Judgment providing as follows:
|
| 243 |
+
|
| 244 |
+
Plaintiffs should have a judgment, against Defendants,
|
| 245 |
+
pursuant to their apportioned fault, in the amount of
|
| 246 |
+
$55,600.00 for lost value of real estate; $3,000.00 for
|
| 247 |
+
1998 and $3,000.00 for 1999 for necessary
|
| 248 |
+
maintenance costs, and $20,000.00 for attorneys fees,
|
| 249 |
+
with prejudgment interest computed at 10% per year
|
| 250 |
+
on each amount from the date incurred, or, in the case
|
| 251 |
+
of attorneys fees, from the date paid, and statutory
|
| 252 |
+
cost of this case. [Defendants] Perdue and [CBHB]
|
| 253 |
+
have been apportioned fault in the amount of forty-
|
| 254 |
+
five percent (45%), and judgment is awarded against
|
| 255 |
+
these Defendants specifically in the amount of
|
| 256 |
+
$60,118.53, and forty-five percent (45%) of statutory
|
| 257 |
+
costs.
|
| 258 |
+
|
| 259 |
+
The Plaintiffs subsequently filed a Motion to Alter or Amend
|
| 260 |
+
Judgment arguing that the trial court should app
|
test_data/legal_real/case_10574918.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LEGAL OPINION: Cirotto v. Am. Self Storage of Pickerington
|
| 2 |
+
COURT: Ohio Court of Appeals
|
| 3 |
+
DATE FILED: 2025-05-08
|
| 4 |
+
SNIPPET & HOLDING:
|
| 5 |
+
|
| 6 |
+
LEGAL OPINION: Cirotto v. Am. Self Storage of Pickerington
|
| 7 |
+
COURT: Ohio Court of Appeals
|
| 8 |
+
DATE FILED: 2025-05-08
|
| 9 |
+
SNIPPET & HOLDING:
|
| 10 |
+
|
| 11 |
+
LEGAL OPINION: Cirotto v. Am. Self Storage of Pickerington
|
| 12 |
+
COURT: Ohio Court of Appeals
|
| 13 |
+
DATE FILED: 2025-05-08
|
| 14 |
+
SNIPPET & HOLDING:
|
| 15 |
+
|
test_data/legal_real/case_10589788.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LEGAL OPINION: Curtin v. United Trademark Holdings, Inc.
|
| 2 |
+
COURT: Court of Appeals for the Federal Circuit
|
| 3 |
+
DATE FILED: 2025-05-22
|
| 4 |
+
SNIPPET & HOLDING:
|
| 5 |
+
|
| 6 |
+
LEGAL OPINION: Curtin v. United Trademark Holdings, Inc.
|
| 7 |
+
COURT: Court of Appeals for the Federal Circuit
|
| 8 |
+
DATE FILED: 2025-05-22
|
| 9 |
+
SNIPPET & HOLDING:
|
| 10 |
+
|
| 11 |
+
LEGAL OPINION: Curtin v. United Trademark Holdings, Inc.
|
| 12 |
+
COURT: Court of Appeals for the Federal Circuit
|
| 13 |
+
DATE FILED: 2025-05-22
|
| 14 |
+
SNIPPET & HOLDING:
|
| 15 |
+
|
test_data/legal_real/case_10639650.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LEGAL OPINION: James E. Carroll, Jr. v. Isle of Palms Pest Control, Inc.
|
| 2 |
+
COURT: Supreme Court of South Carolina
|
| 3 |
+
DATE FILED: 2025-07-23
|
| 4 |
+
SNIPPET & HOLDING:
|
| 5 |
+
|
| 6 |
+
LEGAL OPINION: James E. Carroll, Jr. v. Isle of Palms Pest Control, Inc.
|
| 7 |
+
COURT: Supreme Court of South Carolina
|
| 8 |
+
DATE FILED: 2025-07-23
|
| 9 |
+
SNIPPET & HOLDING:
|
| 10 |
+
|
| 11 |
+
LEGAL OPINION: James E. Carroll, Jr. v. Isle of Palms Pest Control, Inc.
|
| 12 |
+
COURT: Supreme Court of South Carolina
|
| 13 |
+
DATE FILED: 2025-07-23
|
| 14 |
+
SNIPPET & HOLDING:
|
| 15 |
+
|
test_data/legal_real/case_10640482.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LEGAL OPINION: Jessica v. Ohio Dept. of Job & Family Servs.
|
| 2 |
+
COURT: Ohio Court of Appeals
|
| 3 |
+
DATE FILED: 2025-07-24
|
| 4 |
+
SNIPPET & HOLDING:
|
| 5 |
+
|
| 6 |
+
LEGAL OPINION: Jessica v. Ohio Dept. of Job & Family Servs.
|
| 7 |
+
COURT: Ohio Court of Appeals
|
| 8 |
+
DATE FILED: 2025-07-24
|
| 9 |
+
SNIPPET & HOLDING:
|
| 10 |
+
|
| 11 |
+
LEGAL OPINION: Jessica v. Ohio Dept. of Job & Family Servs.
|
| 12 |
+
COURT: Ohio Court of Appeals
|
| 13 |
+
DATE FILED: 2025-07-24
|
| 14 |
+
SNIPPET & HOLDING:
|
| 15 |
+
|