austrai 1.0.0__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.
austrai-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: austrai
3
+ Version: 1.0.0
4
+ Summary: AUSTR.AI — Privacy Firewall fuer KI-Dienste. Lokale Anonymisierung, API Proxy, Desktop App.
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx>=0.27
7
+ Requires-Dist: uvicorn[standard]>=0.34
8
+ Requires-Dist: starlette>=0.41
9
+ Requires-Dist: click>=8.1
10
+ Requires-Dist: pyyaml>=6.0
11
+ Requires-Dist: prompt_toolkit>=3.0
12
+ Requires-Dist: presidio-analyzer>=2.2
13
+ Requires-Dist: spacy>=3.7
14
+ Requires-Dist: pydantic>=2.0
15
+ Requires-Dist: numpy>=1.24
16
+ Provides-Extra: desktop
17
+ Requires-Dist: pywebview>=5.0; extra == "desktop"
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: austrai
3
+ Version: 1.0.0
4
+ Summary: AUSTR.AI — Privacy Firewall fuer KI-Dienste. Lokale Anonymisierung, API Proxy, Desktop App.
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx>=0.27
7
+ Requires-Dist: uvicorn[standard]>=0.34
8
+ Requires-Dist: starlette>=0.41
9
+ Requires-Dist: click>=8.1
10
+ Requires-Dist: pyyaml>=6.0
11
+ Requires-Dist: prompt_toolkit>=3.0
12
+ Requires-Dist: presidio-analyzer>=2.2
13
+ Requires-Dist: spacy>=3.7
14
+ Requires-Dist: pydantic>=2.0
15
+ Requires-Dist: numpy>=1.24
16
+ Provides-Extra: desktop
17
+ Requires-Dist: pywebview>=5.0; extra == "desktop"
@@ -0,0 +1,25 @@
1
+ pyproject.toml
2
+ austrai.egg-info/PKG-INFO
3
+ austrai.egg-info/SOURCES.txt
4
+ austrai.egg-info/dependency_links.txt
5
+ austrai.egg-info/entry_points.txt
6
+ austrai.egg-info/requires.txt
7
+ austrai.egg-info/top_level.txt
8
+ austrai_proxy/__init__.py
9
+ austrai_proxy/__main__.py
10
+ austrai_proxy/cli.py
11
+ austrai_proxy/config.py
12
+ austrai_proxy/interactive.py
13
+ austrai_proxy/server.py
14
+ austrai_proxy/stream_rehydrator.py
15
+ austrai_proxy/core/__init__.py
16
+ austrai_proxy/core/anonymizer.py
17
+ austrai_proxy/core/austrian_recognizers.py
18
+ austrai_proxy/core/codename_engine.py
19
+ austrai_proxy/core/context_learner.py
20
+ austrai_proxy/core/detector.py
21
+ austrai_proxy/core/extractor.py
22
+ austrai_proxy/core/models.py
23
+ austrai_proxy/core/rehydrator.py
24
+ austrai_proxy/core/session_store.py
25
+ austrai_proxy/core/setup.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aai = austrai_proxy.cli:main
@@ -0,0 +1,13 @@
1
+ httpx>=0.27
2
+ uvicorn[standard]>=0.34
3
+ starlette>=0.41
4
+ click>=8.1
5
+ pyyaml>=6.0
6
+ prompt_toolkit>=3.0
7
+ presidio-analyzer>=2.2
8
+ spacy>=3.7
9
+ pydantic>=2.0
10
+ numpy>=1.24
11
+
12
+ [desktop]
13
+ pywebview>=5.0
@@ -0,0 +1 @@
1
+ austrai_proxy
@@ -0,0 +1,3 @@
1
+ """AUSTR.AI Privacy Proxy — transparent anonymization layer for LLM APIs."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ """Entry point for python -m austrai_proxy."""
2
+
3
+ from .cli import main
4
+
5
+ main()
@@ -0,0 +1,470 @@
1
+ """Unified CLI for AUSTR.AI — one command for everything.
2
+
3
+ Usage:
4
+ aai claude Start Claude Code through the privacy proxy
5
+ aai start Start the proxy (for custom apps/SDKs)
6
+ aai app Open the desktop app (clipboard tool)
7
+ aai config Configure API keys and settings
8
+ aai status Show proxy status
9
+ aai stop Stop the proxy
10
+ """
11
+
12
+ import os
13
+ import signal
14
+ import subprocess
15
+ import sys
16
+ import time
17
+
18
+ import click
19
+
20
+ from .config import ProxyConfig, DEFAULT_PORT, CONFIG_DIR
21
+
22
+
23
+ PROXY_PID_FILE = CONFIG_DIR / "proxy.pid"
24
+ DESKTOP_APP = os.path.join(
25
+ os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
26
+ "desktop", "austrai_app.py",
27
+ )
28
+
29
+
30
+ @click.group(invoke_without_command=True)
31
+ @click.pass_context
32
+ def main(ctx):
33
+ """AUSTR.AI — Schuetze deine Daten vor KI-Servern."""
34
+ if ctx.invoked_subcommand is None:
35
+ click.echo(ctx.get_help())
36
+
37
+
38
+ # -----------------------------------------------------------------------
39
+ # aai claude — Start Claude Code through proxy
40
+ # -----------------------------------------------------------------------
41
+
42
+ @main.command()
43
+ @click.argument("extra_args", nargs=-1)
44
+ def claude(extra_args):
45
+ """Claude Code durch den Privacy Proxy starten."""
46
+ config = ProxyConfig.load()
47
+ if not config.anthropic_api_key:
48
+ click.echo("Kein Anthropic API Key konfiguriert.")
49
+ click.echo(" aai config")
50
+ raise SystemExit(1)
51
+
52
+ _ensure_proxy_running(config)
53
+
54
+ click.echo(f"\n🛡 Claude Code startet durch AUSTR.AI Proxy (localhost:{config.port})")
55
+ click.echo(" Alle sensiblen Daten werden automatisch geschuetzt.\n")
56
+
57
+ env = os.environ.copy()
58
+ env["ANTHROPIC_BASE_URL"] = f"http://localhost:{config.port}"
59
+
60
+ cmd = ["claude"] + list(extra_args)
61
+ try:
62
+ os.execvpe("claude", cmd, env)
63
+ except FileNotFoundError:
64
+ click.echo("Claude Code nicht gefunden. Installiere es mit:")
65
+ click.echo(" npm install -g @anthropic-ai/claude-code")
66
+ raise SystemExit(1)
67
+
68
+
69
+ # -----------------------------------------------------------------------
70
+ # aai start — Start the proxy
71
+ # -----------------------------------------------------------------------
72
+
73
+ @main.command()
74
+ @click.option("--port", "-p", default=None, type=int, help="Port (Standard: 8282)")
75
+ @click.option("--background", "-b", is_flag=True, help="Im Hintergrund starten")
76
+ @click.option("--anthropic-key", envvar="ANTHROPIC_API_KEY", default=None)
77
+ @click.option("--openai-key", envvar="OPENAI_API_KEY", default=None)
78
+ def start(port, background, anthropic_key, openai_key):
79
+ """Privacy Proxy starten."""
80
+ config = ProxyConfig.load()
81
+
82
+ if anthropic_key:
83
+ config.anthropic_api_key = anthropic_key
84
+ if openai_key:
85
+ config.openai_api_key = openai_key
86
+ if port:
87
+ config.port = port
88
+
89
+ if not config.anthropic_api_key and not config.openai_api_key:
90
+ click.echo("Kein API Key konfiguriert. Setze mindestens einen:")
91
+ click.echo(" aai config")
92
+ click.echo(" Oder: aai start --anthropic-key sk-ant-...")
93
+ raise SystemExit(1)
94
+
95
+ if background:
96
+ _start_proxy_background(config)
97
+ else:
98
+ _start_proxy_foreground(config)
99
+
100
+
101
+ # -----------------------------------------------------------------------
102
+ # aai stop — Stop the proxy
103
+ # -----------------------------------------------------------------------
104
+
105
+ @main.command()
106
+ def stop():
107
+ """Privacy Proxy stoppen."""
108
+ if PROXY_PID_FILE.exists():
109
+ try:
110
+ pid = int(PROXY_PID_FILE.read_text().strip())
111
+ os.kill(pid, signal.SIGTERM)
112
+ PROXY_PID_FILE.unlink(missing_ok=True)
113
+ click.echo(f"Proxy gestoppt (PID {pid}).")
114
+ except (ProcessLookupError, ValueError):
115
+ PROXY_PID_FILE.unlink(missing_ok=True)
116
+ click.echo("Proxy war bereits gestoppt.")
117
+ else:
118
+ click.echo("Kein laufender Proxy gefunden.")
119
+
120
+
121
+ # -----------------------------------------------------------------------
122
+ # aai app — Open the desktop app
123
+ # -----------------------------------------------------------------------
124
+
125
+ @main.command()
126
+ def app():
127
+ """Desktop-App oeffnen (Clipboard-Tool + Proxy Control)."""
128
+ config = ProxyConfig.load()
129
+
130
+ # Start proxy in background if not running
131
+ _ensure_proxy_running(config)
132
+
133
+ # Find the desktop app
134
+ app_path = _find_desktop_app()
135
+ if not app_path:
136
+ click.echo("Desktop-App nicht gefunden.")
137
+ raise SystemExit(1)
138
+
139
+ click.echo("🛡 AUSTR.AI Desktop-App wird geoeffnet...")
140
+ subprocess.Popen([sys.executable, app_path])
141
+
142
+
143
+ # -----------------------------------------------------------------------
144
+ # aai config — Configure settings
145
+ # -----------------------------------------------------------------------
146
+
147
+ @main.command()
148
+ def config():
149
+ """API Keys und Einstellungen konfigurieren."""
150
+ cfg = ProxyConfig.load()
151
+
152
+ click.echo("\n🛡 AUSTR.AI Konfiguration\n")
153
+
154
+ key = click.prompt(
155
+ "Anthropic API Key",
156
+ default=_mask(cfg.anthropic_api_key) or "(leer — Enter zum Ueberspringen)",
157
+ show_default=False,
158
+ )
159
+ if key and not key.startswith("(") and key != _mask(cfg.anthropic_api_key):
160
+ cfg.anthropic_api_key = key
161
+
162
+ key = click.prompt(
163
+ "OpenAI API Key",
164
+ default=_mask(cfg.openai_api_key) or "(leer — Enter zum Ueberspringen)",
165
+ show_default=False,
166
+ )
167
+ if key and not key.startswith("(") and key != _mask(cfg.openai_api_key):
168
+ cfg.openai_api_key = key
169
+
170
+ cfg.port = click.prompt("Proxy Port", default=cfg.port, type=int)
171
+
172
+ deny = click.prompt(
173
+ "Deny-List (kommagetrennt, z.B. Firmenname,Projektname)",
174
+ default=", ".join(cfg.deny_list) if cfg.deny_list else "(leer)",
175
+ show_default=False,
176
+ )
177
+ if deny and deny != "(leer)":
178
+ cfg.deny_list = [t.strip() for t in deny.split(",") if t.strip()]
179
+ elif deny == "(leer)":
180
+ cfg.deny_list = []
181
+
182
+ cfg.save()
183
+ click.echo(f"\n✅ Gespeichert: {CONFIG_DIR / 'proxy.yaml'}")
184
+ click.echo("\nStarte mit:")
185
+ click.echo(" aai claude — Claude Code durch Proxy")
186
+ click.echo(" aai start — Proxy fuer andere Apps")
187
+ click.echo(" aai app — Desktop-App")
188
+
189
+
190
+ # -----------------------------------------------------------------------
191
+ # aai status — Show status
192
+ # -----------------------------------------------------------------------
193
+
194
+ @main.command(name="anon")
195
+ @click.argument("text", nargs=-1, required=True)
196
+ @click.option("--deny", "-d", multiple=True, help="Zusaetzliche Begriffe anonymisieren")
197
+ @click.option("--output", "-o", default=None, help="Anonymisierten Text in Datei speichern")
198
+ def anonymize(text, deny, output):
199
+ """Text oder Datei anonymisieren (lokal, kein Server-Call)."""
200
+ full_text = " ".join(text)
201
+ if not full_text.strip():
202
+ click.echo("Kein Text angegeben.")
203
+ raise SystemExit(1)
204
+
205
+ # Check if input is a file path
206
+ import os
207
+ if os.path.isfile(full_text):
208
+ click.echo(f"📄 Datei erkannt: {full_text}")
209
+ try:
210
+ from .core.extractor import extract_from_file
211
+ result = extract_from_file(full_text)
212
+ click.echo(f" Format: {result.format}, Seiten: {result.pages}, {len(result.text)} Zeichen")
213
+ full_text = result.text
214
+ except ImportError as e:
215
+ click.echo(f"✗ {e}")
216
+ raise SystemExit(1)
217
+ except Exception as e:
218
+ click.echo(f"✗ Extraktion fehlgeschlagen: {e}")
219
+ raise SystemExit(1)
220
+
221
+ click.echo("⏳ Analysiere lokal...")
222
+
223
+ from .core import get_engine
224
+ engine = get_engine()
225
+ deny_list = list(deny) if deny else None
226
+ result = engine.anonymize(full_text, deny_list=deny_list)
227
+
228
+ if not result.mappings:
229
+ click.echo("ℹ️ Keine sensiblen Daten erkannt.")
230
+ click.echo(full_text)
231
+ return
232
+
233
+ click.echo(f"\n✅ {len(result.mappings)} sensible Begriffe geschuetzt:\n")
234
+ for codename, original in result.mappings.items():
235
+ click.echo(f" {original:30s} → {codename}")
236
+ click.echo(f"\nAnonymisiert:\n{result.anonymized_text}\n")
237
+ click.echo(f"Session: {result.session_id}")
238
+
239
+ # Persist mappings to disk for deanon
240
+ if result.mappings:
241
+ _save_last_session(result.mappings, result.session_id)
242
+
243
+ if output:
244
+ import os
245
+ with open(output, "w", encoding="utf-8") as f:
246
+ f.write(result.anonymized_text)
247
+ click.echo(f"💾 Gespeichert: {output}")
248
+ else:
249
+ import subprocess
250
+ try:
251
+ subprocess.run(["pbcopy"], input=result.anonymized_text.encode(), check=True, timeout=5)
252
+ click.echo("📋 In Zwischenablage kopiert!")
253
+ except Exception:
254
+ pass
255
+
256
+
257
+ @main.command(name="deanon")
258
+ @click.argument("text", nargs=-1, required=True)
259
+ def rehydrate(text):
260
+ """LLM-Antwort de-anonymisieren (Codenames durch Originale ersetzen)."""
261
+ full_text = " ".join(text)
262
+ if not full_text.strip():
263
+ click.echo("Kein Text angegeben.")
264
+ raise SystemExit(1)
265
+
266
+ # Load last session from disk
267
+ mappings = _load_last_session()
268
+ if not mappings:
269
+ click.echo("Keine gespeicherte Session. Zuerst aai anon ausfuehren.")
270
+ raise SystemExit(1)
271
+
272
+ from .core import get_engine
273
+ engine = get_engine()
274
+ restored = engine.rehydrate(full_text, mappings)
275
+
276
+ count = sum(1 for k in mappings if k in full_text)
277
+ click.echo(f"\n✅ {count} Begriffe wiederhergestellt:\n")
278
+ click.echo(restored)
279
+
280
+ import subprocess
281
+ try:
282
+ subprocess.run(["pbcopy"], input=restored.encode(), check=True, timeout=5)
283
+ click.echo("\n📋 In Zwischenablage kopiert!")
284
+ except Exception:
285
+ pass
286
+
287
+
288
+ @main.command(name="shell")
289
+ def shell():
290
+ """Interaktive Shell mit Slash-Commands (/help, /settings, /denylist, ...)."""
291
+ from .interactive import run_interactive
292
+ run_interactive()
293
+
294
+
295
+ @main.command()
296
+ def status():
297
+ """Status anzeigen."""
298
+ cfg = ProxyConfig.load()
299
+
300
+ proxy_running = _is_proxy_running()
301
+
302
+ click.echo(f"\n🛡 AUSTR.AI Status\n")
303
+ click.echo(f" Proxy: {'✓ laeuft' if proxy_running else '✗ gestoppt'}")
304
+ click.echo(f" Port: {cfg.port}")
305
+ click.echo(f" Anthropic: {'✓' if cfg.anthropic_api_key else '✗'}")
306
+ click.echo(f" OpenAI: {'✓' if cfg.openai_api_key else '✗'}")
307
+ click.echo(f" Backend: {"lokal"}")
308
+ click.echo(f" Deny-List: {len(cfg.deny_list)} Begriffe")
309
+ click.echo(f" Config: {CONFIG_DIR / 'proxy.yaml'}")
310
+ click.echo()
311
+
312
+ if proxy_running:
313
+ click.echo(f" Apps verbinden auf: http://localhost:{cfg.port}")
314
+ click.echo(f" Claude Code: aai claude")
315
+ else:
316
+ click.echo(" Starten mit: aai start")
317
+
318
+
319
+ # -----------------------------------------------------------------------
320
+ # Helpers
321
+ # -----------------------------------------------------------------------
322
+
323
+ def _start_proxy_foreground(config):
324
+ """Start proxy in foreground (blocking)."""
325
+ import uvicorn
326
+ from .server import create_app
327
+
328
+ port = config.port
329
+ anth = "✓" if config.anthropic_api_key else "✗"
330
+ oai = "✓" if config.openai_api_key else "✗"
331
+
332
+ click.echo(f"""
333
+ 🛡 AUSTR.AI Privacy Proxy
334
+
335
+ http://localhost:{port}
336
+
337
+ Anthropic: {anth} OpenAI: {oai}
338
+ Backend: {"lokal"}
339
+ Deny-List: {len(config.deny_list)} Begriffe
340
+
341
+ Verbinde deine Apps auf http://localhost:{port}
342
+ Ctrl+C zum Beenden
343
+ """)
344
+
345
+ app = create_app(config)
346
+
347
+ # Save PID
348
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
349
+ PROXY_PID_FILE.write_text(str(os.getpid()))
350
+
351
+ try:
352
+ uvicorn.run(app, host="127.0.0.1", port=port, log_level="info", access_log=False)
353
+ finally:
354
+ PROXY_PID_FILE.unlink(missing_ok=True)
355
+
356
+
357
+ def _start_proxy_background(config):
358
+ """Start proxy as background process."""
359
+ if _is_proxy_running():
360
+ click.echo(f"✓ Proxy laeuft bereits auf Port {config.port}.")
361
+ return
362
+
363
+ # Kill anything on the port (stale process)
364
+ _kill_port(config.port)
365
+
366
+ cmd = [sys.executable, "-m", "austrai_proxy", "start", "--port", str(config.port)]
367
+ proc = subprocess.Popen(
368
+ cmd,
369
+ stdout=subprocess.DEVNULL,
370
+ stderr=subprocess.DEVNULL,
371
+ start_new_session=True,
372
+ )
373
+
374
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
375
+ PROXY_PID_FILE.write_text(str(proc.pid))
376
+
377
+ time.sleep(2)
378
+ if _is_proxy_running():
379
+ click.echo(f"✓ Proxy gestartet im Hintergrund (Port {config.port}, PID {proc.pid})")
380
+ else:
381
+ click.echo("✗ Proxy konnte nicht gestartet werden.")
382
+
383
+
384
+ def _ensure_proxy_running(config):
385
+ """Make sure the proxy is running, start it if not."""
386
+ if not _is_proxy_running():
387
+ if config.anthropic_api_key or config.openai_api_key:
388
+ _start_proxy_background(config)
389
+ else:
390
+ click.echo("Proxy nicht gestartet — kein API Key konfiguriert.")
391
+ click.echo(" aai config")
392
+
393
+
394
+ def _is_proxy_running() -> bool:
395
+ """Check if proxy is running."""
396
+ if not PROXY_PID_FILE.exists():
397
+ return False
398
+ try:
399
+ pid = int(PROXY_PID_FILE.read_text().strip())
400
+ os.kill(pid, 0) # Check if process exists
401
+ return True
402
+ except (ProcessLookupError, ValueError, PermissionError):
403
+ PROXY_PID_FILE.unlink(missing_ok=True)
404
+ return False
405
+
406
+
407
+ def _find_desktop_app() -> str | None:
408
+ """Find the desktop app script."""
409
+ # Try relative to this package
410
+ candidates = [
411
+ DESKTOP_APP,
412
+ os.path.expanduser("~/Applications/AUSTR.AI/austrai_app.py"),
413
+ os.path.expanduser("~/.austrai/austrai_app.py"),
414
+ ]
415
+ for path in candidates:
416
+ if os.path.exists(path):
417
+ return path
418
+ return None
419
+
420
+
421
+ def _kill_port(port: int) -> None:
422
+ """Kill any process using the given port."""
423
+ try:
424
+ result = subprocess.run(
425
+ ["lsof", "-ti", f":{port}"],
426
+ capture_output=True, text=True, timeout=5,
427
+ )
428
+ if result.stdout.strip():
429
+ for pid in result.stdout.strip().split("\n"):
430
+ try:
431
+ os.kill(int(pid), signal.SIGTERM)
432
+ except (ProcessLookupError, ValueError):
433
+ pass
434
+ time.sleep(1)
435
+ except Exception:
436
+ pass
437
+
438
+
439
+ def _save_last_session(mappings: dict, session_id: str) -> None:
440
+ """Persist mappings to disk so deanon can read them."""
441
+ import json
442
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
443
+ session_file = CONFIG_DIR / "last_session.json"
444
+ session_file.write_text(json.dumps({
445
+ "session_id": session_id,
446
+ "mappings": mappings,
447
+ }, ensure_ascii=False))
448
+
449
+
450
+ def _load_last_session() -> dict | None:
451
+ """Load the last saved session mappings from disk."""
452
+ import json
453
+ session_file = CONFIG_DIR / "last_session.json"
454
+ if not session_file.exists():
455
+ return None
456
+ try:
457
+ data = json.loads(session_file.read_text())
458
+ return data.get("mappings")
459
+ except Exception:
460
+ return None
461
+
462
+
463
+ def _mask(key: str) -> str:
464
+ if not key or len(key) < 12:
465
+ return ""
466
+ return key[:8] + "..." + key[-4:]
467
+
468
+
469
+ if __name__ == "__main__":
470
+ main()
@@ -0,0 +1,72 @@
1
+ """Configuration management for AUSTR.AI."""
2
+
3
+ import os
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+ import yaml
8
+
9
+ CONFIG_DIR = Path.home() / ".austrai"
10
+ CONFIG_FILE = CONFIG_DIR / "proxy.yaml"
11
+ DEFAULT_PORT = 8282
12
+
13
+
14
+ @dataclass
15
+ class ProxyConfig:
16
+ anthropic_api_key: str = ""
17
+ openai_api_key: str = ""
18
+ mistral_api_key: str = ""
19
+ google_api_key: str = ""
20
+ port: int = DEFAULT_PORT
21
+ deny_list: list[str] = field(default_factory=list)
22
+ confidence_threshold: float = 0.6
23
+ spacy_model: str = "de_core_news_lg"
24
+
25
+ @classmethod
26
+ def load(cls) -> "ProxyConfig":
27
+ """Load config from file, env vars override file values."""
28
+ config = cls()
29
+
30
+ if CONFIG_FILE.exists():
31
+ try:
32
+ data = yaml.safe_load(CONFIG_FILE.read_text()) or {}
33
+ config.anthropic_api_key = data.get("anthropic_api_key", "")
34
+ config.openai_api_key = data.get("openai_api_key", "")
35
+ config.mistral_api_key = data.get("mistral_api_key", "")
36
+ config.google_api_key = data.get("google_api_key", "")
37
+ config.port = data.get("port", DEFAULT_PORT)
38
+ config.deny_list = data.get("deny_list", [])
39
+ config.confidence_threshold = data.get("confidence_threshold", 0.6)
40
+ config.spacy_model = data.get("spacy_model", "de_core_news_lg")
41
+ except Exception:
42
+ pass
43
+
44
+ # Env vars override file
45
+ if os.environ.get("ANTHROPIC_API_KEY"):
46
+ config.anthropic_api_key = os.environ["ANTHROPIC_API_KEY"]
47
+ if os.environ.get("OPENAI_API_KEY"):
48
+ config.openai_api_key = os.environ["OPENAI_API_KEY"]
49
+ if os.environ.get("MISTRAL_API_KEY"):
50
+ config.mistral_api_key = os.environ["MISTRAL_API_KEY"]
51
+ if os.environ.get("GOOGLE_API_KEY"):
52
+ config.google_api_key = os.environ["GOOGLE_API_KEY"]
53
+ if os.environ.get("AUSTRAI_PORT"):
54
+ config.port = int(os.environ["AUSTRAI_PORT"])
55
+
56
+ return config
57
+
58
+ def save(self) -> None:
59
+ """Save config to file."""
60
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
61
+ data = {
62
+ "anthropic_api_key": self.anthropic_api_key,
63
+ "openai_api_key": self.openai_api_key,
64
+ "mistral_api_key": self.mistral_api_key,
65
+ "google_api_key": self.google_api_key,
66
+ "port": self.port,
67
+ "deny_list": self.deny_list,
68
+ "confidence_threshold": self.confidence_threshold,
69
+ "spacy_model": self.spacy_model,
70
+ }
71
+ CONFIG_FILE.write_text(yaml.dump(data, default_flow_style=False, allow_unicode=True))
72
+ CONFIG_FILE.chmod(0o600)