antstudio 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
antstudio/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """Ant Studio — Build. Run. Control."""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,94 @@
1
+ """Backbone — runs on EVERY command automatically."""
2
+ import time, json, os, platform
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+
6
+ HISTORY_DIR = Path.home() / ".antstudio"
7
+ HISTORY_FILE = HISTORY_DIR / "history.json"
8
+
9
+ class Backbone:
10
+ def __init__(self, command: str):
11
+ self.command = command
12
+ self.start_time = time.time()
13
+ self.quality_scores = {}
14
+ self._guard = None
15
+ self.guard_active = False
16
+
17
+ def start(self):
18
+ try:
19
+ from antguard import Guard
20
+ self._guard = Guard(detect_outbound=True, runtime=True)
21
+ self._guard.__enter__()
22
+ self.guard_active = True
23
+ except ImportError:
24
+ pass
25
+ return self
26
+
27
+ def evaluate(self, step: str, output, context: str = "") -> dict:
28
+ text = str(output) if output else ""
29
+ if not text or len(text) < 3:
30
+ score = {"score": 0.0, "passed": False, "method": "empty"}
31
+ self.quality_scores[step] = score
32
+ return score
33
+ try:
34
+ from llmevalkit import Evaluator
35
+ ev = Evaluator(provider="none", preset="rag", threshold=0.5)
36
+ r = ev.evaluate(answer=text, context=context)
37
+ score = {"score": r.overall_score, "passed": r.passed, "method": "llmevalkit"}
38
+ except ImportError:
39
+ s = min(len(text) / 200, 0.7)
40
+ if context:
41
+ cw = set(context.lower().split()[:30])
42
+ ow = set(text.lower().split())
43
+ s += len(cw & ow) / max(len(cw), 1) * 0.3
44
+ s = min(round(s, 3), 1.0)
45
+ score = {"score": s, "passed": s >= 0.5, "method": "fallback"}
46
+ self.quality_scores[step] = score
47
+ return score
48
+
49
+ def route(self, path: str) -> str:
50
+ try:
51
+ from adaptive_intelligence import AdaptiveEngine
52
+ return AdaptiveEngine().route(path)
53
+ except ImportError:
54
+ ext = Path(path).suffix.lower() if path else ""
55
+ if ext in (".pdf", ".docx", ".doc", ".txt"):
56
+ return "document"
57
+ elif ext in (".csv", ".xlsx", ".parquet"):
58
+ return "timeseries"
59
+ elif ext in (".png", ".jpg", ".jpeg"):
60
+ return "vision"
61
+ elif ext in (".wav", ".mp3"):
62
+ return "audio"
63
+ return "document"
64
+
65
+ def finish(self) -> dict:
66
+ elapsed = time.time() - self.start_time
67
+ data_left = False
68
+ risk = "LOW"
69
+ if self._guard and self.guard_active:
70
+ try:
71
+ self._guard.__exit__(None, None, None)
72
+ data_left = self._guard.did_data_leave()
73
+ risk = self._guard.risk_level().name
74
+ except Exception:
75
+ pass
76
+ report = {
77
+ "command": self.command, "timestamp": datetime.now().isoformat(),
78
+ "duration_seconds": round(elapsed, 2), "quality_scores": self.quality_scores,
79
+ "privacy": {"data_left_system": data_left, "risk_level": risk, "antguard_active": self.guard_active},
80
+ "platform": platform.system(),
81
+ "passed": all(s.get("passed", False) for s in self.quality_scores.values()) if self.quality_scores else True,
82
+ }
83
+ self._save_history(report)
84
+ return report
85
+
86
+ def _save_history(self, report):
87
+ try:
88
+ HISTORY_DIR.mkdir(parents=True, exist_ok=True)
89
+ history = json.loads(HISTORY_FILE.read_text()) if HISTORY_FILE.exists() else []
90
+ report["id"] = len(history) + 1
91
+ history.append(report)
92
+ HISTORY_FILE.write_text(json.dumps(history[-500:], indent=2))
93
+ except Exception:
94
+ pass
antstudio/cli.py ADDED
@@ -0,0 +1,157 @@
1
+ """Ant Studio CLI."""
2
+ import click, json
3
+ from pathlib import Path
4
+
5
+ @click.group()
6
+ @click.version_option("0.1.0", prog_name="antstudio")
7
+ def cli():
8
+ """Ant Studio — Build. Run. Control."""
9
+ pass
10
+
11
+ @cli.group()
12
+ def doc():
13
+ """Document intelligence (DocQWise)."""
14
+ pass
15
+
16
+ @doc.command()
17
+ @click.argument("source")
18
+ @click.option("--fields", "-f", default="vendor,date,amount,invoice_number")
19
+ @click.option("--output", "-o", default="")
20
+ @click.option("--output-db", default="", help="Database connection string")
21
+ @click.option("--table", default="results")
22
+ @click.option("--ocr", is_flag=True)
23
+ @click.option("--engine", default="tesseract")
24
+ @click.option("--model", default="default")
25
+ @click.option("--threshold", default=0.7, type=float)
26
+ @click.option("--extensions", default=".pdf,.docx,.txt,.xlsx,.csv,.png,.jpg,.jpeg")
27
+ def extract(source, fields, output, output_db, table, ocr, engine, model, threshold, extensions):
28
+ """Extract fields from documents (PDF, DOCX, Excel, images, TXT)."""
29
+ from antstudio.doc.extract import run
30
+ run(source=source, fields=[f.strip() for f in fields.split(",")], model=model,
31
+ ocr=ocr, engine=engine, output=output, output_db=output_db, table=table,
32
+ threshold=threshold, extensions=extensions)
33
+
34
+ @doc.command()
35
+ @click.argument("source")
36
+ @click.argument("question")
37
+ @click.option("--rag", default="auto", type=click.Choice(["simple","graph","multimodal","auto"]))
38
+ @click.option("--model", default="")
39
+ @click.option("--system-prompt", default="")
40
+ def ask(source, question, rag, model, system_prompt):
41
+ """Ask questions about documents."""
42
+ from antstudio.doc.ask import run
43
+ run(source=source, question=question, rag=rag, model=model, system_prompt=system_prompt)
44
+
45
+ @cli.group()
46
+ def ts():
47
+ """Temporal intelligence (WavQWise)."""
48
+ pass
49
+
50
+ @ts.command()
51
+ @click.argument("source")
52
+ @click.option("--target", "-t", default="value")
53
+ @click.option("--horizon", "-h", default=30, type=int)
54
+ @click.option("--model", "-m", default="auto")
55
+ @click.option("--output", "-o", default="")
56
+ @click.option("--chart", default="")
57
+ def forecast(source, target, horizon, model, output, chart):
58
+ """Forecast time-series data."""
59
+ from antstudio.ts.forecast import run
60
+ run(source=source, target=target, horizon=horizon, model=model, output=output, chart=chart)
61
+
62
+ @ts.command()
63
+ @click.argument("source")
64
+ @click.option("--target", "-t", default="value")
65
+ @click.option("--method", "-m", default="zscore")
66
+ @click.option("--threshold", default=2.0, type=float)
67
+ @click.option("--output", "-o", default="")
68
+ def anomaly(source, target, method, threshold, output):
69
+ """Detect anomalies in time-series."""
70
+ from antstudio.ts.anomaly import run
71
+ run(source=source, target=target, method=method, threshold=threshold, output=output)
72
+
73
+ @cli.command()
74
+ @click.option("--limit", default=20, type=int)
75
+ def runs(limit):
76
+ """List past pipeline runs (Kubeflow-style)."""
77
+ from antstudio.pipeline import list_runs
78
+ all_runs = list_runs(limit)
79
+ if not all_runs:
80
+ print("\n No pipeline runs yet.\n"); return
81
+ print(f"\n {'ID':<10} {'Pipeline':<40} {'Steps':<12} {'Status':<10} {'Time':<8}")
82
+ print(f" {'─'*10} {'─'*40} {'─'*12} {'─'*10} {'─'*8}")
83
+ for r in all_runs:
84
+ s = r.get("summary", {})
85
+ print(f" {r['run_id']:<10} {r['name'][:39]:<40} {s.get('success',0)}/{s.get('total',0)} passed {r['status']:<10} {r['duration_seconds']:.1f}s")
86
+ print()
87
+
88
+ @cli.command()
89
+ @click.argument("run_id")
90
+ def run_detail(run_id):
91
+ """Show detailed pipeline run (Kubeflow-style)."""
92
+ from antstudio.pipeline import get_run
93
+ data = get_run(run_id)
94
+ if not data:
95
+ print(f"\n Run '{run_id}' not found.\n"); return
96
+ icons = {"success":"+","failed":"x","skipped":"-","pending":"."}
97
+ print(f"\n Pipeline: {data['name']} [{data['run_id']}]")
98
+ print(f" Status: {data['status'].upper()} | {data['duration_seconds']}s | {data['timestamp']}")
99
+ print(f" {'='*60}")
100
+ for i, step in enumerate(data.get("steps", [])):
101
+ ic = icons.get(step["status"], "?")
102
+ dur = f"{step['duration_ms']}ms" if step.get("duration_ms") else ""
103
+ err = f" ({step['error'][:40]})" if step.get("error") else ""
104
+ print(f" [{ic}] {step['name']:<30} {dur:>8}{err}")
105
+ if i < len(data["steps"]) - 1:
106
+ print(f" |"); print(f" v")
107
+ print(f" {'='*60}")
108
+ s = data.get("summary", {})
109
+ print(f" {s.get('success',0)}/{s.get('total',0)} steps passed\n")
110
+
111
+ @cli.command()
112
+ def history():
113
+ """Show execution history."""
114
+ hist_file = Path.home() / ".antstudio" / "history.json"
115
+ if not hist_file.exists():
116
+ print(" No runs yet."); return
117
+ runs = json.loads(hist_file.read_text())
118
+ print(f"\n {'ID':<5} {'Command':<45} {'Quality':<10} {'Privacy':<12} {'Time':<8}")
119
+ print(f" {'─'*5} {'─'*45} {'─'*10} {'─'*12} {'─'*8}")
120
+ for r in runs[-20:]:
121
+ qp = "PASS" if r.get("passed") else "FAIL"
122
+ dl = "LOCAL" if not r["privacy"]["data_left_system"] else "ALERT"
123
+ print(f" {r['id']:<5} {r['command'][:44]:<45} {qp:<10} {dl:<12} {r['duration_seconds']:.1f}s")
124
+ print()
125
+
126
+ @cli.command()
127
+ def models():
128
+ """List available Ollama models."""
129
+ from antstudio.llm.ollama import list_models
130
+ mods = list_models()
131
+ if mods:
132
+ print(f"\n Available models ({len(mods)}):")
133
+ for m in mods: print(f" - {m}")
134
+ else:
135
+ print("\n No models found. Run: ollama serve")
136
+ print()
137
+
138
+ @cli.command()
139
+ def status():
140
+ """System status."""
141
+ print(f"\n Ant Studio v0.1.0")
142
+ libs = {"docqwise":0,"wavqwise":0,"sightrag":0,"sonarwise":0,"adaptive_intelligence":0,"llmevalkit":0,"antguard":0}
143
+ for lib in libs:
144
+ try: __import__(lib); libs[lib] = 1
145
+ except ImportError: pass
146
+ print(f"\n Libraries:")
147
+ for lib, ok in libs.items():
148
+ print(f" [{'+'if ok else '-'}] {lib}")
149
+ from antstudio.llm.ollama import list_models
150
+ mods = list_models()
151
+ print(f"\n Ollama: {'connected ('+str(len(mods))+' models)' if mods else 'not running'}\n")
152
+
153
+ def main():
154
+ cli()
155
+
156
+ if __name__ == "__main__":
157
+ main()
@@ -0,0 +1 @@
1
+ from antstudio.doc import extract, ask
antstudio/doc/ask.py ADDED
@@ -0,0 +1,114 @@
1
+ """Document Q&A — ask questions with RAG modes."""
2
+ from typing import Optional
3
+ from antstudio.backbone import Backbone
4
+ from antstudio.io.reader import read_input
5
+ from antstudio.doc.loader import load_text
6
+ from antstudio.llm.ollama import ask as ollama_ask
7
+
8
+ class Answer:
9
+ def __init__(self, text: str, confidence: float, sources: list, quality: dict, audit: dict):
10
+ self.text = text
11
+ self.confidence = confidence
12
+ self.sources = sources
13
+ self.quality = quality
14
+ self.audit = audit
15
+ def __repr__(self):
16
+ return f"Answer(confidence={self.confidence}, len={len(self.text)})"
17
+
18
+ def run(source: str, question: str, rag: str = "auto", model: str = "",
19
+ system_prompt: str = "", verbose: bool = True, **kwargs) -> Answer:
20
+
21
+ bb = Backbone(f"doc ask {source}")
22
+ bb.start()
23
+
24
+ if verbose:
25
+ print(f"\n Ant Studio v0.1.0 | DocQWise + Ollama + llmevalkit + AntGuard\n")
26
+
27
+ items = read_input(source=source, extensions=".pdf,.docx,.txt")
28
+ if not items:
29
+ audit = bb.finish()
30
+ return Answer("No documents found.", 0, [], {}, audit)
31
+
32
+ # Load all texts
33
+ texts = []
34
+ for fname, raw, fpath in items:
35
+ t = load_text(raw, fname)
36
+ if t and not t.startswith("["):
37
+ texts.append({"text": t, "source": fname})
38
+
39
+ if verbose:
40
+ print(f" [1/4] Loading {'.' * 21} {len(texts)} documents")
41
+
42
+ # Route RAG mode
43
+ if rag == "auto":
44
+ rag = "graph" if len(texts) > 1 else "simple"
45
+ route = bb.route(source)
46
+ if verbose:
47
+ print(f" [2/4] Routing {'.' * 21} {rag} RAG (adaptive: {route})")
48
+
49
+ # Build context
50
+ if rag == "graph":
51
+ context = _graph_rag(texts, question)
52
+ else:
53
+ context = _simple_rag(texts, question)
54
+
55
+ # Generate answer
56
+ sys = system_prompt or "You are a precise document analyst. Answer only from the provided evidence."
57
+ prompt = f"Evidence:\n{context[:4000]}\n\nQuestion: {question}\n\nAnswer:"
58
+ answer_text = ollama_ask(prompt, system=sys, model=model)
59
+
60
+ if verbose:
61
+ print(f" [3/4] Reasoning {'.' * 19} {len(answer_text)} chars")
62
+
63
+ # Quality check
64
+ q = bb.evaluate("answer", answer_text, context[:1000])
65
+ audit = bb.finish()
66
+
67
+ # Find sources
68
+ sources = []
69
+ for t in texts:
70
+ for sent in t["text"].split(".")[:20]:
71
+ if any(w in sent.lower() for w in question.lower().split()[:3]):
72
+ sources.append(f"{t['source']}: {sent.strip()[:80]}")
73
+ if len(sources) >= 3:
74
+ break
75
+
76
+ confidence = q.get("score", 0.5)
77
+
78
+ if verbose:
79
+ dl = audit["privacy"]["data_left_system"]
80
+ print(f" [4/4] Audit {'.' * 23} data_left: {'YES' if dl else 'NO'}")
81
+ print(f"\n Answer: {answer_text[:200]}{'...' if len(answer_text) > 200 else ''}")
82
+ print(f" Confidence: {confidence:.0%}")
83
+ print(f"\n Done in {audit['duration_seconds']}s\n")
84
+
85
+ return Answer(answer_text, confidence, sources, bb.quality_scores, audit)
86
+
87
+
88
+ def _simple_rag(texts: list, question: str) -> str:
89
+ all_text = "\n\n---\n\n".join(t["text"][:2000] for t in texts[:5])
90
+ return all_text
91
+
92
+ def _graph_rag(texts: list, question: str) -> str:
93
+ """Graph-based retrieval — find connected evidence."""
94
+ try:
95
+ from antstudio.doc.extract import _extract_regex
96
+ entities = set()
97
+ for t in texts:
98
+ fields = _extract_regex(t["text"], ["vendor", "invoice_number", "amount"])
99
+ for v in fields.get("fields", {}).values():
100
+ if v:
101
+ entities.add(v.lower())
102
+
103
+ # Find sentences mentioning shared entities
104
+ relevant = []
105
+ q_words = set(question.lower().split())
106
+ for t in texts:
107
+ for sent in t["text"].split("."):
108
+ sent_lower = sent.lower()
109
+ if any(e in sent_lower for e in entities) or any(w in sent_lower for w in q_words):
110
+ relevant.append(f"[{t['source']}] {sent.strip()}")
111
+
112
+ return "\n".join(relevant[:30]) if relevant else _simple_rag(texts, question)
113
+ except Exception:
114
+ return _simple_rag(texts, question)
@@ -0,0 +1,147 @@
1
+ """Document extraction with pipeline tracking."""
2
+ import re
3
+ from typing import List, Optional
4
+ from antstudio.backbone import Backbone
5
+ from antstudio.pipeline import Pipeline
6
+ from antstudio.io.reader import read_input
7
+ from antstudio.io.writer import Results
8
+ from antstudio.doc.loader import load_text
9
+
10
+ def run(source: str = "", fields: Optional[List[str]] = None, model: str = "default",
11
+ ocr: bool = False, engine: str = "tesseract", schema: str = "",
12
+ db: str = "", query: str = "", url: str = "",
13
+ output: str = "", output_db: str = "", table: str = "results",
14
+ threshold: float = 0.7, verbose: bool = True, **kwargs) -> Results:
15
+
16
+ if fields is None:
17
+ fields = ["vendor", "date", "amount", "invoice_number"]
18
+
19
+ bb = Backbone(f"doc extract {source}")
20
+ bb.start()
21
+
22
+ pipe = Pipeline(f"Document Extraction: {source}")
23
+ pipe.start()
24
+
25
+ if verbose:
26
+ print(f"\n Ant Studio v0.1.0 | DocQWise + llmevalkit + AntGuard\n")
27
+
28
+ # Step 1: Read input
29
+ s1 = pipe.add_step("Scan Input", "file_input", {"source": source, "extensions": kwargs.get("extensions", ".pdf,.docx,.txt")})
30
+ s1.start()
31
+ items = read_input(source=source, db=db, query=query, url=url,
32
+ extensions=kwargs.pop("extensions", ".pdf,.docx,.txt,.doc,.xlsx,.csv,.png,.jpg,.jpeg,.bmp,.tiff"))
33
+ if not items:
34
+ s1.fail(f"No files found: {source}")
35
+ pipe.finish()
36
+ if verbose: pipe.print_status()
37
+ return Results([], audit=bb.finish())
38
+ s1.succeed({"file_count": len(items)}, f"{len(items)} files found")
39
+ if verbose:
40
+ print(f" [1/4] Scanning {'.' * 20} {len(items)} files found")
41
+
42
+ # Step 2: Extract
43
+ s2 = pipe.add_step("Extract Fields", "docqwise_extract", {"fields": fields, "model": model, "ocr": ocr})
44
+ s2.start()
45
+ all_rows = []
46
+ for i, (fname, raw, fpath) in enumerate(items):
47
+ text = load_text(raw, fname)
48
+ if not text or text.startswith("["):
49
+ all_rows.append({"_source": fname, "_error": text or "empty", "_confidence": 0, "_threshold": threshold})
50
+ continue
51
+ extracted = _extract_docqwise(text, fields, model)
52
+ if not extracted:
53
+ extracted = _extract_regex(text, fields)
54
+ row = {"_source": fname, "_path": fpath}
55
+ row.update(extracted.get("fields", {}))
56
+ row["_confidence"] = extracted.get("confidence", 0.0)
57
+ row["_threshold"] = threshold
58
+ field_text = " ".join(str(v) for v in extracted.get("fields", {}).values() if v)
59
+ q = bb.evaluate(fname, field_text, text[:500])
60
+ all_rows.append(row)
61
+ s2.succeed({"rows": len(all_rows)}, f"{len(all_rows)}/{len(items)} complete")
62
+ if verbose:
63
+ print(f" [2/4] Extracting {'.' * 18} {len(all_rows)}/{len(items)} complete")
64
+
65
+ # Step 3: Quality
66
+ s3 = pipe.add_step("Quality Check", "llmevalkit", {"threshold": threshold})
67
+ s3.start()
68
+ passed = sum(1 for r in all_rows if r.get("_confidence", 0) >= threshold)
69
+ flagged = len(all_rows) - passed
70
+ s3.quality_score = {"score": passed / max(len(all_rows), 1), "passed": flagged == 0}
71
+ s3.succeed({"passed": passed, "flagged": flagged}, f"{passed} passed, {flagged} flagged")
72
+ if verbose:
73
+ print(f" [3/4] Quality (llmevalkit) {'.' * 10} {passed} passed, {flagged} flagged")
74
+
75
+ # Step 4: Privacy audit
76
+ s4 = pipe.add_step("Privacy Audit", "antguard", {})
77
+ s4.start()
78
+ audit = bb.finish()
79
+ dl = audit["privacy"]["data_left_system"]
80
+ risk = audit["privacy"]["risk_level"]
81
+ s4.succeed({"data_left": dl, "risk": risk}, f"data_left: {'YES' if dl else 'NO'} | risk: {risk}")
82
+ if verbose:
83
+ print(f" [4/4] Privacy (AntGuard) {'.' * 11} data_left: {'YES' if dl else 'NO'} | risk: {risk}")
84
+
85
+ pipe.finish()
86
+ results = Results(all_rows, quality=bb.quality_scores, audit=audit)
87
+
88
+ # Output
89
+ if output:
90
+ # Step 5: Save output
91
+ s5 = pipe.add_step("Save Output", "export", {"path": output})
92
+ s5.start()
93
+ results.save(output)
94
+ s5.succeed({"path": output, "rows": results.count})
95
+ if verbose: print(f"\n Results saved: {output} ({results.count} rows)")
96
+
97
+ if output_db:
98
+ results.to_database(output_db, table=table)
99
+
100
+ if flagged > 0 and output:
101
+ flagged_path = output.replace(".", "_flagged.")
102
+ results.flagged.save(flagged_path)
103
+ if verbose: print(f" Flagged items: {flagged_path} ({flagged} rows)")
104
+
105
+ # Print pipeline view
106
+ if verbose:
107
+ pipe.print_status()
108
+
109
+ return results
110
+
111
+
112
+ def _extract_docqwise(text, fields, model):
113
+ try:
114
+ from docqwise import DocQWise
115
+ dq = DocQWise(model=model)
116
+ result = dq.extract(text, fields=fields)
117
+ return {"fields": result.fields, "confidence": result.confidence}
118
+ except ImportError:
119
+ return {}
120
+ except Exception:
121
+ return {}
122
+
123
+ def _extract_regex(text, fields):
124
+ extracted = {}
125
+ patterns = {
126
+ "vendor": [r"(?:vendor|supplier|from|company)[:\s]+([A-Z][A-Za-z\s&.,]+?)(?:\n|$)", r"^([A-Z][A-Z\s&.,]{3,30})\s*(?:LLC|Inc|Corp|Ltd)?"],
127
+ "date": [r"(?:date|dated|invoice date)[:\s]+(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})", r"(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})"],
128
+ "amount": [r"(?:total|amount|balance|due)[:\s]*\$?([\d,]+\.?\d*)", r"\$\s*([\d,]+\.?\d*)"],
129
+ "invoice_number": [r"(?:invoice|inv|invoice no|invoice #)[:\s#]*([A-Za-z0-9-]+)"],
130
+ "gst": [r"(?:gst|gstin|tax id)[:\s]*([A-Z0-9]{10,20})"],
131
+ "due_date": [r"(?:due date|payment due|due)[:\s]+(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})"],
132
+ }
133
+ found = 0
134
+ for field in fields:
135
+ key = field.strip().lower().replace(" ", "_")
136
+ for pk, pats in patterns.items():
137
+ if key == pk or key.replace("_", "") == pk.replace("_", ""):
138
+ for pat in pats:
139
+ m = re.search(pat, text, re.IGNORECASE | re.MULTILINE)
140
+ if m:
141
+ extracted[field.strip()] = m.group(1).strip()
142
+ found += 1
143
+ break
144
+ break
145
+ if field.strip() not in extracted:
146
+ extracted[field.strip()] = ""
147
+ return {"fields": extracted, "confidence": round(found / max(len(fields), 1), 3)}
@@ -0,0 +1,68 @@
1
+ """Load ANY file type — PDF, DOCX, Excel, CSV, images, TXT."""
2
+
3
+ def load_text(raw_bytes: bytes, filename: str) -> str:
4
+ ext = filename.lower().rsplit(".", 1)[-1] if "." in filename else ""
5
+
6
+ if ext == "pdf":
7
+ return _load_pdf(raw_bytes)
8
+ elif ext == "docx":
9
+ return _load_docx(raw_bytes)
10
+ elif ext in ("xlsx", "xls"):
11
+ return _load_excel(raw_bytes, filename)
12
+ elif ext == "csv":
13
+ return raw_bytes.decode("utf-8", errors="ignore")
14
+ elif ext in ("png", "jpg", "jpeg", "bmp", "tiff", "webp"):
15
+ return _load_image_ocr(raw_bytes, filename)
16
+ elif ext in ("txt", "md", "json", "xml", "html", "log", "yaml", "yml"):
17
+ return raw_bytes.decode("utf-8", errors="ignore")
18
+ else:
19
+ try:
20
+ return raw_bytes.decode("utf-8", errors="ignore")
21
+ except Exception:
22
+ return f"[Binary file: {filename}]"
23
+
24
+ def _load_pdf(raw_bytes):
25
+ try:
26
+ import fitz
27
+ doc = fitz.open(stream=raw_bytes, filetype="pdf")
28
+ return "\n".join(page.get_text() for page in doc)
29
+ except ImportError:
30
+ try:
31
+ from docqwise import DocQWise
32
+ return DocQWise().load_pdf(raw_bytes)
33
+ except ImportError:
34
+ return "[pip install PyMuPDF]"
35
+
36
+ def _load_docx(raw_bytes):
37
+ try:
38
+ import docx, io
39
+ doc = docx.Document(io.BytesIO(raw_bytes))
40
+ return "\n".join(p.text for p in doc.paragraphs)
41
+ except ImportError:
42
+ return "[pip install python-docx]"
43
+
44
+ def _load_excel(raw_bytes, filename):
45
+ try:
46
+ import pandas as pd, io
47
+ df = pd.read_excel(io.BytesIO(raw_bytes))
48
+ return df.to_string(index=False)
49
+ except ImportError:
50
+ return "[pip install openpyxl pandas]"
51
+
52
+ def _load_image_ocr(raw_bytes, filename):
53
+ try:
54
+ import pytesseract
55
+ from PIL import Image
56
+ import io
57
+ img = Image.open(io.BytesIO(raw_bytes))
58
+ return pytesseract.image_to_string(img)
59
+ except ImportError:
60
+ try:
61
+ import easyocr, io, numpy as np
62
+ from PIL import Image
63
+ img = np.array(Image.open(io.BytesIO(raw_bytes)))
64
+ reader = easyocr.Reader(["en"])
65
+ results = reader.readtext(img)
66
+ return "\n".join(r[1] for r in results)
67
+ except ImportError:
68
+ return f"[Image: {filename} — pip install pytesseract Pillow OR easyocr]"
File without changes
antstudio/io/reader.py ADDED
@@ -0,0 +1,56 @@
1
+ """Universal input reader."""
2
+ import os
3
+ from pathlib import Path
4
+ from typing import List, Tuple
5
+
6
+ def read_input(source: str = "", db: str = "", query: str = "",
7
+ azure: str = "", s3: str = "", container: str = "",
8
+ path: str = "", url: str = "", extensions: str = ".pdf,.docx,.txt,.csv",
9
+ recursive: bool = True, max_files: int = 0) -> List[Tuple[str, bytes, str]]:
10
+ results = []
11
+ exts = [e.strip().lower() for e in extensions.split(",") if e.strip()]
12
+
13
+ if source and os.path.isfile(source):
14
+ with open(source, "rb") as f:
15
+ return [(os.path.basename(source), f.read(), os.path.abspath(source))]
16
+
17
+ if source and os.path.isdir(source):
18
+ files = []
19
+ if recursive:
20
+ for root, _, fns in os.walk(source):
21
+ for fn in fns:
22
+ if any(fn.lower().endswith(e) for e in exts):
23
+ files.append(os.path.join(root, fn))
24
+ else:
25
+ files = [os.path.join(source, fn) for fn in os.listdir(source)
26
+ if os.path.isfile(os.path.join(source, fn)) and any(fn.lower().endswith(e) for e in exts)]
27
+ if max_files > 0:
28
+ files = files[:max_files]
29
+ for fp in files:
30
+ try:
31
+ with open(fp, "rb") as f:
32
+ results.append((os.path.basename(fp), f.read(), os.path.abspath(fp)))
33
+ except Exception:
34
+ pass
35
+ return results
36
+
37
+ if db:
38
+ try:
39
+ import sqlalchemy, pandas as pd
40
+ engine = sqlalchemy.create_engine(db)
41
+ df = pd.read_sql(query or "SELECT * FROM documents", engine)
42
+ import json
43
+ data = json.dumps(df.to_dict("records")).encode()
44
+ return [("db_query.json", data, db)]
45
+ except Exception as e:
46
+ print(f" DB error: {e}")
47
+
48
+ if url:
49
+ try:
50
+ import httpx
51
+ resp = httpx.get(url, follow_redirects=True, timeout=30)
52
+ return [(url.split("/")[-1] or "download", resp.content, url)]
53
+ except Exception as e:
54
+ print(f" URL error: {e}")
55
+
56
+ return results