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 +17 -0
- austrai-1.0.0/austrai.egg-info/PKG-INFO +17 -0
- austrai-1.0.0/austrai.egg-info/SOURCES.txt +25 -0
- austrai-1.0.0/austrai.egg-info/dependency_links.txt +1 -0
- austrai-1.0.0/austrai.egg-info/entry_points.txt +2 -0
- austrai-1.0.0/austrai.egg-info/requires.txt +13 -0
- austrai-1.0.0/austrai.egg-info/top_level.txt +1 -0
- austrai-1.0.0/austrai_proxy/__init__.py +3 -0
- austrai-1.0.0/austrai_proxy/__main__.py +5 -0
- austrai-1.0.0/austrai_proxy/cli.py +470 -0
- austrai-1.0.0/austrai_proxy/config.py +72 -0
- austrai-1.0.0/austrai_proxy/core/__init__.py +124 -0
- austrai-1.0.0/austrai_proxy/core/anonymizer.py +84 -0
- austrai-1.0.0/austrai_proxy/core/austrian_recognizers.py +522 -0
- austrai-1.0.0/austrai_proxy/core/codename_engine.py +123 -0
- austrai-1.0.0/austrai_proxy/core/context_learner.py +314 -0
- austrai-1.0.0/austrai_proxy/core/detector.py +366 -0
- austrai-1.0.0/austrai_proxy/core/extractor.py +103 -0
- austrai-1.0.0/austrai_proxy/core/models.py +13 -0
- austrai-1.0.0/austrai_proxy/core/rehydrator.py +103 -0
- austrai-1.0.0/austrai_proxy/core/session_store.py +79 -0
- austrai-1.0.0/austrai_proxy/core/setup.py +37 -0
- austrai-1.0.0/austrai_proxy/interactive.py +421 -0
- austrai-1.0.0/austrai_proxy/server.py +406 -0
- austrai-1.0.0/austrai_proxy/stream_rehydrator.py +91 -0
- austrai-1.0.0/pyproject.toml +30 -0
- austrai-1.0.0/setup.cfg +4 -0
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
austrai_proxy
|
|
@@ -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)
|