Spaces:
Sleeping
Sleeping
JerameeUC commited on
Commit Β·
071c820
1
Parent(s): aa2c39f
6th commit
Browse files- FLATTENED_CODE.txt +0 -0
- app/app.py +88 -69
- app/app/app.py +0 -65
- app/app/routes.py +0 -72
- app/routes.py +2 -2
- core/config.py +68 -1
- core/logging.py +73 -0
- core/types.py +33 -0
- flat_tree_filter.py +213 -0
- guardrails/pii_redaction.py +111 -1
- guardrails/safety.py +108 -1
- tree.txt +103 -0
- tree_filter.py +108 -0
FLATTENED_CODE.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
app/app.py
CHANGED
|
@@ -1,53 +1,49 @@
|
|
| 1 |
# /app/app.py
|
| 2 |
#!/usr/bin/env python3
|
| 3 |
-
# app.py β aiohttp + Bot Framework
|
| 4 |
-
|
| 5 |
-
import os
|
| 6 |
-
import sys
|
| 7 |
-
import json
|
| 8 |
-
from logic import handle_text
|
| 9 |
from aiohttp import web
|
| 10 |
-
from botbuilder.core import BotFrameworkAdapter, BotFrameworkAdapterSettings, TurnContext
|
| 11 |
-
from botbuilder.schema import Activity
|
| 12 |
-
import aiohttp_cors
|
| 13 |
from pathlib import Path
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
else:
|
| 41 |
-
await turn_context.send_activity("
|
| 42 |
-
else:
|
| 43 |
-
await turn_context.send_activity(f"[{turn_context.activity.type}] event received.")
|
| 44 |
-
|
| 45 |
-
# -------------------------------------------------------------------
|
| 46 |
-
# Adapter / bot setup
|
| 47 |
-
# -------------------------------------------------------------------
|
| 48 |
-
APP_ID = os.environ.get("MicrosoftAppId") or None
|
| 49 |
-
APP_PASSWORD = os.environ.get("MicrosoftAppPassword") or None
|
| 50 |
|
|
|
|
|
|
|
|
|
|
| 51 |
adapter_settings = BotFrameworkAdapterSettings(APP_ID, APP_PASSWORD)
|
| 52 |
adapter = BotFrameworkAdapter(adapter_settings)
|
| 53 |
|
|
@@ -59,17 +55,30 @@ async def on_error(context: TurnContext, error: Exception):
|
|
| 59 |
print(f"[on_turn_error][send_activity_failed] {send_err}", file=sys.stderr, flush=True)
|
| 60 |
|
| 61 |
adapter.on_turn_error = on_error
|
| 62 |
-
bot =
|
| 63 |
-
|
| 64 |
-
# -
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
async def messages(req: web.Request) -> web.Response:
|
| 68 |
-
# Content-Type can include charset; do a contains check
|
| 69 |
ctype = (req.headers.get("Content-Type") or "").lower()
|
| 70 |
if "application/json" not in ctype:
|
| 71 |
return web.Response(status=415, text="Unsupported Media Type: expected application/json")
|
| 72 |
-
|
| 73 |
try:
|
| 74 |
body = await req.json()
|
| 75 |
except json.JSONDecodeError:
|
|
@@ -77,20 +86,11 @@ async def messages(req: web.Request) -> web.Response:
|
|
| 77 |
|
| 78 |
activity = Activity().deserialize(body)
|
| 79 |
auth_header = req.headers.get("Authorization")
|
| 80 |
-
|
| 81 |
invoke_response = await adapter.process_activity(activity, auth_header, bot.on_turn)
|
| 82 |
if invoke_response:
|
| 83 |
-
# For invoke activities, adapter returns explicit status/body
|
| 84 |
return web.json_response(data=invoke_response.body, status=invoke_response.status)
|
| 85 |
-
# Acknowledge standard message activities
|
| 86 |
return web.Response(status=202, text="Accepted")
|
| 87 |
|
| 88 |
-
async def home(_req: web.Request) -> web.Response:
|
| 89 |
-
return web.Response(
|
| 90 |
-
text="Bot is running. POST Bot Framework activities to /api/messages.",
|
| 91 |
-
content_type="text/plain"
|
| 92 |
-
)
|
| 93 |
-
|
| 94 |
async def messages_get(_req: web.Request) -> web.Response:
|
| 95 |
return web.Response(
|
| 96 |
text="This endpoint only accepts POST (Bot Framework activities).",
|
|
@@ -98,6 +98,12 @@ async def messages_get(_req: web.Request) -> web.Response:
|
|
| 98 |
status=405
|
| 99 |
)
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
async def healthz(_req: web.Request) -> web.Response:
|
| 102 |
return web.json_response({"status": "ok"})
|
| 103 |
|
|
@@ -107,33 +113,46 @@ async def plain_chat(req: web.Request) -> web.Response:
|
|
| 107 |
except Exception:
|
| 108 |
return web.json_response({"error": "Invalid JSON"}, status=400)
|
| 109 |
user_text = payload.get("text", "")
|
| 110 |
-
reply =
|
| 111 |
return web.json_response({"reply": reply})
|
| 112 |
|
| 113 |
-
# ----------------------------------------
|
| 114 |
-
# App factory and entrypoint
|
| 115 |
-
# -------------------------------------------------------------------
|
| 116 |
-
from pathlib import Path
|
| 117 |
-
|
| 118 |
def create_app() -> web.Application:
|
| 119 |
app = web.Application()
|
|
|
|
|
|
|
| 120 |
app.router.add_get("/", home)
|
| 121 |
app.router.add_get("/healthz", healthz)
|
| 122 |
app.router.add_get("/api/messages", messages_get)
|
| 123 |
app.router.add_post("/api/messages", messages)
|
| 124 |
app.router.add_post("/plain-chat", plain_chat)
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
static_dir = Path(__file__).parent / "static"
|
| 127 |
if static_dir.exists():
|
| 128 |
app.router.add_static("/static/", path=static_dir, show_index=True)
|
| 129 |
else:
|
| 130 |
-
|
| 131 |
|
| 132 |
return app
|
| 133 |
|
| 134 |
app = create_app()
|
| 135 |
|
| 136 |
if __name__ == "__main__":
|
| 137 |
-
|
| 138 |
-
port = int(os.environ.get("PORT", 3978))
|
| 139 |
-
web.run_app(app, host=host, port=port)
|
|
|
|
| 1 |
# /app/app.py
|
| 2 |
#!/usr/bin/env python3
|
| 3 |
+
# app.py β aiohttp + Bot Framework (root-level). Routes added explicitly.
|
| 4 |
+
import os, sys, json
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
from aiohttp import web
|
|
|
|
|
|
|
|
|
|
| 6 |
from pathlib import Path
|
| 7 |
+
from botbuilder.core import BotFrameworkAdapter, BotFrameworkAdapterSettings, TurnContext, ActivityHandler
|
| 8 |
+
from botbuilder.schema import Activity
|
| 9 |
|
| 10 |
+
# Config / logging
|
| 11 |
+
from core.config import settings
|
| 12 |
+
from core.logging import setup_logging, get_logger
|
| 13 |
+
|
| 14 |
+
setup_logging(level=settings.log_level, json_logs=settings.json_logs)
|
| 15 |
+
log = get_logger("bootstrap")
|
| 16 |
+
log.info("starting", extra={"config": settings.to_dict()})
|
| 17 |
+
|
| 18 |
+
# Bot impl: prefer user's SimpleBot, fallback to tiny bot
|
| 19 |
+
try:
|
| 20 |
+
from bot import SimpleBot as BotImpl # user's ActivityHandler
|
| 21 |
+
except Exception:
|
| 22 |
+
class BotImpl(ActivityHandler):
|
| 23 |
+
async def on_turn(self, turn_context: TurnContext):
|
| 24 |
+
if (turn_context.activity.type or "").lower() == "message":
|
| 25 |
+
text = (turn_context.activity.text or "").strip()
|
| 26 |
+
if not text:
|
| 27 |
+
await turn_context.send_activity("Input was empty. Type 'help' for usage.")
|
| 28 |
+
return
|
| 29 |
+
lower = text.lower()
|
| 30 |
+
if lower == "help":
|
| 31 |
+
await turn_context.send_activity("Try: echo <msg> | reverse: <msg> | capabilities")
|
| 32 |
+
elif lower == "capabilities":
|
| 33 |
+
await turn_context.send_activity("- echo\n- reverse\n- help\n- capabilities")
|
| 34 |
+
elif lower.startswith("reverse:"):
|
| 35 |
+
payload = text.split(":", 1)[1].strip()
|
| 36 |
+
await turn_context.send_activity(payload[::-1])
|
| 37 |
+
elif lower.startswith("echo "):
|
| 38 |
+
await turn_context.send_activity(text[5:])
|
| 39 |
+
else:
|
| 40 |
+
await turn_context.send_activity("Unsupported command. Type 'help' for examples.")
|
| 41 |
else:
|
| 42 |
+
await turn_context.send_activity(f"[{turn_context.activity.type}] event received.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
+
# Adapter / credentials
|
| 45 |
+
APP_ID = os.environ.get("MicrosoftAppId") or settings.microsoft_app_id
|
| 46 |
+
APP_PASSWORD = os.environ.get("MicrosoftAppPassword") or settings.microsoft_app_password
|
| 47 |
adapter_settings = BotFrameworkAdapterSettings(APP_ID, APP_PASSWORD)
|
| 48 |
adapter = BotFrameworkAdapter(adapter_settings)
|
| 49 |
|
|
|
|
| 55 |
print(f"[on_turn_error][send_activity_failed] {send_err}", file=sys.stderr, flush=True)
|
| 56 |
|
| 57 |
adapter.on_turn_error = on_error
|
| 58 |
+
bot = BotImpl()
|
| 59 |
+
|
| 60 |
+
# Prefer project logic for /plain-chat; otherwise fallback to simple helpers
|
| 61 |
+
try:
|
| 62 |
+
from logic import handle_text as _handle_text
|
| 63 |
+
except Exception:
|
| 64 |
+
from skills import normalize, reverse_text, is_empty
|
| 65 |
+
def _handle_text(user_text: str) -> str:
|
| 66 |
+
text = (user_text or "").strip()
|
| 67 |
+
if not text:
|
| 68 |
+
return "Please provide text."
|
| 69 |
+
cmd = normalize(text)
|
| 70 |
+
if cmd in {"help", "capabilities"}:
|
| 71 |
+
return "Try: reverse <text> | or just say anything"
|
| 72 |
+
if cmd.startswith("reverse "):
|
| 73 |
+
original = text.split(" ", 1)[1] if " " in text else ""
|
| 74 |
+
return reverse_text(original)
|
| 75 |
+
return f"You said: {text}"
|
| 76 |
+
|
| 77 |
+
# -------------------- HTTP handlers (module-level) --------------------
|
| 78 |
async def messages(req: web.Request) -> web.Response:
|
|
|
|
| 79 |
ctype = (req.headers.get("Content-Type") or "").lower()
|
| 80 |
if "application/json" not in ctype:
|
| 81 |
return web.Response(status=415, text="Unsupported Media Type: expected application/json")
|
|
|
|
| 82 |
try:
|
| 83 |
body = await req.json()
|
| 84 |
except json.JSONDecodeError:
|
|
|
|
| 86 |
|
| 87 |
activity = Activity().deserialize(body)
|
| 88 |
auth_header = req.headers.get("Authorization")
|
|
|
|
| 89 |
invoke_response = await adapter.process_activity(activity, auth_header, bot.on_turn)
|
| 90 |
if invoke_response:
|
|
|
|
| 91 |
return web.json_response(data=invoke_response.body, status=invoke_response.status)
|
|
|
|
| 92 |
return web.Response(status=202, text="Accepted")
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
async def messages_get(_req: web.Request) -> web.Response:
|
| 95 |
return web.Response(
|
| 96 |
text="This endpoint only accepts POST (Bot Framework activities).",
|
|
|
|
| 98 |
status=405
|
| 99 |
)
|
| 100 |
|
| 101 |
+
async def home(_req: web.Request) -> web.Response:
|
| 102 |
+
return web.Response(
|
| 103 |
+
text="Bot is running. POST Bot Framework activities to /api/messages.",
|
| 104 |
+
content_type="text/plain"
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
async def healthz(_req: web.Request) -> web.Response:
|
| 108 |
return web.json_response({"status": "ok"})
|
| 109 |
|
|
|
|
| 113 |
except Exception:
|
| 114 |
return web.json_response({"error": "Invalid JSON"}, status=400)
|
| 115 |
user_text = payload.get("text", "")
|
| 116 |
+
reply = _handle_text(user_text)
|
| 117 |
return web.json_response({"reply": reply})
|
| 118 |
|
| 119 |
+
# -------------------- App factory --------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
def create_app() -> web.Application:
|
| 121 |
app = web.Application()
|
| 122 |
+
|
| 123 |
+
# Add routes explicitly (as requested)
|
| 124 |
app.router.add_get("/", home)
|
| 125 |
app.router.add_get("/healthz", healthz)
|
| 126 |
app.router.add_get("/api/messages", messages_get)
|
| 127 |
app.router.add_post("/api/messages", messages)
|
| 128 |
app.router.add_post("/plain-chat", plain_chat)
|
| 129 |
|
| 130 |
+
# Optional CORS (if installed)
|
| 131 |
+
try:
|
| 132 |
+
import aiohttp_cors
|
| 133 |
+
cors = aiohttp_cors.setup(app, defaults={
|
| 134 |
+
"*": aiohttp_cors.ResourceOptions(
|
| 135 |
+
allow_credentials=True,
|
| 136 |
+
expose_headers="*",
|
| 137 |
+
allow_headers="*",
|
| 138 |
+
allow_methods=["GET","POST","OPTIONS"],
|
| 139 |
+
)
|
| 140 |
+
})
|
| 141 |
+
for route in list(app.router.routes()):
|
| 142 |
+
cors.add(route)
|
| 143 |
+
except Exception:
|
| 144 |
+
pass
|
| 145 |
+
|
| 146 |
+
# Static (./static)
|
| 147 |
static_dir = Path(__file__).parent / "static"
|
| 148 |
if static_dir.exists():
|
| 149 |
app.router.add_static("/static/", path=static_dir, show_index=True)
|
| 150 |
else:
|
| 151 |
+
log.warning("static directory not found", extra={"path": str(static_dir)})
|
| 152 |
|
| 153 |
return app
|
| 154 |
|
| 155 |
app = create_app()
|
| 156 |
|
| 157 |
if __name__ == "__main__":
|
| 158 |
+
web.run_app(app, host=settings.host, port=settings.port)
|
|
|
|
|
|
app/app/app.py
DELETED
|
@@ -1,65 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
# app/app.py β aiohttp + Bot Framework bootstrap
|
| 3 |
-
|
| 4 |
-
import os, sys
|
| 5 |
-
from aiohttp import web
|
| 6 |
-
from pathlib import Path
|
| 7 |
-
from botbuilder.core import BotFrameworkAdapter, BotFrameworkAdapterSettings, TurnContext
|
| 8 |
-
|
| 9 |
-
from app.routes import init_routes
|
| 10 |
-
from bot import SimpleBot
|
| 11 |
-
|
| 12 |
-
# Credentials
|
| 13 |
-
APP_ID = os.environ.get("MicrosoftAppId") or None
|
| 14 |
-
APP_PASSWORD = os.environ.get("MicrosoftAppPassword") or None
|
| 15 |
-
|
| 16 |
-
adapter_settings = BotFrameworkAdapterSettings(APP_ID, APP_PASSWORD)
|
| 17 |
-
adapter = BotFrameworkAdapter(adapter_settings)
|
| 18 |
-
|
| 19 |
-
async def on_error(context: TurnContext, error: Exception):
|
| 20 |
-
print(f"[on_turn_error] {error}", file=sys.stderr, flush=True)
|
| 21 |
-
try:
|
| 22 |
-
await context.send_activity("Oops. Something went wrong!")
|
| 23 |
-
except Exception as send_err:
|
| 24 |
-
print(f"[on_turn_error][send_activity_failed] {send_err}", file=sys.stderr, flush=True)
|
| 25 |
-
|
| 26 |
-
adapter.on_turn_error = on_error
|
| 27 |
-
|
| 28 |
-
# Bot instance
|
| 29 |
-
bot = SimpleBot()
|
| 30 |
-
|
| 31 |
-
def create_app() -> web.Application:
|
| 32 |
-
app = web.Application()
|
| 33 |
-
init_routes(app, adapter, bot)
|
| 34 |
-
|
| 35 |
-
# Optional CORS
|
| 36 |
-
try:
|
| 37 |
-
import aiohttp_cors
|
| 38 |
-
cors = aiohttp_cors.setup(app, defaults={
|
| 39 |
-
"*": aiohttp_cors.ResourceOptions(
|
| 40 |
-
allow_credentials=True,
|
| 41 |
-
expose_headers="*",
|
| 42 |
-
allow_headers="*",
|
| 43 |
-
allow_methods=["GET","POST","OPTIONS"],
|
| 44 |
-
)
|
| 45 |
-
})
|
| 46 |
-
for route in list(app.router.routes()):
|
| 47 |
-
cors.add(route)
|
| 48 |
-
except Exception:
|
| 49 |
-
pass
|
| 50 |
-
|
| 51 |
-
# Static folder if present
|
| 52 |
-
static_dir = Path(__file__).parent / "static"
|
| 53 |
-
if static_dir.exists():
|
| 54 |
-
app.router.add_static("/static/", path=static_dir, show_index=True)
|
| 55 |
-
else:
|
| 56 |
-
print(f"[warn] static directory not found: {static_dir}", flush=True)
|
| 57 |
-
|
| 58 |
-
return app
|
| 59 |
-
|
| 60 |
-
app = create_app()
|
| 61 |
-
|
| 62 |
-
if __name__ == "__main__":
|
| 63 |
-
host = os.environ.get("HOST", "127.0.0.1") # use 0.0.0.0 in containers
|
| 64 |
-
port = int(os.environ.get("PORT", 3978))
|
| 65 |
-
web.run_app(app, host=host, port=port)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/app/routes.py
DELETED
|
@@ -1,72 +0,0 @@
|
|
| 1 |
-
# app/routes.py β HTTP handlers
|
| 2 |
-
import json
|
| 3 |
-
from aiohttp import web
|
| 4 |
-
from botbuilder.core import TurnContext
|
| 5 |
-
from botbuilder.schema import Activity
|
| 6 |
-
|
| 7 |
-
# Prefer project logic if available
|
| 8 |
-
try:
|
| 9 |
-
from logic import handle_text as _handle_text
|
| 10 |
-
except Exception:
|
| 11 |
-
from skills import normalize, reverse_text, is_empty
|
| 12 |
-
def _handle_text(user_text: str) -> str:
|
| 13 |
-
text = (user_text or "").strip()
|
| 14 |
-
if not text:
|
| 15 |
-
return "Please provide text."
|
| 16 |
-
cmd = normalize(text)
|
| 17 |
-
if cmd in {"help", "capabilities"}:
|
| 18 |
-
return "Try: reverse <text> | or just say anything"
|
| 19 |
-
if cmd.startswith("reverse "):
|
| 20 |
-
original = text.split(" ", 1)[1] if " " in text else ""
|
| 21 |
-
return reverse_text(original)
|
| 22 |
-
return f"You said: {text}"
|
| 23 |
-
|
| 24 |
-
def init_routes(app: web.Application, adapter, bot) -> None:
|
| 25 |
-
async def messages(req: web.Request) -> web.Response:
|
| 26 |
-
ctype = (req.headers.get("Content-Type") or "").lower()
|
| 27 |
-
if "application/json" not in ctype:
|
| 28 |
-
return web.Response(status=415, text="Unsupported Media Type: expected application/json")
|
| 29 |
-
try:
|
| 30 |
-
body = await req.json()
|
| 31 |
-
except json.JSONDecodeError:
|
| 32 |
-
return web.Response(status=400, text="Invalid JSON body")
|
| 33 |
-
|
| 34 |
-
activity = Activity().deserialize(body)
|
| 35 |
-
auth_header = req.headers.get("Authorization")
|
| 36 |
-
|
| 37 |
-
invoke_response = await adapter.process_activity(activity, auth_header, bot.on_turn)
|
| 38 |
-
if invoke_response:
|
| 39 |
-
return web.json_response(data=invoke_response.body, status=invoke_response.status)
|
| 40 |
-
return web.Response(status=202, text="Accepted")
|
| 41 |
-
|
| 42 |
-
async def messages_get(_req: web.Request) -> web.Response:
|
| 43 |
-
return web.Response(
|
| 44 |
-
text="This endpoint only accepts POST (Bot Framework activities).",
|
| 45 |
-
content_type="text/plain",
|
| 46 |
-
status=405
|
| 47 |
-
)
|
| 48 |
-
|
| 49 |
-
async def home(_req: web.Request) -> web.Response:
|
| 50 |
-
return web.Response(
|
| 51 |
-
text="Bot is running. POST Bot Framework activities to /api/messages.",
|
| 52 |
-
content_type="text/plain"
|
| 53 |
-
)
|
| 54 |
-
|
| 55 |
-
async def healthz(_req: web.Request) -> web.Response:
|
| 56 |
-
return web.json_response({"status": "ok"})
|
| 57 |
-
|
| 58 |
-
async def plain_chat(req: web.Request) -> web.Response:
|
| 59 |
-
try:
|
| 60 |
-
payload = await req.json()
|
| 61 |
-
except Exception:
|
| 62 |
-
return web.json_response({"error": "Invalid JSON"}, status=400)
|
| 63 |
-
user_text = payload.get("text", "")
|
| 64 |
-
reply = _handle_text(user_text)
|
| 65 |
-
return web.json_response({"reply": reply})
|
| 66 |
-
|
| 67 |
-
# Wire routes
|
| 68 |
-
app.router.add_get("/", home)
|
| 69 |
-
app.router.add_get("/healthz", healthz)
|
| 70 |
-
app.router.add_get("/api/messages", messages_get)
|
| 71 |
-
app.router.add_post("/api/messages", messages)
|
| 72 |
-
app.router.add_post("/plain-chat", plain_chat)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/routes.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
| 1 |
# /app/routes.py β HTTP handlers
|
|
|
|
| 2 |
import json
|
| 3 |
from aiohttp import web
|
| 4 |
-
from botbuilder.core import TurnContext
|
| 5 |
from botbuilder.schema import Activity
|
| 6 |
|
| 7 |
# Prefer project logic if available
|
| 8 |
try:
|
| 9 |
-
from logic import handle_text as _handle_text
|
| 10 |
except Exception:
|
| 11 |
from skills import normalize, reverse_text, is_empty
|
| 12 |
def _handle_text(user_text: str) -> str:
|
|
|
|
| 1 |
# /app/routes.py β HTTP handlers
|
| 2 |
+
# routes.py β HTTP handlers (root-level, no /app package)
|
| 3 |
import json
|
| 4 |
from aiohttp import web
|
|
|
|
| 5 |
from botbuilder.schema import Activity
|
| 6 |
|
| 7 |
# Prefer project logic if available
|
| 8 |
try:
|
| 9 |
+
from logic import handle_text as _handle_text # user-defined
|
| 10 |
except Exception:
|
| 11 |
from skills import normalize, reverse_text, is_empty
|
| 12 |
def _handle_text(user_text: str) -> str:
|
core/config.py
CHANGED
|
@@ -1,4 +1,71 @@
|
|
| 1 |
# /core/config.py
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
settings = Settings()
|
|
|
|
| 1 |
# /core/config.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
import os
|
| 4 |
+
from dataclasses import dataclass, field
|
| 5 |
+
from typing import List, Optional
|
| 6 |
|
| 7 |
+
|
| 8 |
+
def _as_bool(v: Optional[str], default: bool = False) -> bool:
|
| 9 |
+
if v is None:
|
| 10 |
+
return default
|
| 11 |
+
return v.strip().lower() in {"1", "true", "yes", "y", "on"}
|
| 12 |
+
|
| 13 |
+
def _as_int(v: Optional[str], default: int) -> int:
|
| 14 |
+
try:
|
| 15 |
+
return int(v) if v is not None else default
|
| 16 |
+
except ValueError:
|
| 17 |
+
return default
|
| 18 |
+
|
| 19 |
+
def _as_list(v: Optional[str], default: List[str] | None = None) -> List[str]:
|
| 20 |
+
if not v:
|
| 21 |
+
return list(default or [])
|
| 22 |
+
return [item.strip() for item in v.split(",") if item.strip()]
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@dataclass(slots=True)
|
| 26 |
+
class Settings:
|
| 27 |
+
# Runtime / environment
|
| 28 |
+
env: str = field(default_factory=lambda: os.getenv("ENV", "dev"))
|
| 29 |
+
debug: bool = field(default_factory=lambda: _as_bool(os.getenv("DEBUG"), False))
|
| 30 |
+
|
| 31 |
+
# Host/port
|
| 32 |
+
host: str = field(default_factory=lambda: os.getenv("HOST", "127.0.0.1"))
|
| 33 |
+
port: int = field(default_factory=lambda: _as_int(os.getenv("PORT"), 3978))
|
| 34 |
+
|
| 35 |
+
# Logging
|
| 36 |
+
log_level: str = field(default_factory=lambda: os.getenv("LOG_LEVEL", "INFO"))
|
| 37 |
+
json_logs: bool = field(default_factory=lambda: _as_bool(os.getenv("JSON_LOGS"), False))
|
| 38 |
+
|
| 39 |
+
# CORS
|
| 40 |
+
cors_allow_origins: List[str] = field(
|
| 41 |
+
default_factory=lambda: _as_list(os.getenv("CORS_ALLOW_ORIGINS"), ["*"])
|
| 42 |
+
)
|
| 43 |
+
cors_allow_methods: List[str] = field(
|
| 44 |
+
default_factory=lambda: _as_list(os.getenv("CORS_ALLOW_METHODS"), ["GET", "POST", "OPTIONS"])
|
| 45 |
+
)
|
| 46 |
+
cors_allow_headers: List[str] = field(
|
| 47 |
+
default_factory=lambda: _as_list(os.getenv("CORS_ALLOW_HEADERS"), ["*"])
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
# Bot Framework credentials
|
| 51 |
+
microsoft_app_id: Optional[str] = field(default_factory=lambda: os.getenv("MicrosoftAppId"))
|
| 52 |
+
microsoft_app_password: Optional[str] = field(default_factory=lambda: os.getenv("MicrosoftAppPassword"))
|
| 53 |
+
|
| 54 |
+
def to_dict(self) -> dict:
|
| 55 |
+
return {
|
| 56 |
+
"env": self.env,
|
| 57 |
+
"debug": self.debug,
|
| 58 |
+
"host": self.host,
|
| 59 |
+
"port": self.port,
|
| 60 |
+
"log_level": self.log_level,
|
| 61 |
+
"json_logs": self.json_logs,
|
| 62 |
+
"cors_allow_origins": self.cors_allow_origins,
|
| 63 |
+
"cors_allow_methods": self.cors_allow_methods,
|
| 64 |
+
"cors_allow_headers": self.cors_allow_headers,
|
| 65 |
+
"microsoft_app_id": bool(self.microsoft_app_id),
|
| 66 |
+
"microsoft_app_password": bool(self.microsoft_app_password),
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# singleton-style settings object
|
| 71 |
settings = Settings()
|
core/logging.py
CHANGED
|
@@ -1 +1,74 @@
|
|
| 1 |
# /core/logging.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# /core/logging.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
import json
|
| 4 |
+
import logging
|
| 5 |
+
import sys
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from typing import Optional
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
# Optional: human-friendly console colors if installed
|
| 11 |
+
import colorama # type: ignore
|
| 12 |
+
colorama.init()
|
| 13 |
+
_HAS_COLOR = True
|
| 14 |
+
except Exception: # pragma: no cover
|
| 15 |
+
_HAS_COLOR = False
|
| 16 |
+
|
| 17 |
+
# Very small JSON formatter (avoids extra deps)
|
| 18 |
+
class JsonFormatter(logging.Formatter):
|
| 19 |
+
def format(self, record: logging.LogRecord) -> str: # type: ignore[override]
|
| 20 |
+
payload = {
|
| 21 |
+
"ts": datetime.utcfromtimestamp(record.created).isoformat(timespec="milliseconds") + "Z",
|
| 22 |
+
"level": record.levelname,
|
| 23 |
+
"logger": record.name,
|
| 24 |
+
"msg": record.getMessage(),
|
| 25 |
+
}
|
| 26 |
+
if record.exc_info:
|
| 27 |
+
payload["exc_info"] = self.formatException(record.exc_info)
|
| 28 |
+
return json.dumps(payload, ensure_ascii=False)
|
| 29 |
+
|
| 30 |
+
class ConsoleFormatter(logging.Formatter):
|
| 31 |
+
def format(self, record: logging.LogRecord) -> str: # type: ignore[override]
|
| 32 |
+
ts = datetime.utcfromtimestamp(record.created).strftime("%H:%M:%S")
|
| 33 |
+
lvl = record.levelname
|
| 34 |
+
name = record.name
|
| 35 |
+
msg = record.getMessage()
|
| 36 |
+
|
| 37 |
+
if _HAS_COLOR:
|
| 38 |
+
COLORS = {
|
| 39 |
+
"DEBUG": "\033[37m",
|
| 40 |
+
"INFO": "\033[36m",
|
| 41 |
+
"WARNING": "\033[33m",
|
| 42 |
+
"ERROR": "\033[31m",
|
| 43 |
+
"CRITICAL": "\033[41m",
|
| 44 |
+
}
|
| 45 |
+
RESET = "\033[0m"
|
| 46 |
+
color = COLORS.get(lvl, "")
|
| 47 |
+
return f"{ts} {color}{lvl:<8}{RESET} {name}: {msg}"
|
| 48 |
+
return f"{ts} {lvl:<8} {name}: {msg}"
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
_initialized = False
|
| 52 |
+
|
| 53 |
+
def setup_logging(level: str = "INFO", json_logs: bool = False) -> None:
|
| 54 |
+
"""
|
| 55 |
+
Initialize root logger once.
|
| 56 |
+
"""
|
| 57 |
+
global _initialized
|
| 58 |
+
if _initialized:
|
| 59 |
+
return
|
| 60 |
+
_initialized = True
|
| 61 |
+
|
| 62 |
+
root = logging.getLogger()
|
| 63 |
+
root.setLevel(level.upper())
|
| 64 |
+
|
| 65 |
+
handler = logging.StreamHandler(sys.stdout)
|
| 66 |
+
handler.setFormatter(JsonFormatter() if json_logs else ConsoleFormatter())
|
| 67 |
+
root.handlers[:] = [handler]
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def get_logger(name: Optional[str] = None) -> logging.Logger:
|
| 71 |
+
"""
|
| 72 |
+
Get a logger (call setup_logging() first to configure formatting).
|
| 73 |
+
"""
|
| 74 |
+
return logging.getLogger(name or "app")
|
core/types.py
CHANGED
|
@@ -1 +1,34 @@
|
|
| 1 |
# /core/types.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# /core/types.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
from dataclasses import dataclass, field, asdict
|
| 4 |
+
from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict
|
| 5 |
+
|
| 6 |
+
Role = Literal["system", "user", "assistant"]
|
| 7 |
+
|
| 8 |
+
# Basic chat message
|
| 9 |
+
@dataclass(slots=True)
|
| 10 |
+
class ChatMessage:
|
| 11 |
+
role: Role
|
| 12 |
+
content: str
|
| 13 |
+
|
| 14 |
+
# Pair-based history (simple UI / anon_bot style)
|
| 15 |
+
ChatTurn = List[str] # [user, bot]
|
| 16 |
+
ChatHistory = List[ChatTurn] # [[u,b], [u,b], ...]
|
| 17 |
+
|
| 18 |
+
# Plain chat API payloads (/plain-chat)
|
| 19 |
+
@dataclass(slots=True)
|
| 20 |
+
class PlainChatRequest:
|
| 21 |
+
text: str
|
| 22 |
+
|
| 23 |
+
@dataclass(slots=True)
|
| 24 |
+
class PlainChatResponse:
|
| 25 |
+
reply: str
|
| 26 |
+
meta: Optional[Dict[str, Any]] = None
|
| 27 |
+
|
| 28 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 29 |
+
return asdict(self)
|
| 30 |
+
|
| 31 |
+
# Optional error shape for consistent JSON error responses
|
| 32 |
+
class ErrorPayload(TypedDict, total=False):
|
| 33 |
+
error: str
|
| 34 |
+
detail: str
|
flat_tree_filter.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# flatten_anytree.py β Flatten a folder tree (code/config) into one text file.
|
| 3 |
+
# Usage:
|
| 4 |
+
# python flatten_anytree.py [ROOT_DIR] [OUTPUT_FILE]
|
| 5 |
+
# Examples:
|
| 6 |
+
# python flatten_anytree.py C:\path\to\repo FLATTENED_CODE.txt
|
| 7 |
+
# python flatten_anytree.py . out.txt --include-exts .py,.ipynb --exclude-dirs .git,node_modules
|
| 8 |
+
#
|
| 9 |
+
# New in this patched version:
|
| 10 |
+
# - Skips common .gitignore-style junk by default (node_modules, .venv, __pycache__, caches, etc.).
|
| 11 |
+
# - Skips noisy/secret files like .env, .env.*, *.log, *.tmp, *.pyc by default.
|
| 12 |
+
# - Adds CLI flags: --exclude-dirs, --exclude-files, --exclude-globs to extend ignores.
|
| 13 |
+
# - Removes ".env" from default INCLUDE_EXTS for safety (you can still include via flags).
|
| 14 |
+
#
|
| 15 |
+
import json
|
| 16 |
+
import os
|
| 17 |
+
import sys
|
| 18 |
+
import fnmatch
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from typing import Iterable, Set, List
|
| 21 |
+
|
| 22 |
+
INCLUDE_EXTS: Set[str] = {
|
| 23 |
+
".py", ".ipynb", ".json", ".md", ".txt", ".yml", ".yaml",
|
| 24 |
+
".ini", ".cfg", ".conf", ".service", ".sh", ".bat",
|
| 25 |
+
".js", ".ts", ".tsx", ".jsx", ".css", ".html",
|
| 26 |
+
".toml", ".dockerfile"
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
EXCLUDE_DIRS: Set[str] = {
|
| 30 |
+
".git", ".hg", ".svn", "__pycache__", "node_modules",
|
| 31 |
+
".venv", "venv", "env", "dist", "build",
|
| 32 |
+
"artifacts", "logs", ".idea", ".vscode", ".pytest_cache",
|
| 33 |
+
".mypy_cache", ".ruff_cache", ".tox", ".nox", ".hypothesis",
|
| 34 |
+
".cache", ".gradle", ".parcel-cache", ".next", ".turbo",
|
| 35 |
+
".pnpm-store", ".yarn", ".yarn/cache", ".nuxt", ".svelte-kit"
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
# Filenames to always skip
|
| 39 |
+
EXCLUDE_FILES: Set[str] = {
|
| 40 |
+
".DS_Store", "Thumbs.db", ".coverage", ".python-version",
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
# Glob patterns to skip (gitignore-like, simple fnmatch on the basename)
|
| 44 |
+
EXCLUDE_GLOBS: List[str] = [
|
| 45 |
+
"*.log", "*.tmp", "*.temp", "*.bak", "*.swp", "*.swo",
|
| 46 |
+
"*.pyc", "*.pyo", "*.pyd", "*.class",
|
| 47 |
+
"*.lock", "*.pid",
|
| 48 |
+
"*.egg-info", "*.eggs",
|
| 49 |
+
"*.sqlite", "*.sqlite3", "*.db", "*.pkl",
|
| 50 |
+
".env", ".env.*",
|
| 51 |
+
]
|
| 52 |
+
|
| 53 |
+
MAX_FILE_BYTES_DEFAULT = 2_000_000 # 2 MB safety default
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def is_included_file(path: Path, include_exts: Set[str]) -> bool:
|
| 57 |
+
if not path.is_file():
|
| 58 |
+
return False
|
| 59 |
+
# Dockerfile special-case: no suffix
|
| 60 |
+
if path.name.lower() == "dockerfile":
|
| 61 |
+
return True
|
| 62 |
+
return path.suffix.lower() in include_exts
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def read_ipynb_code_cells(nb_path: Path) -> str:
|
| 66 |
+
try:
|
| 67 |
+
data = json.loads(nb_path.read_text(encoding="utf-8"))
|
| 68 |
+
except Exception as e:
|
| 69 |
+
return f"[ERROR reading notebook JSON: {e}]"
|
| 70 |
+
cells = data.get("cells", [])
|
| 71 |
+
out_lines: List[str] = []
|
| 72 |
+
count = 0
|
| 73 |
+
for c in cells:
|
| 74 |
+
if c.get("cell_type") == "code":
|
| 75 |
+
count += 1
|
| 76 |
+
src = c.get("source", [])
|
| 77 |
+
code = "".join(src)
|
| 78 |
+
out_lines.append(f"# %% [code cell {count}]")
|
| 79 |
+
out_lines.append(code.rstrip() + "\\n")
|
| 80 |
+
if not out_lines:
|
| 81 |
+
return "[No code cells found]"
|
| 82 |
+
return "\\n".join(out_lines)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def read_text_file(path: Path) -> str:
|
| 86 |
+
try:
|
| 87 |
+
if path.suffix.lower() == ".ipynb":
|
| 88 |
+
return read_ipynb_code_cells(path)
|
| 89 |
+
return path.read_text(encoding="utf-8", errors="replace")
|
| 90 |
+
except Exception as e:
|
| 91 |
+
return f"[ERROR reading file: {e}]"
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def walk_files(root: Path,
|
| 95 |
+
exclude_dirs: Set[str],
|
| 96 |
+
include_exts: Set[str],
|
| 97 |
+
max_bytes: int,
|
| 98 |
+
follow_symlinks: bool,
|
| 99 |
+
exclude_files: Set[str],
|
| 100 |
+
exclude_globs: List[str]) -> Iterable[Path]:
|
| 101 |
+
for dirpath, dirnames, filenames in os.walk(root, followlinks=follow_symlinks):
|
| 102 |
+
# prune excluded dirs in-place
|
| 103 |
+
dirnames[:] = [d for d in dirnames if d not in exclude_dirs]
|
| 104 |
+
for name in filenames:
|
| 105 |
+
# filename-level filters
|
| 106 |
+
if name in exclude_files:
|
| 107 |
+
continue
|
| 108 |
+
if any(fnmatch.fnmatch(name, pat) for pat in exclude_globs):
|
| 109 |
+
continue
|
| 110 |
+
|
| 111 |
+
p = Path(dirpath) / name
|
| 112 |
+
if is_included_file(p, include_exts):
|
| 113 |
+
try:
|
| 114 |
+
if p.stat().st_size <= max_bytes:
|
| 115 |
+
yield p
|
| 116 |
+
except Exception:
|
| 117 |
+
continue
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def parse_str_set_arg(raw: str, default: Set[str]) -> Set[str]:
|
| 121 |
+
# Parse comma-separated items into a set of strings (filenames or dirnames).
|
| 122 |
+
if raw is None or not str(raw).strip():
|
| 123 |
+
return set(default)
|
| 124 |
+
return {s.strip() for s in raw.split(",") if s.strip()}
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def parse_list_arg(raw: str, default: Set[str]) -> Set[str]:
|
| 128 |
+
# Parse comma-separated items; empty -> default. Example: ".py,.ipynb,.md"
|
| 129 |
+
if raw is None or not str(raw).strip():
|
| 130 |
+
return set(default)
|
| 131 |
+
items = [s.strip() for s in raw.split(",") if s.strip()]
|
| 132 |
+
# normalize extensions to lowercase with a leading dot when applicable
|
| 133 |
+
norm: Set[str] = set()
|
| 134 |
+
for it in items:
|
| 135 |
+
it_low = it.lower()
|
| 136 |
+
if it_low == "dockerfile":
|
| 137 |
+
norm.add("dockerfile") # handled specially
|
| 138 |
+
elif it_low.startswith("."):
|
| 139 |
+
norm.add(it_low)
|
| 140 |
+
else:
|
| 141 |
+
norm.add("." + it_low)
|
| 142 |
+
return norm
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def main(argv: List[str]) -> int:
|
| 146 |
+
import argparse
|
| 147 |
+
|
| 148 |
+
ap = argparse.ArgumentParser(
|
| 149 |
+
description="Flatten a folder tree (code/config) into one text file with file headers."
|
| 150 |
+
)
|
| 151 |
+
ap.add_argument("root", nargs="?", default=".", help="Root directory to scan (default: current dir)")
|
| 152 |
+
ap.add_argument("out", nargs="?", default="FLATTENED_CODE.txt", help="Output text file (default: FLATTENED_CODE.txt)")
|
| 153 |
+
ap.add_argument("--include-exts", dest="include_exts", default="",
|
| 154 |
+
help="Comma-separated list of extensions to include (e.g. .py,.ipynb,.md). Default uses a sane preset.")
|
| 155 |
+
ap.add_argument("--exclude-dirs", dest="exclude_dirs", default="",
|
| 156 |
+
help="Comma-separated list of directory names to exclude (in addition to defaults).")
|
| 157 |
+
ap.add_argument("--exclude-files", dest="exclude_files", default="",
|
| 158 |
+
help="Comma-separated list of filenames to exclude (in addition to defaults).")
|
| 159 |
+
ap.add_argument("--exclude-globs", dest="exclude_globs", default="",
|
| 160 |
+
help="Comma-separated list of glob patterns to exclude (e.g. *.log,*.tmp,.env, .env.*).")
|
| 161 |
+
ap.add_argument("--max-bytes", dest="max_bytes", type=int, default=MAX_FILE_BYTES_DEFAULT,
|
| 162 |
+
help=f"Skip files larger than this many bytes (default: {MAX_FILE_BYTES_DEFAULT}).")
|
| 163 |
+
ap.add_argument("--follow-symlinks", action="store_true", help="Follow symlinks while walking the tree.")
|
| 164 |
+
args = ap.parse_args(argv)
|
| 165 |
+
|
| 166 |
+
root = Path(args.root).expanduser()
|
| 167 |
+
out_path = Path(args.out).expanduser()
|
| 168 |
+
|
| 169 |
+
if not root.exists():
|
| 170 |
+
print(f"Root path not found: {root}", file=sys.stderr)
|
| 171 |
+
return 1
|
| 172 |
+
|
| 173 |
+
include_exts = parse_list_arg(args.include_exts, INCLUDE_EXTS)
|
| 174 |
+
|
| 175 |
+
exclude_dirs = set(EXCLUDE_DIRS)
|
| 176 |
+
if args.exclude_dirs:
|
| 177 |
+
exclude_dirs |= {d.strip() for d in args.exclude_dirs.split(",") if d.strip()}
|
| 178 |
+
|
| 179 |
+
exclude_files = set(EXCLUDE_FILES)
|
| 180 |
+
if args.exclude_files:
|
| 181 |
+
exclude_files |= {f.strip() for f in args.exclude_files.split(",") if f.strip()}
|
| 182 |
+
|
| 183 |
+
exclude_globs = list(EXCLUDE_GLOBS)
|
| 184 |
+
if args.exclude_globs:
|
| 185 |
+
exclude_globs += [g.strip() for g in args.exclude_globs.split(",") if g.strip()]
|
| 186 |
+
|
| 187 |
+
files = sorted(
|
| 188 |
+
walk_files(root, exclude_dirs, include_exts, args.max_bytes, args.follow_symlinks, exclude_files, exclude_globs)
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
| 192 |
+
with out_path.open("w", encoding="utf-8") as out:
|
| 193 |
+
out.write(f"# Flattened code dump for: {root.resolve()}\\n")
|
| 194 |
+
out.write(f"# Files included: {len(files)}\\n\\n")
|
| 195 |
+
for p in files:
|
| 196 |
+
try:
|
| 197 |
+
rel = p.relative_to(root)
|
| 198 |
+
except Exception:
|
| 199 |
+
rel = p
|
| 200 |
+
out.write("\\n" + "=" * 80 + "\\n")
|
| 201 |
+
out.write(f"BEGIN FILE: {rel}\\n")
|
| 202 |
+
out.write("=" * 80 + "\\n\\n")
|
| 203 |
+
out.write(read_text_file(p))
|
| 204 |
+
out.write("\\n" + "=" * 80 + "\\n")
|
| 205 |
+
out.write(f"END FILE: {rel}\\n")
|
| 206 |
+
out.write("=" * 80 + "\\n")
|
| 207 |
+
|
| 208 |
+
print(f"Wrote: {out_path}")
|
| 209 |
+
return 0
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
if __name__ == "__main__":
|
| 213 |
+
raise SystemExit(main(sys.argv[1:]))
|
guardrails/pii_redaction.py
CHANGED
|
@@ -1,3 +1,113 @@
|
|
| 1 |
# /guardrails/pii_redaction.py
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# /guardrails/pii_redaction.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
import re
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from typing import Dict, List, Tuple
|
| 6 |
|
| 7 |
+
# ---- Types -------------------------------------------------------------------
|
| 8 |
+
@dataclass(frozen=True)
|
| 9 |
+
class PiiMatch:
|
| 10 |
+
kind: str
|
| 11 |
+
value: str
|
| 12 |
+
span: Tuple[int, int]
|
| 13 |
+
replacement: str
|
| 14 |
+
|
| 15 |
+
# ---- Patterns ----------------------------------------------------------------
|
| 16 |
+
# Focus on high-signal, low-false-positive patterns
|
| 17 |
+
_PATTERNS: Dict[str, re.Pattern] = {
|
| 18 |
+
"email": re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b"),
|
| 19 |
+
"phone": re.compile(
|
| 20 |
+
r"\b(?:\+?\d{1,3}[-.\s]?)?(?:\(?\d{3}\)?[-.\s]?)\d{3}[-.\s]?\d{4}\b"
|
| 21 |
+
),
|
| 22 |
+
"ssn": re.compile(r"\b\d{3}-\d{2}-\d{4}\b"),
|
| 23 |
+
"ip": re.compile(
|
| 24 |
+
r"\b(?:(?:25[0-5]|2[0-4]\d|1?\d{1,2})\.){3}(?:25[0-5]|2[0-4]\d|1?\d{1,2})\b"
|
| 25 |
+
),
|
| 26 |
+
"url": re.compile(r"\bhttps?://[^\s]+"),
|
| 27 |
+
# Broad CC finder; we filter with Luhn
|
| 28 |
+
"cc": re.compile(r"\b(?:\d[ -]?){13,19}\b"),
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
def _only_digits(s: str) -> str:
|
| 32 |
+
return "".join(ch for ch in s if ch.isdigit())
|
| 33 |
+
|
| 34 |
+
def _luhn_ok(number: str) -> bool:
|
| 35 |
+
try:
|
| 36 |
+
digits = [int(x) for x in number]
|
| 37 |
+
except ValueError:
|
| 38 |
+
return False
|
| 39 |
+
parity = len(digits) % 2
|
| 40 |
+
total = 0
|
| 41 |
+
for i, d in enumerate(digits):
|
| 42 |
+
if i % 2 == parity:
|
| 43 |
+
d *= 2
|
| 44 |
+
if d > 9:
|
| 45 |
+
d -= 9
|
| 46 |
+
total += d
|
| 47 |
+
return total % 10 == 0
|
| 48 |
+
|
| 49 |
+
# ---- Redaction core -----------------------------------------------------------
|
| 50 |
+
def redact_with_report(
|
| 51 |
+
text: str,
|
| 52 |
+
*,
|
| 53 |
+
mask_map: Dict[str, str] | None = None,
|
| 54 |
+
preserve_cc_last4: bool = True,
|
| 55 |
+
) -> tuple[str, List[PiiMatch]]:
|
| 56 |
+
"""
|
| 57 |
+
Return (redacted_text, findings). Keeps non-overlapping highest-priority matches.
|
| 58 |
+
"""
|
| 59 |
+
if not text:
|
| 60 |
+
return text, []
|
| 61 |
+
|
| 62 |
+
mask_map = mask_map or {
|
| 63 |
+
"email": "[EMAIL]",
|
| 64 |
+
"phone": "[PHONE]",
|
| 65 |
+
"ssn": "[SSN]",
|
| 66 |
+
"ip": "[IP]",
|
| 67 |
+
"url": "[URL]",
|
| 68 |
+
"cc": "[CC]", # overridden if preserve_cc_last4
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
matches: List[PiiMatch] = []
|
| 72 |
+
for kind, pat in _PATTERNS.items():
|
| 73 |
+
for m in pat.finditer(text):
|
| 74 |
+
raw = m.group(0)
|
| 75 |
+
if kind == "cc":
|
| 76 |
+
digits = _only_digits(raw)
|
| 77 |
+
if len(digits) < 13 or len(digits) > 19 or not _luhn_ok(digits):
|
| 78 |
+
continue
|
| 79 |
+
if preserve_cc_last4 and len(digits) >= 4:
|
| 80 |
+
repl = f"[CCβ’β’β’β’{digits[-4:]}]"
|
| 81 |
+
else:
|
| 82 |
+
repl = mask_map["cc"]
|
| 83 |
+
else:
|
| 84 |
+
repl = mask_map.get(kind, "[REDACTED]")
|
| 85 |
+
|
| 86 |
+
matches.append(PiiMatch(kind=kind, value=raw, span=m.span(), replacement=repl))
|
| 87 |
+
|
| 88 |
+
# Resolve overlaps by keeping earliest, then skipping overlapping tails
|
| 89 |
+
matches.sort(key=lambda x: (x.span[0], -(x.span[1] - x.span[0])))
|
| 90 |
+
resolved: List[PiiMatch] = []
|
| 91 |
+
last_end = -1
|
| 92 |
+
for m in matches:
|
| 93 |
+
if m.span[0] >= last_end:
|
| 94 |
+
resolved.append(m)
|
| 95 |
+
last_end = m.span[1]
|
| 96 |
+
|
| 97 |
+
# Build redacted string
|
| 98 |
+
out = []
|
| 99 |
+
idx = 0
|
| 100 |
+
for m in resolved:
|
| 101 |
+
s, e = m.span
|
| 102 |
+
out.append(text[idx:s])
|
| 103 |
+
out.append(m.replacement)
|
| 104 |
+
idx = e
|
| 105 |
+
out.append(text[idx:])
|
| 106 |
+
return "".join(out), resolved
|
| 107 |
+
|
| 108 |
+
# ---- Minimal compatibility API -----------------------------------------------
|
| 109 |
+
def redact(t: str) -> str:
|
| 110 |
+
"""
|
| 111 |
+
Backwards-compatible simple API: return redacted text only.
|
| 112 |
+
"""
|
| 113 |
+
return redact_with_report(t)[0]
|
guardrails/safety.py
CHANGED
|
@@ -1 +1,108 @@
|
|
| 1 |
-
# /guardrails/safety.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /guardrails/safety.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
import re
|
| 4 |
+
from dataclasses import dataclass, field, asdict
|
| 5 |
+
from typing import Dict, List, Tuple
|
| 6 |
+
|
| 7 |
+
from .pii_redaction import redact_with_report, PiiMatch
|
| 8 |
+
|
| 9 |
+
# ---- Config ------------------------------------------------------------------
|
| 10 |
+
@dataclass(slots=True)
|
| 11 |
+
class SafetyConfig:
|
| 12 |
+
redact_pii: bool = True
|
| 13 |
+
block_on_jailbreak: bool = True
|
| 14 |
+
block_on_malicious_code: bool = True
|
| 15 |
+
mask_secrets: str = "[SECRET]"
|
| 16 |
+
|
| 17 |
+
# Signals kept intentionally lightweight (no extra deps)
|
| 18 |
+
_PROMPT_INJECTION = [
|
| 19 |
+
r"\bignore (all|previous) (instructions|directions)\b",
|
| 20 |
+
r"\boverride (your|all) (rules|guardrails|safety)\b",
|
| 21 |
+
r"\bpretend to be (?:an|a) (?:unfiltered|unsafe) model\b",
|
| 22 |
+
r"\bjailbreak\b",
|
| 23 |
+
r"\bdisabl(e|ing) (safety|guardrails)\b",
|
| 24 |
+
]
|
| 25 |
+
_MALICIOUS_CODE = [
|
| 26 |
+
r"\brm\s+-rf\b", r"\bdel\s+/s\b", r"\bformat\s+c:\b",
|
| 27 |
+
r"\b(?:curl|wget)\s+.+\|\s*(?:bash|sh)\b",
|
| 28 |
+
r"\bnc\s+-e\b", r"\bpowershell\b",
|
| 29 |
+
]
|
| 30 |
+
# Common token patterns (subset; add more as needed)
|
| 31 |
+
_SECRETS = [
|
| 32 |
+
r"\bAKIA[0-9A-Z]{16}\b", # AWS access key id
|
| 33 |
+
r"\bgh[pousr]_[A-Za-z0-9]{36}\b", # GitHub token
|
| 34 |
+
r"\bxox[abprs]-[A-Za-z0-9-]{10,}\b", # Slack token
|
| 35 |
+
r"\bAIza[0-9A-Za-z\-_]{35}\b", # Google API key
|
| 36 |
+
r"\bS[Kk]-[A-Za-z0-9-]{20,}\b", # generic "sk-" style keys
|
| 37 |
+
]
|
| 38 |
+
# Keep profanity list mild to avoid overblocking
|
| 39 |
+
_PROFANITY = [r"\bdamn\b", r"\bhell\b"]
|
| 40 |
+
|
| 41 |
+
def _scan(patterns: List[str], text: str) -> List[Tuple[str, Tuple[int, int]]]:
|
| 42 |
+
hits: List[Tuple[str, Tuple[int, int]]] = []
|
| 43 |
+
for p in patterns:
|
| 44 |
+
for m in re.finditer(p, text, flags=re.IGNORECASE):
|
| 45 |
+
hits.append((m.group(0), m.span()))
|
| 46 |
+
return hits
|
| 47 |
+
|
| 48 |
+
# ---- Report ------------------------------------------------------------------
|
| 49 |
+
@dataclass(slots=True)
|
| 50 |
+
class SafetyReport:
|
| 51 |
+
original_text: str
|
| 52 |
+
sanitized_text: str
|
| 53 |
+
pii: List[PiiMatch] = field(default_factory=list)
|
| 54 |
+
secrets: List[Tuple[str, Tuple[int, int]]] = field(default_factory=list)
|
| 55 |
+
prompt_injection: List[Tuple[str, Tuple[int, int]]] = field(default_factory=list)
|
| 56 |
+
malicious_code: List[Tuple[str, Tuple[int, int]]] = field(default_factory=list)
|
| 57 |
+
profanity: List[Tuple[str, Tuple[int, int]]] = field(default_factory=list)
|
| 58 |
+
action: str = "allow" # "allow" | "warn" | "block"
|
| 59 |
+
|
| 60 |
+
def to_dict(self) -> Dict[str, object]:
|
| 61 |
+
d = asdict(self)
|
| 62 |
+
d["pii"] = [asdict(p) for p in self.pii]
|
| 63 |
+
return d
|
| 64 |
+
|
| 65 |
+
# ---- API ---------------------------------------------------------------------
|
| 66 |
+
def assess(text: str, cfg: SafetyConfig | None = None) -> SafetyReport:
|
| 67 |
+
cfg = cfg or SafetyConfig()
|
| 68 |
+
sanitized = text or ""
|
| 69 |
+
|
| 70 |
+
# 1) PII redaction
|
| 71 |
+
pii_hits: List[PiiMatch] = []
|
| 72 |
+
if cfg.redact_pii:
|
| 73 |
+
sanitized, pii_hits = redact_with_report(sanitized)
|
| 74 |
+
|
| 75 |
+
# 2) Secrets detection (masked, but keep record)
|
| 76 |
+
secrets = _scan(_SECRETS, sanitized)
|
| 77 |
+
for val, (s, e) in secrets:
|
| 78 |
+
sanitized = sanitized[:s] + cfg.mask_secrets + sanitized[e:]
|
| 79 |
+
|
| 80 |
+
# 3) Prompt-injection & malicious code
|
| 81 |
+
inj = _scan(_PROMPT_INJECTION, sanitized)
|
| 82 |
+
mal = _scan(_MALICIOUS_CODE, sanitized)
|
| 83 |
+
|
| 84 |
+
# 4) Mild profanity signal (does not block)
|
| 85 |
+
prof = _scan(_PROFANITY, sanitized)
|
| 86 |
+
|
| 87 |
+
# Decide action
|
| 88 |
+
action = "allow"
|
| 89 |
+
if (cfg.block_on_jailbreak and inj) or (cfg.block_on_malicious_code and mal):
|
| 90 |
+
action = "block"
|
| 91 |
+
elif secrets or pii_hits or prof:
|
| 92 |
+
action = "warn"
|
| 93 |
+
|
| 94 |
+
return SafetyReport(
|
| 95 |
+
original_text=text or "",
|
| 96 |
+
sanitized_text=sanitized,
|
| 97 |
+
pii=pii_hits,
|
| 98 |
+
secrets=secrets,
|
| 99 |
+
prompt_injection=inj,
|
| 100 |
+
malicious_code=mal,
|
| 101 |
+
profanity=prof,
|
| 102 |
+
action=action,
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
def sanitize_user_input(text: str, cfg: SafetyConfig | None = None) -> tuple[str, SafetyReport]:
|
| 106 |
+
"""Convenience wrapper used by HTTP routes/bots."""
|
| 107 |
+
rep = assess(text, cfg)
|
| 108 |
+
return rep.sanitized_text, rep
|
tree.txt
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
C:\Users\User\Agentic-Chat-bot-
|
| 2 |
+
βββ agenticcore
|
| 3 |
+
β βββ chatbot
|
| 4 |
+
β β βββ __init__.py
|
| 5 |
+
β β βββ services.py
|
| 6 |
+
β βββ __init__.py
|
| 7 |
+
β βββ cli.py
|
| 8 |
+
β βββ providers_unified.py
|
| 9 |
+
β βββ web_agentic.py
|
| 10 |
+
βββ anon_bot
|
| 11 |
+
β βββ handler.py
|
| 12 |
+
β βββ rules.py
|
| 13 |
+
βββ app
|
| 14 |
+
β βββ app
|
| 15 |
+
β β βββ app.py
|
| 16 |
+
β β βββ routes.py
|
| 17 |
+
β βββ assets
|
| 18 |
+
β β βββ html
|
| 19 |
+
β β βββ agenticcore_frontend.html
|
| 20 |
+
β β βββ chat.html
|
| 21 |
+
β β βββ chat_console.html
|
| 22 |
+
β β βββ chat_minimal.html
|
| 23 |
+
β βββ components
|
| 24 |
+
β βββ mbf_bot
|
| 25 |
+
β β βββ __init__.py
|
| 26 |
+
β β βββ bot.py
|
| 27 |
+
β β βββ skills.py
|
| 28 |
+
β βββ app.py
|
| 29 |
+
β βββ routes.py
|
| 30 |
+
βββ core
|
| 31 |
+
β βββ config.py
|
| 32 |
+
β βββ logging.py
|
| 33 |
+
β βββ types.py
|
| 34 |
+
βββ docs
|
| 35 |
+
β βββ slides
|
| 36 |
+
β βββ architecture.md
|
| 37 |
+
β βββ design.md
|
| 38 |
+
β βββ DEV_DOC.md
|
| 39 |
+
β βββ flowchart.png
|
| 40 |
+
β βββ results.md
|
| 41 |
+
βββ examples
|
| 42 |
+
β βββ example.py
|
| 43 |
+
βββ guardrails
|
| 44 |
+
β βββ pii_redaction.py
|
| 45 |
+
β βββ safety.py
|
| 46 |
+
βββ integrations
|
| 47 |
+
β βββ azure
|
| 48 |
+
β β βββ bot_framework.py
|
| 49 |
+
β βββ botframework
|
| 50 |
+
β β βββ bots
|
| 51 |
+
β β β βββ echo_bot.py
|
| 52 |
+
β β βββ app.py
|
| 53 |
+
β β βββ bot.py
|
| 54 |
+
β βββ email
|
| 55 |
+
β β βββ ticket_stub.py
|
| 56 |
+
β βββ web
|
| 57 |
+
β βββ fastapi
|
| 58 |
+
β βββ web_agentic.py
|
| 59 |
+
βββ logged_in_bot
|
| 60 |
+
β βββ handler.py
|
| 61 |
+
β βββ sentiment_azure.py
|
| 62 |
+
β βββ tools.py
|
| 63 |
+
βββ memory
|
| 64 |
+
β βββ rag
|
| 65 |
+
β β βββ data
|
| 66 |
+
β β βββ indexer.py
|
| 67 |
+
β β βββ retriever.py
|
| 68 |
+
β βββ sessions.py
|
| 69 |
+
β βββ store.py
|
| 70 |
+
βββ nlu
|
| 71 |
+
β βββ pipeline.py
|
| 72 |
+
β βββ prompts.py
|
| 73 |
+
β βββ router.py
|
| 74 |
+
βββ notebooks
|
| 75 |
+
β βββ ChatbotIntegration.ipynb
|
| 76 |
+
β βββ SimpleTraditionalChatbot.ipynb
|
| 77 |
+
βββ samples
|
| 78 |
+
β βββ service.py
|
| 79 |
+
βββ scripts
|
| 80 |
+
β βββ check_compliance.py
|
| 81 |
+
β βββ run_local.sh
|
| 82 |
+
β βββ seed_data.py
|
| 83 |
+
βββ tests
|
| 84 |
+
β βββ smoke_test.py
|
| 85 |
+
β βββ test_anon_bot.py
|
| 86 |
+
β βββ test_guardrails.py
|
| 87 |
+
β βββ test_logged_in_bot.py
|
| 88 |
+
β βββ test_memory.py
|
| 89 |
+
β βββ test_nlu.py
|
| 90 |
+
β βββ test_routes.py
|
| 91 |
+
βββ tools
|
| 92 |
+
β βββ quick_sanity.py
|
| 93 |
+
βββ .gitignore
|
| 94 |
+
βββ app.zip
|
| 95 |
+
βββ flat_tree_filter.py
|
| 96 |
+
βββ FLATTENED_CODE.txt
|
| 97 |
+
βββ LICENSE
|
| 98 |
+
βββ Makefile
|
| 99 |
+
βββ pyproject.toml
|
| 100 |
+
βββ README.md
|
| 101 |
+
βββ requirements.txt
|
| 102 |
+
βββ tree.txt
|
| 103 |
+
βββ tree_filter.py
|
tree_filter.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
r"""
|
| 3 |
+
Write a tree view of a folder to a file.
|
| 4 |
+
|
| 5 |
+
Usage:
|
| 6 |
+
python tree.py # current folder -> tree.txt
|
| 7 |
+
python tree.py C:\proj -o proj-tree.txt
|
| 8 |
+
python tree.py . --max-depth 3
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import os, argparse, fnmatch
|
| 12 |
+
|
| 13 |
+
EXCLUDE_DIRS = {
|
| 14 |
+
".git", ".hg", ".svn", "__pycache__", "node_modules",
|
| 15 |
+
".venv", "venv", "env", "dist", "build",
|
| 16 |
+
"artifacts", "logs", ".idea", ".vscode", ".pytest_cache",
|
| 17 |
+
".mypy_cache", ".ruff_cache", ".tox", ".nox", ".hypothesis",
|
| 18 |
+
".cache", ".gradle", ".parcel-cache", ".next", ".turbo",
|
| 19 |
+
".pnpm-store", ".yarn", ".yarn/cache", ".nuxt", ".svelte-kit",
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
EXCLUDE_FILES = {".DS_Store", "Thumbs.db", ".coverage", ".python-version"}
|
| 23 |
+
|
| 24 |
+
EXCLUDE_GLOBS = [
|
| 25 |
+
"*.log", "*.tmp", "*.temp", "*.bak", "*.swp", "*.swo",
|
| 26 |
+
"*.pyc", "*.pyo", "*.pyd", "*.class",
|
| 27 |
+
"*.lock", "*.pid",
|
| 28 |
+
"*.egg-info", "*.eggs",
|
| 29 |
+
"*.sqlite", "*.sqlite3", "*.db", "*.pkl",
|
| 30 |
+
".env", ".env.*",
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _entries_sorted(path, exclude_dirs=None, exclude_files=None, exclude_globs=None):
|
| 35 |
+
exclude_dirs = set(EXCLUDE_DIRS if exclude_dirs is None else exclude_dirs)
|
| 36 |
+
exclude_files = set(EXCLUDE_FILES if exclude_files is None else exclude_files)
|
| 37 |
+
exclude_globs = list(EXCLUDE_GLOBS if exclude_globs is None else exclude_globs)
|
| 38 |
+
try:
|
| 39 |
+
with os.scandir(path) as it:
|
| 40 |
+
items = []
|
| 41 |
+
for e in it:
|
| 42 |
+
name = e.name
|
| 43 |
+
if name in exclude_files:
|
| 44 |
+
continue
|
| 45 |
+
if any(fnmatch.fnmatch(name, pat) for pat in exclude_globs):
|
| 46 |
+
continue
|
| 47 |
+
if e.is_dir(follow_symlinks=False) and name in exclude_dirs:
|
| 48 |
+
continue
|
| 49 |
+
items.append(e)
|
| 50 |
+
except PermissionError:
|
| 51 |
+
return []
|
| 52 |
+
items.sort(key=lambda e: (not e.is_dir(follow_symlinks=False), e.name.lower()))
|
| 53 |
+
return items
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _draw(root, out, max_depth=None, follow_symlinks=False, prefix="", exclude_dirs=None, exclude_files=None, exclude_globs=None):
|
| 57 |
+
if max_depth is not None and max_depth < 0:
|
| 58 |
+
return
|
| 59 |
+
items = _entries_sorted(root, exclude_dirs=exclude_dirs, exclude_files=exclude_files, exclude_globs=exclude_globs)
|
| 60 |
+
for i, e in enumerate(items):
|
| 61 |
+
last = (i == len(items) - 1)
|
| 62 |
+
connector = "βββ " if last else "βββ "
|
| 63 |
+
line = f"{prefix}{connector}{e.name}"
|
| 64 |
+
if e.is_symlink():
|
| 65 |
+
try:
|
| 66 |
+
line += f" -> {os.readlink(e.path)}"
|
| 67 |
+
except OSError:
|
| 68 |
+
pass
|
| 69 |
+
print(line, file=out)
|
| 70 |
+
if e.is_dir(follow_symlinks=follow_symlinks):
|
| 71 |
+
new_prefix = prefix + (" " if last else "β ")
|
| 72 |
+
next_depth = None if max_depth is None else max_depth - 1
|
| 73 |
+
if next_depth is None or next_depth >= 0:
|
| 74 |
+
_draw(e.path, out, next_depth, follow_symlinks, new_prefix, exclude_dirs, exclude_files, exclude_globs)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def main():
|
| 78 |
+
ap = argparse.ArgumentParser(description="Print a folder tree to a file.")
|
| 79 |
+
ap.add_argument("path", nargs="?", default=".", help="Root folder (default: .)")
|
| 80 |
+
ap.add_argument("-o", "--out", default="tree.txt", help="Output file (default: tree.txt)")
|
| 81 |
+
ap.add_argument("--max-depth", type=int, help="Limit recursion depth")
|
| 82 |
+
ap.add_argument("--follow-symlinks", action="store_true", help="Recurse into symlinked dirs")
|
| 83 |
+
ap.add_argument("--exclude-dirs", default="", help="Comma-separated dir names to exclude (in addition to defaults).")
|
| 84 |
+
ap.add_argument("--exclude-files", default="", help="Comma-separated file names to exclude (in addition to defaults).")
|
| 85 |
+
ap.add_argument("--exclude-globs", default="", help="Comma-separated glob patterns to exclude (e.g. *.log,*.tmp,.env,.env.*).")
|
| 86 |
+
args = ap.parse_args()
|
| 87 |
+
|
| 88 |
+
# Merge defaults with CLI-specified excludes
|
| 89 |
+
exclude_dirs = set(EXCLUDE_DIRS)
|
| 90 |
+
if args.exclude_dirs:
|
| 91 |
+
exclude_dirs |= {d.strip() for d in args.exclude_dirs.split(",") if d.strip()}
|
| 92 |
+
exclude_files = set(EXCLUDE_FILES)
|
| 93 |
+
if args.exclude_files:
|
| 94 |
+
exclude_files |= {f.strip() for f in args.exclude_files.split(",") if f.strip()}
|
| 95 |
+
exclude_globs = list(EXCLUDE_GLOBS)
|
| 96 |
+
if args.exclude_globs:
|
| 97 |
+
exclude_globs += [g.strip() for g in args.exclude_globs.split(",") if g.strip()]
|
| 98 |
+
|
| 99 |
+
root = os.path.abspath(args.path)
|
| 100 |
+
with open(args.out, "w", encoding="utf-8") as f:
|
| 101 |
+
print(root, file=f)
|
| 102 |
+
_draw(root, f, args.max_depth, args.follow_symlinks, "", exclude_dirs, exclude_files, exclude_globs)
|
| 103 |
+
|
| 104 |
+
print(f"Wrote {args.out}")
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
if __name__ == "__main__":
|
| 108 |
+
main()
|