JerameeUC commited on
Commit
071c820
Β·
1 Parent(s): aa2c39f

6th commit

Browse files
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 Echo bot
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
- # Your bot implementation
18
- # -------------------------------------------------------------------
19
- # Make sure this exists at packages/bots/echo_bot.py
20
- # from bots.echo_bot import EchoBot
21
- # Minimal inline fallback if you want to test quickly:
22
- class EchoBot:
23
- async def on_turn(self, turn_context: TurnContext):
24
- if turn_context.activity.type == "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
-
30
- lower = text.lower()
31
- if lower == "help":
32
- await turn_context.send_activity("Try: echo <msg> | reverse: <msg> | capabilities")
33
- elif lower == "capabilities":
34
- await turn_context.send_activity("- echo\n- reverse\n- help\n- capabilities")
35
- elif lower.startswith("reverse:"):
36
- payload = text.split(":", 1)[1].strip()
37
- await turn_context.send_activity(payload[::-1])
38
- elif lower.startswith("echo "):
39
- await turn_context.send_activity(text[5:])
 
 
 
 
 
 
40
  else:
41
- await turn_context.send_activity("Unsupported command. Type 'help' for examples.")
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 = EchoBot()
63
-
64
- # -------------------------------------------------------------------
65
- # HTTP handlers
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 = handle_text(user_text)
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
- print(f"[warn] static directory not found: {static_dir}", flush=True)
131
 
132
  return app
133
 
134
  app = create_app()
135
 
136
  if __name__ == "__main__":
137
- host = os.environ.get("HOST", "127.0.0.1") # use 0.0.0.0 in containers
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
- class Settings: pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- def redact(t): return t
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()