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.
@@ -0,0 +1,6 @@
1
+ .env
2
+ *.log
3
+ __pycache__/
4
+ .git/
5
+ .vscode/
6
+ run_publish.bat
@@ -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,4 @@
1
+ """
2
+ AIN Research Library
3
+ """
4
+ __version__ = "0.1.1"
@@ -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"