subramaniansrc commited on
Commit
0f026a1
Β·
verified Β·
1 Parent(s): ca0a53e

Create utils.py

Browse files
Files changed (1) hide show
  1. utils.py +135 -0
utils.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Reusable helper utilities for the University Admissions RAG Chatbot.
3
+ """
4
+
5
+ import hashlib
6
+ import os
7
+ import re
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from logging_config import get_logger
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ # ── File helpers ─────────────────────────────────────────────────────────────
18
+
19
+ def validate_file(
20
+ filepath: str,
21
+ allowed_extensions: tuple[str, ...] = (".pdf", ".docx", ".txt"),
22
+ max_size_mb: int = 20,
23
+ ) -> tuple[bool, str]:
24
+ """Return (is_valid, reason)."""
25
+ path = Path(filepath)
26
+
27
+ if not path.exists():
28
+ return False, f"File not found: {filepath}"
29
+
30
+ suffix = path.suffix.lower()
31
+ if suffix not in allowed_extensions:
32
+ return False, (
33
+ f"Extension '{suffix}' not allowed. "
34
+ f"Supported: {', '.join(allowed_extensions)}"
35
+ )
36
+
37
+ size_mb = path.stat().st_size / (1024 * 1024)
38
+ if size_mb > max_size_mb:
39
+ return False, f"File exceeds {max_size_mb} MB limit ({size_mb:.1f} MB)."
40
+
41
+ return True, "OK"
42
+
43
+
44
+ def file_checksum(filepath: str) -> str:
45
+ """MD5 checksum of a file (for deduplication)."""
46
+ h = hashlib.md5()
47
+ with open(filepath, "rb") as f:
48
+ for chunk in iter(lambda: f.read(8192), b""):
49
+ h.update(chunk)
50
+ return h.hexdigest()
51
+
52
+
53
+ def ensure_dir(path: str) -> str:
54
+ Path(path).mkdir(parents=True, exist_ok=True)
55
+ return path
56
+
57
+
58
+ # ── Text helpers ─────────────────────────────────────────────────────────────
59
+
60
+ def sanitize_input(text: str, max_length: int = 1000) -> str:
61
+ """Strip control characters and enforce length cap."""
62
+ text = re.sub(r"[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]", "", text)
63
+ return text.strip()[:max_length]
64
+
65
+
66
+ def truncate_text(text: str, max_chars: int = 300) -> str:
67
+ if len(text) <= max_chars:
68
+ return text
69
+ return text[:max_chars].rstrip() + "…"
70
+
71
+
72
+ # ── Timing helpers ────────────────────────────────────────────────────────────
73
+
74
+ class Timer:
75
+ """Context-manager timer."""
76
+
77
+ def __enter__(self) -> "Timer":
78
+ self.start = time.perf_counter()
79
+ return self
80
+
81
+ def __exit__(self, *_: Any) -> None:
82
+ self.elapsed = time.perf_counter() - self.start
83
+
84
+ def __str__(self) -> str:
85
+ return f"{self.elapsed:.3f}s"
86
+
87
+
88
+ # ── Chat history helpers ──────────────────────────────────────────────────────
89
+
90
+ def format_history_for_context(
91
+ history: list[tuple[str, str]],
92
+ max_turns: int = 6,
93
+ ) -> str:
94
+ """Convert Gradio-style [(user, bot), ...] history to a plain string."""
95
+ recent = history[-max_turns:]
96
+ lines: list[str] = []
97
+ for user_msg, bot_msg in recent:
98
+ if user_msg:
99
+ lines.append(f"User: {user_msg}")
100
+ if bot_msg:
101
+ lines.append(f"Assistant: {bot_msg}")
102
+ return "\n".join(lines)
103
+
104
+
105
+ def export_chat_history(history: list[tuple[str, str]]) -> str:
106
+ """Return a plain-text transcript."""
107
+ if not history:
108
+ return "No conversation history to export."
109
+ lines = ["University Admissions Assistant β€” Chat Transcript", "=" * 52, ""]
110
+ for i, (user_msg, bot_msg) in enumerate(history, 1):
111
+ lines.append(f"[Turn {i}]")
112
+ lines.append(f"You: {user_msg}")
113
+ lines.append(f"Assistant: {bot_msg}")
114
+ lines.append("")
115
+ return "\n".join(lines)
116
+
117
+
118
+ # ── Source formatting ─────────────────────────────────────────────────────────
119
+
120
+ def format_sources(docs: list[Any]) -> str:
121
+ """Format LangChain Document objects into a readable source list."""
122
+ if not docs:
123
+ return ""
124
+ seen: set[str] = set()
125
+ parts: list[str] = []
126
+ for doc in docs:
127
+ src = doc.metadata.get("source", "Unknown source")
128
+ page = doc.metadata.get("page")
129
+ label = f"πŸ“„ {os.path.basename(src)}"
130
+ if page is not None:
131
+ label += f" (p. {page + 1})"
132
+ if label not in seen:
133
+ seen.add(label)
134
+ parts.append(label)
135
+ return "\n".join(parts)