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 +2 -0
- antstudio/backbone/__init__.py +94 -0
- antstudio/cli.py +157 -0
- antstudio/doc/__init__.py +1 -0
- antstudio/doc/ask.py +114 -0
- antstudio/doc/extract.py +147 -0
- antstudio/doc/loader.py +68 -0
- antstudio/io/__init__.py +0 -0
- antstudio/io/reader.py +56 -0
- antstudio/io/writer.py +77 -0
- antstudio/llm/__init__.py +0 -0
- antstudio/llm/ollama.py +35 -0
- antstudio/pipeline.py +184 -0
- antstudio/ts/__init__.py +1 -0
- antstudio/ts/anomaly.py +73 -0
- antstudio/ts/forecast.py +87 -0
- antstudio-0.1.0.dist-info/METADATA +310 -0
- antstudio-0.1.0.dist-info/RECORD +22 -0
- antstudio-0.1.0.dist-info/WHEEL +5 -0
- antstudio-0.1.0.dist-info/entry_points.txt +2 -0
- antstudio-0.1.0.dist-info/licenses/LICENSE +201 -0
- antstudio-0.1.0.dist-info/top_level.txt +1 -0
antstudio/__init__.py
ADDED
|
@@ -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)
|
antstudio/doc/extract.py
ADDED
|
@@ -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)}
|
antstudio/doc/loader.py
ADDED
|
@@ -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]"
|
antstudio/io/__init__.py
ADDED
|
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
|