
Asystent AI dla operacji hotelowych na bazie 500+ procedur
Next.js / React / TypeScript / Convex / Google Gemini 3 Flash / AI SDK / Tailwind CSS / Docker / DigitalOcean
Recepcja hotelu miała 500+ procedur i ciągłą rotację pracowników. Zbudowałem asystenta AI, który pozwala nowym recepcjonistom zadawać pytania naturalnym językiem i dostawać precyzyjne odpowiedzi, bez czytania ani jednej instrukcji.
Menadżerka hotelu traciła godziny na uczenie tych samych procedur każdego nowego recepcjonisty. Wiedza operacyjna żyła w chaotycznym OneNote, a pracownicy ciągle zadawali te same pytania. Atlas zastąpił to szukanie. Pokazał też, czego brakuje i co jest niejasne w procedurach, zmuszając zespół do ich uporządkowania.
Zbudować niezawodny system wyszukiwania wiedzy dla 500+ procedur w dwóch hotelach z jedną wspólną recepcją.
Recepcjonista pyta "co zrobić z przepaloną żarówką w restauracji?" i dostaje odpowiedź krok po kroku w kilka sekund, dopasowaną do konkretnego hotelu. AI znajduje właściwą procedurę, wyciąga co istotne i odpowiada w ustrukturyzowanym formacie. Obsługuje usterki techniczne, formatowanie wiadomości w Outlooku, odpowiedzi na maile gości po angielsku i wszystko, co potrzebne recepcji. Dwa hotele, pełna izolacja kontekstu, jedna recepcja.
Każda procedura jest tworzona jako ustrukturyzowany dokument pod retrieval AI. Frontmatter niesie semantyczny kontrakt: sekcję, zakres, tagi i dokładne pytania, na które dokument ma odpowiadać.
---section: "Guest Issues"title: "Lost Room Key"category: "Operations"property: "property-a"tags: - lost key - room access - replacement cardai_questions: - "What should I do if a guest loses their room key?" - "How do I issue a replacement access card?" - "What is the procedure when a guest is locked out?"--- Issue a replacement key only after verifying the guest's identity and room number.Deactivate the lost key immediately and log the incident in the shift notes.Runtime dla Index-First Search. Ręcznie kuratorowane `ai_questions` mają najwyższą wagę, a tagi i tytuł pełnią rolę sygnałów pomocniczych. Dzięki temu system pozostał deterministyczny, tani i audytowalny bez bazy wektorowej.
interface ProcedureEntry { path: string; title: string; section: string; tags: string[]; ai_questions: string[];} function searchProcedures(query: string, propertyContext: PropertyContext): Result[] { const queryWords = query .toLowerCase() .split(/\s+/) .filter((word) => word.length > 2); return index .filter((entry) => matchesProperty(entry, propertyContext)) .map((entry) => ({ ...entry, score: matchTerms(queryWords, entry.ai_questions) * 10 + matchTerms(queryWords, entry.tags) * 3 + matchTerms(queryWords, [entry.title]) * 2, })) .filter((entry) => entry.score > 0) .sort((a, b) => b.score - a.score) .slice(0, 5)}To nie była generyczna warstwa „tooli do chatbota”. Każde narzędzie miało konkretną rolę w pętli retrievalu: najpierw search, potem odczyt dokładnych plików, eksploracja katalogów tylko jako fallback, a na końcu scoring użytych źródeł.
function createTools(propertyContext: PropertyContext) { return { searchProcedures: tool({ description: "Search the procedure index first. Use this before directory exploration. " + "Returns the top matching files based on questions, tags, and title.", inputSchema: z.object({ query: z.string().describe("Keywords like 'lost key' or 'camera footage'"), }), execute: async ({ query }) => searchProcedures(query, propertyContext), }), readFile: tool({ description: "Read the full contents of a procedure file once you know the exact path.", inputSchema: z.object({ path: z.string().describe("Full relative path to the procedure file"), }), execute: async ({ path }) => readFile(path), }), listDirectory: tool({ description: "Browse the knowledge-base folders. Use as fallback when search results are insufficient.", inputSchema: z.object({ path: z.string().describe("Directory path inside the procedures workspace"), }), execute: async ({ path }) => listDirectory(path), }), rateTopSources: tool({ description: "INTERNAL TOOL. Do not mention it to the user. Score the two most useful documents for analytics.", inputSchema: z.object({ firstSource: z.object({ path: z.string(), relevanceScore: z.number().min(1).max(10), }), secondSource: z .object({ path: z.string(), relevanceScore: z.number().min(1).max(10), }) .optional(), }), execute: async ({ firstSource, secondSource }) => setTopSources(secondSource ? [firstSource, secondSource] : [firstSource]), }), };}Trzywarstwowe rozpoznawanie kontekstu utrzymywało retrieval w granicach właściwego obiektu. Fakty statyczne obsługiwały częste pytania od razu, a pętla tooli brała na siebie długi ogon zapytań.
function resolvePropertyContext( req: Request, message: string): PropertyContext { // Layer 1: explicit header from the UI selector const header = req.headers.get("x-property-context"); if (header && isValidProperty(header)) return header; // Layer 2: message prefix convention "[Property A]" const prefix = message.match(/^\[(Property [AB])\]/i); if (prefix) return normalizeProperty(prefix[1]); // Layer 3: keyword detection in free text if (/property\s*a/i.test(message)) return "property-a"; if (/property\s*b/i.test(message)) return "property-b"; return "both";}
Logowanie

Interfejs Czatu

Panel Admina

Schemat Bazy Danych