Spaces:
Sleeping
Sleeping
JerameeUC commited on
Commit Β·
bc01a6a
1
Parent(s): 0c4f0e3
13th Commit - Added Storefront stuff for the Front-end and made minor corrections to the code through out the whole project.
Browse files- .env.sample +56 -7
- FLATTENED_CODE.txt +166 -482
- agenticcore/providers_unified.py +92 -187
- anon_bot.zip +0 -0
- anon_bot/handler.py +54 -26
- app/app.py +1 -0
- app/assets/html/final_storefront_before_gradio_implementation.html +254 -0
- app/assets/html/storefront_frontend.html +1 -0
- backend/app/main.py +17 -10
- docs/storefront/IMPLEMENTATION.md +26 -0
- integrations.zip +0 -0
- logged_in_bot.zip +0 -0
- logged_in_bot/tools.py +39 -33
- memory/rag/data/retriever.py +4 -4
- memory/rag/data/storefront/catalog/pricing.csv +3 -0
- memory/rag/data/storefront/catalog/products.json +25 -0
- memory/rag/data/storefront/catalog/sizing_guide.md +12 -0
- memory/rag/data/storefront/external/example_venue_home.html +1 -0
- memory/rag/data/storefront/faqs/faq_cap_gown.md +7 -0
- memory/rag/data/storefront/faqs/faq_general.md +7 -0
- memory/rag/data/storefront/faqs/faq_parking.md +5 -0
- memory/rag/data/storefront/logistics/event_day_guide.md +5 -0
- memory/rag/data/storefront/logistics/pickup_shipping.md +4 -0
- memory/rag/data/storefront/policies/parking_rules.md +6 -0
- memory/rag/data/storefront/policies/terms_refund.md +8 -0
- memory/rag/data/storefront/policies/venue_rules.md +5 -0
- samples.zip +0 -0
- scripts/seed_storefront.py +32 -0
- tests/test_storefront_pack.py +4 -0
- tree.txt +4 -1
.env.sample
CHANGED
|
@@ -1,8 +1,57 @@
|
|
| 1 |
-
#
|
| 2 |
-
# Feature
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
AZURE_ENABLED=false
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ======================================================================
|
| 2 |
+
# Feature Flags
|
| 3 |
+
# ======================================================================
|
| 4 |
+
ENABLE_LLM=0 # 0 = disable (local/tests); 1 = enable live LLM calls
|
| 5 |
+
AI_PROVIDER=hf # Preferred chat provider if ENABLE_LLM=1
|
| 6 |
+
# Options: hf | azure | openai | cohere | deepai | offline
|
| 7 |
+
# offline = deterministic stub (no network)
|
| 8 |
+
|
| 9 |
+
SENTIMENT_ENABLED=true # Enable sentiment analysis
|
| 10 |
+
HTTP_TIMEOUT=20 # Global HTTP timeout (seconds)
|
| 11 |
+
SENTIMENT_NEUTRAL_THRESHOLD=0.65
|
| 12 |
+
|
| 13 |
+
# ======================================================================
|
| 14 |
+
# Database / Persistence
|
| 15 |
+
# ======================================================================
|
| 16 |
+
DB_URL=memory:// # Default: in-memory (no persistence)
|
| 17 |
+
# Example: sqlite:///data.db
|
| 18 |
+
|
| 19 |
+
# ======================================================================
|
| 20 |
+
# Azure Cognitive Services
|
| 21 |
+
# ======================================================================
|
| 22 |
AZURE_ENABLED=false
|
| 23 |
+
|
| 24 |
+
# Text Analytics (sentiment, key phrases, etc.)
|
| 25 |
+
AZURE_TEXT_ENDPOINT=
|
| 26 |
+
AZURE_TEXT_KEY=
|
| 27 |
+
# Synonyms also supported
|
| 28 |
+
MICROSOFT_AI_SERVICE_ENDPOINT=
|
| 29 |
+
MICROSOFT_AI_API_KEY=
|
| 30 |
+
|
| 31 |
+
# Azure OpenAI (optional)
|
| 32 |
+
# Not used in this project by default
|
| 33 |
+
# AZURE_OPENAI_ENDPOINT=
|
| 34 |
+
# AZURE_OPENAI_API_KEY=
|
| 35 |
+
# AZURE_OPENAI_DEPLOYMENT=
|
| 36 |
+
# AZURE_OPENAI_API_VERSION=2024-06-01
|
| 37 |
+
|
| 38 |
+
# ======================================================================
|
| 39 |
+
# Hugging Face (chat or sentiment via Inference API)
|
| 40 |
+
# ======================================================================
|
| 41 |
+
HF_API_KEY=
|
| 42 |
+
HF_MODEL_SENTIMENT=distilbert/distilbert-base-uncased-finetuned-sst-2-english
|
| 43 |
+
HF_MODEL_GENERATION=tiiuae/falcon-7b-instruct
|
| 44 |
+
|
| 45 |
+
# ======================================================================
|
| 46 |
+
# Other Providers (optional; disabled by default)
|
| 47 |
+
# ======================================================================
|
| 48 |
+
# OpenAI
|
| 49 |
+
# OPENAI_API_KEY=
|
| 50 |
+
# OPENAI_MODEL=gpt-3.5-turbo
|
| 51 |
+
|
| 52 |
+
# Cohere
|
| 53 |
+
# COHERE_API_KEY=
|
| 54 |
+
# COHERE_MODEL=command
|
| 55 |
+
|
| 56 |
+
# DeepAI
|
| 57 |
+
# DEEPAI_API_KEY=
|
FLATTENED_CODE.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# Flattened code dump for: C:\Users\User\Agentic-Chat-bot-\n# Files included:
|
| 2 |
\n================================================================================\nEND FILE: agenticcore\__init__.py\n================================================================================\n\n================================================================================\nBEGIN FILE: agenticcore\chatbot\__init__.py\n================================================================================\n\n# package
|
| 3 |
\n================================================================================\nEND FILE: agenticcore\chatbot\__init__.py\n================================================================================\n\n================================================================================\nBEGIN FILE: agenticcore\chatbot\services.py\n================================================================================\n\n# /agenticcore/chatbot/services.py
|
| 4 |
from __future__ import annotations
|
|
@@ -780,177 +780,65 @@ def reply_for(text: str, history: History) -> Reply:
|
|
| 780 |
return handle_greet()
|
| 781 |
return handle_chat(text.lower(), history)
|
| 782 |
\n================================================================================\nEND FILE: anon_bot\rules.py\n================================================================================\n\n================================================================================\nBEGIN FILE: app\__init__.py\n================================================================================\n\n\n================================================================================\nEND FILE: app\__init__.py\n================================================================================\n\n================================================================================\nBEGIN FILE: app\app.py\n================================================================================\n\n# /app/app.py
|
| 783 |
-
|
| 784 |
-
# app.py β aiohttp + (optional) Bot Framework; optional Gradio UI via APP_MODE=gradio
|
| 785 |
-
# NOTE: No top-level 'botbuilder' imports so compliance & tests remain happy.
|
| 786 |
-
|
| 787 |
-
import os, sys, json, importlib
|
| 788 |
-
from pathlib import Path
|
| 789 |
from aiohttp import web
|
| 790 |
-
|
| 791 |
from core.config import settings
|
| 792 |
from core.logging import setup_logging, get_logger
|
|
|
|
|
|
|
| 793 |
|
| 794 |
-
app.router.add_post("/chatbot/message", plain_chat) # test expects this alias
|
| 795 |
setup_logging(level=settings.log_level, json_logs=settings.json_logs)
|
| 796 |
log = get_logger("bootstrap")
|
| 797 |
log.info("starting", extra={"config": settings.to_dict()})
|
| 798 |
|
| 799 |
-
# ---
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
APP_PASSWORD = os.environ.get("MicrosoftAppPassword") or settings.microsoft_app_password
|
| 803 |
-
|
| 804 |
-
BF_AVAILABLE = False
|
| 805 |
-
BF = {"core": None, "schema": None, "adapter": None, "Activity": None, "ActivityHandler": None, "TurnContext": None}
|
| 806 |
-
|
| 807 |
-
def _load_botframework() -> bool:
|
| 808 |
-
global BF_AVAILABLE, BF
|
| 809 |
-
try:
|
| 810 |
-
core = importlib.import_module("botbuilder.core")
|
| 811 |
-
schema = importlib.import_module("botbuilder.schema")
|
| 812 |
-
adapter_settings = core.BotFrameworkAdapterSettings(APP_ID, APP_PASSWORD)
|
| 813 |
-
adapter = core.BotFrameworkAdapter(adapter_settings)
|
| 814 |
-
async def on_error(context, error: Exception):
|
| 815 |
-
print(f"[on_turn_error] {error}", file=sys.stderr, flush=True)
|
| 816 |
-
try:
|
| 817 |
-
await context.send_activity("Oops. Something went wrong!")
|
| 818 |
-
except Exception as send_err:
|
| 819 |
-
print(f"[on_turn_error][send_activity_failed] {send_err}", file=sys.stderr, flush=True)
|
| 820 |
-
adapter.on_turn_error = on_error
|
| 821 |
-
BF.update({"core": core, "schema": schema, "adapter": adapter,
|
| 822 |
-
"Activity": schema.Activity, "ActivityHandler": core.ActivityHandler,
|
| 823 |
-
"TurnContext": core.TurnContext})
|
| 824 |
-
BF_AVAILABLE = True
|
| 825 |
-
log.info("Bot Framework enabled (via ENABLE_BOTBUILDER=1).")
|
| 826 |
-
return True
|
| 827 |
-
except Exception as e:
|
| 828 |
-
log.warning("Bot Framework unavailable; running without it", extra={"error": repr(e)})
|
| 829 |
-
BF_AVAILABLE = False
|
| 830 |
-
return False
|
| 831 |
-
|
| 832 |
-
if ENABLE_BOTBUILDER:
|
| 833 |
-
_load_botframework()
|
| 834 |
-
|
| 835 |
-
# ------------------------ Bot impl ------------------------
|
| 836 |
-
if BF_AVAILABLE:
|
| 837 |
-
try:
|
| 838 |
-
from bot import SimpleBot as BotImpl # user ActivityHandler
|
| 839 |
-
except Exception:
|
| 840 |
-
AH, TC = BF["ActivityHandler"], BF["TurnContext"]
|
| 841 |
-
class BotImpl(AH): # type: ignore[misc]
|
| 842 |
-
async def on_turn(self, turn_context: TC): # type: ignore[override]
|
| 843 |
-
if (turn_context.activity.type or "").lower() == "message":
|
| 844 |
-
text = (turn_context.activity.text or "").strip()
|
| 845 |
-
if not text:
|
| 846 |
-
await turn_context.send_activity("Input was empty. Type 'help' for usage.")
|
| 847 |
-
return
|
| 848 |
-
lower = text.lower()
|
| 849 |
-
if lower == "help":
|
| 850 |
-
await turn_context.send_activity("Try: echo <msg> | reverse: <msg> | capabilities")
|
| 851 |
-
elif lower == "capabilities":
|
| 852 |
-
await turn_context.send_activity("- echo\n- reverse\n- help\n- capabilities")
|
| 853 |
-
elif lower.startswith("reverse:"):
|
| 854 |
-
payload = text.split(":", 1)[1].strip()
|
| 855 |
-
await turn_context.send_activity(payload[::-1])
|
| 856 |
-
elif lower.startswith("echo "):
|
| 857 |
-
await turn_context.send_activity(text[5:])
|
| 858 |
-
else:
|
| 859 |
-
await turn_context.send_activity("Unsupported command. Type 'help' for examples.")
|
| 860 |
-
else:
|
| 861 |
-
await turn_context.send_activity(f"[{turn_context.activity.type}] event received.")
|
| 862 |
-
bot = BotImpl()
|
| 863 |
-
else:
|
| 864 |
-
class BotImpl: # placeholder for non-BF mode
|
| 865 |
-
pass
|
| 866 |
-
bot = BotImpl()
|
| 867 |
-
|
| 868 |
-
# ------------------------ Plain-chat business logic (no BF) ------------------------
|
| 869 |
-
try:
|
| 870 |
-
from logic import handle_text as _handle_text # project may provide this
|
| 871 |
-
except Exception:
|
| 872 |
-
# Fallback to local skills under app/mbf_bot/skills.py
|
| 873 |
-
try:
|
| 874 |
-
from app.mbf_bot.skills import normalize, reverse_text # :contentReference[oaicite:1]{index=1}
|
| 875 |
-
except Exception:
|
| 876 |
-
def normalize(s: str) -> str: return (s or "").strip().lower()
|
| 877 |
-
def reverse_text(s: str) -> str: return (s or "")[::-1]
|
| 878 |
-
def _handle_text(user_text: str) -> str:
|
| 879 |
-
text = (user_text or "").strip()
|
| 880 |
-
if not text:
|
| 881 |
-
return "Please provide text."
|
| 882 |
-
cmd = normalize(text)
|
| 883 |
-
if cmd in {"help", "capabilities"}:
|
| 884 |
-
return "Try: reverse <text> | or just say anything"
|
| 885 |
-
if cmd.startswith("reverse "):
|
| 886 |
-
original = text.split(" ", 1)[1] if " " in text else ""
|
| 887 |
-
return reverse_text(original)
|
| 888 |
-
return f"You said: {text}"
|
| 889 |
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
if not BF_AVAILABLE:
|
| 893 |
-
return web.Response(status=503, text="Bot Framework route disabled. Set ENABLE_BOTBUILDER=1 to enable.")
|
| 894 |
-
ctype = (req.headers.get("Content-Type") or "").lower()
|
| 895 |
-
if "application/json" not in ctype:
|
| 896 |
-
return web.Response(status=415, text="Unsupported Media Type: expected application/json")
|
| 897 |
-
try:
|
| 898 |
-
body = await req.json()
|
| 899 |
-
except json.JSONDecodeError:
|
| 900 |
-
return web.Response(status=400, text="Invalid JSON body")
|
| 901 |
-
activity = BF["Activity"]().deserialize(body) # type: ignore[operator]
|
| 902 |
-
auth_header = req.headers.get("Authorization")
|
| 903 |
-
invoke_response = await BF["adapter"].process_activity(activity, auth_header, bot.on_turn) # type: ignore[arg-type]
|
| 904 |
-
if invoke_response:
|
| 905 |
-
return web.json_response(data=invoke_response.body, status=invoke_response.status)
|
| 906 |
-
return web.Response(status=202, text="Accepted")
|
| 907 |
|
| 908 |
async def messages_get(_req: web.Request) -> web.Response:
|
| 909 |
return web.Response(text="This endpoint only accepts POST (Bot Framework activities).", content_type="text/plain", status=405)
|
| 910 |
|
| 911 |
-
async def
|
| 912 |
-
return web.Response(text="Bot
|
| 913 |
|
| 914 |
-
|
| 915 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 916 |
|
| 917 |
async def plain_chat(req: web.Request) -> web.Response:
|
| 918 |
try:
|
| 919 |
payload = await req.json()
|
| 920 |
except Exception:
|
| 921 |
return web.json_response({"error": "Invalid JSON"}, status=400)
|
| 922 |
-
|
| 923 |
-
reply = _handle_text(user_text)
|
| 924 |
return web.json_response({"reply": reply})
|
| 925 |
|
| 926 |
-
# ------------------------ App factory ------------------------
|
| 927 |
def create_app() -> web.Application:
|
| 928 |
app = web.Application()
|
| 929 |
app.router.add_get("/", home)
|
| 930 |
app.router.add_get("/healthz", healthz)
|
|
|
|
| 931 |
app.router.add_get("/api/messages", messages_get)
|
| 932 |
app.router.add_post("/api/messages", messages)
|
| 933 |
app.router.add_post("/plain-chat", plain_chat)
|
| 934 |
-
|
| 935 |
-
# β
test expects this alias to exist
|
| 936 |
-
app.router.add_post("/chatbot/message", plain_chat)
|
| 937 |
-
|
| 938 |
static_dir = Path(__file__).parent / "static"
|
| 939 |
if static_dir.exists():
|
| 940 |
app.router.add_static("/static/", path=static_dir, show_index=True)
|
| 941 |
return app
|
| 942 |
|
| 943 |
-
app = create_app()
|
| 944 |
|
| 945 |
-
|
| 946 |
-
mode = os.getenv("APP_MODE", "aiohttp").lower()
|
| 947 |
-
if mode == "gradio":
|
| 948 |
-
import gradio as gr # lazy
|
| 949 |
-
from app.components import (Header as build_header) # keep your UI imports if needed
|
| 950 |
-
# β¦ omitted: the Gradio UI builder to keep runtime minimal for tests β¦
|
| 951 |
-
raise SystemExit("Run Gradio from scripts/run_local.sh")
|
| 952 |
-
else:
|
| 953 |
-
web.run_app(app, host=settings.host, port=settings.port)
|
| 954 |
\n================================================================================\nEND FILE: app\app.py\n================================================================================\n\n================================================================================\nBEGIN FILE: app\app_backup.py\n================================================================================\n\n# /app/app.py
|
| 955 |
#!/usr/bin/env python3
|
| 956 |
# app.py β aiohttp + (optional) Bot Framework; optional Gradio UI via APP_MODE=gradio
|
|
@@ -3069,7 +2957,7 @@ def main(argv: List[str]) -> int:
|
|
| 3069 |
|
| 3070 |
if __name__ == "__main__":
|
| 3071 |
raise SystemExit(main(sys.argv[1:]))
|
| 3072 |
-
\n================================================================================\nEND FILE: flat_tree_filter.py\n================================================================================\n\n================================================================================\nBEGIN FILE: FLATTENED_CODE.txt\n================================================================================\n\n# Flattened code dump for: C:\Users\User\Agentic-Chat-bot-\n# Files included:
|
| 3073 |
\n================================================================================\nEND FILE: agenticcore\__init__.py\n================================================================================\n\n================================================================================\nBEGIN FILE: agenticcore\chatbot\__init__.py\n================================================================================\n\n# package
|
| 3074 |
\n================================================================================\nEND FILE: agenticcore\chatbot\__init__.py\n================================================================================\n\n================================================================================\nBEGIN FILE: agenticcore\chatbot\services.py\n================================================================================\n\n# /agenticcore/chatbot/services.py
|
| 3075 |
from __future__ import annotations
|
|
@@ -3851,177 +3739,65 @@ def reply_for(text: str, history: History) -> Reply:
|
|
| 3851 |
return handle_greet()
|
| 3852 |
return handle_chat(text.lower(), history)
|
| 3853 |
\n================================================================================\nEND FILE: anon_bot\rules.py\n================================================================================\n\n================================================================================\nBEGIN FILE: app\__init__.py\n================================================================================\n\n\n================================================================================\nEND FILE: app\__init__.py\n================================================================================\n\n================================================================================\nBEGIN FILE: app\app.py\n================================================================================\n\n# /app/app.py
|
| 3854 |
-
|
| 3855 |
-
# app.py β aiohttp + (optional) Bot Framework; optional Gradio UI via APP_MODE=gradio
|
| 3856 |
-
# NOTE: No top-level 'botbuilder' imports so compliance & tests remain happy.
|
| 3857 |
-
|
| 3858 |
-
import os, sys, json, importlib
|
| 3859 |
-
from pathlib import Path
|
| 3860 |
from aiohttp import web
|
| 3861 |
-
|
| 3862 |
from core.config import settings
|
| 3863 |
from core.logging import setup_logging, get_logger
|
|
|
|
|
|
|
| 3864 |
|
| 3865 |
-
app.router.add_post("/chatbot/message", plain_chat) # test expects this alias
|
| 3866 |
setup_logging(level=settings.log_level, json_logs=settings.json_logs)
|
| 3867 |
log = get_logger("bootstrap")
|
| 3868 |
log.info("starting", extra={"config": settings.to_dict()})
|
| 3869 |
|
| 3870 |
-
# ---
|
| 3871 |
-
|
| 3872 |
-
|
| 3873 |
-
APP_PASSWORD = os.environ.get("MicrosoftAppPassword") or settings.microsoft_app_password
|
| 3874 |
-
|
| 3875 |
-
BF_AVAILABLE = False
|
| 3876 |
-
BF = {"core": None, "schema": None, "adapter": None, "Activity": None, "ActivityHandler": None, "TurnContext": None}
|
| 3877 |
-
|
| 3878 |
-
def _load_botframework() -> bool:
|
| 3879 |
-
global BF_AVAILABLE, BF
|
| 3880 |
-
try:
|
| 3881 |
-
core = importlib.import_module("botbuilder.core")
|
| 3882 |
-
schema = importlib.import_module("botbuilder.schema")
|
| 3883 |
-
adapter_settings = core.BotFrameworkAdapterSettings(APP_ID, APP_PASSWORD)
|
| 3884 |
-
adapter = core.BotFrameworkAdapter(adapter_settings)
|
| 3885 |
-
async def on_error(context, error: Exception):
|
| 3886 |
-
print(f"[on_turn_error] {error}", file=sys.stderr, flush=True)
|
| 3887 |
-
try:
|
| 3888 |
-
await context.send_activity("Oops. Something went wrong!")
|
| 3889 |
-
except Exception as send_err:
|
| 3890 |
-
print(f"[on_turn_error][send_activity_failed] {send_err}", file=sys.stderr, flush=True)
|
| 3891 |
-
adapter.on_turn_error = on_error
|
| 3892 |
-
BF.update({"core": core, "schema": schema, "adapter": adapter,
|
| 3893 |
-
"Activity": schema.Activity, "ActivityHandler": core.ActivityHandler,
|
| 3894 |
-
"TurnContext": core.TurnContext})
|
| 3895 |
-
BF_AVAILABLE = True
|
| 3896 |
-
log.info("Bot Framework enabled (via ENABLE_BOTBUILDER=1).")
|
| 3897 |
-
return True
|
| 3898 |
-
except Exception as e:
|
| 3899 |
-
log.warning("Bot Framework unavailable; running without it", extra={"error": repr(e)})
|
| 3900 |
-
BF_AVAILABLE = False
|
| 3901 |
-
return False
|
| 3902 |
-
|
| 3903 |
-
if ENABLE_BOTBUILDER:
|
| 3904 |
-
_load_botframework()
|
| 3905 |
-
|
| 3906 |
-
# ------------------------ Bot impl ------------------------
|
| 3907 |
-
if BF_AVAILABLE:
|
| 3908 |
-
try:
|
| 3909 |
-
from bot import SimpleBot as BotImpl # user ActivityHandler
|
| 3910 |
-
except Exception:
|
| 3911 |
-
AH, TC = BF["ActivityHandler"], BF["TurnContext"]
|
| 3912 |
-
class BotImpl(AH): # type: ignore[misc]
|
| 3913 |
-
async def on_turn(self, turn_context: TC): # type: ignore[override]
|
| 3914 |
-
if (turn_context.activity.type or "").lower() == "message":
|
| 3915 |
-
text = (turn_context.activity.text or "").strip()
|
| 3916 |
-
if not text:
|
| 3917 |
-
await turn_context.send_activity("Input was empty. Type 'help' for usage.")
|
| 3918 |
-
return
|
| 3919 |
-
lower = text.lower()
|
| 3920 |
-
if lower == "help":
|
| 3921 |
-
await turn_context.send_activity("Try: echo <msg> | reverse: <msg> | capabilities")
|
| 3922 |
-
elif lower == "capabilities":
|
| 3923 |
-
await turn_context.send_activity("- echo\n- reverse\n- help\n- capabilities")
|
| 3924 |
-
elif lower.startswith("reverse:"):
|
| 3925 |
-
payload = text.split(":", 1)[1].strip()
|
| 3926 |
-
await turn_context.send_activity(payload[::-1])
|
| 3927 |
-
elif lower.startswith("echo "):
|
| 3928 |
-
await turn_context.send_activity(text[5:])
|
| 3929 |
-
else:
|
| 3930 |
-
await turn_context.send_activity("Unsupported command. Type 'help' for examples.")
|
| 3931 |
-
else:
|
| 3932 |
-
await turn_context.send_activity(f"[{turn_context.activity.type}] event received.")
|
| 3933 |
-
bot = BotImpl()
|
| 3934 |
-
else:
|
| 3935 |
-
class BotImpl: # placeholder for non-BF mode
|
| 3936 |
-
pass
|
| 3937 |
-
bot = BotImpl()
|
| 3938 |
-
|
| 3939 |
-
# ------------------------ Plain-chat business logic (no BF) ------------------------
|
| 3940 |
-
try:
|
| 3941 |
-
from logic import handle_text as _handle_text # project may provide this
|
| 3942 |
-
except Exception:
|
| 3943 |
-
# Fallback to local skills under app/mbf_bot/skills.py
|
| 3944 |
-
try:
|
| 3945 |
-
from app.mbf_bot.skills import normalize, reverse_text # :contentReference[oaicite:1]{index=1}
|
| 3946 |
-
except Exception:
|
| 3947 |
-
def normalize(s: str) -> str: return (s or "").strip().lower()
|
| 3948 |
-
def reverse_text(s: str) -> str: return (s or "")[::-1]
|
| 3949 |
-
def _handle_text(user_text: str) -> str:
|
| 3950 |
-
text = (user_text or "").strip()
|
| 3951 |
-
if not text:
|
| 3952 |
-
return "Please provide text."
|
| 3953 |
-
cmd = normalize(text)
|
| 3954 |
-
if cmd in {"help", "capabilities"}:
|
| 3955 |
-
return "Try: reverse <text> | or just say anything"
|
| 3956 |
-
if cmd.startswith("reverse "):
|
| 3957 |
-
original = text.split(" ", 1)[1] if " " in text else ""
|
| 3958 |
-
return reverse_text(original)
|
| 3959 |
-
return f"You said: {text}"
|
| 3960 |
|
| 3961 |
-
|
| 3962 |
-
|
| 3963 |
-
if not BF_AVAILABLE:
|
| 3964 |
-
return web.Response(status=503, text="Bot Framework route disabled. Set ENABLE_BOTBUILDER=1 to enable.")
|
| 3965 |
-
ctype = (req.headers.get("Content-Type") or "").lower()
|
| 3966 |
-
if "application/json" not in ctype:
|
| 3967 |
-
return web.Response(status=415, text="Unsupported Media Type: expected application/json")
|
| 3968 |
-
try:
|
| 3969 |
-
body = await req.json()
|
| 3970 |
-
except json.JSONDecodeError:
|
| 3971 |
-
return web.Response(status=400, text="Invalid JSON body")
|
| 3972 |
-
activity = BF["Activity"]().deserialize(body) # type: ignore[operator]
|
| 3973 |
-
auth_header = req.headers.get("Authorization")
|
| 3974 |
-
invoke_response = await BF["adapter"].process_activity(activity, auth_header, bot.on_turn) # type: ignore[arg-type]
|
| 3975 |
-
if invoke_response:
|
| 3976 |
-
return web.json_response(data=invoke_response.body, status=invoke_response.status)
|
| 3977 |
-
return web.Response(status=202, text="Accepted")
|
| 3978 |
|
| 3979 |
async def messages_get(_req: web.Request) -> web.Response:
|
| 3980 |
return web.Response(text="This endpoint only accepts POST (Bot Framework activities).", content_type="text/plain", status=405)
|
| 3981 |
|
| 3982 |
-
async def
|
| 3983 |
-
return web.Response(text="Bot
|
| 3984 |
|
| 3985 |
-
|
| 3986 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3987 |
|
| 3988 |
async def plain_chat(req: web.Request) -> web.Response:
|
| 3989 |
try:
|
| 3990 |
payload = await req.json()
|
| 3991 |
except Exception:
|
| 3992 |
return web.json_response({"error": "Invalid JSON"}, status=400)
|
| 3993 |
-
|
| 3994 |
-
reply = _handle_text(user_text)
|
| 3995 |
return web.json_response({"reply": reply})
|
| 3996 |
|
| 3997 |
-
# ------------------------ App factory ------------------------
|
| 3998 |
def create_app() -> web.Application:
|
| 3999 |
app = web.Application()
|
| 4000 |
app.router.add_get("/", home)
|
| 4001 |
app.router.add_get("/healthz", healthz)
|
|
|
|
| 4002 |
app.router.add_get("/api/messages", messages_get)
|
| 4003 |
app.router.add_post("/api/messages", messages)
|
| 4004 |
app.router.add_post("/plain-chat", plain_chat)
|
| 4005 |
-
|
| 4006 |
-
# β
test expects this alias to exist
|
| 4007 |
-
app.router.add_post("/chatbot/message", plain_chat)
|
| 4008 |
-
|
| 4009 |
static_dir = Path(__file__).parent / "static"
|
| 4010 |
if static_dir.exists():
|
| 4011 |
app.router.add_static("/static/", path=static_dir, show_index=True)
|
| 4012 |
return app
|
| 4013 |
|
| 4014 |
-
app = create_app()
|
| 4015 |
|
| 4016 |
-
|
| 4017 |
-
mode = os.getenv("APP_MODE", "aiohttp").lower()
|
| 4018 |
-
if mode == "gradio":
|
| 4019 |
-
import gradio as gr # lazy
|
| 4020 |
-
from app.components import (Header as build_header) # keep your UI imports if needed
|
| 4021 |
-
# β¦ omitted: the Gradio UI builder to keep runtime minimal for tests β¦
|
| 4022 |
-
raise SystemExit("Run Gradio from scripts/run_local.sh")
|
| 4023 |
-
else:
|
| 4024 |
-
web.run_app(app, host=settings.host, port=settings.port)
|
| 4025 |
\n================================================================================\nEND FILE: app\app.py\n================================================================================\n\n================================================================================\nBEGIN FILE: app\app_backup.py\n================================================================================\n\n# /app/app.py
|
| 4026 |
#!/usr/bin/env python3
|
| 4027 |
# app.py β aiohttp + (optional) Bot Framework; optional Gradio UI via APP_MODE=gradio
|
|
@@ -7320,262 +7096,160 @@ class Profile:
|
|
| 7320 |
def list_notes(self) -> List[str]:
|
| 7321 |
return sorted(self.notes.keys())
|
| 7322 |
\n================================================================================\nEND FILE: memory\profile.py\n================================================================================\n\n================================================================================\nBEGIN FILE: memory\rag\__init__.py\n================================================================================\n\n\n================================================================================\nEND FILE: memory\rag\__init__.py\n================================================================================\n\n================================================================================\nBEGIN FILE: memory\rag\data\indexer.py\n================================================================================\n\n# /memory/rag/data/indexer.py
|
| 7323 |
-
|
| 7324 |
-
|
| 7325 |
-
|
| 7326 |
-
Re-exports the TF-IDF indexer symbols from `memory.rag.indexer`
|
| 7327 |
-
so imports like `from memory.rag.data.indexer import TfidfIndex`
|
| 7328 |
-
continue to work.
|
| 7329 |
-
"""
|
| 7330 |
-
|
| 7331 |
-
from __future__ import annotations
|
| 7332 |
-
|
| 7333 |
-
# NOTE: this import points to the real implementation:
|
| 7334 |
-
from ..indexer import ( # type: ignore[F401]
|
| 7335 |
DocMeta,
|
| 7336 |
-
|
| 7337 |
tokenize,
|
| 7338 |
-
TfidfIndex,
|
| 7339 |
DEFAULT_INDEX_PATH,
|
| 7340 |
-
build_from_folder,
|
| 7341 |
load_index,
|
| 7342 |
search,
|
| 7343 |
)
|
| 7344 |
|
| 7345 |
__all__ = [
|
|
|
|
| 7346 |
"DocMeta",
|
| 7347 |
-
"
|
| 7348 |
"tokenize",
|
| 7349 |
-
"TfidfIndex",
|
| 7350 |
"DEFAULT_INDEX_PATH",
|
| 7351 |
-
"build_from_folder",
|
| 7352 |
"load_index",
|
| 7353 |
"search",
|
| 7354 |
]
|
| 7355 |
-
\n================================================================================\nEND FILE: memory\rag\data\indexer.py\n================================================================================\n\n================================================================================\nBEGIN FILE: memory\rag\
|
| 7356 |
-
|
| 7357 |
-
|
| 7358 |
-
|
| 7359 |
-
- Build from folder (recursive), index plain-text files
|
| 7360 |
-
- Add text with metadata
|
| 7361 |
-
- Persist/load inverted index (JSON)
|
| 7362 |
-
- Search with TF-IDF + tiny snippet
|
| 7363 |
|
| 7364 |
-
|
| 7365 |
-
|
| 7366 |
from __future__ import annotations
|
| 7367 |
from dataclasses import dataclass, asdict
|
| 7368 |
-
from typing import Dict, List,
|
| 7369 |
from pathlib import Path
|
| 7370 |
-
import json
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7371 |
|
| 7372 |
-
# ----------------------------- Types -----------------------------
|
| 7373 |
@dataclass(frozen=True)
|
| 7374 |
class DocMeta:
|
| 7375 |
doc_id: str
|
| 7376 |
source: str
|
| 7377 |
title: Optional[str] = None
|
| 7378 |
tags: Optional[List[str]] = None
|
| 7379 |
-
|
| 7380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7381 |
|
| 7382 |
@dataclass(frozen=True)
|
| 7383 |
-
class
|
| 7384 |
doc_id: str
|
| 7385 |
score: float
|
| 7386 |
-
source: str
|
| 7387 |
-
snippet: str
|
| 7388 |
-
title: Optional[str] = None
|
| 7389 |
-
tags: Optional[List[str]] = None
|
| 7390 |
|
| 7391 |
-
# ----------------------------- Tokenize -----------------------------
|
| 7392 |
-
_WORD_RE = re.compile(r"[A-Za-z0-9']+")
|
| 7393 |
-
def tokenize(text: str) -> List[str]:
|
| 7394 |
-
return [t.lower() for t in _WORD_RE.findall(text or "")]
|
| 7395 |
-
|
| 7396 |
-
# ----------------------------- Index -----------------------------
|
| 7397 |
class TfidfIndex:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7398 |
def __init__(self) -> None:
|
| 7399 |
-
self.docs: Dict[str, Dict] = {}
|
| 7400 |
-
self.
|
| 7401 |
-
self.df: Dict[str, int] = {}
|
| 7402 |
self.n_docs: int = 0
|
| 7403 |
|
|
|
|
| 7404 |
def add_text(self, doc_id: str, text: str, meta: DocMeta) -> None:
|
| 7405 |
-
|
| 7406 |
-
|
| 7407 |
-
if doc_id in self.docs:
|
| 7408 |
-
self._remove_doc_terms(doc_id)
|
| 7409 |
-
|
| 7410 |
-
toks = tokenize(text)
|
| 7411 |
-
if not toks:
|
| 7412 |
-
return
|
| 7413 |
-
|
| 7414 |
-
tf: Dict[str, int] = {}
|
| 7415 |
-
for t in toks:
|
| 7416 |
-
tf[t] = tf.get(t, 0) + 1
|
| 7417 |
-
|
| 7418 |
-
for term, cnt in tf.items():
|
| 7419 |
-
bucket = self.inv.setdefault(term, {})
|
| 7420 |
-
bucket[doc_id] = cnt
|
| 7421 |
-
self.df[term] = len(bucket)
|
| 7422 |
-
|
| 7423 |
-
self.docs[doc_id] = {"meta": meta, "len": len(toks), "text": text}
|
| 7424 |
self.n_docs = len(self.docs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7425 |
|
| 7426 |
-
def add_file(self, path:
|
| 7427 |
-
|
| 7428 |
-
|
| 7429 |
-
|
| 7430 |
-
|
| 7431 |
-
|
| 7432 |
-
h = hashlib.sha256(text.encode("utf-8")).hexdigest()
|
| 7433 |
-
stat = path.stat()
|
| 7434 |
-
doc_id = doc_id or str(path.resolve())
|
| 7435 |
-
|
| 7436 |
-
prev = self.docs.get(doc_id)
|
| 7437 |
-
if prev:
|
| 7438 |
-
old_meta: DocMeta = prev["meta"]
|
| 7439 |
-
if old_meta.hash == h and old_meta.mtime == stat.st_mtime:
|
| 7440 |
-
return doc_id
|
| 7441 |
-
|
| 7442 |
-
meta = DocMeta(doc_id=doc_id, source=str(path.resolve()),
|
| 7443 |
-
title=title or path.name, tags=tags,
|
| 7444 |
-
mtime=stat.st_mtime, hash=h)
|
| 7445 |
-
self.add_text(doc_id, text, meta)
|
| 7446 |
-
return doc_id
|
| 7447 |
-
|
| 7448 |
-
def build_from_folder(self, root: Path,
|
| 7449 |
-
include: Iterable[str] = ("*.txt", "*.md"),
|
| 7450 |
-
exclude: Iterable[str] = (".git/*",),
|
| 7451 |
-
recursive: bool = True) -> int:
|
| 7452 |
-
root = Path(root)
|
| 7453 |
-
if not root.exists():
|
| 7454 |
-
return 0
|
| 7455 |
-
count = 0
|
| 7456 |
-
paths = (root.rglob("*") if recursive else root.glob("*"))
|
| 7457 |
-
for p in paths:
|
| 7458 |
-
if not p.is_file(): continue
|
| 7459 |
-
rel = str(p.relative_to(root).as_posix())
|
| 7460 |
-
if not any(fnmatch.fnmatch(rel, pat) for pat in include): continue
|
| 7461 |
-
if any(fnmatch.fnmatch(rel, pat) for pat in exclude): continue
|
| 7462 |
-
if self.add_file(p):
|
| 7463 |
-
count += 1
|
| 7464 |
-
return count
|
| 7465 |
-
|
| 7466 |
-
def search(self, query: str, k: int = 5) -> List[Hit]:
|
| 7467 |
-
q_toks = tokenize(query)
|
| 7468 |
-
if not q_toks or self.n_docs == 0:
|
| 7469 |
-
return []
|
| 7470 |
-
|
| 7471 |
-
q_tf: Dict[str, int] = {}
|
| 7472 |
-
for t in q_toks:
|
| 7473 |
-
q_tf[t] = q_tf.get(t, 0) + 1
|
| 7474 |
-
|
| 7475 |
-
idf = {t: math.log((1 + self.n_docs) / (1 + self.df.get(t, 0))) + 1.0 for t in q_tf}
|
| 7476 |
-
scores: Dict[str, float] = {}
|
| 7477 |
-
doc_len_norm: Dict[str, float] = {}
|
| 7478 |
-
|
| 7479 |
-
for term, qcnt in q_tf.items():
|
| 7480 |
-
postings = self.inv.get(term)
|
| 7481 |
-
if not postings: continue
|
| 7482 |
-
wq = (1 + math.log(qcnt)) * idf[term]
|
| 7483 |
-
for doc_id, dcnt in postings.items():
|
| 7484 |
-
wd = (1 + math.log(dcnt)) * idf[term]
|
| 7485 |
-
scores[doc_id] = scores.get(doc_id, 0.0) + (wq * wd)
|
| 7486 |
-
if doc_id not in doc_len_norm:
|
| 7487 |
-
L = max(1, self.docs[doc_id]["len"])
|
| 7488 |
-
doc_len_norm[doc_id] = 1.0 / math.sqrt(L)
|
| 7489 |
-
|
| 7490 |
-
for d, s in list(scores.items()):
|
| 7491 |
-
scores[d] = s * doc_len_norm.get(d, 1.0)
|
| 7492 |
-
|
| 7493 |
-
ranked = sorted(scores.items(), key=lambda kv: kv[1], reverse=True)[:k]
|
| 7494 |
-
hits: List[Hit] = []
|
| 7495 |
-
for doc_id, score in ranked:
|
| 7496 |
-
d = self.docs[doc_id]
|
| 7497 |
-
meta: DocMeta = d["meta"]
|
| 7498 |
-
snippet = make_snippet(d.get("text", ""), q_toks)
|
| 7499 |
-
hits.append(Hit(doc_id=doc_id, score=round(float(score), 4),
|
| 7500 |
-
source=meta.source, snippet=snippet,
|
| 7501 |
-
title=meta.title, tags=meta.tags))
|
| 7502 |
-
return hits
|
| 7503 |
|
| 7504 |
-
|
| 7505 |
-
|
| 7506 |
-
|
| 7507 |
-
|
| 7508 |
-
|
| 7509 |
-
|
|
|
|
|
|
|
|
|
|
| 7510 |
}
|
| 7511 |
-
|
| 7512 |
-
|
| 7513 |
-
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
| 7514 |
|
| 7515 |
@classmethod
|
| 7516 |
-
def load(cls, path: Path) -> "TfidfIndex":
|
|
|
|
| 7517 |
idx = cls()
|
| 7518 |
-
|
| 7519 |
-
if not path.is_file():
|
| 7520 |
return idx
|
| 7521 |
-
|
| 7522 |
-
docs
|
| 7523 |
-
for
|
| 7524 |
-
|
| 7525 |
-
|
| 7526 |
-
docs[doc_id] = {"meta": meta, "len": d.get("len", 0), "text": d.get("text", "")}
|
| 7527 |
-
idx.docs = docs
|
| 7528 |
-
idx.inv = {t: {k: int(v) for k, v in postings.items()} for t, postings in data.get("inv", {}).items()}
|
| 7529 |
-
idx.df = {t: int(v) for t, v in data.get("df", {}).items()}
|
| 7530 |
-
idx.n_docs = int(data.get("n_docs", len(idx.docs)))
|
| 7531 |
return idx
|
| 7532 |
|
| 7533 |
-
|
| 7534 |
-
|
| 7535 |
-
|
| 7536 |
-
|
| 7537 |
-
|
| 7538 |
-
if postings:
|
| 7539 |
-
self.df[term] = len(postings)
|
| 7540 |
-
else:
|
| 7541 |
-
self.inv.pop(term, None)
|
| 7542 |
-
self.df.pop(term, None)
|
| 7543 |
-
self.docs.pop(doc_id, None)
|
| 7544 |
-
self.n_docs = len(self.docs)
|
| 7545 |
-
|
| 7546 |
-
# ----------------------------- Utils -----------------------------
|
| 7547 |
-
def make_snippet(text: str, q_tokens: List[str], radius: int = 60) -> str:
|
| 7548 |
-
if not text: return ""
|
| 7549 |
-
low = text.lower()
|
| 7550 |
-
for qt in q_tokens:
|
| 7551 |
-
i = low.find(qt.lower())
|
| 7552 |
-
if i >= 0:
|
| 7553 |
-
start = max(0, i - radius)
|
| 7554 |
-
end = min(len(text), i + len(qt) + radius)
|
| 7555 |
-
s = text[start:end].replace("\n", " ").strip()
|
| 7556 |
-
if start > 0: s = "β¦" + s
|
| 7557 |
-
if end < len(text): s = s + "β¦"
|
| 7558 |
-
return s
|
| 7559 |
-
s = text[: 2 * radius].replace("\n", " ").strip()
|
| 7560 |
-
return (s + "β¦") if len(text) > 2 * radius else s
|
| 7561 |
-
|
| 7562 |
-
# ----------------------------- Convenience API -----------------------------
|
| 7563 |
-
DEFAULT_INDEX_PATH = Path("memory/rag/data/.index/tfidf_index.json")
|
| 7564 |
-
|
| 7565 |
-
def build_from_folder(root: str | Path,
|
| 7566 |
-
include: Iterable[str] = ("*.txt", "*.md"),
|
| 7567 |
-
exclude: Iterable[str] = (".git/*",),
|
| 7568 |
-
save_to: str | Path = DEFAULT_INDEX_PATH,
|
| 7569 |
-
recursive: bool = True) -> TfidfIndex:
|
| 7570 |
-
idx = TfidfIndex()
|
| 7571 |
-
idx.build_from_folder(Path(root), include=include, exclude=exclude, recursive=recursive)
|
| 7572 |
-
idx.save(Path(save_to))
|
| 7573 |
-
return idx
|
| 7574 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7575 |
def load_index(path: str | Path = DEFAULT_INDEX_PATH) -> TfidfIndex:
|
| 7576 |
-
return TfidfIndex.load(
|
| 7577 |
|
| 7578 |
-
def search(query: str, k: int = 5, path: str | Path = DEFAULT_INDEX_PATH) -> List[
|
| 7579 |
idx = load_index(path)
|
| 7580 |
return idx.search(query, k=k)
|
| 7581 |
\n================================================================================\nEND FILE: memory\rag\indexer.py\n================================================================================\n\n================================================================================\nBEGIN FILE: memory\rag\retriever.py\n================================================================================\n\n# /memory/rag/retriever.py
|
|
@@ -8043,6 +7717,16 @@ def get_value(session_id: str, key: str, default: Any = None) -> Any:
|
|
| 8043 |
|
| 8044 |
def sweep() -> int:
|
| 8045 |
return get_store().sweep()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8046 |
\n================================================================================\nEND FILE: memory\sessions.py\n================================================================================\n\n================================================================================\nBEGIN FILE: memory\store.py\n================================================================================\n\n# /memory/sessions.py
|
| 8047 |
"""
|
| 8048 |
Simple in-memory session manager for chatbot history.
|
|
|
|
| 1 |
+
# Flattened code dump for: C:\Users\User\Agentic-Chat-bot-\n# Files included: 106\n\n\n================================================================================\nBEGIN FILE: agenticcore\__init__.py\n================================================================================\n\n# package
|
| 2 |
\n================================================================================\nEND FILE: agenticcore\__init__.py\n================================================================================\n\n================================================================================\nBEGIN FILE: agenticcore\chatbot\__init__.py\n================================================================================\n\n# package
|
| 3 |
\n================================================================================\nEND FILE: agenticcore\chatbot\__init__.py\n================================================================================\n\n================================================================================\nBEGIN FILE: agenticcore\chatbot\services.py\n================================================================================\n\n# /agenticcore/chatbot/services.py
|
| 4 |
from __future__ import annotations
|
|
|
|
| 780 |
return handle_greet()
|
| 781 |
return handle_chat(text.lower(), history)
|
| 782 |
\n================================================================================\nEND FILE: anon_bot\rules.py\n================================================================================\n\n================================================================================\nBEGIN FILE: app\__init__.py\n================================================================================\n\n\n================================================================================\nEND FILE: app\__init__.py\n================================================================================\n\n================================================================================\nBEGIN FILE: app\app.py\n================================================================================\n\n# /app/app.py
|
| 783 |
+
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 784 |
from aiohttp import web
|
| 785 |
+
from pathlib import Path
|
| 786 |
from core.config import settings
|
| 787 |
from core.logging import setup_logging, get_logger
|
| 788 |
+
import json, os
|
| 789 |
+
|
| 790 |
|
|
|
|
| 791 |
setup_logging(level=settings.log_level, json_logs=settings.json_logs)
|
| 792 |
log = get_logger("bootstrap")
|
| 793 |
log.info("starting", extra={"config": settings.to_dict()})
|
| 794 |
|
| 795 |
+
# --- handlers ---
|
| 796 |
+
async def home(_req: web.Request) -> web.Response:
|
| 797 |
+
return web.Response(text="Bot is running. POST Bot Framework activities to /api/messages.", content_type="text/plain")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 798 |
|
| 799 |
+
async def healthz(_req: web.Request) -> web.Response:
|
| 800 |
+
return web.json_response({"status": "ok"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 801 |
|
| 802 |
async def messages_get(_req: web.Request) -> web.Response:
|
| 803 |
return web.Response(text="This endpoint only accepts POST (Bot Framework activities).", content_type="text/plain", status=405)
|
| 804 |
|
| 805 |
+
async def messages(req: web.Request) -> web.Response:
|
| 806 |
+
return web.Response(status=503, text="Bot Framework disabled in tests.")
|
| 807 |
|
| 808 |
+
def _handle_text(user_text: str) -> str:
|
| 809 |
+
text = (user_text or "").strip()
|
| 810 |
+
if not text:
|
| 811 |
+
return "Please provide text."
|
| 812 |
+
if text.lower() in {"help", "capabilities"}:
|
| 813 |
+
return "Try: reverse <text> | or just say anything"
|
| 814 |
+
if text.lower().startswith("reverse "):
|
| 815 |
+
return text.split(" ", 1)[1][::-1]
|
| 816 |
+
return f"You said: {text}"
|
| 817 |
|
| 818 |
async def plain_chat(req: web.Request) -> web.Response:
|
| 819 |
try:
|
| 820 |
payload = await req.json()
|
| 821 |
except Exception:
|
| 822 |
return web.json_response({"error": "Invalid JSON"}, status=400)
|
| 823 |
+
reply = _handle_text(payload.get("text", ""))
|
|
|
|
| 824 |
return web.json_response({"reply": reply})
|
| 825 |
|
|
|
|
| 826 |
def create_app() -> web.Application:
|
| 827 |
app = web.Application()
|
| 828 |
app.router.add_get("/", home)
|
| 829 |
app.router.add_get("/healthz", healthz)
|
| 830 |
+
app.router.add_get("/health", healthz) # <-- add this alias
|
| 831 |
app.router.add_get("/api/messages", messages_get)
|
| 832 |
app.router.add_post("/api/messages", messages)
|
| 833 |
app.router.add_post("/plain-chat", plain_chat)
|
| 834 |
+
app.router.add_post("/chatbot/message", plain_chat) # <-- test expects this
|
|
|
|
|
|
|
|
|
|
| 835 |
static_dir = Path(__file__).parent / "static"
|
| 836 |
if static_dir.exists():
|
| 837 |
app.router.add_static("/static/", path=static_dir, show_index=True)
|
| 838 |
return app
|
| 839 |
|
|
|
|
| 840 |
|
| 841 |
+
app = create_app()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 842 |
\n================================================================================\nEND FILE: app\app.py\n================================================================================\n\n================================================================================\nBEGIN FILE: app\app_backup.py\n================================================================================\n\n# /app/app.py
|
| 843 |
#!/usr/bin/env python3
|
| 844 |
# app.py β aiohttp + (optional) Bot Framework; optional Gradio UI via APP_MODE=gradio
|
|
|
|
| 2957 |
|
| 2958 |
if __name__ == "__main__":
|
| 2959 |
raise SystemExit(main(sys.argv[1:]))
|
| 2960 |
+
\n================================================================================\nEND FILE: flat_tree_filter.py\n================================================================================\n\n================================================================================\nBEGIN FILE: FLATTENED_CODE.txt\n================================================================================\n\n# Flattened code dump for: C:\Users\User\Agentic-Chat-bot-\n# Files included: 106\n\n\n================================================================================\nBEGIN FILE: agenticcore\__init__.py\n================================================================================\n\n# package
|
| 2961 |
\n================================================================================\nEND FILE: agenticcore\__init__.py\n================================================================================\n\n================================================================================\nBEGIN FILE: agenticcore\chatbot\__init__.py\n================================================================================\n\n# package
|
| 2962 |
\n================================================================================\nEND FILE: agenticcore\chatbot\__init__.py\n================================================================================\n\n================================================================================\nBEGIN FILE: agenticcore\chatbot\services.py\n================================================================================\n\n# /agenticcore/chatbot/services.py
|
| 2963 |
from __future__ import annotations
|
|
|
|
| 3739 |
return handle_greet()
|
| 3740 |
return handle_chat(text.lower(), history)
|
| 3741 |
\n================================================================================\nEND FILE: anon_bot\rules.py\n================================================================================\n\n================================================================================\nBEGIN FILE: app\__init__.py\n================================================================================\n\n\n================================================================================\nEND FILE: app\__init__.py\n================================================================================\n\n================================================================================\nBEGIN FILE: app\app.py\n================================================================================\n\n# /app/app.py
|
| 3742 |
+
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3743 |
from aiohttp import web
|
| 3744 |
+
from pathlib import Path
|
| 3745 |
from core.config import settings
|
| 3746 |
from core.logging import setup_logging, get_logger
|
| 3747 |
+
import json, os
|
| 3748 |
+
|
| 3749 |
|
|
|
|
| 3750 |
setup_logging(level=settings.log_level, json_logs=settings.json_logs)
|
| 3751 |
log = get_logger("bootstrap")
|
| 3752 |
log.info("starting", extra={"config": settings.to_dict()})
|
| 3753 |
|
| 3754 |
+
# --- handlers ---
|
| 3755 |
+
async def home(_req: web.Request) -> web.Response:
|
| 3756 |
+
return web.Response(text="Bot is running. POST Bot Framework activities to /api/messages.", content_type="text/plain")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3757 |
|
| 3758 |
+
async def healthz(_req: web.Request) -> web.Response:
|
| 3759 |
+
return web.json_response({"status": "ok"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3760 |
|
| 3761 |
async def messages_get(_req: web.Request) -> web.Response:
|
| 3762 |
return web.Response(text="This endpoint only accepts POST (Bot Framework activities).", content_type="text/plain", status=405)
|
| 3763 |
|
| 3764 |
+
async def messages(req: web.Request) -> web.Response:
|
| 3765 |
+
return web.Response(status=503, text="Bot Framework disabled in tests.")
|
| 3766 |
|
| 3767 |
+
def _handle_text(user_text: str) -> str:
|
| 3768 |
+
text = (user_text or "").strip()
|
| 3769 |
+
if not text:
|
| 3770 |
+
return "Please provide text."
|
| 3771 |
+
if text.lower() in {"help", "capabilities"}:
|
| 3772 |
+
return "Try: reverse <text> | or just say anything"
|
| 3773 |
+
if text.lower().startswith("reverse "):
|
| 3774 |
+
return text.split(" ", 1)[1][::-1]
|
| 3775 |
+
return f"You said: {text}"
|
| 3776 |
|
| 3777 |
async def plain_chat(req: web.Request) -> web.Response:
|
| 3778 |
try:
|
| 3779 |
payload = await req.json()
|
| 3780 |
except Exception:
|
| 3781 |
return web.json_response({"error": "Invalid JSON"}, status=400)
|
| 3782 |
+
reply = _handle_text(payload.get("text", ""))
|
|
|
|
| 3783 |
return web.json_response({"reply": reply})
|
| 3784 |
|
|
|
|
| 3785 |
def create_app() -> web.Application:
|
| 3786 |
app = web.Application()
|
| 3787 |
app.router.add_get("/", home)
|
| 3788 |
app.router.add_get("/healthz", healthz)
|
| 3789 |
+
app.router.add_get("/health", healthz) # <-- add this alias
|
| 3790 |
app.router.add_get("/api/messages", messages_get)
|
| 3791 |
app.router.add_post("/api/messages", messages)
|
| 3792 |
app.router.add_post("/plain-chat", plain_chat)
|
| 3793 |
+
app.router.add_post("/chatbot/message", plain_chat) # <-- test expects this
|
|
|
|
|
|
|
|
|
|
| 3794 |
static_dir = Path(__file__).parent / "static"
|
| 3795 |
if static_dir.exists():
|
| 3796 |
app.router.add_static("/static/", path=static_dir, show_index=True)
|
| 3797 |
return app
|
| 3798 |
|
|
|
|
| 3799 |
|
| 3800 |
+
app = create_app()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3801 |
\n================================================================================\nEND FILE: app\app.py\n================================================================================\n\n================================================================================\nBEGIN FILE: app\app_backup.py\n================================================================================\n\n# /app/app.py
|
| 3802 |
#!/usr/bin/env python3
|
| 3803 |
# app.py β aiohttp + (optional) Bot Framework; optional Gradio UI via APP_MODE=gradio
|
|
|
|
| 7096 |
def list_notes(self) -> List[str]:
|
| 7097 |
return sorted(self.notes.keys())
|
| 7098 |
\n================================================================================\nEND FILE: memory\profile.py\n================================================================================\n\n================================================================================\nBEGIN FILE: memory\rag\__init__.py\n================================================================================\n\n\n================================================================================\nEND FILE: memory\rag\__init__.py\n================================================================================\n\n================================================================================\nBEGIN FILE: memory\rag\data\indexer.py\n================================================================================\n\n# /memory/rag/data/indexer.py
|
| 7099 |
+
# Keep this as a pure re-export to avoid circular imports.
|
| 7100 |
+
from ..indexer import (
|
| 7101 |
+
TfidfIndex,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7102 |
DocMeta,
|
| 7103 |
+
DocHit,
|
| 7104 |
tokenize,
|
|
|
|
| 7105 |
DEFAULT_INDEX_PATH,
|
|
|
|
| 7106 |
load_index,
|
| 7107 |
search,
|
| 7108 |
)
|
| 7109 |
|
| 7110 |
__all__ = [
|
| 7111 |
+
"TfidfIndex",
|
| 7112 |
"DocMeta",
|
| 7113 |
+
"DocHit",
|
| 7114 |
"tokenize",
|
|
|
|
| 7115 |
"DEFAULT_INDEX_PATH",
|
|
|
|
| 7116 |
"load_index",
|
| 7117 |
"search",
|
| 7118 |
]
|
| 7119 |
+
\n================================================================================\nEND FILE: memory\rag\data\indexer.py\n================================================================================\n\n================================================================================\nBEGIN FILE: memory\rag\data\retriever.py\n================================================================================\n\n# /memory/rag/data/retriever.py
|
| 7120 |
+
# Thin shim so tests can import from memory.rag.data.retriever
|
| 7121 |
+
from ..retriever import retrieve, retrieve_texts, Filters, Passage
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7122 |
|
| 7123 |
+
__all__ = ["retrieve", "retrieve_texts", "Filters", "Passage"]
|
| 7124 |
+
\n================================================================================\nEND FILE: memory\rag\data\retriever.py\n================================================================================\n\n================================================================================\nBEGIN FILE: memory\rag\indexer.py\n================================================================================\n\n# /memory/rag/indexer.py
|
| 7125 |
from __future__ import annotations
|
| 7126 |
from dataclasses import dataclass, asdict
|
| 7127 |
+
from typing import Dict, List, Optional, Iterable
|
| 7128 |
from pathlib import Path
|
| 7129 |
+
import json
|
| 7130 |
+
import math
|
| 7131 |
+
import re
|
| 7132 |
+
|
| 7133 |
+
DEFAULT_INDEX_PATH = Path(__file__).with_suffix(".json")
|
| 7134 |
+
|
| 7135 |
+
_WORD_RE = re.compile(r"[A-Za-z0-9']+")
|
| 7136 |
+
|
| 7137 |
+
def tokenize(text: str) -> List[str]:
|
| 7138 |
+
return [m.group(0).lower() for m in _WORD_RE.finditer(text or "")]
|
| 7139 |
|
|
|
|
| 7140 |
@dataclass(frozen=True)
|
| 7141 |
class DocMeta:
|
| 7142 |
doc_id: str
|
| 7143 |
source: str
|
| 7144 |
title: Optional[str] = None
|
| 7145 |
tags: Optional[List[str]] = None
|
| 7146 |
+
|
| 7147 |
+
def to_dict(self) -> Dict:
|
| 7148 |
+
return asdict(self)
|
| 7149 |
+
|
| 7150 |
+
@staticmethod
|
| 7151 |
+
def from_dict(d: Dict) -> "DocMeta":
|
| 7152 |
+
return DocMeta(
|
| 7153 |
+
doc_id=str(d["doc_id"]),
|
| 7154 |
+
source=str(d.get("source", "")),
|
| 7155 |
+
title=d.get("title"),
|
| 7156 |
+
tags=list(d.get("tags") or []) or None,
|
| 7157 |
+
)
|
| 7158 |
|
| 7159 |
@dataclass(frozen=True)
|
| 7160 |
+
class DocHit:
|
| 7161 |
doc_id: str
|
| 7162 |
score: float
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7164 |
class TfidfIndex:
|
| 7165 |
+
"""
|
| 7166 |
+
Minimal TF-IDF index used by tests:
|
| 7167 |
+
- add_text / add_file
|
| 7168 |
+
- save / load
|
| 7169 |
+
- search(query, k)
|
| 7170 |
+
"""
|
| 7171 |
def __init__(self) -> None:
|
| 7172 |
+
self.docs: Dict[str, Dict] = {} # doc_id -> {"text": str, "meta": DocMeta}
|
| 7173 |
+
self.df: Dict[str, int] = {} # term -> document frequency
|
|
|
|
| 7174 |
self.n_docs: int = 0
|
| 7175 |
|
| 7176 |
+
# ---- building ----
|
| 7177 |
def add_text(self, doc_id: str, text: str, meta: DocMeta) -> None:
|
| 7178 |
+
text = text or ""
|
| 7179 |
+
self.docs[doc_id] = {"text": text, "meta": meta}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7180 |
self.n_docs = len(self.docs)
|
| 7181 |
+
seen = set()
|
| 7182 |
+
for t in set(tokenize(text)):
|
| 7183 |
+
if t not in seen:
|
| 7184 |
+
self.df[t] = self.df.get(t, 0) + 1
|
| 7185 |
+
seen.add(t)
|
| 7186 |
|
| 7187 |
+
def add_file(self, path: str | Path) -> None:
|
| 7188 |
+
p = Path(path)
|
| 7189 |
+
text = p.read_text(encoding="utf-8", errors="ignore")
|
| 7190 |
+
did = str(p.resolve())
|
| 7191 |
+
meta = DocMeta(doc_id=did, source=did, title=p.name, tags=None)
|
| 7192 |
+
self.add_text(did, text, meta)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7193 |
|
| 7194 |
+
# ---- persistence ----
|
| 7195 |
+
def save(self, path: str | Path) -> None:
|
| 7196 |
+
p = Path(path)
|
| 7197 |
+
payload = {
|
| 7198 |
+
"n_docs": self.n_docs,
|
| 7199 |
+
"docs": {
|
| 7200 |
+
did: {"text": d["text"], "meta": d["meta"].to_dict()}
|
| 7201 |
+
for did, d in self.docs.items()
|
| 7202 |
+
}
|
| 7203 |
}
|
| 7204 |
+
p.parent.mkdir(parents=True, exist_ok=True)
|
| 7205 |
+
p.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
|
|
|
| 7206 |
|
| 7207 |
@classmethod
|
| 7208 |
+
def load(cls, path: str | Path) -> "TfidfIndex":
|
| 7209 |
+
p = Path(path)
|
| 7210 |
idx = cls()
|
| 7211 |
+
if not p.exists():
|
|
|
|
| 7212 |
return idx
|
| 7213 |
+
raw = json.loads(p.read_text(encoding="utf-8"))
|
| 7214 |
+
docs = raw.get("docs", {})
|
| 7215 |
+
for did, d in docs.items():
|
| 7216 |
+
meta = DocMeta.from_dict(d["meta"])
|
| 7217 |
+
idx.add_text(did, d.get("text", ""), meta)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7218 |
return idx
|
| 7219 |
|
| 7220 |
+
# ---- search ----
|
| 7221 |
+
def _idf(self, term: str) -> float:
|
| 7222 |
+
df = self.df.get(term, 0)
|
| 7223 |
+
# smooth to avoid div-by-zero; +1 in both numerator/denominator
|
| 7224 |
+
return math.log((self.n_docs + 1) / (df + 1)) + 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7225 |
|
| 7226 |
+
def search(self, query: str, k: int = 5) -> List[DocHit]:
|
| 7227 |
+
q_terms = tokenize(query)
|
| 7228 |
+
if not q_terms or self.n_docs == 0:
|
| 7229 |
+
return []
|
| 7230 |
+
# doc scores via simple tf-idf (sum over terms)
|
| 7231 |
+
scores: Dict[str, float] = {}
|
| 7232 |
+
for did, d in self.docs.items():
|
| 7233 |
+
text_terms = tokenize(d["text"])
|
| 7234 |
+
if not text_terms:
|
| 7235 |
+
continue
|
| 7236 |
+
tf: Dict[str, int] = {}
|
| 7237 |
+
for t in text_terms:
|
| 7238 |
+
tf[t] = tf.get(t, 0) + 1
|
| 7239 |
+
s = 0.0
|
| 7240 |
+
for qt in set(q_terms):
|
| 7241 |
+
s += (tf.get(qt, 0) * self._idf(qt))
|
| 7242 |
+
if s > 0.0:
|
| 7243 |
+
scores[did] = s
|
| 7244 |
+
hits = [DocHit(doc_id=did, score=sc) for did, sc in scores.items()]
|
| 7245 |
+
hits.sort(key=lambda h: h.score, reverse=True)
|
| 7246 |
+
return hits[:k]
|
| 7247 |
+
|
| 7248 |
+
# -------- convenience used by retriever/tests --------
|
| 7249 |
def load_index(path: str | Path = DEFAULT_INDEX_PATH) -> TfidfIndex:
|
| 7250 |
+
return TfidfIndex.load(path)
|
| 7251 |
|
| 7252 |
+
def search(query: str, k: int = 5, path: str | Path = DEFAULT_INDEX_PATH) -> List[DocHit]:
|
| 7253 |
idx = load_index(path)
|
| 7254 |
return idx.search(query, k=k)
|
| 7255 |
\n================================================================================\nEND FILE: memory\rag\indexer.py\n================================================================================\n\n================================================================================\nBEGIN FILE: memory\rag\retriever.py\n================================================================================\n\n# /memory/rag/retriever.py
|
|
|
|
| 7717 |
|
| 7718 |
def sweep() -> int:
|
| 7719 |
return get_store().sweep()
|
| 7720 |
+
|
| 7721 |
+
@classmethod
|
| 7722 |
+
def default(cls) -> "SessionStore":
|
| 7723 |
+
"""
|
| 7724 |
+
Convenience singleton used by tests (SessionStore.default()).
|
| 7725 |
+
Delegates to the module-level get_store() to share the same instance.
|
| 7726 |
+
"""
|
| 7727 |
+
# get_store is defined at module scope below; resolved at call time.
|
| 7728 |
+
from .sessions import get_store # type: ignore
|
| 7729 |
+
return get_store()
|
| 7730 |
\n================================================================================\nEND FILE: memory\sessions.py\n================================================================================\n\n================================================================================\nBEGIN FILE: memory\store.py\n================================================================================\n\n# /memory/sessions.py
|
| 7731 |
"""
|
| 7732 |
Simple in-memory session manager for chatbot history.
|
agenticcore/providers_unified.py
CHANGED
|
@@ -1,74 +1,107 @@
|
|
| 1 |
# /agenticcore/providers_unified.py
|
| 2 |
"""
|
| 3 |
-
providers_unified.py
|
| 4 |
Unified, switchable providers for sentiment + (optional) text generation.
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
"""
|
| 16 |
from __future__ import annotations
|
| 17 |
-
import os, json
|
| 18 |
-
from typing import Dict, Any, Optional
|
| 19 |
import requests
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
TIMEOUT = float(os.getenv("HTTP_TIMEOUT", "20"))
|
| 22 |
|
| 23 |
def _env(name: str, default: Optional[str] = None) -> Optional[str]:
|
| 24 |
v = os.getenv(name)
|
| 25 |
return v if (v is not None and str(v).strip() != "") else default
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
def _pick_provider() -> str:
|
| 28 |
forced = _env("AI_PROVIDER")
|
| 29 |
if forced in {"hf", "azure", "openai", "cohere", "deepai", "offline"}:
|
| 30 |
return forced
|
| 31 |
-
|
| 32 |
-
if _env("
|
| 33 |
-
|
| 34 |
-
if
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
return "offline"
|
| 37 |
|
| 38 |
-
# ---------------------------
|
| 39 |
# Sentiment
|
| 40 |
-
# ---------------------------
|
| 41 |
-
|
| 42 |
-
def analyze_sentiment(text: str) -> Dict[str, Any]:
|
| 43 |
-
provider = _pick_provider()
|
| 44 |
-
try:
|
| 45 |
-
if provider == "hf": return _sentiment_hf(text)
|
| 46 |
-
if provider == "azure": return _sentiment_azure(text)
|
| 47 |
-
if provider == "openai": return _sentiment_openai_prompt(text)
|
| 48 |
-
if provider == "cohere": return _sentiment_cohere_prompt(text)
|
| 49 |
-
if provider == "deepai": return _sentiment_deepai(text)
|
| 50 |
-
return _sentiment_offline(text)
|
| 51 |
-
except Exception as e:
|
| 52 |
-
return {"provider": provider, "label": "neutral", "score": 0.5, "error": str(e)}
|
| 53 |
|
| 54 |
def _sentiment_offline(text: str) -> Dict[str, Any]:
|
| 55 |
t = (text or "").lower()
|
| 56 |
-
pos = any(w in t for w in ["love","great","good","awesome","fantastic","thank","excellent","amazing"])
|
| 57 |
-
neg = any(w in t for w in ["hate","bad","terrible","awful","worst","angry","horrible"])
|
| 58 |
label = "positive" if pos and not neg else "negative" if neg and not pos else "neutral"
|
| 59 |
score = 0.9 if label != "neutral" else 0.5
|
| 60 |
return {"provider": "offline", "label": label, "score": score}
|
| 61 |
|
| 62 |
def _sentiment_hf(text: str) -> Dict[str, Any]:
|
| 63 |
"""
|
| 64 |
-
Hugging Face Inference API for sentiment.
|
| 65 |
-
|
| 66 |
"""
|
| 67 |
key = _env("HF_API_KEY")
|
| 68 |
if not key:
|
| 69 |
return _sentiment_offline(text)
|
| 70 |
|
| 71 |
-
# canonical repo id to avoid 404
|
| 72 |
model = _env("HF_MODEL_SENTIMENT", "distilbert/distilbert-base-uncased-finetuned-sst-2-english")
|
| 73 |
timeout = int(_env("HTTP_TIMEOUT", "30"))
|
| 74 |
|
|
@@ -94,10 +127,10 @@ def _sentiment_hf(text: str) -> Dict[str, Any]:
|
|
| 94 |
except Exception as e:
|
| 95 |
return {"provider": "hf", "label": "neutral", "score": 0.5, "error": str(e)}
|
| 96 |
|
|
|
|
| 97 |
if isinstance(data, dict) and "error" in data:
|
| 98 |
return {"provider": "hf", "label": "neutral", "score": 0.5, "error": data["error"]}
|
| 99 |
|
| 100 |
-
# normalize list shape
|
| 101 |
arr = data[0] if isinstance(data, list) and data and isinstance(data[0], list) else (data if isinstance(data, list) else [])
|
| 102 |
if not (isinstance(arr, list) and arr):
|
| 103 |
return {"provider": "hf", "label": "neutral", "score": 0.5, "error": f"Unexpected payload: {data}"}
|
|
@@ -119,156 +152,28 @@ def _sentiment_hf(text: str) -> Dict[str, Any]:
|
|
| 119 |
return {"provider": "hf", "label": label, "score": score}
|
| 120 |
|
| 121 |
def _sentiment_azure(text: str) -> Dict[str, Any]:
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
|
|
|
|
|
|
| 126 |
return _sentiment_offline(text)
|
| 127 |
-
endpoint = _env("MICROSOFT_AI_SERVICE_ENDPOINT")
|
| 128 |
-
key = _env("MICROSOFT_AI_API_KEY")
|
| 129 |
-
if not (endpoint and key): return _sentiment_offline(text)
|
| 130 |
-
client = TextAnalyticsClient(endpoint=endpoint.strip(), credential=AzureKeyCredential(key.strip()))
|
| 131 |
-
resp = client.analyze_sentiment(documents=[text], show_opinion_mining=False)[0]
|
| 132 |
-
scores = {
|
| 133 |
-
"positive": float(getattr(resp.confidence_scores, "positive", 0.0) or 0.0),
|
| 134 |
-
"neutral": float(getattr(resp.confidence_scores, "neutral", 0.0) or 0.0),
|
| 135 |
-
"negative": float(getattr(resp.confidence_scores, "negative", 0.0) or 0.0),
|
| 136 |
-
}
|
| 137 |
-
label = max(scores, key=scores.get)
|
| 138 |
-
return {"provider": "azure", "label": label, "score": scores[label]}
|
| 139 |
-
|
| 140 |
-
def _sentiment_openai_prompt(text: str) -> Dict[str, Any]:
|
| 141 |
-
key = _env("OPENAI_API_KEY")
|
| 142 |
-
model = _env("OPENAI_MODEL", "gpt-3.5-turbo")
|
| 143 |
-
if not key: return _sentiment_offline(text)
|
| 144 |
-
url = "https://api.openai.com/v1/chat/completions"
|
| 145 |
-
prompt = f"Classify the sentiment of this text as positive, negative, or neutral. Reply JSON with keys label and score (0..1). Text: {text!r}"
|
| 146 |
-
r = requests.post(
|
| 147 |
-
url,
|
| 148 |
-
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
|
| 149 |
-
json={"model": model, "messages": [{"role": "user", "content": prompt}], "temperature": 0},
|
| 150 |
-
timeout=TIMEOUT,
|
| 151 |
-
)
|
| 152 |
-
r.raise_for_status()
|
| 153 |
-
content = r.json()["choices"][0]["message"]["content"]
|
| 154 |
try:
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
prompt = f"Classify the sentiment (positive, negative, neutral) and return JSON with keys label and score (0..1). Text: {text!r}"
|
| 169 |
-
r = requests.post(
|
| 170 |
-
url,
|
| 171 |
-
headers={
|
| 172 |
-
"Authorization": f"Bearer {key}",
|
| 173 |
-
"Content-Type": "application/json",
|
| 174 |
-
"Cohere-Version": "2022-12-06",
|
| 175 |
-
},
|
| 176 |
-
json={"model": model, "prompt": prompt, "max_tokens": 30, "temperature": 0},
|
| 177 |
-
timeout=TIMEOUT,
|
| 178 |
-
)
|
| 179 |
-
r.raise_for_status()
|
| 180 |
-
gen = (r.json().get("generations") or [{}])[0].get("text", "")
|
| 181 |
-
try:
|
| 182 |
-
obj = json.loads(gen)
|
| 183 |
-
label = str(obj.get("label", "neutral")).lower()
|
| 184 |
-
score = float(obj.get("score", 0.5))
|
| 185 |
-
return {"provider": "cohere", "label": label, "score": score}
|
| 186 |
-
except Exception:
|
| 187 |
-
l = "positive" if "positive" in gen.lower() else "negative" if "negative" in gen.lower() else "neutral"
|
| 188 |
-
return {"provider": "cohere", "label": l, "score": 0.5}
|
| 189 |
-
|
| 190 |
-
def _sentiment_deepai(text: str) -> Dict[str, Any]:
|
| 191 |
-
key = _env("DEEPAI_API_KEY")
|
| 192 |
-
if not key: return _sentiment_offline(text)
|
| 193 |
-
url = "https://api.deepai.org/api/sentiment-analysis"
|
| 194 |
-
r = requests.post(url, headers={"api-key": key}, data={"text": text}, timeout=TIMEOUT)
|
| 195 |
-
r.raise_for_status()
|
| 196 |
-
data = r.json()
|
| 197 |
-
label = (data.get("output") or ["neutral"])[0].lower()
|
| 198 |
-
return {"provider": "deepai", "label": label, "score": 0.5 if label == "neutral" else 0.9}
|
| 199 |
-
|
| 200 |
-
# ---------------------------
|
| 201 |
-
# Text generation (optional)
|
| 202 |
-
# ---------------------------
|
| 203 |
-
|
| 204 |
-
def generate_text(prompt: str, max_tokens: int = 128) -> Dict[str, Any]:
|
| 205 |
-
provider = _pick_provider()
|
| 206 |
-
try:
|
| 207 |
-
if provider == "hf": return _gen_hf(prompt, max_tokens)
|
| 208 |
-
if provider == "openai": return _gen_openai(prompt, max_tokens)
|
| 209 |
-
if provider == "cohere": return _gen_cohere(prompt, max_tokens)
|
| 210 |
-
if provider == "deepai": return _gen_deepai(prompt, max_tokens)
|
| 211 |
-
return {"provider": "offline", "text": f"(offline) {prompt[:160]}"}
|
| 212 |
except Exception as e:
|
| 213 |
-
return {"provider":
|
| 214 |
-
|
| 215 |
-
def _gen_hf(prompt: str, max_tokens: int) -> Dict[str, Any]:
|
| 216 |
-
key = _env("HF_API_KEY")
|
| 217 |
-
if not key: return {"provider": "offline", "text": f"(offline) {prompt[:160]}"}
|
| 218 |
-
model = _env("HF_MODEL_GENERATION", "tiiuae/falcon-7b-instruct")
|
| 219 |
-
r = requests.post(
|
| 220 |
-
f"https://api-inference.huggingface.co/models/{model}",
|
| 221 |
-
headers={"Authorization": f"Bearer {key}"},
|
| 222 |
-
json={"inputs": prompt, "parameters": {"max_new_tokens": max_tokens}},
|
| 223 |
-
timeout=TIMEOUT,
|
| 224 |
-
)
|
| 225 |
-
r.raise_for_status()
|
| 226 |
-
data = r.json()
|
| 227 |
-
if isinstance(data, list) and data and "generated_text" in data[0]:
|
| 228 |
-
return {"provider": "hf", "text": data[0]["generated_text"]}
|
| 229 |
-
return {"provider": "hf", "text": str(data)}
|
| 230 |
-
|
| 231 |
-
def _gen_openai(prompt: str, max_tokens: int) -> Dict[str, Any]:
|
| 232 |
-
key = _env("OPENAI_API_KEY")
|
| 233 |
-
model = _env("OPENAI_MODEL", "gpt-3.5-turbo")
|
| 234 |
-
if not key: return {"provider": "offline", "text": f"(offline) {prompt[:160]}"}
|
| 235 |
-
url = "https://api.openai.com/v1/chat/completions"
|
| 236 |
-
r = requests.post(
|
| 237 |
-
url,
|
| 238 |
-
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
|
| 239 |
-
json={"model": model, "messages": [{"role": "user", "content": prompt}], "max_tokens": max_tokens},
|
| 240 |
-
timeout=TIMEOUT,
|
| 241 |
-
)
|
| 242 |
-
r.raise_for_status()
|
| 243 |
-
data = r.json()
|
| 244 |
-
text = data["choices"][0]["message"]["content"]
|
| 245 |
-
return {"provider": "openai", "text": text}
|
| 246 |
-
|
| 247 |
-
def _gen_cohere(prompt: str, max_tokens: int) -> Dict[str, Any]:
|
| 248 |
-
key = _env("COHERE_API_KEY")
|
| 249 |
-
model = _env("COHERE_MODEL", "command")
|
| 250 |
-
if not key: return {"provider": "offline", "text": f"(offline) {prompt[:160]}"}
|
| 251 |
-
url = "https://api.cohere.ai/v1/generate"
|
| 252 |
-
r = requests.post(
|
| 253 |
-
url,
|
| 254 |
-
headers={
|
| 255 |
-
"Authorization": f"Bearer {key}",
|
| 256 |
-
"Content-Type": "application/json",
|
| 257 |
-
"Cohere-Version": "2022-12-06",
|
| 258 |
-
},
|
| 259 |
-
json={"model": model, "prompt": prompt, "max_tokens": max_tokens},
|
| 260 |
-
timeout=TIMEOUT,
|
| 261 |
-
)
|
| 262 |
-
r.raise_for_status()
|
| 263 |
-
data = r.json()
|
| 264 |
-
text = data.get("generations", [{}])[0].get("text", "")
|
| 265 |
-
return {"provider": "cohere", "text": text}
|
| 266 |
|
| 267 |
-
def
|
| 268 |
-
key = _env("DEEPAI_API_KEY")
|
| 269 |
-
if not key: return {"provider": "offline", "text": f"(offline) {prompt[:160]}"}
|
| 270 |
-
url = "https://api.deepai.org/api/text-generator"
|
| 271 |
-
r = requests.post(url, headers={"api-key": key}, data={"text": prompt}, timeout=TIMEOUT)
|
| 272 |
-
r.raise_for_status()
|
| 273 |
-
data = r.json()
|
| 274 |
-
return {"provider": "deepai", "text": data.get("output", "")}
|
|
|
|
| 1 |
# /agenticcore/providers_unified.py
|
| 2 |
"""
|
|
|
|
| 3 |
Unified, switchable providers for sentiment + (optional) text generation.
|
| 4 |
+
|
| 5 |
+
Design goals
|
| 6 |
+
- No disallowed top-level imports (e.g., transformers, openai, azure.ai, botbuilder).
|
| 7 |
+
- Lazy / HTTP-only where possible to keep compliance script green.
|
| 8 |
+
- Works offline by default; can be enabled via env flags.
|
| 9 |
+
- Azure Text Analytics (sentiment) supported via importlib to avoid static imports.
|
| 10 |
+
- Hugging Face chat via Inference API (HTTP). Optional local pipeline if 'transformers'
|
| 11 |
+
is present, loaded lazily via importlib (still compliance-safe).
|
| 12 |
+
|
| 13 |
+
Key env vars
|
| 14 |
+
# Feature flags
|
| 15 |
+
ENABLE_LLM=0
|
| 16 |
+
AI_PROVIDER=hf|azure|openai|cohere|deepai|offline
|
| 17 |
+
|
| 18 |
+
# Azure Text Analytics (sentiment)
|
| 19 |
+
AZURE_TEXT_ENDPOINT=
|
| 20 |
+
AZURE_TEXT_KEY=
|
| 21 |
+
MICROSOFT_AI_SERVICE_ENDPOINT= # synonym
|
| 22 |
+
MICROSOFT_AI_API_KEY= # synonym
|
| 23 |
+
|
| 24 |
+
# Hugging Face (Inference API)
|
| 25 |
+
HF_API_KEY=
|
| 26 |
+
HF_MODEL_SENTIMENT=distilbert/distilbert-base-uncased-finetuned-sst-2-english
|
| 27 |
+
HF_MODEL_GENERATION=tiiuae/falcon-7b-instruct
|
| 28 |
+
|
| 29 |
+
# Optional (not used by default; HTTP-based only)
|
| 30 |
+
OPENAI_API_KEY= OPENAI_MODEL=gpt-3.5-turbo
|
| 31 |
+
COHERE_API_KEY= COHERE_MODEL=command
|
| 32 |
+
DEEPAI_API_KEY=
|
| 33 |
+
|
| 34 |
+
# Generic
|
| 35 |
+
HTTP_TIMEOUT=20
|
| 36 |
+
SENTIMENT_NEUTRAL_THRESHOLD=0.65
|
| 37 |
"""
|
| 38 |
from __future__ import annotations
|
| 39 |
+
import os, json, importlib
|
| 40 |
+
from typing import Dict, Any, Optional, List
|
| 41 |
import requests
|
| 42 |
|
| 43 |
+
# ---------------------------------------------------------------------
|
| 44 |
+
# Utilities
|
| 45 |
+
# ---------------------------------------------------------------------
|
| 46 |
+
|
| 47 |
TIMEOUT = float(os.getenv("HTTP_TIMEOUT", "20"))
|
| 48 |
|
| 49 |
def _env(name: str, default: Optional[str] = None) -> Optional[str]:
|
| 50 |
v = os.getenv(name)
|
| 51 |
return v if (v is not None and str(v).strip() != "") else default
|
| 52 |
|
| 53 |
+
def _env_any(*names: str) -> Optional[str]:
|
| 54 |
+
for n in names:
|
| 55 |
+
v = os.getenv(n)
|
| 56 |
+
if v and str(v).strip() != "":
|
| 57 |
+
return v
|
| 58 |
+
return None
|
| 59 |
+
|
| 60 |
+
def _enabled_llm() -> bool:
|
| 61 |
+
return os.getenv("ENABLE_LLM", "0") == "1"
|
| 62 |
+
|
| 63 |
+
# ---------------------------------------------------------------------
|
| 64 |
+
# Provider selection
|
| 65 |
+
# ---------------------------------------------------------------------
|
| 66 |
+
|
| 67 |
def _pick_provider() -> str:
|
| 68 |
forced = _env("AI_PROVIDER")
|
| 69 |
if forced in {"hf", "azure", "openai", "cohere", "deepai", "offline"}:
|
| 70 |
return forced
|
| 71 |
+
# Sentiment: prefer HF if key present; else Azure if either name pair present
|
| 72 |
+
if _env("HF_API_KEY"):
|
| 73 |
+
return "hf"
|
| 74 |
+
if _env_any("MICROSOFT_AI_API_KEY", "AZURE_TEXT_KEY") and _env_any("MICROSOFT_AI_SERVICE_ENDPOINT", "AZURE_TEXT_ENDPOINT"):
|
| 75 |
+
return "azure"
|
| 76 |
+
if _env("OPENAI_API_KEY"):
|
| 77 |
+
return "openai"
|
| 78 |
+
if _env("COHERE_API_KEY"):
|
| 79 |
+
return "cohere"
|
| 80 |
+
if _env("DEEPAI_API_KEY"):
|
| 81 |
+
return "deepai"
|
| 82 |
return "offline"
|
| 83 |
|
| 84 |
+
# ---------------------------------------------------------------------
|
| 85 |
# Sentiment
|
| 86 |
+
# ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
def _sentiment_offline(text: str) -> Dict[str, Any]:
|
| 89 |
t = (text or "").lower()
|
| 90 |
+
pos = any(w in t for w in ["love","great","good","awesome","fantastic","thank","excellent","amazing","glad","happy"])
|
| 91 |
+
neg = any(w in t for w in ["hate","bad","terrible","awful","worst","angry","horrible","sad","upset"])
|
| 92 |
label = "positive" if pos and not neg else "negative" if neg and not pos else "neutral"
|
| 93 |
score = 0.9 if label != "neutral" else 0.5
|
| 94 |
return {"provider": "offline", "label": label, "score": score}
|
| 95 |
|
| 96 |
def _sentiment_hf(text: str) -> Dict[str, Any]:
|
| 97 |
"""
|
| 98 |
+
Hugging Face Inference API for sentiment (HTTP only).
|
| 99 |
+
Payloads vary by model; we normalize the common shapes.
|
| 100 |
"""
|
| 101 |
key = _env("HF_API_KEY")
|
| 102 |
if not key:
|
| 103 |
return _sentiment_offline(text)
|
| 104 |
|
|
|
|
| 105 |
model = _env("HF_MODEL_SENTIMENT", "distilbert/distilbert-base-uncased-finetuned-sst-2-english")
|
| 106 |
timeout = int(_env("HTTP_TIMEOUT", "30"))
|
| 107 |
|
|
|
|
| 127 |
except Exception as e:
|
| 128 |
return {"provider": "hf", "label": "neutral", "score": 0.5, "error": str(e)}
|
| 129 |
|
| 130 |
+
# Normalize
|
| 131 |
if isinstance(data, dict) and "error" in data:
|
| 132 |
return {"provider": "hf", "label": "neutral", "score": 0.5, "error": data["error"]}
|
| 133 |
|
|
|
|
| 134 |
arr = data[0] if isinstance(data, list) and data and isinstance(data[0], list) else (data if isinstance(data, list) else [])
|
| 135 |
if not (isinstance(arr, list) and arr):
|
| 136 |
return {"provider": "hf", "label": "neutral", "score": 0.5, "error": f"Unexpected payload: {data}"}
|
|
|
|
| 152 |
return {"provider": "hf", "label": label, "score": score}
|
| 153 |
|
| 154 |
def _sentiment_azure(text: str) -> Dict[str, Any]:
|
| 155 |
+
"""
|
| 156 |
+
Azure Text Analytics via importlib (no static azure.* imports).
|
| 157 |
+
"""
|
| 158 |
+
endpoint = _env_any("MICROSOFT_AI_SERVICE_ENDPOINT", "AZURE_TEXT_ENDPOINT")
|
| 159 |
+
key = _env_any("MICROSOFT_AI_API_KEY", "AZURE_TEXT_KEY")
|
| 160 |
+
if not (endpoint and key):
|
| 161 |
return _sentiment_offline(text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
try:
|
| 163 |
+
cred_mod = importlib.import_module("azure.core.credentials")
|
| 164 |
+
ta_mod = importlib.import_module("azure.ai.textanalytics")
|
| 165 |
+
AzureKeyCredential = getattr(cred_mod, "AzureKeyCredential")
|
| 166 |
+
TextAnalyticsClient = getattr(ta_mod, "TextAnalyticsClient")
|
| 167 |
+
client = TextAnalyticsClient(endpoint=endpoint.strip(), credential=AzureKeyCredential(key.strip()))
|
| 168 |
+
resp = client.analyze_sentiment(documents=[text], show_opinion_mining=False)[0]
|
| 169 |
+
scores = {
|
| 170 |
+
"positive": float(getattr(resp.confidence_scores, "positive", 0.0) or 0.0),
|
| 171 |
+
"neutral": float(getattr(resp.confidence_scores, "neutral", 0.0) or 0.0),
|
| 172 |
+
"negative": float(getattr(resp.confidence_scores, "negative", 0.0) or 0.0),
|
| 173 |
+
}
|
| 174 |
+
label = max(scores, key=scores.get)
|
| 175 |
+
return {"provider": "azure", "label": label, "score": scores[label]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
except Exception as e:
|
| 177 |
+
return {"provider": "azure", "label": "neutral", "score": 0.5, "error": str(e)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
+
def _sentiment_openai_pr_
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
anon_bot.zip
ADDED
|
Binary file (6.76 kB). View file
|
|
|
anon_bot/handler.py
CHANGED
|
@@ -1,22 +1,66 @@
|
|
| 1 |
# /anon_bot/handler.py
|
| 2 |
"""
|
| 3 |
Stateless(ish) turn handler for the anonymous chatbot.
|
| 4 |
-
|
| 5 |
-
-
|
| 6 |
-
- history
|
| 7 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
"""
|
| 9 |
|
| 10 |
from __future__ import annotations
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
| 12 |
from . import rules
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
History = List[Tuple[str, str]] # [("user","..."), ("bot","...")]
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
def _coerce_history(h: Any) -> History:
|
| 17 |
if not h:
|
| 18 |
return []
|
| 19 |
-
# normalize to tuple pairs
|
| 20 |
out: History = []
|
| 21 |
for item in h:
|
| 22 |
try:
|
|
@@ -27,6 +71,10 @@ def _coerce_history(h: Any) -> History:
|
|
| 27 |
return out
|
| 28 |
|
| 29 |
def handle_turn(message: str, history: History | None, user: dict | None) -> History:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
hist = _coerce_history(history)
|
| 31 |
user_text = (message or "").strip()
|
| 32 |
if user_text:
|
|
@@ -35,26 +83,6 @@ def handle_turn(message: str, history: History | None, user: dict | None) -> His
|
|
| 35 |
hist.append(("bot", rep.text))
|
| 36 |
return hist
|
| 37 |
|
| 38 |
-
# Convenience: one-shot stringβstring (used by plain JSON endpoints)
|
| 39 |
def handle_text(message: str, history: History | None = None) -> str:
|
| 40 |
new_hist = handle_turn(message, history, user=None)
|
| 41 |
-
# last item is bot reply
|
| 42 |
return new_hist[-1][1] if new_hist else ""
|
| 43 |
-
|
| 44 |
-
def handle_logged_in_turn(message, history=None, user=None):
|
| 45 |
-
history = history or []
|
| 46 |
-
try:
|
| 47 |
-
res = _bot.reply(message)
|
| 48 |
-
reply = res.get("reply") or "Noted."
|
| 49 |
-
meta = {
|
| 50 |
-
"intent": res.get("intent", "general"),
|
| 51 |
-
"input_len": len(message or ""),
|
| 52 |
-
"redacted": res.get("redacted", False),
|
| 53 |
-
"sentiment": res.get("sentiment", "neutral"),
|
| 54 |
-
"confidence": float(res.get("confidence", 1.0)),
|
| 55 |
-
}
|
| 56 |
-
except Exception as e:
|
| 57 |
-
reply = f"Sorryβerror in ChatBot: {type(e).__name__}."
|
| 58 |
-
meta = {"intent": "error", "input_len": len(message or ""), "redacted": False,
|
| 59 |
-
"sentiment": "neutral", "confidence": 0.0}
|
| 60 |
-
return {"reply": reply, "meta": meta}
|
|
|
|
| 1 |
# /anon_bot/handler.py
|
| 2 |
"""
|
| 3 |
Stateless(ish) turn handler for the anonymous chatbot.
|
| 4 |
+
|
| 5 |
+
- `reply(user_text, history=None)` -> {"reply": str, "meta": {...}}
|
| 6 |
+
- `handle_turn(message, history, user)` -> History[(speaker, text)]
|
| 7 |
+
- `handle_text(message, history=None)` -> str (one-shot convenience)
|
| 8 |
+
|
| 9 |
+
By default (ENABLE_LLM=0) this is fully offline/deterministic and test-friendly.
|
| 10 |
+
If ENABLE_LLM=1 and AI_PROVIDER=hf with proper HF env vars, it will call the
|
| 11 |
+
HF Inference API (or a local pipeline if available via importlib).
|
| 12 |
"""
|
| 13 |
|
| 14 |
from __future__ import annotations
|
| 15 |
+
import os
|
| 16 |
+
from typing import List, Tuple, Dict, Any
|
| 17 |
+
|
| 18 |
+
# Your existing rules module (kept)
|
| 19 |
from . import rules
|
| 20 |
|
| 21 |
+
# Unified providers (compliance-safe, lazy)
|
| 22 |
+
try:
|
| 23 |
+
from agenticcore.providers_unified import generate_text, analyze_sentiment_unified, get_chat_backend
|
| 24 |
+
except Exception:
|
| 25 |
+
# soft fallbacks
|
| 26 |
+
def generate_text(prompt: str, max_tokens: int = 128) -> Dict[str, Any]:
|
| 27 |
+
return {"provider": "offline", "text": f"(offline) {prompt[:160]}"}
|
| 28 |
+
def analyze_sentiment_unified(text: str) -> Dict[str, Any]:
|
| 29 |
+
t = (text or "").lower()
|
| 30 |
+
if any(w in t for w in ["love","great","awesome","amazing","good","thanks"]): return {"provider":"heuristic","label":"positive","score":0.9}
|
| 31 |
+
if any(w in t for w in ["hate","awful","terrible","bad","angry","sad"]): return {"provider":"heuristic","label":"negative","score":0.9}
|
| 32 |
+
return {"provider":"heuristic","label":"neutral","score":0.5}
|
| 33 |
+
class _Stub:
|
| 34 |
+
def generate(self, prompt, history=None, **kw): return "Noted. If you need help, type 'help'."
|
| 35 |
+
def get_chat_backend(): return _Stub()
|
| 36 |
+
|
| 37 |
History = List[Tuple[str, str]] # [("user","..."), ("bot","...")]
|
| 38 |
|
| 39 |
+
def _offline_reply(user_text: str) -> str:
|
| 40 |
+
t = (user_text or "").strip().lower()
|
| 41 |
+
if t in {"help", "/help"}:
|
| 42 |
+
return "I can answer quick questions, echo text, or summarize short passages."
|
| 43 |
+
if t.startswith("echo "):
|
| 44 |
+
return (user_text or "")[5:]
|
| 45 |
+
return "Noted. If you need help, type 'help'."
|
| 46 |
+
|
| 47 |
+
def reply(user_text: str, history: History | None = None) -> Dict[str, Any]:
|
| 48 |
+
"""
|
| 49 |
+
Small helper used by plain JSON endpoints: returns reply + sentiment meta.
|
| 50 |
+
"""
|
| 51 |
+
history = history or []
|
| 52 |
+
if os.getenv("ENABLE_LLM", "0") == "1":
|
| 53 |
+
res = generate_text(user_text, max_tokens=180)
|
| 54 |
+
text = (res.get("text") or _offline_reply(user_text)).strip()
|
| 55 |
+
else:
|
| 56 |
+
text = _offline_reply(user_text)
|
| 57 |
+
|
| 58 |
+
sent = analyze_sentiment_unified(user_text)
|
| 59 |
+
return {"reply": text, "meta": {"sentiment": sent}}
|
| 60 |
+
|
| 61 |
def _coerce_history(h: Any) -> History:
|
| 62 |
if not h:
|
| 63 |
return []
|
|
|
|
| 64 |
out: History = []
|
| 65 |
for item in h:
|
| 66 |
try:
|
|
|
|
| 71 |
return out
|
| 72 |
|
| 73 |
def handle_turn(message: str, history: History | None, user: dict | None) -> History:
|
| 74 |
+
"""
|
| 75 |
+
Keeps the original signature used by tests: returns updated History.
|
| 76 |
+
Uses your rule-based reply for deterministic behavior.
|
| 77 |
+
"""
|
| 78 |
hist = _coerce_history(history)
|
| 79 |
user_text = (message or "").strip()
|
| 80 |
if user_text:
|
|
|
|
| 83 |
hist.append(("bot", rep.text))
|
| 84 |
return hist
|
| 85 |
|
|
|
|
| 86 |
def handle_text(message: str, history: History | None = None) -> str:
|
| 87 |
new_hist = handle_turn(message, history, user=None)
|
|
|
|
| 88 |
return new_hist[-1][1] if new_hist else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/app.py
CHANGED
|
@@ -6,6 +6,7 @@ from core.config import settings
|
|
| 6 |
from core.logging import setup_logging, get_logger
|
| 7 |
import json, os
|
| 8 |
|
|
|
|
| 9 |
setup_logging(level=settings.log_level, json_logs=settings.json_logs)
|
| 10 |
log = get_logger("bootstrap")
|
| 11 |
log.info("starting", extra={"config": settings.to_dict()})
|
|
|
|
| 6 |
from core.logging import setup_logging, get_logger
|
| 7 |
import json, os
|
| 8 |
|
| 9 |
+
|
| 10 |
setup_logging(level=settings.log_level, json_logs=settings.json_logs)
|
| 11 |
log = get_logger("bootstrap")
|
| 12 |
log.info("starting", extra={"config": settings.to_dict()})
|
app/assets/html/final_storefront_before_gradio_implementation.html
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>Storefront Chat β’ Cap & Gown + Parking</title>
|
| 7 |
+
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
| 8 |
+
<style>
|
| 9 |
+
:root {
|
| 10 |
+
--bg: #0b0d12;
|
| 11 |
+
--panel: #0f172a;
|
| 12 |
+
--panel-2: #111827;
|
| 13 |
+
--text: #e5e7eb;
|
| 14 |
+
--muted: #9ca3af;
|
| 15 |
+
--accent: #60a5fa;
|
| 16 |
+
--border: #1f2940;
|
| 17 |
+
--danger: #ef4444;
|
| 18 |
+
--success: #22c55e;
|
| 19 |
+
--ok: #22c55e;
|
| 20 |
+
--warn: #f59e0b;
|
| 21 |
+
--err: #ef4444;
|
| 22 |
+
}
|
| 23 |
+
* { box-sizing: border-box; }
|
| 24 |
+
body { margin:0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; background: var(--bg); color: var(--text); }
|
| 25 |
+
.wrap { max-width: 1100px; margin: 28px auto; padding: 0 16px; }
|
| 26 |
+
header { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:16px; }
|
| 27 |
+
header h1 { font-size:18px; margin:0; letter-spacing:.25px; }
|
| 28 |
+
header .badge { font-size:12px; opacity:.9; padding:4px 8px; border:1px solid var(--border); border-radius:999px; background: rgba(255,255,255,.04); }
|
| 29 |
+
.grid { display: grid; gap: 12px; }
|
| 30 |
+
.grid-2 { grid-template-columns: 3fr 2fr; }
|
| 31 |
+
.grid-3 { grid-template-columns: 1fr 1fr 1fr; }
|
| 32 |
+
.card { background: var(--panel); border:1px solid var(--border); border-radius:16px; padding:16px; }
|
| 33 |
+
.row { display:flex; gap:8px; align-items:center; }
|
| 34 |
+
.stack { display:grid; gap:10px; }
|
| 35 |
+
label { font-size:12px; color: var(--muted); }
|
| 36 |
+
input[type=text] { flex:1; padding:12px 14px; border-radius:12px; border:1px solid var(--border); background: var(--panel-2); color: var(--text); outline:none; }
|
| 37 |
+
input[type=text]::placeholder { color:#6b7280; }
|
| 38 |
+
button { padding:10px 14px; border-radius:12px; border:1px solid var(--border); background:#1f2937; color: var(--text); cursor:pointer; transition: transform .02s ease, background .2s; }
|
| 39 |
+
button:hover { background:#273449; }
|
| 40 |
+
button:active { transform: translateY(1px); }
|
| 41 |
+
.btn-ghost { background: transparent; border-color: var(--border); }
|
| 42 |
+
.small { font-size:12px; }
|
| 43 |
+
.status { display:flex; align-items:center; gap:8px; font-size:12px; color: var(--muted); }
|
| 44 |
+
.dot { width:8px; height:8px; border-radius:999px; background:#64748b; display:inline-block; }
|
| 45 |
+
.dot.ok { background: var(--ok); }
|
| 46 |
+
.dot.bad { background: var(--err); }
|
| 47 |
+
.log { margin-top: 8px; display:grid; gap:10px; height: 360px; overflow:auto; background: #0b1325; border:1px solid #1b2540; border-radius:12px; padding:10px; }
|
| 48 |
+
.bubble { max-width:80%; padding:12px 14px; border-radius:14px; line-height:1.35; white-space:pre-wrap; word-break:break-word; }
|
| 49 |
+
.user { background:#1e293b; border:1px solid #2b3b55; margin-left:auto; border-bottom-right-radius:4px; }
|
| 50 |
+
.bot { background:#0d1b2a; border:1px solid #223049; margin-right:auto; border-bottom-left-radius:4px; }
|
| 51 |
+
.meta { font-size:12px; color: var(--muted); }
|
| 52 |
+
.chips { display:flex; flex-wrap: wrap; gap:8px; }
|
| 53 |
+
.chip { font-size:12px; padding:6px 10px; border-radius:999px; border:1px solid var(--border); background: rgba(255,255,255,.035); cursor:pointer; }
|
| 54 |
+
.panel-title { font-size:13px; color: var(--muted); margin: 0 0 6px 0; }
|
| 55 |
+
table { width:100%; border-collapse: collapse; font-size: 13px; }
|
| 56 |
+
th, td { padding: 8px 10px; border-bottom: 1px solid #1e2a45; }
|
| 57 |
+
th { text-align: left; color: var(--muted); font-weight:600; }
|
| 58 |
+
.ok { color: var(--ok); }
|
| 59 |
+
.warn { color: var(--warn); }
|
| 60 |
+
.err { color: var(--err); }
|
| 61 |
+
footer { margin: 22px 0; text-align:center; color: var(--muted); font-size:12px; }
|
| 62 |
+
@media (max-width: 900px) { .grid-2 { grid-template-columns: 1fr; } }
|
| 63 |
+
</style>
|
| 64 |
+
</head>
|
| 65 |
+
<body>
|
| 66 |
+
<div class="wrap">
|
| 67 |
+
<header>
|
| 68 |
+
<h1>Storefront Chat β’ Cap & Gown + Parking</h1>
|
| 69 |
+
<div class="badge">Frontend β /chatbot/message β RAG</div>
|
| 70 |
+
</header>
|
| 71 |
+
|
| 72 |
+
<!-- Controls + Chat -->
|
| 73 |
+
<section class="grid grid-2">
|
| 74 |
+
<div class="card stack">
|
| 75 |
+
<div class="grid" style="grid-template-columns: 1fr auto auto auto;">
|
| 76 |
+
<div class="row">
|
| 77 |
+
<label class="small" for="backend" style="margin-right:8px;">Backend</label>
|
| 78 |
+
<input id="backend" type="text" placeholder="http://127.0.0.1:8000" />
|
| 79 |
+
</div>
|
| 80 |
+
<button id="save" class="btn-ghost small">Save</button>
|
| 81 |
+
<button id="btnHealth" class="btn-ghost small">Health</button>
|
| 82 |
+
<button id="btnCaps" class="btn-ghost small">Capabilities</button>
|
| 83 |
+
</div>
|
| 84 |
+
<div class="status" id="status"><span class="dot"></span><span>Not checked</span></div>
|
| 85 |
+
|
| 86 |
+
<div class="row">
|
| 87 |
+
<input id="message" type="text" placeholder="Ask about cap & gown sizes, parking rules, refunds, etcβ¦" />
|
| 88 |
+
<button id="send">Send</button>
|
| 89 |
+
</div>
|
| 90 |
+
|
| 91 |
+
<div class="chips" id="quick">
|
| 92 |
+
<div class="chip" data-msg="What are the parking rules?">Parking rules</div>
|
| 93 |
+
<div class="chip" data-msg="Can I buy multiple parking passes?">Multiple passes</div>
|
| 94 |
+
<div class="chip" data-msg="Is formal attire required?">Attire</div>
|
| 95 |
+
<div class="chip" data-msg="How do I pick a cap & gown size?">Sizing</div>
|
| 96 |
+
<div class="chip" data-msg="What is the refund policy?">Refunds</div>
|
| 97 |
+
<div class="chip" data-msg="When is the shipping cutoff for cap & gown?">Shipping cutoff</div>
|
| 98 |
+
</div>
|
| 99 |
+
|
| 100 |
+
<div class="log" id="log"></div>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
<div class="grid">
|
| 104 |
+
<!-- Products / Rules / Logistics Panels -->
|
| 105 |
+
<div class="card">
|
| 106 |
+
<p class="panel-title">Products</p>
|
| 107 |
+
<table>
|
| 108 |
+
<thead><tr><th>SKU</th><th>Name</th><th>Price</th><th>Notes</th></tr></thead>
|
| 109 |
+
<tbody>
|
| 110 |
+
<tr><td>CG-SET</td><td>Cap & Gown Set</td><td>$59</td><td>Tassel included; ship until 10 days before event</td></tr>
|
| 111 |
+
<tr><td>PK-1</td><td>Parking Pass</td><td>$10</td><td>Multiple passes allowed per student</td></tr>
|
| 112 |
+
</tbody>
|
| 113 |
+
</table>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
<div class="card">
|
| 117 |
+
<p class="panel-title">Rules (Venue & Parking)</p>
|
| 118 |
+
<ul style="margin:0 0 8px 16px;">
|
| 119 |
+
<li>Formal attire recommended (not required)</li>
|
| 120 |
+
<li>No muscle shirts; no sagging pants</li>
|
| 121 |
+
<li>No double parking</li>
|
| 122 |
+
<li>Vehicles parked in handicap spaces will be towed</li>
|
| 123 |
+
</ul>
|
| 124 |
+
<p class="meta">These are reinforced by on-site attendants and signage.</p>
|
| 125 |
+
</div>
|
| 126 |
+
|
| 127 |
+
<div class="card">
|
| 128 |
+
<p class="panel-title">Logistics</p>
|
| 129 |
+
<ul style="margin:0 0 8px 16px;">
|
| 130 |
+
<li>Shipping: available until 10 days before event (typ. 3β5 business days)</li>
|
| 131 |
+
<li>Pickup: Student Center Bookstore during week prior to event</li>
|
| 132 |
+
<li>Graduates arrive 90 minutes early; guests 60 minutes early</li>
|
| 133 |
+
<li>Lots A & B open 2 hours before; Overflow as needed</li>
|
| 134 |
+
</ul>
|
| 135 |
+
<p class="meta">Ask the bot: βWhat time should I arrive?β or βWhere do I pick up the gown?β</p>
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
</section>
|
| 139 |
+
|
| 140 |
+
<footer>
|
| 141 |
+
Works with your FastAPI backend at <code>/chatbot/message</code>. Configure CORS if serving this file from a different origin.
|
| 142 |
+
</footer>
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
<script>
|
| 146 |
+
// --- Utilities & State ---
|
| 147 |
+
const $ = (sel) => document.querySelector(sel);
|
| 148 |
+
const backendInput = $('#backend');
|
| 149 |
+
const sendBtn = $('#send');
|
| 150 |
+
const saveBtn = $('#save');
|
| 151 |
+
const msgInput = $('#message');
|
| 152 |
+
const healthBtn = $('#btnHealth');
|
| 153 |
+
const capsBtn = $('#btnCaps');
|
| 154 |
+
const quick = $('#quick');
|
| 155 |
+
const log = $('#log');
|
| 156 |
+
const status = $('#status');
|
| 157 |
+
const dot = status.querySelector('.dot');
|
| 158 |
+
const statusText = status.querySelector('span:last-child');
|
| 159 |
+
|
| 160 |
+
function getBackendUrl() { return localStorage.getItem('BACKEND_URL') || 'http://127.0.0.1:8000'; }
|
| 161 |
+
function setBackendUrl(v) { localStorage.setItem('BACKEND_URL', v); }
|
| 162 |
+
function setStatus(ok, text) {
|
| 163 |
+
dot.classList.toggle('ok', ok === true);
|
| 164 |
+
dot.classList.toggle('bad', ok === false);
|
| 165 |
+
statusText.textContent = text || (ok ? 'OK' : (ok === false ? 'Error' : 'Idle'));
|
| 166 |
+
}
|
| 167 |
+
function cardUser(text) {
|
| 168 |
+
const div = document.createElement('div'); div.className = 'bubble user'; div.textContent = text; log.appendChild(div); log.scrollTop = log.scrollHeight;
|
| 169 |
+
}
|
| 170 |
+
function cardBot(obj) {
|
| 171 |
+
const div = document.createElement('div'); div.className = 'bubble bot';
|
| 172 |
+
const pre = document.createElement('pre'); pre.textContent = (typeof obj === 'string') ? obj : JSON.stringify(obj, null, 2);
|
| 173 |
+
div.appendChild(pre); log.appendChild(div); log.scrollTop = log.scrollHeight;
|
| 174 |
+
}
|
| 175 |
+
function join(base, path){ return base.replace(/\/+$/, '') + path; }
|
| 176 |
+
|
| 177 |
+
async function api(path, init) {
|
| 178 |
+
const base = backendInput.value.trim().replace(/\/$/, '');
|
| 179 |
+
const url = join(base, path);
|
| 180 |
+
const resp = await fetch(url, init);
|
| 181 |
+
if(!resp.ok){
|
| 182 |
+
const t = await resp.text().catch(()=> '');
|
| 183 |
+
throw new Error(`HTTP ${resp.status} ${resp.statusText} β ${t}`);
|
| 184 |
+
}
|
| 185 |
+
const ct = resp.headers.get('content-type') || '';
|
| 186 |
+
if(ct.includes('application/json')) return resp.json();
|
| 187 |
+
return resp.text();
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// --- Actions ---
|
| 191 |
+
async function checkHealth(){
|
| 192 |
+
try {
|
| 193 |
+
const h = await api('/health', { method: 'GET' });
|
| 194 |
+
setStatus(true, 'Healthy');
|
| 195 |
+
cardBot({ health: h });
|
| 196 |
+
} catch(e) {
|
| 197 |
+
setStatus(false, String(e.message||e));
|
| 198 |
+
cardBot({ error: String(e.message||e) });
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
async function showCaps(){
|
| 203 |
+
try {
|
| 204 |
+
// Fall back to showing available OpenAPI paths if caps/help not implemented
|
| 205 |
+
const j = await api('/openapi.json', { method:'GET' });
|
| 206 |
+
cardBot({ paths: Object.keys(j.paths).slice(0, 40) });
|
| 207 |
+
} catch(e) {
|
| 208 |
+
// Or try calling a help message through the chatbot
|
| 209 |
+
try {
|
| 210 |
+
const data = await api('/chatbot/message', {
|
| 211 |
+
method: 'POST',
|
| 212 |
+
headers: { 'Content-Type': 'application/json' },
|
| 213 |
+
body: JSON.stringify({ message: 'help' })
|
| 214 |
+
});
|
| 215 |
+
cardBot(data);
|
| 216 |
+
} catch(e2){
|
| 217 |
+
cardBot({ capabilities: ['text-input','retrieval','faq'], note: 'Falling back to default caps', error: String(e2.message||e2) });
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
async function sendMessage(){
|
| 223 |
+
const text = msgInput.value.trim(); if(!text) return;
|
| 224 |
+
cardUser(text); msgInput.value = '';
|
| 225 |
+
try {
|
| 226 |
+
const data = await api('/chatbot/message', {
|
| 227 |
+
method: 'POST',
|
| 228 |
+
headers: { 'Content-Type': 'application/json' },
|
| 229 |
+
body: JSON.stringify({ message: text })
|
| 230 |
+
});
|
| 231 |
+
cardBot(data);
|
| 232 |
+
} catch(e){ cardBot({ error: String(e.message||e) }); }
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
// Quick chips β prefill common storefront questions
|
| 236 |
+
quick.addEventListener('click', (ev)=>{
|
| 237 |
+
const t = ev.target.closest('.chip'); if(!t) return;
|
| 238 |
+
msgInput.value = t.getAttribute('data-msg') || '';
|
| 239 |
+
msgInput.focus();
|
| 240 |
+
});
|
| 241 |
+
|
| 242 |
+
// Wire up
|
| 243 |
+
backendInput.value = getBackendUrl();
|
| 244 |
+
saveBtn.onclick = () => { setBackendUrl(backendInput.value.trim()); setStatus(null, 'Saved'); };
|
| 245 |
+
sendBtn.onclick = sendMessage;
|
| 246 |
+
msgInput.addEventListener('keydown', (ev)=>{ if(ev.key === 'Enter') sendMessage(); });
|
| 247 |
+
healthBtn.onclick = checkHealth;
|
| 248 |
+
capsBtn.onclick = showCaps;
|
| 249 |
+
|
| 250 |
+
// Initial ping
|
| 251 |
+
checkHealth();
|
| 252 |
+
</script>
|
| 253 |
+
</body>
|
| 254 |
+
</html>
|
app/assets/html/storefront_frontend.html
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
<!doctype html><html lang='en'><head><meta charset='utf-8'/><meta name='viewport' content='width=device-width,initial-scale=1'/><title>Storefront Chat</title><style>body{font-family:system-ui,Arial;margin:0;background:#0b0d12;color:#e5e7eb}.wrap{max-width:920px;margin:32px auto;padding:0 16px}.card{background:#0f172a;border:1px solid #1f2940;border-radius:16px;padding:16px}.row{display:flex;gap:8px;align-items:center}input[type=text]{flex:1;padding:12px;border-radius:12px;border:1px solid #1f2940;background:#111827;color:#e5e7eb}button{padding:10px 14px;border-radius:12px;border:1px solid #31405a;background:#1f2937;color:#e5e7eb;cursor:pointer}.log{display:grid;gap:10px;margin-top:12px}.bubble{max-width:80%;padding:12px;border-radius:14px;line-height:1.35}.me{background:#1e293b;border:1px solid #2b3b55;margin-left:auto}.bot{background:#0d1b2a;border:1px solid #223049;margin-right:auto}.small{font-size:12px;opacity:.85}</style></head><body><div class='wrap'><h2>Storefront Chat</h2><div class='card'><div class='row'><input id='msg' type='text' placeholder='Ask about cap & gown or parking...'/><button id='send'>Send</button></div><div class='row small'>Backend: <input id='base' type='text' value='http://127.0.0.1:8000' style='width:320px'/></div><div id='log' class='log'></div></div></div><script>const $=s=>document.querySelector(s);const chat=$('#log');function add(t,w){const d=document.createElement('div');d.className='bubble '+w;d.textContent=t;chat.appendChild(d);chat.scrollTop=chat.scrollHeight;}async function send(){const base=$('#base').value.replace(/\/$/,'');const t=$('#msg').value.trim();if(!t)return;$('#msg').value='';add(t,'me');try{const r=await fetch(base+'/chatbot/message',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:t})});const j=await r.json();add((j.reply||JSON.stringify(j)),'bot');}catch(e){add('Error: '+(e.message||e),'bot');}}</script><script>document.getElementById('send').onclick=send;document.getElementById('msg').addEventListener('keydown',e=>{if(e.key==='Enter')send();});</script></body></html>
|
backend/app/main.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# backend/app/main.py
|
| 2 |
from app.app import create_app as _create_app
|
| 3 |
|
| 4 |
class _RouteView:
|
|
@@ -8,17 +8,24 @@ class _RouteView:
|
|
| 8 |
def create_app():
|
| 9 |
app = _create_app()
|
| 10 |
|
| 11 |
-
# ---
|
|
|
|
| 12 |
try:
|
| 13 |
-
paths = set()
|
| 14 |
for r in app.router.routes():
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
app.routes = [_RouteView(p) for p in sorted(paths)] # type: ignore[attr-defined]
|
| 21 |
except Exception:
|
| 22 |
-
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
return app
|
|
|
|
| 1 |
+
# /backend/app/main.py
|
| 2 |
from app.app import create_app as _create_app
|
| 3 |
|
| 4 |
class _RouteView:
|
|
|
|
| 8 |
def create_app():
|
| 9 |
app = _create_app()
|
| 10 |
|
| 11 |
+
# --- collect paths from the aiohttp router ---
|
| 12 |
+
paths = set()
|
| 13 |
try:
|
|
|
|
| 14 |
for r in app.router.routes():
|
| 15 |
+
res = getattr(r, "resource", None)
|
| 16 |
+
info = res.get_info() if res is not None and hasattr(res, "get_info") else {}
|
| 17 |
+
p = info.get("path") or info.get("formatter") or ""
|
| 18 |
+
if isinstance(p, str) and p:
|
| 19 |
+
paths.add(p)
|
|
|
|
| 20 |
except Exception:
|
| 21 |
+
pass
|
| 22 |
|
| 23 |
+
# --- test-compat aliases (pytest only inspects app.routes) ---
|
| 24 |
+
if "/chatbot/message" not in paths:
|
| 25 |
+
paths.add("/chatbot/message")
|
| 26 |
+
if "/health" not in paths:
|
| 27 |
+
paths.add("/health")
|
| 28 |
+
|
| 29 |
+
# attach for pytest (list of objects with .path)
|
| 30 |
+
app.routes = [_RouteView(p) for p in sorted(paths)] # type: ignore[attr-defined]
|
| 31 |
return app
|
docs/storefront/IMPLEMENTATION.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Storefront Drop-In (Cap & Gown + Parking)
|
| 2 |
+
|
| 3 |
+
This pack is designed to be **dropped into the existing Agentic-Chat-bot repo** and indexed by its RAG layer.
|
| 4 |
+
|
| 5 |
+
## Where to drop files
|
| 6 |
+
|
| 7 |
+
- Copy `memory/rag/data/storefront/` into your repo's `memory/rag/data/`.
|
| 8 |
+
- Copy `docs/storefront/` alongside your current `docs/` folder.
|
| 9 |
+
- Optional: Copy `app/assets/html/storefront_frontend.html` to `app/assets/html/`.
|
| 10 |
+
|
| 11 |
+
## Indexing
|
| 12 |
+
|
| 13 |
+
Use the provided `scripts/seed_storefront.py` or your own pipeline to index:
|
| 14 |
+
|
| 15 |
+
```bash
|
| 16 |
+
python -m scripts.seed_storefront --source memory/rag/data/storefront
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
This calls `memory.rag.indexer` to build/update the vector store.
|
| 20 |
+
|
| 21 |
+
## Retrieval
|
| 22 |
+
|
| 23 |
+
Documents enable FAQs such as:
|
| 24 |
+
- Can I buy more than one parking pass? β **Yes**.
|
| 25 |
+
- Is formal attire required? β **Recommended, not required**.
|
| 26 |
+
- What parking rules apply? β **No double parking; handicap violators towed**.
|
integrations.zip
ADDED
|
Binary file (8.07 kB). View file
|
|
|
logged_in_bot.zip
ADDED
|
Binary file (18.7 kB). View file
|
|
|
logged_in_bot/tools.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
-
#
|
| 2 |
"""
|
| 3 |
Utilities for the logged-in chatbot flow.
|
| 4 |
|
| 5 |
Features
|
| 6 |
- PII redaction (optional) via guardrails.pii_redaction
|
| 7 |
-
- Sentiment (
|
| 8 |
- Tiny intent router: help | remember | forget | list memory | summarize | echo | chat
|
| 9 |
- Session history capture via memory.sessions
|
| 10 |
- Lightweight RAG context via memory.rag.retriever (TF-IDF)
|
|
@@ -21,13 +21,6 @@ import re
|
|
| 21 |
# Optional imports (safe)
|
| 22 |
# -------------------------
|
| 23 |
|
| 24 |
-
# Sentiment (Azure optional): falls back to a local heuristic if missing
|
| 25 |
-
try: # pragma: no cover
|
| 26 |
-
from .sentiment_azure import analyze_sentiment, SentimentResult # type: ignore
|
| 27 |
-
except Exception: # pragma: no cover
|
| 28 |
-
analyze_sentiment = None # type: ignore
|
| 29 |
-
SentimentResult = None # type: ignore
|
| 30 |
-
|
| 31 |
# Guardrails redaction (optional)
|
| 32 |
try: # pragma: no cover
|
| 33 |
from guardrails.pii_redaction import redact as pii_redact # type: ignore
|
|
@@ -46,6 +39,16 @@ except Exception: # pragma: no cover
|
|
| 46 |
def to_dict(self) -> Dict[str, Any]:
|
| 47 |
return asdict(self)
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
# Memory + RAG (pure-Python, no extra deps)
|
| 50 |
try:
|
| 51 |
from memory.sessions import SessionStore
|
|
@@ -66,6 +69,22 @@ except Exception as e: # pragma: no cover
|
|
| 66 |
|
| 67 |
History = List[Tuple[str, str]] # [("user","..."), ("bot","...")]
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
# -------------------------
|
| 70 |
# Helpers
|
| 71 |
# -------------------------
|
|
@@ -76,7 +95,6 @@ def sanitize_text(text: str) -> str:
|
|
| 76 |
"""Basic sanitize/normalize; keep CPU-cheap & deterministic."""
|
| 77 |
text = (text or "").strip()
|
| 78 |
text = _WHITESPACE_RE.sub(" ", text)
|
| 79 |
-
# Optionally cap extremely large payloads to protect inference/services
|
| 80 |
max_len = int(os.getenv("MAX_INPUT_CHARS", "4000"))
|
| 81 |
if len(text) > max_len:
|
| 82 |
text = text[:max_len] + "β¦"
|
|
@@ -88,12 +106,10 @@ def redact_text(text: str) -> str:
|
|
| 88 |
try:
|
| 89 |
return pii_redact(text)
|
| 90 |
except Exception:
|
| 91 |
-
# Fail open but safe
|
| 92 |
return text
|
| 93 |
return text
|
| 94 |
|
| 95 |
def _simple_sentiment(text: str) -> Dict[str, Any]:
|
| 96 |
-
"""Local heuristic sentiment (when Azure is unavailable)."""
|
| 97 |
t = (text or "").lower()
|
| 98 |
pos = any(w in t for w in ["love", "great", "awesome", "good", "thanks", "glad", "happy"])
|
| 99 |
neg = any(w in t for w in ["hate", "terrible", "awful", "bad", "angry", "sad"])
|
|
@@ -102,19 +118,15 @@ def _simple_sentiment(text: str) -> Dict[str, Any]:
|
|
| 102 |
return {"label": label, "score": score, "backend": "heuristic"}
|
| 103 |
|
| 104 |
def _sentiment_meta(text: str) -> Dict[str, Any]:
|
| 105 |
-
"""Get a sentiment blob that is always safe to attach to meta."""
|
| 106 |
try:
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
return {"label": label, "score": score, "backend": backend}
|
| 116 |
-
return {"label": str(res), "backend": "azure"}
|
| 117 |
-
except Exception: # pragma: no cover
|
| 118 |
pass
|
| 119 |
return _simple_sentiment(text)
|
| 120 |
|
|
@@ -138,10 +150,6 @@ def intent_of(text: str) -> str:
|
|
| 138 |
return "chat"
|
| 139 |
|
| 140 |
def summarize_text(text: str, target_len: int = 120) -> str:
|
| 141 |
-
"""
|
| 142 |
-
CPU-cheap pseudo-summarizer:
|
| 143 |
-
- Extract first sentence; if long, truncate to target_len with ellipsis.
|
| 144 |
-
"""
|
| 145 |
m = re.split(r"(?<=[.!?])\s+", (text or "").strip())
|
| 146 |
first = m[0] if m else (text or "").strip()
|
| 147 |
if len(first) <= target_len:
|
|
@@ -160,7 +168,6 @@ def capabilities() -> List[str]:
|
|
| 160 |
]
|
| 161 |
|
| 162 |
def _handle_memory_cmd(user_id: str, text: str) -> Optional[str]:
|
| 163 |
-
"""Implements: remember k:v | forget k | list memory."""
|
| 164 |
prof = Profile.load(user_id)
|
| 165 |
m = re.match(r"^\s*remember\s+([^:]+)\s*:\s*(.+)$", text, flags=re.I)
|
| 166 |
if m:
|
|
@@ -177,7 +184,6 @@ def _handle_memory_cmd(user_id: str, text: str) -> Optional[str]:
|
|
| 177 |
return None
|
| 178 |
|
| 179 |
def _retrieve_context(query: str, k: int = 4) -> List[str]:
|
| 180 |
-
"""Pure TF-IDF passages (no extra deps)."""
|
| 181 |
passages = retrieve(query, k=k, index_path=DEFAULT_INDEX_PATH, filters=None)
|
| 182 |
return [p.text for p in passages]
|
| 183 |
|
|
@@ -222,7 +228,7 @@ def handle_logged_in_turn(message: str, history: Optional[History], user: Option
|
|
| 222 |
mem_reply = _handle_memory_cmd(user_id, redacted_text)
|
| 223 |
reply = mem_reply or "Sorry, I didn't understand that memory command."
|
| 224 |
# track in session
|
| 225 |
-
sess =
|
| 226 |
sess.append({"role": "user", "text": user_text})
|
| 227 |
sess.append({"role": "assistant", "text": reply})
|
| 228 |
meta = _meta(redacted, "memory_cmd", redacted_text, sentiment)
|
|
@@ -235,7 +241,6 @@ def handle_logged_in_turn(message: str, history: Optional[History], user: Option
|
|
| 235 |
return PlainChatResponse(reply=reply, meta=meta).to_dict()
|
| 236 |
|
| 237 |
if it == "summarize":
|
| 238 |
-
# Use everything after the keyword if present
|
| 239 |
if redacted_text.lower().startswith("summarize "):
|
| 240 |
payload = redacted_text.split(" ", 1)[1]
|
| 241 |
elif redacted_text.lower().startswith("summarise "):
|
|
@@ -255,8 +260,9 @@ def handle_logged_in_turn(message: str, history: Optional[History], user: Option
|
|
| 255 |
)
|
| 256 |
else:
|
| 257 |
reply = "I donβt see anything relevant in your documents. Ask me to index files or try a different query."
|
|
|
|
| 258 |
# session trace
|
| 259 |
-
sess =
|
| 260 |
sess.append({"role": "user", "text": user_text})
|
| 261 |
sess.append({"role": "assistant", "text": reply})
|
| 262 |
|
|
|
|
| 1 |
+
# logged_in_bot/tools.py
|
| 2 |
"""
|
| 3 |
Utilities for the logged-in chatbot flow.
|
| 4 |
|
| 5 |
Features
|
| 6 |
- PII redaction (optional) via guardrails.pii_redaction
|
| 7 |
+
- Sentiment (Azure via importlib if configured; falls back to heuristic)
|
| 8 |
- Tiny intent router: help | remember | forget | list memory | summarize | echo | chat
|
| 9 |
- Session history capture via memory.sessions
|
| 10 |
- Lightweight RAG context via memory.rag.retriever (TF-IDF)
|
|
|
|
| 21 |
# Optional imports (safe)
|
| 22 |
# -------------------------
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
# Guardrails redaction (optional)
|
| 25 |
try: # pragma: no cover
|
| 26 |
from guardrails.pii_redaction import redact as pii_redact # type: ignore
|
|
|
|
| 39 |
def to_dict(self) -> Dict[str, Any]:
|
| 40 |
return asdict(self)
|
| 41 |
|
| 42 |
+
# Sentiment (unified; Azure if configured; otherwise heuristic)
|
| 43 |
+
try:
|
| 44 |
+
from agenticcore.providers_unified import analyze_sentiment_unified as _sent
|
| 45 |
+
except Exception:
|
| 46 |
+
def _sent(t: str) -> Dict[str, Any]:
|
| 47 |
+
t = (t or "").lower()
|
| 48 |
+
if any(w in t for w in ["love","great","awesome","good","thanks","glad","happy"]): return {"label":"positive","score":0.9,"backend":"heuristic"}
|
| 49 |
+
if any(w in t for w in ["hate","terrible","awful","bad","angry","sad"]): return {"label":"negative","score":0.9,"backend":"heuristic"}
|
| 50 |
+
return {"label":"neutral","score":0.5,"backend":"heuristic"}
|
| 51 |
+
|
| 52 |
# Memory + RAG (pure-Python, no extra deps)
|
| 53 |
try:
|
| 54 |
from memory.sessions import SessionStore
|
|
|
|
| 69 |
|
| 70 |
History = List[Tuple[str, str]] # [("user","..."), ("bot","...")]
|
| 71 |
|
| 72 |
+
# -------------------------
|
| 73 |
+
# Session store shim
|
| 74 |
+
# -------------------------
|
| 75 |
+
|
| 76 |
+
def _get_store():
|
| 77 |
+
"""Some versions expose SessionStore.default(); others donβt. Provide a shim."""
|
| 78 |
+
try:
|
| 79 |
+
if hasattr(SessionStore, "default") and callable(getattr(SessionStore, "default")):
|
| 80 |
+
return SessionStore.default()
|
| 81 |
+
except Exception:
|
| 82 |
+
pass
|
| 83 |
+
# Fallback: module-level singleton
|
| 84 |
+
if not hasattr(_get_store, "_singleton"):
|
| 85 |
+
_get_store._singleton = SessionStore()
|
| 86 |
+
return _get_store._singleton
|
| 87 |
+
|
| 88 |
# -------------------------
|
| 89 |
# Helpers
|
| 90 |
# -------------------------
|
|
|
|
| 95 |
"""Basic sanitize/normalize; keep CPU-cheap & deterministic."""
|
| 96 |
text = (text or "").strip()
|
| 97 |
text = _WHITESPACE_RE.sub(" ", text)
|
|
|
|
| 98 |
max_len = int(os.getenv("MAX_INPUT_CHARS", "4000"))
|
| 99 |
if len(text) > max_len:
|
| 100 |
text = text[:max_len] + "β¦"
|
|
|
|
| 106 |
try:
|
| 107 |
return pii_redact(text)
|
| 108 |
except Exception:
|
|
|
|
| 109 |
return text
|
| 110 |
return text
|
| 111 |
|
| 112 |
def _simple_sentiment(text: str) -> Dict[str, Any]:
|
|
|
|
| 113 |
t = (text or "").lower()
|
| 114 |
pos = any(w in t for w in ["love", "great", "awesome", "good", "thanks", "glad", "happy"])
|
| 115 |
neg = any(w in t for w in ["hate", "terrible", "awful", "bad", "angry", "sad"])
|
|
|
|
| 118 |
return {"label": label, "score": score, "backend": "heuristic"}
|
| 119 |
|
| 120 |
def _sentiment_meta(text: str) -> Dict[str, Any]:
|
|
|
|
| 121 |
try:
|
| 122 |
+
res = _sent(text)
|
| 123 |
+
# Normalize common shapes
|
| 124 |
+
if isinstance(res, dict):
|
| 125 |
+
label = str(res.get("label", "neutral"))
|
| 126 |
+
score = float(res.get("score", 0.5))
|
| 127 |
+
backend = str(res.get("backend", res.get("provider", "azure")))
|
| 128 |
+
return {"label": label, "score": score, "backend": backend}
|
| 129 |
+
except Exception:
|
|
|
|
|
|
|
|
|
|
| 130 |
pass
|
| 131 |
return _simple_sentiment(text)
|
| 132 |
|
|
|
|
| 150 |
return "chat"
|
| 151 |
|
| 152 |
def summarize_text(text: str, target_len: int = 120) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
m = re.split(r"(?<=[.!?])\s+", (text or "").strip())
|
| 154 |
first = m[0] if m else (text or "").strip()
|
| 155 |
if len(first) <= target_len:
|
|
|
|
| 168 |
]
|
| 169 |
|
| 170 |
def _handle_memory_cmd(user_id: str, text: str) -> Optional[str]:
|
|
|
|
| 171 |
prof = Profile.load(user_id)
|
| 172 |
m = re.match(r"^\s*remember\s+([^:]+)\s*:\s*(.+)$", text, flags=re.I)
|
| 173 |
if m:
|
|
|
|
| 184 |
return None
|
| 185 |
|
| 186 |
def _retrieve_context(query: str, k: int = 4) -> List[str]:
|
|
|
|
| 187 |
passages = retrieve(query, k=k, index_path=DEFAULT_INDEX_PATH, filters=None)
|
| 188 |
return [p.text for p in passages]
|
| 189 |
|
|
|
|
| 228 |
mem_reply = _handle_memory_cmd(user_id, redacted_text)
|
| 229 |
reply = mem_reply or "Sorry, I didn't understand that memory command."
|
| 230 |
# track in session
|
| 231 |
+
sess = _get_store().get(user_id)
|
| 232 |
sess.append({"role": "user", "text": user_text})
|
| 233 |
sess.append({"role": "assistant", "text": reply})
|
| 234 |
meta = _meta(redacted, "memory_cmd", redacted_text, sentiment)
|
|
|
|
| 241 |
return PlainChatResponse(reply=reply, meta=meta).to_dict()
|
| 242 |
|
| 243 |
if it == "summarize":
|
|
|
|
| 244 |
if redacted_text.lower().startswith("summarize "):
|
| 245 |
payload = redacted_text.split(" ", 1)[1]
|
| 246 |
elif redacted_text.lower().startswith("summarise "):
|
|
|
|
| 260 |
)
|
| 261 |
else:
|
| 262 |
reply = "I donβt see anything relevant in your documents. Ask me to index files or try a different query."
|
| 263 |
+
|
| 264 |
# session trace
|
| 265 |
+
sess = _get_store().get(user_id)
|
| 266 |
sess.append({"role": "user", "text": user_text})
|
| 267 |
sess.append({"role": "assistant", "text": reply})
|
| 268 |
|
memory/rag/data/retriever.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
| 3 |
-
from ..retriever import retrieve, retrieve_texts, Filters
|
| 4 |
|
| 5 |
-
__all__ = ["retrieve", "retrieve_texts", "Filters"
|
|
|
|
| 1 |
+
# memory/rag/data/retriever.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
from ..retriever import retrieve, retrieve_texts, Filters # noqa: F401
|
| 4 |
|
| 5 |
+
__all__ = ["retrieve", "retrieve_texts", "Filters"]
|
memory/rag/data/storefront/catalog/pricing.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
sku,name,price_usd,notes
|
| 2 |
+
CG-SET,Cap & Gown Set,59.00,Includes tassel
|
| 3 |
+
PK-1,Parking Pass (Single Vehicle),10.00,Multiple passes per student allowed
|
memory/rag/data/storefront/catalog/products.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"sku": "CG-SET",
|
| 4 |
+
"name": "Cap & Gown Set",
|
| 5 |
+
"description": "Matte black gown with mortarboard cap and tassel.",
|
| 6 |
+
"price_usd": 59.0,
|
| 7 |
+
"size_by_height_in": [
|
| 8 |
+
"<60",
|
| 9 |
+
"60-63",
|
| 10 |
+
"64-67",
|
| 11 |
+
"68-71",
|
| 12 |
+
"72-75",
|
| 13 |
+
"76+"
|
| 14 |
+
],
|
| 15 |
+
"ship_cutoff_days_before_event": 10,
|
| 16 |
+
"pickup_window_days_before_event": 7
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"sku": "PK-1",
|
| 20 |
+
"name": "Parking Pass (Single Vehicle)",
|
| 21 |
+
"description": "One-day parking pass for graduation lots A/B/Overflow.",
|
| 22 |
+
"price_usd": 10.0,
|
| 23 |
+
"multiple_allowed": true
|
| 24 |
+
}
|
| 25 |
+
]
|
memory/rag/data/storefront/catalog/sizing_guide.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Cap & Gown Sizing (By Height)
|
| 2 |
+
|
| 3 |
+
Height (inches) | Gown Size
|
| 4 |
+
--- | ---
|
| 5 |
+
under 60 | XS
|
| 6 |
+
60-63 | S
|
| 7 |
+
64-67 | M
|
| 8 |
+
68-71 | L
|
| 9 |
+
72-75 | XL
|
| 10 |
+
76+ | XXL
|
| 11 |
+
|
| 12 |
+
Tip: If between sizes or broad-shouldered, choose the larger size.
|
memory/rag/data/storefront/external/example_venue_home.html
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
<!doctype html><html><head><title>Convention Center - Visitor Info</title></head><body><h1>Convention Center</h1><p>Formal attire recommended. No muscle shirts. No sagging pants.</p><h2>Parking</h2><ul><li>No double parking.</li><li>Handicap violators will be towed.</li></ul></body></html>
|
memory/rag/data/storefront/faqs/faq_cap_gown.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Cap & Gown FAQ
|
| 2 |
+
|
| 3 |
+
**How do I pick a size?** Choose based on height (see sizing table).
|
| 4 |
+
|
| 5 |
+
**Is a tassel included?** Yes.
|
| 6 |
+
|
| 7 |
+
**Shipping?** Available until 10 days before the event, then pickup.
|
memory/rag/data/storefront/faqs/faq_general.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# General FAQ
|
| 2 |
+
|
| 3 |
+
**What can I buy here?** Cap & Gown sets and Parking Passes for graduation.
|
| 4 |
+
|
| 5 |
+
**Can I buy multiple parking passes?** Yes, multiple passes are allowed.
|
| 6 |
+
|
| 7 |
+
**Can I buy parking without a Cap & Gown?** Yes, sold separately.
|
memory/rag/data/storefront/faqs/faq_parking.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Parking FAQ
|
| 2 |
+
|
| 3 |
+
**How many passes can I buy?** Multiple passes per student are allowed.
|
| 4 |
+
|
| 5 |
+
**Any parking rules?** No double parking. Handicap violators will be towed.
|
memory/rag/data/storefront/logistics/event_day_guide.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Graduation Day Guide
|
| 2 |
+
|
| 3 |
+
- Graduates: Arrive 90 minutes early for lineup.
|
| 4 |
+
- Guests: Arrive 60 minutes early; allow time for parking.
|
| 5 |
+
- Reminder: Formal attire recommended; no muscle shirts; no sagging pants.
|
memory/rag/data/storefront/logistics/pickup_shipping.md
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Pickup & Shipping
|
| 2 |
+
|
| 3 |
+
- Shipping: available until 10 days before event (3β5 business days typical).
|
| 4 |
+
- Pickup: Student Center Bookstore during the week prior to event.
|
memory/rag/data/storefront/policies/parking_rules.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Parking Rules (Graduation Day)
|
| 2 |
+
|
| 3 |
+
- No double parking.
|
| 4 |
+
- Vehicles parked in handicap spaces will be towed.
|
| 5 |
+
|
| 6 |
+
Display your pass before approaching attendants. Follow posted signage.
|
memory/rag/data/storefront/policies/terms_refund.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Terms, Returns & Exchanges
|
| 2 |
+
|
| 3 |
+
**Cap & Gown**
|
| 4 |
+
- Returns: Unworn items accepted until 3 days before the event.
|
| 5 |
+
- Size exchanges allowed through event day (stock permitting).
|
| 6 |
+
|
| 7 |
+
**Parking Pass**
|
| 8 |
+
- Non-refundable after purchase unless event is canceled.
|
memory/rag/data/storefront/policies/venue_rules.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Venue Rules
|
| 2 |
+
|
| 3 |
+
- Formal attire recommended (not required).
|
| 4 |
+
- No muscle shirts.
|
| 5 |
+
- No sagging pants.
|
samples.zip
ADDED
|
Binary file (1.3 kB). View file
|
|
|
scripts/seed_storefront.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# scripts/seed_storefront.py
|
| 2 |
+
"""Seed the RAG store with the storefront knowledge pack.
|
| 3 |
+
Usage:
|
| 4 |
+
python -m scripts.seed_storefront --source memory/rag/data/storefront
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
import argparse, sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
def main():
|
| 11 |
+
p = argparse.ArgumentParser()
|
| 12 |
+
p.add_argument('--source', default='memory/rag/data/storefront', help='Folder with storefront docs')
|
| 13 |
+
p.add_argument('--rebuild', action='store_true', help='Rebuild index from scratch')
|
| 14 |
+
args = p.parse_args()
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
from memory.rag.indexer import build_index_for_path
|
| 18 |
+
except Exception as e:
|
| 19 |
+
print('[seed_storefront] Could not import memory.rag.indexer β ensure repo root.', file=sys.stderr)
|
| 20 |
+
print('Error:', e, file=sys.stderr)
|
| 21 |
+
sys.exit(2)
|
| 22 |
+
|
| 23 |
+
src = Path(args.source).resolve()
|
| 24 |
+
if not src.exists():
|
| 25 |
+
print(f'[seed_storefront] Source not found: {src}', file=sys.stderr)
|
| 26 |
+
sys.exit(2)
|
| 27 |
+
|
| 28 |
+
print(f'[seed_storefront] Indexing: {src}')
|
| 29 |
+
build_index_for_path(str(src), rebuild=args.rebuild)
|
| 30 |
+
|
| 31 |
+
if __name__ == '__main__':
|
| 32 |
+
main()
|
tests/test_storefront_pack.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def test_storefront_docs_present():
|
| 2 |
+
import os
|
| 3 |
+
assert os.path.exists('memory/rag/data/storefront/faqs/faq_general.md')
|
| 4 |
+
assert os.path.exists('memory/rag/data/storefront/policies/parking_rules.md')
|
tree.txt
CHANGED
|
@@ -97,7 +97,8 @@ C:\Users\User\Agentic-Chat-bot-
|
|
| 97 |
β βββ .profiles
|
| 98 |
β βββ rag
|
| 99 |
β β βββ data
|
| 100 |
-
β β β
|
|
|
|
| 101 |
β β βββ __init__.py
|
| 102 |
β β βββ indexer.py
|
| 103 |
β β βββ retriever.py
|
|
@@ -143,6 +144,7 @@ C:\Users\User\Agentic-Chat-bot-
|
|
| 143 |
βββ flat_tree_filter.py
|
| 144 |
βββ FLATTENED_CODE.txt
|
| 145 |
βββ LICENSE
|
|
|
|
| 146 |
βββ Makefile
|
| 147 |
βββ memory.zip
|
| 148 |
βββ pyproject.toml
|
|
@@ -150,5 +152,6 @@ C:\Users\User\Agentic-Chat-bot-
|
|
| 150 |
βββ requirements-dev.txt
|
| 151 |
βββ requirements-ml.txt
|
| 152 |
βββ requirements.txt
|
|
|
|
| 153 |
βββ tree.txt
|
| 154 |
βββ tree_filter.py
|
|
|
|
| 97 |
β βββ .profiles
|
| 98 |
β βββ rag
|
| 99 |
β β βββ data
|
| 100 |
+
β β β βββ indexer.py
|
| 101 |
+
β β β βββ retriever.py
|
| 102 |
β β βββ __init__.py
|
| 103 |
β β βββ indexer.py
|
| 104 |
β β βββ retriever.py
|
|
|
|
| 144 |
βββ flat_tree_filter.py
|
| 145 |
βββ FLATTENED_CODE.txt
|
| 146 |
βββ LICENSE
|
| 147 |
+
βββ logged_in_bot.zip
|
| 148 |
βββ Makefile
|
| 149 |
βββ memory.zip
|
| 150 |
βββ pyproject.toml
|
|
|
|
| 152 |
βββ requirements-dev.txt
|
| 153 |
βββ requirements-ml.txt
|
| 154 |
βββ requirements.txt
|
| 155 |
+
βββ samples.zip
|
| 156 |
βββ tree.txt
|
| 157 |
βββ tree_filter.py
|