ain-research 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ain_research-0.1.1/.gitignore +6 -0
- ain_research-0.1.1/PKG-INFO +75 -0
- ain_research-0.1.1/README.md +63 -0
- ain_research-0.1.1/ain_research/__init__.py +4 -0
- ain_research-0.1.1/ain_research/cli.py +42 -0
- ain_research-0.1.1/ain_research/config.py +60 -0
- ain_research-0.1.1/ain_research/core/__init__.py +1 -0
- ain_research-0.1.1/ain_research/core/db_manager.py +246 -0
- ain_research-0.1.1/ain_research/core/orchestrator.py +68 -0
- ain_research-0.1.1/ain_research/core/organize.py +38 -0
- ain_research-0.1.1/ain_research/daemon.py +204 -0
- ain_research-0.1.1/ain_research/mcp_server.py +77 -0
- ain_research-0.1.1/pyproject.toml +26 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ain-research
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: AIN Infinite Research Daemon. Engineered to sit on top of LLMs to autonomously guide research, provide storage, and retrieval functionality via CLI and MCP.
|
|
5
|
+
Author: That-Tech-Geek
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Requires-Dist: mcp>=1.2.0
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# AIN Research Daemon Library
|
|
14
|
+
|
|
15
|
+
Welcome to the **AIN Infinite Research Daemon** (`ain-research`), an open-source Python library built by **That-Tech-Geek**. This library provides the core infrastructure for an Autonomous Intelligence Network (AIN), sitting seamlessly on top of LLMs to guide, automate, and orchestrate open-ended domain research.
|
|
16
|
+
|
|
17
|
+
## What is it?
|
|
18
|
+
|
|
19
|
+
`ain-research` is a sophisticated pipeline for automated research ingestion, storage, and retrieval. It allows LLMs and autonomous agents to asynchronously fetch papers from ArXiv, crawl open-source repositories from GitHub, and store them securely into a local SQLite queue before finally syncing them into a hyper-fast, modular knowledge base (the Vault).
|
|
20
|
+
|
|
21
|
+
Key capabilities include:
|
|
22
|
+
- **Infinite Research Daemon**: Time-boxed, autonomous daemon that fetches domain-specific research using intelligent rate-limiting and rotating APIs.
|
|
23
|
+
- **LLM Steering Integration**: It is engineered to sit on top of LLMs. You can use the CLI or built-in **MCP Server** to dynamically add new search queries, refine categorizations, and prioritize processing.
|
|
24
|
+
- **Atomic Concurrency Control**: Uses OS-level kernel FileLocks to ensure zero data corruption during heavy concurrent reading/writing by different agents.
|
|
25
|
+
- **Model Context Protocol (MCP)**: Directly expose your AIN Second Brain to any MCP-compatible LLM client (like Claude Desktop or other agents), allowing them to queue research, check system health, and compile the wiki on demand.
|
|
26
|
+
|
|
27
|
+
## Use Cases
|
|
28
|
+
|
|
29
|
+
1. **Automated Overnight Crawling**
|
|
30
|
+
Let the daemon run overnight (`ain-daemon`). It crawls ArXiv and GitHub based on your configured categories (e.g., Quant Finance, LLMs) and stores the results securely. By the time you wake up, your Vault is packed with fresh, tagged Markdown files.
|
|
31
|
+
|
|
32
|
+
2. **LLM-Guided Research**
|
|
33
|
+
If you have a primary agent researching "Agentic Frameworks", the agent can use the MCP server (`ain-mcp`) to dynamically add "Agentic Frameworks" to the daytime priority ingestion queue. The daemon will immediately fetch relevant papers and repos, syncing them into the Vault for the LLM to read.
|
|
34
|
+
|
|
35
|
+
3. **Hyper-fast Retrieval**
|
|
36
|
+
The core indexing engine (`ain compile`) resolves thousands of nodes into lightweight Maps of Content (MOCs) and exact tag inversions, enabling LLMs to quickly query specific subjects without context-window bloat.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install ain-research
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
### 1. Configure Workspace
|
|
47
|
+
By default, the package uses `~/.ain/` as the root workspace. You can override this using the `AIN_WORKSPACE` environment variable.
|
|
48
|
+
|
|
49
|
+
### 2. Start the Daemon
|
|
50
|
+
```bash
|
|
51
|
+
ain-daemon
|
|
52
|
+
```
|
|
53
|
+
*Runs the overnight crawler. By default, it operates actively between 22:00 and 23:59.*
|
|
54
|
+
|
|
55
|
+
### 3. Add to Queue via CLI
|
|
56
|
+
```bash
|
|
57
|
+
ain queue add --arxiv 2305.14314
|
|
58
|
+
ain queue add --github karpathy/nanoGPT
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 4. Compile the Knowledge Base
|
|
62
|
+
```bash
|
|
63
|
+
ain compile
|
|
64
|
+
```
|
|
65
|
+
*Generates the Maps of Content (MOCs), Mermaid network graphs, and visualizer data.*
|
|
66
|
+
|
|
67
|
+
### 5. Launch MCP Server
|
|
68
|
+
To allow LLMs to natively hook into the daemon's research capabilities:
|
|
69
|
+
```bash
|
|
70
|
+
ain-mcp
|
|
71
|
+
```
|
|
72
|
+
*(Configure this in your LLM client's MCP configuration settings).*
|
|
73
|
+
|
|
74
|
+
## Author
|
|
75
|
+
**That-Tech-Geek**
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# AIN Research Daemon Library
|
|
2
|
+
|
|
3
|
+
Welcome to the **AIN Infinite Research Daemon** (`ain-research`), an open-source Python library built by **That-Tech-Geek**. This library provides the core infrastructure for an Autonomous Intelligence Network (AIN), sitting seamlessly on top of LLMs to guide, automate, and orchestrate open-ended domain research.
|
|
4
|
+
|
|
5
|
+
## What is it?
|
|
6
|
+
|
|
7
|
+
`ain-research` is a sophisticated pipeline for automated research ingestion, storage, and retrieval. It allows LLMs and autonomous agents to asynchronously fetch papers from ArXiv, crawl open-source repositories from GitHub, and store them securely into a local SQLite queue before finally syncing them into a hyper-fast, modular knowledge base (the Vault).
|
|
8
|
+
|
|
9
|
+
Key capabilities include:
|
|
10
|
+
- **Infinite Research Daemon**: Time-boxed, autonomous daemon that fetches domain-specific research using intelligent rate-limiting and rotating APIs.
|
|
11
|
+
- **LLM Steering Integration**: It is engineered to sit on top of LLMs. You can use the CLI or built-in **MCP Server** to dynamically add new search queries, refine categorizations, and prioritize processing.
|
|
12
|
+
- **Atomic Concurrency Control**: Uses OS-level kernel FileLocks to ensure zero data corruption during heavy concurrent reading/writing by different agents.
|
|
13
|
+
- **Model Context Protocol (MCP)**: Directly expose your AIN Second Brain to any MCP-compatible LLM client (like Claude Desktop or other agents), allowing them to queue research, check system health, and compile the wiki on demand.
|
|
14
|
+
|
|
15
|
+
## Use Cases
|
|
16
|
+
|
|
17
|
+
1. **Automated Overnight Crawling**
|
|
18
|
+
Let the daemon run overnight (`ain-daemon`). It crawls ArXiv and GitHub based on your configured categories (e.g., Quant Finance, LLMs) and stores the results securely. By the time you wake up, your Vault is packed with fresh, tagged Markdown files.
|
|
19
|
+
|
|
20
|
+
2. **LLM-Guided Research**
|
|
21
|
+
If you have a primary agent researching "Agentic Frameworks", the agent can use the MCP server (`ain-mcp`) to dynamically add "Agentic Frameworks" to the daytime priority ingestion queue. The daemon will immediately fetch relevant papers and repos, syncing them into the Vault for the LLM to read.
|
|
22
|
+
|
|
23
|
+
3. **Hyper-fast Retrieval**
|
|
24
|
+
The core indexing engine (`ain compile`) resolves thousands of nodes into lightweight Maps of Content (MOCs) and exact tag inversions, enabling LLMs to quickly query specific subjects without context-window bloat.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install ain-research
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
### 1. Configure Workspace
|
|
35
|
+
By default, the package uses `~/.ain/` as the root workspace. You can override this using the `AIN_WORKSPACE` environment variable.
|
|
36
|
+
|
|
37
|
+
### 2. Start the Daemon
|
|
38
|
+
```bash
|
|
39
|
+
ain-daemon
|
|
40
|
+
```
|
|
41
|
+
*Runs the overnight crawler. By default, it operates actively between 22:00 and 23:59.*
|
|
42
|
+
|
|
43
|
+
### 3. Add to Queue via CLI
|
|
44
|
+
```bash
|
|
45
|
+
ain queue add --arxiv 2305.14314
|
|
46
|
+
ain queue add --github karpathy/nanoGPT
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 4. Compile the Knowledge Base
|
|
50
|
+
```bash
|
|
51
|
+
ain compile
|
|
52
|
+
```
|
|
53
|
+
*Generates the Maps of Content (MOCs), Mermaid network graphs, and visualizer data.*
|
|
54
|
+
|
|
55
|
+
### 5. Launch MCP Server
|
|
56
|
+
To allow LLMs to natively hook into the daemon's research capabilities:
|
|
57
|
+
```bash
|
|
58
|
+
ain-mcp
|
|
59
|
+
```
|
|
60
|
+
*(Configure this in your LLM client's MCP configuration settings).*
|
|
61
|
+
|
|
62
|
+
## Author
|
|
63
|
+
**That-Tech-Geek**
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
from ain_research.core import db_manager, orchestrator
|
|
4
|
+
from ain_research.config import ensure_workspace
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
parser = argparse.ArgumentParser(description="AIN Infinite Research CLI")
|
|
8
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
9
|
+
|
|
10
|
+
# `ain compile`
|
|
11
|
+
compile_parser = subparsers.add_parser("compile", help="Compile the Vault Index and Tags")
|
|
12
|
+
|
|
13
|
+
# `ain queue`
|
|
14
|
+
queue_parser = subparsers.add_parser("queue", help="Manage the Ingestion Queue")
|
|
15
|
+
queue_subparsers = queue_parser.add_subparsers(dest="queue_command")
|
|
16
|
+
|
|
17
|
+
queue_add = queue_subparsers.add_parser("add", help="Add to queue")
|
|
18
|
+
queue_add.add_argument("--arxiv", type=str, help="ArXiv ID")
|
|
19
|
+
|
|
20
|
+
queue_list = queue_subparsers.add_parser("list", help="List pending queue items")
|
|
21
|
+
|
|
22
|
+
args = parser.parse_args()
|
|
23
|
+
ensure_workspace()
|
|
24
|
+
|
|
25
|
+
if args.command == "compile":
|
|
26
|
+
orchestrator.compile_wiki()
|
|
27
|
+
elif args.command == "queue":
|
|
28
|
+
if args.queue_command == "add":
|
|
29
|
+
if args.arxiv:
|
|
30
|
+
identifier = f"arxiv_{args.arxiv}"
|
|
31
|
+
db_manager.enqueue_item("arxiv", identifier, f"ArXiv {args.arxiv}", "", ["arxiv", "queued"], [f"https://arxiv.org/abs/{args.arxiv}"])
|
|
32
|
+
print(f"[+] Added {args.arxiv} to ingestion queue.")
|
|
33
|
+
elif args.queue_command == "list":
|
|
34
|
+
pending = db_manager.get_pending_queue(limit=50)
|
|
35
|
+
print(f"Pending Items: {len(pending)}")
|
|
36
|
+
for item in pending:
|
|
37
|
+
print(f" - [{item['item_type'].upper()}] {item['identifier']} (retries: {item['retry_count']})")
|
|
38
|
+
else:
|
|
39
|
+
parser.print_help()
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
main()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
# Base configuration for AIN
|
|
6
|
+
DEFAULT_WORKSPACE = os.path.expanduser("~/.ain")
|
|
7
|
+
AIN_WORKSPACE = os.environ.get("AIN_WORKSPACE", DEFAULT_WORKSPACE)
|
|
8
|
+
|
|
9
|
+
CRAWL_START_TIME = os.environ.get("CRAWL_START_TIME", "22:00")
|
|
10
|
+
CRAWL_END_TIME = os.environ.get("CRAWL_END_TIME", "23:59:59")
|
|
11
|
+
|
|
12
|
+
# Sub-directories
|
|
13
|
+
WIKI_DIR = os.path.join(AIN_WORKSPACE, "vault", "wiki")
|
|
14
|
+
DB_DIR = os.path.join(AIN_WORKSPACE, "data")
|
|
15
|
+
|
|
16
|
+
FOLDERS = {
|
|
17
|
+
"inbox": os.path.join(WIKI_DIR, "01_Inbox"),
|
|
18
|
+
"quant_finance": os.path.join(WIKI_DIR, "02_Research", "Quant_Finance"),
|
|
19
|
+
"machine_learning": os.path.join(WIKI_DIR, "02_Research", "Machine_Learning"),
|
|
20
|
+
"technology": os.path.join(WIKI_DIR, "02_Research", "Technology"),
|
|
21
|
+
"mathematics": os.path.join(WIKI_DIR, "02_Research", "Mathematics"),
|
|
22
|
+
"quantum_physics": os.path.join(WIKI_DIR, "02_Research", "Quantum_Physics"),
|
|
23
|
+
"economics": os.path.join(WIKI_DIR, "03_Research", "Economics"),
|
|
24
|
+
"business": os.path.join(WIKI_DIR, "03_Research", "Business"),
|
|
25
|
+
"vc_sourcing": os.path.join(WIKI_DIR, "04_VC_Sourcing"),
|
|
26
|
+
"publications": os.path.join(WIKI_DIR, "05_Publications")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def ensure_workspace():
|
|
30
|
+
"""Ensure all required workspace directories exist."""
|
|
31
|
+
os.makedirs(WIKI_DIR, exist_ok=True)
|
|
32
|
+
os.makedirs(DB_DIR, exist_ok=True)
|
|
33
|
+
for folder_path in FOLDERS.values():
|
|
34
|
+
os.makedirs(folder_path, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
# Default categories
|
|
37
|
+
DEFAULT_ARXIV_CATEGORIES = {
|
|
38
|
+
"Finance": {"query": "cat:q-fin.*", "tags": ["finance", "quant", "arxiv"]},
|
|
39
|
+
"Trading_Algorithms": {"query": "cat:q-fin.TR OR cat:q-fin.CP OR cat:q-fin.MF", "tags": ["trading-algorithms", "algorithmic-trading", "quant", "arxiv"]},
|
|
40
|
+
"Technology": {"query": "cat:cs.CR OR cat:cs.DC OR cat:cs.NI OR cat:cs.SE OR cat:cs.SY", "tags": ["technology", "computer-science", "arxiv"]},
|
|
41
|
+
"Machine_Learning": {"query": "cat:cs.LG OR cat:cs.AI OR cat:stat.ML", "tags": ["machine-learning", "ai", "arxiv"]},
|
|
42
|
+
"Business_Econ": {"query": "cat:econ.* OR cat:q-fin.EC", "tags": ["economics", "business", "arxiv"]},
|
|
43
|
+
"Mathematics": {"query": "cat:math.*", "tags": ["math", "theory", "arxiv"]},
|
|
44
|
+
"Quantum_Physics": {"query": "cat:quant-ph", "tags": ["physics", "quantum", "arxiv"]}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def get_arxiv_categories():
|
|
48
|
+
config_file = os.path.join(AIN_WORKSPACE, "arxiv_categories.json")
|
|
49
|
+
if os.path.exists(config_file):
|
|
50
|
+
try:
|
|
51
|
+
with open(config_file, "r") as f:
|
|
52
|
+
return json.load(f)
|
|
53
|
+
except:
|
|
54
|
+
pass
|
|
55
|
+
return DEFAULT_ARXIV_CATEGORIES
|
|
56
|
+
|
|
57
|
+
def save_arxiv_categories(cats):
|
|
58
|
+
os.makedirs(AIN_WORKSPACE, exist_ok=True)
|
|
59
|
+
with open(os.path.join(AIN_WORKSPACE, "arxiv_categories.json"), "w") as f:
|
|
60
|
+
json.dump(cats, f, indent=4)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Core functionalities
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import json
|
|
5
|
+
import sqlite3
|
|
6
|
+
import traceback
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from ain_research.config import DB_DIR, WIKI_DIR
|
|
10
|
+
|
|
11
|
+
VAULT_DIR = os.path.dirname(WIKI_DIR)
|
|
12
|
+
LOCK_FILE = os.path.join(VAULT_DIR, "ain_vault.lock")
|
|
13
|
+
DB_FILE = os.path.join(DB_DIR, "ain_system.db")
|
|
14
|
+
|
|
15
|
+
# Ensure vault and db directories exist
|
|
16
|
+
os.makedirs(VAULT_DIR, exist_ok=True)
|
|
17
|
+
os.makedirs(DB_DIR, exist_ok=True)
|
|
18
|
+
|
|
19
|
+
# --- 1. OS-Level Kernel Atomic File Lock ---
|
|
20
|
+
class FileLock:
|
|
21
|
+
"""
|
|
22
|
+
Highly robust, OS-level atomic file lock context manager.
|
|
23
|
+
Uses standard library os.open with O_CREAT and O_EXCL to achieve atomic lock acquisition.
|
|
24
|
+
"""
|
|
25
|
+
def __init__(self, lock_file_path=LOCK_FILE, timeout=15):
|
|
26
|
+
self.lock_file_path = lock_file_path
|
|
27
|
+
self.timeout = timeout
|
|
28
|
+
self.has_lock = False
|
|
29
|
+
|
|
30
|
+
def acquire(self):
|
|
31
|
+
start_time = time.time()
|
|
32
|
+
while time.time() - start_time < self.timeout:
|
|
33
|
+
try:
|
|
34
|
+
fd = os.open(self.lock_file_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
35
|
+
os.close(fd)
|
|
36
|
+
self.has_lock = True
|
|
37
|
+
return True
|
|
38
|
+
except FileExistsError:
|
|
39
|
+
time.sleep(0.05)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
print(f"[!] Warning: Lock acquisition encountered unexpected error: {e}", file=sys.stderr)
|
|
42
|
+
time.sleep(0.05)
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
def release(self):
|
|
46
|
+
if self.has_lock:
|
|
47
|
+
try:
|
|
48
|
+
os.remove(self.lock_file_path)
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
self.has_lock = False
|
|
52
|
+
|
|
53
|
+
def __enter__(self):
|
|
54
|
+
if not self.acquire():
|
|
55
|
+
print(f"[!] Lock acquisition timed out after {self.timeout} seconds for {self.lock_file_path}. Proceeding under unsafe fallback mode.", file=sys.stderr)
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
59
|
+
self.release()
|
|
60
|
+
|
|
61
|
+
# --- 2. SQLite Database Queue & Error Manager ---
|
|
62
|
+
def get_db_connection():
|
|
63
|
+
"""Returns an active SQLite database connection with a high timeout to handle locks gracefully."""
|
|
64
|
+
conn = sqlite3.connect(DB_FILE, timeout=10.0)
|
|
65
|
+
conn.row_factory = sqlite3.Row
|
|
66
|
+
return conn
|
|
67
|
+
|
|
68
|
+
def init_db():
|
|
69
|
+
"""Initializes SQLite queue and error monitoring tables."""
|
|
70
|
+
conn = get_db_connection()
|
|
71
|
+
cursor = conn.cursor()
|
|
72
|
+
|
|
73
|
+
cursor.execute("""
|
|
74
|
+
CREATE TABLE IF NOT EXISTS ingestion_queue (
|
|
75
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
76
|
+
item_type TEXT NOT NULL,
|
|
77
|
+
identifier TEXT UNIQUE NOT NULL,
|
|
78
|
+
title TEXT NOT NULL,
|
|
79
|
+
content TEXT NOT NULL,
|
|
80
|
+
tags TEXT NOT NULL,
|
|
81
|
+
sources TEXT NOT NULL,
|
|
82
|
+
category TEXT,
|
|
83
|
+
status TEXT DEFAULT 'pending',
|
|
84
|
+
retry_count INTEGER DEFAULT 0,
|
|
85
|
+
error_log TEXT,
|
|
86
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
87
|
+
processed_at TIMESTAMP
|
|
88
|
+
)
|
|
89
|
+
""")
|
|
90
|
+
|
|
91
|
+
cursor.execute("""
|
|
92
|
+
CREATE TABLE IF NOT EXISTS system_errors (
|
|
93
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
94
|
+
component TEXT NOT NULL,
|
|
95
|
+
error_message TEXT NOT NULL,
|
|
96
|
+
traceback TEXT NOT NULL,
|
|
97
|
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
98
|
+
)
|
|
99
|
+
""")
|
|
100
|
+
|
|
101
|
+
conn.commit()
|
|
102
|
+
conn.close()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def enqueue_item(item_type, identifier, title, content, tags, sources, category=None):
|
|
106
|
+
"""
|
|
107
|
+
Safely transactionally enqueues a fetched research item.
|
|
108
|
+
Returns True if successfully enqueued, False if it was already processed or queued.
|
|
109
|
+
"""
|
|
110
|
+
init_db()
|
|
111
|
+
conn = get_db_connection()
|
|
112
|
+
cursor = conn.cursor()
|
|
113
|
+
|
|
114
|
+
tags_json = json.dumps(tags)
|
|
115
|
+
sources_json = json.dumps(sources)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
cursor.execute("""
|
|
119
|
+
INSERT INTO ingestion_queue (item_type, identifier, title, content, tags, sources, category, status)
|
|
120
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')
|
|
121
|
+
""", (item_type, identifier, title, content, tags_json, sources_json, category))
|
|
122
|
+
conn.commit()
|
|
123
|
+
return True
|
|
124
|
+
except sqlite3.IntegrityError:
|
|
125
|
+
return False
|
|
126
|
+
except Exception as e:
|
|
127
|
+
log_system_error("QueueManager", f"Failed to enqueue {identifier}: {e}", traceback.format_exc())
|
|
128
|
+
return False
|
|
129
|
+
finally:
|
|
130
|
+
conn.close()
|
|
131
|
+
|
|
132
|
+
def log_system_error(component, error_message, traceback_str=None):
|
|
133
|
+
"""Logs a system or crawl failure to the SQLite error database."""
|
|
134
|
+
init_db()
|
|
135
|
+
try:
|
|
136
|
+
conn = get_db_connection()
|
|
137
|
+
cursor = conn.cursor()
|
|
138
|
+
tb = traceback_str if traceback_str else "".join(traceback.format_stack())
|
|
139
|
+
cursor.execute("""
|
|
140
|
+
INSERT INTO system_errors (component, error_message, traceback)
|
|
141
|
+
VALUES (?, ?, ?)
|
|
142
|
+
""", (component, error_message, tb))
|
|
143
|
+
conn.commit()
|
|
144
|
+
conn.close()
|
|
145
|
+
print(f"[!] Database logged error inside component '{component}': {error_message}", file=sys.stderr)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
print(f"[!!] Critical Error writing to error database: {e}", file=sys.stderr)
|
|
148
|
+
|
|
149
|
+
def get_pending_queue(limit=100):
|
|
150
|
+
"""Retrieves list of pending queue items for processing."""
|
|
151
|
+
init_db()
|
|
152
|
+
conn = get_db_connection()
|
|
153
|
+
cursor = conn.cursor()
|
|
154
|
+
cursor.execute("""
|
|
155
|
+
SELECT id, item_type, identifier, title, content, tags, sources, category, retry_count
|
|
156
|
+
FROM ingestion_queue
|
|
157
|
+
WHERE status = 'pending' AND retry_count < 3
|
|
158
|
+
ORDER BY created_at ASC
|
|
159
|
+
LIMIT ?
|
|
160
|
+
""", (limit,))
|
|
161
|
+
rows = cursor.fetchall()
|
|
162
|
+
conn.close()
|
|
163
|
+
|
|
164
|
+
items = []
|
|
165
|
+
for r in rows:
|
|
166
|
+
items.append({
|
|
167
|
+
"id": r["id"],
|
|
168
|
+
"item_type": r["item_type"],
|
|
169
|
+
"identifier": r["identifier"],
|
|
170
|
+
"title": r["title"],
|
|
171
|
+
"content": r["content"],
|
|
172
|
+
"tags": json.loads(r["tags"]),
|
|
173
|
+
"sources": json.loads(r["sources"]),
|
|
174
|
+
"category": r["category"],
|
|
175
|
+
"retry_count": r["retry_count"]
|
|
176
|
+
})
|
|
177
|
+
return items
|
|
178
|
+
|
|
179
|
+
def mark_item_processed(item_id):
|
|
180
|
+
"""Marks a queue item as successfully processed."""
|
|
181
|
+
conn = get_db_connection()
|
|
182
|
+
cursor = conn.cursor()
|
|
183
|
+
cursor.execute("""
|
|
184
|
+
UPDATE ingestion_queue
|
|
185
|
+
SET status = 'processed', processed_at = CURRENT_TIMESTAMP
|
|
186
|
+
WHERE id = ?
|
|
187
|
+
""", (item_id,))
|
|
188
|
+
conn.commit()
|
|
189
|
+
conn.close()
|
|
190
|
+
|
|
191
|
+
def mark_item_failed(item_id, error_msg):
|
|
192
|
+
"""Increments retry counts and logs failure details on a queue item."""
|
|
193
|
+
conn = get_db_connection()
|
|
194
|
+
cursor = conn.cursor()
|
|
195
|
+
cursor.execute("""
|
|
196
|
+
UPDATE ingestion_queue
|
|
197
|
+
SET retry_count = retry_count + 1, error_log = ?
|
|
198
|
+
WHERE id = ?
|
|
199
|
+
""", (error_msg, item_id))
|
|
200
|
+
|
|
201
|
+
cursor.execute("SELECT retry_count, identifier, item_type FROM ingestion_queue WHERE id = ?", (item_id,))
|
|
202
|
+
row = cursor.fetchone()
|
|
203
|
+
dead_letter = False
|
|
204
|
+
dead_letter_info = None
|
|
205
|
+
if row and row["retry_count"] >= 3:
|
|
206
|
+
cursor.execute("UPDATE ingestion_queue SET status = 'failed' WHERE id = ?", (item_id,))
|
|
207
|
+
dead_letter = True
|
|
208
|
+
dead_letter_info = (row['item_type'], row['identifier'])
|
|
209
|
+
|
|
210
|
+
conn.commit()
|
|
211
|
+
conn.close()
|
|
212
|
+
|
|
213
|
+
if dead_letter and dead_letter_info:
|
|
214
|
+
log_system_error(
|
|
215
|
+
f"DeadLetterQueue_{dead_letter_info[0]}",
|
|
216
|
+
f"Ingestion failed permanently for {dead_letter_info[1]} after 3 attempts. Error: {error_msg}"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def get_system_metrics():
|
|
220
|
+
"""Gathers high-signal health metrics for the ain status CLI."""
|
|
221
|
+
init_db()
|
|
222
|
+
conn = get_db_connection()
|
|
223
|
+
cursor = conn.cursor()
|
|
224
|
+
|
|
225
|
+
metrics = {}
|
|
226
|
+
try:
|
|
227
|
+
cursor.execute("SELECT status, count(*) as count FROM ingestion_queue GROUP BY status")
|
|
228
|
+
q_stats = {row["status"]: row["count"] for row in cursor.fetchall()}
|
|
229
|
+
metrics["queue_pending"] = q_stats.get("pending", 0)
|
|
230
|
+
metrics["queue_processed"] = q_stats.get("processed", 0)
|
|
231
|
+
metrics["queue_failed"] = q_stats.get("failed", 0)
|
|
232
|
+
|
|
233
|
+
cursor.execute("SELECT count(*) as count FROM system_errors")
|
|
234
|
+
metrics["total_errors"] = cursor.fetchone()["count"]
|
|
235
|
+
|
|
236
|
+
cursor.execute("SELECT component, error_message, timestamp FROM system_errors ORDER BY timestamp DESC LIMIT 5")
|
|
237
|
+
metrics["recent_errors"] = [dict(row) for row in cursor.fetchall()]
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
metrics["db_error"] = str(e)
|
|
241
|
+
finally:
|
|
242
|
+
conn.close()
|
|
243
|
+
|
|
244
|
+
return metrics
|
|
245
|
+
|
|
246
|
+
init_db()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from ain_research.config import WIKI_DIR, FOLDERS
|
|
6
|
+
from ain_research.core import db_manager
|
|
7
|
+
|
|
8
|
+
def clean_title_to_filename(title):
|
|
9
|
+
clean = re.sub(r"[^\w\s\-]", "", title)
|
|
10
|
+
clean = re.sub(r"[\s\-]+", "_", clean)
|
|
11
|
+
return clean.strip("_")
|
|
12
|
+
|
|
13
|
+
def compile_wiki():
|
|
14
|
+
print("[*] Launching AIN Wiki Compiler...")
|
|
15
|
+
|
|
16
|
+
with db_manager.FileLock():
|
|
17
|
+
all_pages = {}
|
|
18
|
+
|
|
19
|
+
# Step 1: Scan all files
|
|
20
|
+
for root, _, files in os.walk(WIKI_DIR):
|
|
21
|
+
for file in files:
|
|
22
|
+
if file.endswith(".md") and not file.endswith("_MOC.md") and file != "INDEX.md":
|
|
23
|
+
file_path = os.path.join(root, file)
|
|
24
|
+
slug = os.path.splitext(file)[0]
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
28
|
+
content = f.read()
|
|
29
|
+
except Exception as e:
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
title_match = re.search(r"^#\s+(.+)$", content, re.MULTILINE)
|
|
33
|
+
title = title_match.group(1).strip() if title_match else slug.replace("_", " ")
|
|
34
|
+
|
|
35
|
+
tags = []
|
|
36
|
+
tags_match = re.search(r"tags:\s*\[(.*?)\]", content)
|
|
37
|
+
if tags_match:
|
|
38
|
+
tags = [t.strip().strip('"').strip("'") for t in tags_match.group(1).split(",") if t.strip()]
|
|
39
|
+
|
|
40
|
+
all_pages[slug] = {
|
|
41
|
+
"title": title,
|
|
42
|
+
"slug": slug,
|
|
43
|
+
"file_path": file_path,
|
|
44
|
+
"tags": tags
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Step 2: Build tag index
|
|
48
|
+
tag_index = {}
|
|
49
|
+
for slug, info in all_pages.items():
|
|
50
|
+
for tag in info["tags"]:
|
|
51
|
+
if tag not in tag_index:
|
|
52
|
+
tag_index[tag] = []
|
|
53
|
+
tag_index[tag].append(slug)
|
|
54
|
+
|
|
55
|
+
tag_index_path = os.path.join(os.path.dirname(WIKI_DIR), "tag_index.json")
|
|
56
|
+
with open(tag_index_path, "w", encoding="utf-8") as f:
|
|
57
|
+
json.dump(tag_index, f, indent=2)
|
|
58
|
+
|
|
59
|
+
# Step 3: Build simple INDEX.md
|
|
60
|
+
index_content = f"# AIN Second Brain Index\n\nGenerated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n## Recent Nodes\n"
|
|
61
|
+
|
|
62
|
+
for slug, info in list(all_pages.items())[:50]:
|
|
63
|
+
index_content += f"- [[{slug}]] - {info['title']}\n"
|
|
64
|
+
|
|
65
|
+
with open(os.path.join(WIKI_DIR, "INDEX.md"), "w", encoding="utf-8") as f:
|
|
66
|
+
f.write(index_content)
|
|
67
|
+
|
|
68
|
+
print(f"[+] Compiled {len(all_pages)} research pages into the index.")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from ain_research.config import FOLDERS
|
|
3
|
+
from ain_research.core import db_manager
|
|
4
|
+
|
|
5
|
+
def organize_inbox():
|
|
6
|
+
"""Reads pending items from SQLite queue and writes them to the Inbox."""
|
|
7
|
+
print("[*] Organizing Inbox from database queue...")
|
|
8
|
+
pending = db_manager.get_pending_queue(limit=500)
|
|
9
|
+
|
|
10
|
+
if not pending:
|
|
11
|
+
print(" -> No pending items to organize.")
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
processed = 0
|
|
15
|
+
inbox_dir = FOLDERS["inbox"]
|
|
16
|
+
|
|
17
|
+
with db_manager.FileLock():
|
|
18
|
+
for item in pending:
|
|
19
|
+
identifier = item["identifier"]
|
|
20
|
+
content = item["content"]
|
|
21
|
+
|
|
22
|
+
# Sanitize filename
|
|
23
|
+
safe_filename = "".join([c if c.isalnum() or c in ['-', '_'] else "_" for c in identifier]) + ".md"
|
|
24
|
+
file_path = os.path.join(inbox_dir, safe_filename)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
28
|
+
f.write(content)
|
|
29
|
+
db_manager.mark_item_processed(item["id"])
|
|
30
|
+
processed += 1
|
|
31
|
+
except Exception as e:
|
|
32
|
+
print(f"[!] Error writing {identifier} to disk: {e}")
|
|
33
|
+
db_manager.mark_item_failed(item["id"], str(e))
|
|
34
|
+
|
|
35
|
+
print(f"[+] Inbox organization complete. Wrote {processed} new nodes.")
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
organize_inbox()
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import json
|
|
5
|
+
import urllib.request
|
|
6
|
+
import urllib.parse
|
|
7
|
+
import urllib.error
|
|
8
|
+
import xml.etree.ElementTree as ET
|
|
9
|
+
import argparse
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
import traceback
|
|
12
|
+
|
|
13
|
+
from ain_research.config import (
|
|
14
|
+
AIN_WORKSPACE,
|
|
15
|
+
CRAWL_START_TIME,
|
|
16
|
+
CRAWL_END_TIME,
|
|
17
|
+
get_arxiv_categories,
|
|
18
|
+
save_arxiv_categories,
|
|
19
|
+
ensure_workspace
|
|
20
|
+
)
|
|
21
|
+
from ain_research.core import db_manager
|
|
22
|
+
|
|
23
|
+
# Reconfigure standard output encoding
|
|
24
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
25
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
26
|
+
|
|
27
|
+
# State Files
|
|
28
|
+
ARXIV_STATE_FILE = os.path.join(AIN_WORKSPACE, "daemon_state.json")
|
|
29
|
+
|
|
30
|
+
ARXIV_CATEGORIES = get_arxiv_categories()
|
|
31
|
+
EXISTING_RESEARCH_TITLES = set()
|
|
32
|
+
|
|
33
|
+
def is_in_active_window(start_time_str="22:00", end_time_str="23:59:59"):
|
|
34
|
+
now = datetime.now().time()
|
|
35
|
+
try:
|
|
36
|
+
start_time = datetime.strptime(start_time_str, "%H:%M").time()
|
|
37
|
+
except ValueError:
|
|
38
|
+
start_time = datetime.strptime(start_time_str, "%H:%M:%S").time()
|
|
39
|
+
try:
|
|
40
|
+
end_time = datetime.strptime(end_time_str, "%H:%M").time()
|
|
41
|
+
except ValueError:
|
|
42
|
+
end_time = datetime.strptime(end_time_str, "%H:%M:%S").time()
|
|
43
|
+
|
|
44
|
+
if start_time <= end_time:
|
|
45
|
+
return start_time <= now <= end_time
|
|
46
|
+
else:
|
|
47
|
+
return now >= start_time or now <= end_time
|
|
48
|
+
|
|
49
|
+
def load_arxiv_state():
|
|
50
|
+
if os.path.exists(ARXIV_STATE_FILE):
|
|
51
|
+
try:
|
|
52
|
+
with open(ARXIV_STATE_FILE, "r") as f:
|
|
53
|
+
state = json.load(f)
|
|
54
|
+
if state and isinstance(next(iter(state.values())), int):
|
|
55
|
+
state = {cat: {"latest_seen_id": "", "last_run": ""} for cat in ARXIV_CATEGORIES.keys()}
|
|
56
|
+
return state
|
|
57
|
+
except:
|
|
58
|
+
pass
|
|
59
|
+
return {cat: {"latest_seen_id": "", "last_run": ""} for cat in ARXIV_CATEGORIES.keys()}
|
|
60
|
+
|
|
61
|
+
def save_arxiv_state(state):
|
|
62
|
+
os.makedirs(AIN_WORKSPACE, exist_ok=True)
|
|
63
|
+
with open(ARXIV_STATE_FILE, "w") as f:
|
|
64
|
+
json.dump(state, f, indent=4)
|
|
65
|
+
|
|
66
|
+
def clean_filename(title):
|
|
67
|
+
clean = "".join([c if c.isalnum() or c.isspace() else "_" for c in title])
|
|
68
|
+
clean = clean.replace(" ", "_")
|
|
69
|
+
clean = "_".join(filter(None, clean.split("_")))
|
|
70
|
+
return clean[:100]
|
|
71
|
+
|
|
72
|
+
def trigger_sync(is_shutdown=False):
|
|
73
|
+
if is_shutdown:
|
|
74
|
+
print("\n[!] Graceful Shutdown Triggered! Running final AIN Sync...")
|
|
75
|
+
else:
|
|
76
|
+
print("[*] Triggering AIN Sync pipeline...")
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
from ain_research.core import organize, orchestrator
|
|
80
|
+
organize.organize_inbox()
|
|
81
|
+
orchestrator.compile_wiki()
|
|
82
|
+
print("[+] AIN Sync completed successfully.")
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print(f"[!] AIN Sync encountered an error: {e}")
|
|
85
|
+
traceback.print_exc()
|
|
86
|
+
|
|
87
|
+
def fetch_arxiv_papers(category, query_data, state_entry, batch_size=20):
|
|
88
|
+
print(f"[{datetime.now().strftime('%H:%M:%S')}] Daemon: Fetching ArXiv '{category}'...")
|
|
89
|
+
query = query_data["query"]
|
|
90
|
+
tags = query_data["tags"]
|
|
91
|
+
latest_seen_id = state_entry.get("latest_seen_id", "")
|
|
92
|
+
start_offset = 0
|
|
93
|
+
max_pages = 2
|
|
94
|
+
processed = 0
|
|
95
|
+
new_latest_id = None
|
|
96
|
+
stop_fetching = False
|
|
97
|
+
|
|
98
|
+
for page in range(max_pages):
|
|
99
|
+
url = f"https://export.arxiv.org/api/query?search_query={urllib.parse.quote(query)}&start={start_offset}&max_results={batch_size}&sortBy=submittedDate&sortOrder=descending"
|
|
100
|
+
req = urllib.request.Request(url, headers={'User-Agent': 'AIN-Daemon-Bot'})
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
response = urllib.request.urlopen(req)
|
|
104
|
+
xml_data = response.read()
|
|
105
|
+
root = ET.fromstring(xml_data)
|
|
106
|
+
ns = {'atom': 'http://www.w3.org/2005/Atom'}
|
|
107
|
+
entries = root.findall('atom:entry', ns)
|
|
108
|
+
|
|
109
|
+
if not entries:
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
if page == 0 and len(entries) > 0:
|
|
113
|
+
first_id = entries[0].find('atom:id', ns)
|
|
114
|
+
if first_id is not None and first_id.text:
|
|
115
|
+
new_latest_id = first_id.text.strip()
|
|
116
|
+
|
|
117
|
+
for entry in entries:
|
|
118
|
+
id_elem = entry.find('atom:id', ns)
|
|
119
|
+
link = id_elem.text.strip() if id_elem is not None else ""
|
|
120
|
+
|
|
121
|
+
if latest_seen_id and link == latest_seen_id:
|
|
122
|
+
stop_fetching = True
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
title_elem = entry.find('atom:title', ns)
|
|
126
|
+
title = title_elem.text.replace('\\n', ' ').strip() if title_elem is not None else "Untitled"
|
|
127
|
+
summary_elem = entry.find('atom:summary', ns)
|
|
128
|
+
summary = summary_elem.text.replace('\\n', ' ').strip() if summary_elem is not None else "No abstract."
|
|
129
|
+
|
|
130
|
+
safe_title = clean_filename(title)
|
|
131
|
+
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
132
|
+
|
|
133
|
+
md_content = f"""---
|
|
134
|
+
title: "{title.replace('"', "'")}"
|
|
135
|
+
date: "{date_str}"
|
|
136
|
+
tags: {json.dumps(tags)}
|
|
137
|
+
sources: ["{link}"]
|
|
138
|
+
---
|
|
139
|
+
# {title}
|
|
140
|
+
## Source: {link}
|
|
141
|
+
## Abstract
|
|
142
|
+
{summary}
|
|
143
|
+
"""
|
|
144
|
+
identifier = f"Daemon_ArXiv_{category}_{safe_title}"
|
|
145
|
+
success = db_manager.enqueue_item("arxiv", identifier, title, md_content, tags, [link], category)
|
|
146
|
+
if success:
|
|
147
|
+
processed += 1
|
|
148
|
+
|
|
149
|
+
if stop_fetching or len(entries) < batch_size:
|
|
150
|
+
break
|
|
151
|
+
|
|
152
|
+
start_offset += batch_size
|
|
153
|
+
time.sleep(2)
|
|
154
|
+
|
|
155
|
+
except Exception as e:
|
|
156
|
+
print(f" -> Error fetching ArXiv: {e}")
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
if new_latest_id:
|
|
160
|
+
state_entry["latest_seen_id"] = new_latest_id
|
|
161
|
+
state_entry["last_run"] = datetime.now().strftime("%Y-%m-%d")
|
|
162
|
+
|
|
163
|
+
return processed > 0
|
|
164
|
+
|
|
165
|
+
def main():
|
|
166
|
+
parser = argparse.ArgumentParser(description="AIN Research Ingestion Daemon")
|
|
167
|
+
parser.add_argument("--force", action="store_true", help="Force run outside active window")
|
|
168
|
+
parser.add_argument("--start-time", type=str, default=CRAWL_START_TIME)
|
|
169
|
+
parser.add_argument("--end-time", type=str, default=CRAWL_END_TIME)
|
|
170
|
+
args = parser.parse_args()
|
|
171
|
+
|
|
172
|
+
ensure_workspace()
|
|
173
|
+
arxiv_state = load_arxiv_state()
|
|
174
|
+
arxiv_keys = list(ARXIV_CATEGORIES.keys())
|
|
175
|
+
arxiv_idx = 0
|
|
176
|
+
|
|
177
|
+
while True:
|
|
178
|
+
try:
|
|
179
|
+
if not args.force and not is_in_active_window(args.start_time, args.end_time):
|
|
180
|
+
print(f"[*] Current time outside active window ({args.start_time}-{args.end_time}). Exiting gracefully...")
|
|
181
|
+
trigger_sync(is_shutdown=True)
|
|
182
|
+
sys.exit(0)
|
|
183
|
+
|
|
184
|
+
if arxiv_keys:
|
|
185
|
+
cat_name = arxiv_keys[arxiv_idx]
|
|
186
|
+
cat_data = ARXIV_CATEGORIES[cat_name]
|
|
187
|
+
state_entry = arxiv_state.get(cat_name, {"latest_seen_id": "", "last_run": ""})
|
|
188
|
+
|
|
189
|
+
fetch_arxiv_papers(cat_name, cat_data, state_entry)
|
|
190
|
+
arxiv_state[cat_name] = state_entry
|
|
191
|
+
save_arxiv_state(arxiv_state)
|
|
192
|
+
arxiv_idx = (arxiv_idx + 1) % len(arxiv_keys)
|
|
193
|
+
|
|
194
|
+
time.sleep(5)
|
|
195
|
+
|
|
196
|
+
except KeyboardInterrupt:
|
|
197
|
+
trigger_sync(is_shutdown=True)
|
|
198
|
+
break
|
|
199
|
+
except Exception as e:
|
|
200
|
+
print(f"[!] Daemon Error: {e}")
|
|
201
|
+
time.sleep(10)
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
main()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from mcp.server.fastmcp import FastMCP
|
|
2
|
+
from ain_research.core import db_manager, orchestrator
|
|
3
|
+
from ain_research.config import (
|
|
4
|
+
ensure_workspace,
|
|
5
|
+
get_arxiv_categories,
|
|
6
|
+
save_arxiv_categories
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
# Initialize FastMCP Server
|
|
10
|
+
mcp = FastMCP("AIN-Research-Daemon")
|
|
11
|
+
|
|
12
|
+
@mcp.tool()
|
|
13
|
+
def queue_arxiv(arxiv_id: str) -> str:
|
|
14
|
+
"""Queue an ArXiv paper for autonomous research ingestion."""
|
|
15
|
+
ensure_workspace()
|
|
16
|
+
identifier = f"arxiv_{arxiv_id}"
|
|
17
|
+
success = db_manager.enqueue_item(
|
|
18
|
+
"arxiv",
|
|
19
|
+
identifier,
|
|
20
|
+
f"ArXiv {arxiv_id}",
|
|
21
|
+
"",
|
|
22
|
+
["arxiv", "queued", "llm-guided"],
|
|
23
|
+
[f"https://arxiv.org/abs/{arxiv_id}"]
|
|
24
|
+
)
|
|
25
|
+
if success:
|
|
26
|
+
return f"Successfully queued ArXiv paper {arxiv_id}."
|
|
27
|
+
return f"Failed to queue {arxiv_id} (may already exist)."
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@mcp.tool()
|
|
32
|
+
def get_queue_status() -> str:
|
|
33
|
+
"""Check the status of the research ingestion queue."""
|
|
34
|
+
ensure_workspace()
|
|
35
|
+
pending = db_manager.get_pending_queue(limit=100)
|
|
36
|
+
metrics = db_manager.get_system_metrics()
|
|
37
|
+
|
|
38
|
+
status = f"Queue Metrics:\nPending: {metrics.get('queue_pending', 0)}\nProcessed: {metrics.get('queue_processed', 0)}\nFailed: {metrics.get('queue_failed', 0)}\n\n"
|
|
39
|
+
if pending:
|
|
40
|
+
status += "Next 5 items to process:\n"
|
|
41
|
+
for item in pending[:5]:
|
|
42
|
+
status += f"- [{item['item_type'].upper()}] {item['identifier']}\n"
|
|
43
|
+
return status
|
|
44
|
+
|
|
45
|
+
@mcp.tool()
|
|
46
|
+
def trigger_compile() -> str:
|
|
47
|
+
"""Trigger a compilation of the AIN knowledge base to update Maps of Content."""
|
|
48
|
+
ensure_workspace()
|
|
49
|
+
try:
|
|
50
|
+
orchestrator.compile_wiki()
|
|
51
|
+
return "Compilation completed successfully."
|
|
52
|
+
except Exception as e:
|
|
53
|
+
return f"Compilation failed: {e}"
|
|
54
|
+
|
|
55
|
+
@mcp.tool()
|
|
56
|
+
def add_arxiv_category(category_name: str, query: str, tags: list[str]) -> str:
|
|
57
|
+
"""
|
|
58
|
+
Add a new research category for the automated daemon to crawl on ArXiv.
|
|
59
|
+
Args:
|
|
60
|
+
category_name: Name of the category (e.g., 'Agentic_AI').
|
|
61
|
+
query: ArXiv search query (e.g., 'cat:cs.AI OR cat:cs.LG').
|
|
62
|
+
tags: List of tags to apply to downloaded papers.
|
|
63
|
+
"""
|
|
64
|
+
ensure_workspace()
|
|
65
|
+
cats = get_arxiv_categories()
|
|
66
|
+
cats[category_name] = {"query": query, "tags": tags}
|
|
67
|
+
save_arxiv_categories(cats)
|
|
68
|
+
return f"Added new ArXiv research trajectory: {category_name}"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main():
|
|
73
|
+
ensure_workspace()
|
|
74
|
+
mcp.run()
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
main()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ain-research"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "AIN Infinite Research Daemon. Engineered to sit on top of LLMs to autonomously guide research, provide storage, and retrieval functionality via CLI and MCP."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "That-Tech-Geek" }
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"mcp>=1.2.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
ain = "ain_research.cli:main"
|
|
25
|
+
ain-daemon = "ain_research.daemon:main"
|
|
26
|
+
ain-mcp = "ain_research.mcp_server:main"
|