diff --git a/demos/customer_support_voice_agent/.env.example b/demos/customer_support_voice_agent/.env.example new file mode 100644 index 00000000..a182344b --- /dev/null +++ b/demos/customer_support_voice_agent/.env.example @@ -0,0 +1,3 @@ +OPENAI_API_KEY=your openai api key here +FIRECRAWL_API_KEY=your firecrawl api key here +MEMORI_API_KEY=your memori api key here \ No newline at end of file diff --git a/demos/customer_support_voice_agent/.streamlit/config.toml b/demos/customer_support_voice_agent/.streamlit/config.toml new file mode 100644 index 00000000..f0cf6164 --- /dev/null +++ b/demos/customer_support_voice_agent/.streamlit/config.toml @@ -0,0 +1,10 @@ +[theme] +base = "light" + +# Optional overrides: +# primaryColor = "#0b57d0" +# backgroundColor = "#ffffff" +# secondaryBackgroundColor = "#f5f7fb" +# textColor = "#000000" + + diff --git a/demos/customer_support_voice_agent/README.md b/demos/customer_support_voice_agent/README.md new file mode 100644 index 00000000..25a79ac7 --- /dev/null +++ b/demos/customer_support_voice_agent/README.md @@ -0,0 +1,46 @@ +## Customer Support Voice Agent + +Customer-support assistant powered by **OpenAI GPT‑4o**, **OpenAI TTS**, **Memori v3**, and **Firecrawl**. +Paste your docs/FAQ URLs, ingest them into Memori, and chat with a voice-enabled support agent on top of that knowledge. + +### Features + +- **Company‑agnostic**: Works for any product/company docs you point it at. +- **Memori v3 knowledge base**: Docs are crawled with Firecrawl and stored in a Memori‑backed SQLite DB. +- **Chat + Voice UI**: Streamlit chat interface with optional audio playback. +- **Persistent memory**: Conversations and ingested docs are stored for future questions. + +### Setup + +Install dependencies (use any Python 3.11+ environment you like): + +```bash +cd demos/customer_support_voice_agent +python -m pip install -r requirements.txt +``` + +Create a `.env` file (copy from `.env.example`) and set: + +- `OPENAI_API_KEY` – required (chat + TTS). +- `FIRECRAWL_API_KEY` – required to ingest docs via Firecrawl. +- `MEMORI_API_KEY` – optional, for Memori Advanced Augmentation / higher quotas. +- `SQLITE_DB_PATH` – optional, defaults to `./memori.sqlite`. + +### Run + +```bash +streamlit run app.py +``` + +In the **sidebar**: + +1. Enter your **Firecrawl**, **Memori** (optional), and **OpenAI** API keys. +2. (Optionally) set a **Company Name**. +3. Paste one or more documentation URLs (one per line) under **“Ingest Docs into Memori”** and click **“Extract & store to Memori”**. + +Then use the main chat box to ask customer‑support questions about your product. + +> Note: `ingest_studio1.py` is kept as an example script for the original Studio1 demo. +> For most use cases, you can ignore it and use the sidebar‑based ingestion instead. + + diff --git a/demos/customer_support_voice_agent/app.py b/demos/customer_support_voice_agent/app.py new file mode 100644 index 00000000..489a25b0 --- /dev/null +++ b/demos/customer_support_voice_agent/app.py @@ -0,0 +1,444 @@ +""" +Customer Support Voice Agent with Memori v3 + +Streamlit app: +- Single chat interface for customer support on top of your own docs/FAQs. +- Uses Memori v3 + OpenAI GPT-4o for grounded answers. +- Uses OpenAI TTS for optional voice responses. + +Prereqs: +- Set OPENAI_API_KEY (and optional SQLITE_DB_PATH, e.g. ./memori.sqlite). +- Set FIRECRAWL_API_KEY to ingest documentation URLs into Memori. +""" + +import base64 +import os +from io import BytesIO +from typing import Optional + +import streamlit as st +from dotenv import load_dotenv +from firecrawl import FirecrawlApp +from memori import Memori +from openai import OpenAI +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker + + +load_dotenv() + + +def _load_inline_image(path: str, height_px: int) -> str: + """Return an inline tag for a local PNG, or empty string on failure.""" + try: + with open(path, "rb") as f: + encoded = base64.b64encode(f.read()).decode() + return ( + f"Logo" + ) + except Exception: + return "" + + +def _init_memori_with_openai() -> Optional[Memori]: + """Initialize Memori v3 + OpenAI client, mirroring ai_consultant_agent.""" + openai_key = os.getenv("OPENAI_API_KEY", "") + if not openai_key: + st.warning("OPENAI_API_KEY is not set – Memori v3 will not be active.") + return None + + try: + db_path = os.getenv("SQLITE_DB_PATH", "./memori.sqlite") + database_url = f"sqlite:///{db_path}" + engine = create_engine( + database_url, + pool_pre_ping=True, + connect_args={"check_same_thread": False}, + ) + + # Optional DB connectivity check + with engine.connect() as conn: + conn.execute(text("SELECT 1")) + + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + client = OpenAI(api_key=openai_key) + mem = Memori(conn=SessionLocal).openai.register(client) + # Generic attribution for customer-support use-cases + mem.attribution( + entity_id="customer-support-user", process_id="customer-support" + ) + mem.config.storage.build() + + st.session_state.memori = mem + st.session_state.openai_client = client + return mem + except Exception as e: + st.warning(f"Memori v3 initialization note: {e}") + return None + + +def _synth_audio(text: str, client: OpenAI) -> Optional[BytesIO]: + """Call OpenAI TTS to synthesize speech for the given text.""" + try: + # Using audio.speech.create (high-level helper) if available + result = client.audio.speech.create( + model="gpt-4o-mini-tts", + voice="alloy", + input=text, + ) + audio_bytes = result.read() if hasattr(result, "read") else result + if isinstance(audio_bytes, bytes): + return BytesIO(audio_bytes) + return None + except Exception as e: + st.warning(f"TTS error: {e}") + return None + + +def _ingest_urls_with_firecrawl(mem: Memori, client: OpenAI, urls: list[str]) -> int: + """Ingest one or more documentation base URLs into Memori using Firecrawl.""" + firecrawl_key = os.getenv("FIRECRAWL_API_KEY", "") + if not firecrawl_key: + raise RuntimeError("FIRECRAWL_API_KEY is not set – cannot ingest docs.") + + app = FirecrawlApp(api_key=firecrawl_key) + all_pages = [] + + for base_url in urls: + try: + job = app.crawl( + base_url, + limit=50, + scrape_options={ + "formats": ["markdown", "html"], + "onlyMainContent": True, + }, + ) + + # Normalize Firecrawl response into a list of page dicts (mirrors ingest_studio1). + if isinstance(job, dict): + pages = job.get("data") or job.get("pages") or job + else: + pages = ( + getattr(job, "data", None) + or getattr(job, "pages", None) + or getattr(job, "results", None) + ) + if pages is None: + if hasattr(job, "model_dump"): + data = job.model_dump() + elif hasattr(job, "dict"): + data = job.dict() + else: + data = job + pages = ( + data.get("data") + or data.get("pages") + or data.get("results") + or data + ) + + if isinstance(pages, list): + all_pages.extend(pages) + elif isinstance(pages, dict): + all_pages.append(pages) + except Exception as e: + st.warning(f"Firecrawl issue while crawling {base_url}: {e}") + + # Deduplicate by URL + dedup_pages = [] + seen_urls = set() + for page in all_pages: + url = None + if isinstance(page, dict): + meta = page.get("metadata") or {} + url = page.get("url") or meta.get("sourceURL") + key = url or id(page) + if key in seen_urls: + continue + seen_urls.add(key) + dedup_pages.append(page) + + company_name = st.session_state.get("company_name") or "the company" + + # Ingest pages into Memori by sending them through the registered OpenAI client. + ingested = 0 + for idx, page in enumerate(dedup_pages, start=1): + if isinstance(page, dict): + page_dict = page + else: + if hasattr(page, "model_dump"): + page_dict = page.model_dump() + elif hasattr(page, "dict"): + page_dict = page.dict() + else: + continue + + metadata = page_dict.get("metadata") or {} + url = page_dict.get("url") or metadata.get("sourceURL") or urls[0] + + markdown = ( + page_dict.get("markdown") + or page_dict.get("text") + or page_dict.get("content") + or "" + ) + if not markdown: + continue + + title = page_dict.get("title") or metadata.get("title") or f"Page {idx}" + + doc_text = f"""{company_name} Documentation Page +Title: {title} +URL: {url} + +Content: +{markdown} +""" + + try: + _ = client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + { + "role": "user", + "content": ( + "Store the following documentation page in memory for " + "future customer-support conversations. Respond with a " + "short acknowledgement only.\n\n" + f"{doc_text}" + ), + } + ], + ) + ingested += 1 + except Exception as e: + st.warning(f"Memori/OpenAI issue ingesting {url}: {e}") + + # Flush writes to storage without closing the adapter (app keeps running). + try: + adapter = getattr(mem.config.storage, "adapter", None) + if adapter is not None: + adapter.commit() + except Exception as e: + st.warning(f"Memori commit note: {e}") + + return ingested + + +def main(): + # Page config + st.set_page_config( + page_title="Customer Support Voice Agent", + layout="wide", + ) + + # Initialize session state + if "memori" not in st.session_state or "openai_client" not in st.session_state: + _init_memori_with_openai() + + if "messages" not in st.session_state: + st.session_state.messages = [] + + if "company_name" not in st.session_state: + st.session_state.company_name = "" + + # Inline title logos (reuse existing assets from other agents) + memori_img_inline = _load_inline_image( + "../job_search_agent/assets/Memori_Logo.png", height_px=90 + ) + + title_html = f""" +
+

+ Customer Support Voice Agent with + {memori_img_inline} +

+
+""" + st.markdown(title_html, unsafe_allow_html=True) + + # Sidebar + with st.sidebar: + st.subheader("🔑 API & Storage") + + firecrawl_api_key_input = st.text_input( + "Firecrawl API Key", + value=os.getenv("FIRECRAWL_API_KEY", ""), + type="password", + help="Used to crawl/scrape your documentation URLs into Memori.", + ) + + memori_api_key_input = st.text_input( + "Memori API Key (optional)", + value=os.getenv("MEMORI_API_KEY", ""), + type="password", + help="Used for Memori Advanced Augmentation and higher quotas.", + ) + + openai_api_key_input = st.text_input( + "OpenAI API Key", + value=os.getenv("OPENAI_API_KEY", ""), + type="password", + help="Your OpenAI API key for GPT-4o and TTS.", + ) + + company_name_input = st.text_input( + "Company Name (optional)", + value=st.session_state.company_name, + help="Used to personalize prompts and titles.", + ) + st.session_state.company_name = company_name_input.strip() + + if st.button("Save Settings"): + if openai_api_key_input: + os.environ["OPENAI_API_KEY"] = openai_api_key_input + if firecrawl_api_key_input: + os.environ["FIRECRAWL_API_KEY"] = firecrawl_api_key_input + if memori_api_key_input: + os.environ["MEMORI_API_KEY"] = memori_api_key_input + st.success("✅ Settings saved for this session. Re-initializing Memori...") + _init_memori_with_openai() + + st.markdown("---") + st.markdown("### 📚 Ingest Docs into Memori") + ingest_urls_text = st.text_area( + "Documentation URLs (one per line)", + placeholder="https://docs.yourcompany.com\nhttps://yourcompany.com/help", + height=140, + ) + if st.button("Extract & store to Memori"): + urls = [u.strip() for u in ingest_urls_text.splitlines() if u.strip()] + if not urls: + st.warning("Please enter at least one URL to ingest.") + elif ( + "memori" not in st.session_state + or "openai_client" not in st.session_state + ): + st.warning( + "Memori / OpenAI client not initialized yet – check your API key above." + ) + else: + try: + count = _ingest_urls_with_firecrawl( + st.session_state.memori, + st.session_state.openai_client, + urls, + ) + st.success( + f"✅ Ingested {count} documentation page(s) into Memori." + ) + except Exception as e: + st.error(f"❌ Ingestion error: {e}") + + st.markdown("---") + st.markdown("### 💡 About the Agent") + st.markdown( + """ + This agent answers customer-support questions for **your own product or company**: + - Docs, FAQs, services, pricing, and onboarding flows + - Product capabilities and common troubleshooting steps + + Knowledge is built from whatever documentation URLs you ingest + (e.g. `https://docs.yourcompany.com`) via **Firecrawl** and stored in **Memori v3**. + + Responses are powered by **OpenAI GPT-4o** and can optionally be read aloud using **OpenAI TTS**. + """ + ) + + if "openai_client" not in st.session_state: + st.warning( + "⚠️ OPENAI_API_KEY missing or Memori v3 failed to initialize – " + "LLM responses will not work." + ) + st.stop() + + client: OpenAI = st.session_state.openai_client + mem: Memori = st.session_state.memori + + # Toggle for voice output + col_voice, _ = st.columns([1, 3]) + with col_voice: + enable_voice = st.checkbox("🔊 Enable voice responses", value=True) + + # Chat history + for message in st.session_state.messages: + with st.chat_message(message["role"]): + st.markdown(message["content"]) + + user_input = st.chat_input("Ask a customer-support question…") + + if user_input: + # Display user message + st.session_state.messages.append({"role": "user", "content": user_input}) + with st.chat_message("user"): + st.markdown(user_input) + + # Assistant response + with st.chat_message("assistant"): + with st.spinner("🤔 Thinking with your ingested knowledge…"): + try: + # (Optional) Use Memori search to fetch relevant Studio1 context + kb_snippets = [] + try: + # Only call .search if this Memori instance actually exposes it. + if hasattr(mem, "search"): + # Limit to most relevant 5 items + kb_snippets = mem.search(user_input, limit=5) or [] + except Exception as search_err: + # Non-fatal – the assistant can still answer without KB snippets. + st.warning(f"Memori search issue: {search_err}") + + kb_context = "" + if kb_snippets: + kb_context = "Here are some relevant snippets from the company knowledge base:\n" + for snip in kb_snippets: + kb_context += f"- {snip}\n" + + # Resolve company name for this request + company_name = ( + st.session_state.get("company_name") or "your company" + ) + + system_prompt = f"""You are a helpful customer support assistant for {company_name}. + +Use ONLY the company's documentation and prior stored content in Memori to answer. +If something is unclear or not covered, say that it isn't in the docs instead of hallucinating. + +Context from the knowledge base (may be partial): +{kb_context} +""" + + response = client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_input}, + ], + ) + answer = response.choices[0].message.content or "" + + st.session_state.messages.append( + {"role": "assistant", "content": answer} + ) + st.markdown(answer) + + # Optional voice output + if enable_voice and answer.strip(): + audio_buf = _synth_audio(answer, client) + if audio_buf is not None: + audio_bytes = audio_buf.getvalue() + st.audio(audio_bytes, format="audio/mp3") + except Exception as e: + err = f"❌ Error generating answer: {e}" + st.session_state.messages.append( + {"role": "assistant", "content": err} + ) + st.error(err) + + +if __name__ == "__main__": + main() diff --git a/demos/customer_support_voice_agent/assets/Memori_Logo.png b/demos/customer_support_voice_agent/assets/Memori_Logo.png new file mode 100644 index 00000000..4b4416b8 Binary files /dev/null and b/demos/customer_support_voice_agent/assets/Memori_Logo.png differ diff --git a/demos/customer_support_voice_agent/ingest_studio1.py b/demos/customer_support_voice_agent/ingest_studio1.py new file mode 100644 index 00000000..fb026b98 --- /dev/null +++ b/demos/customer_support_voice_agent/ingest_studio1.py @@ -0,0 +1,319 @@ +""" +Example ingestion script for the original Studio1 Customer Support demo. + +Today, the recommended way to ingest docs is via the **Streamlit sidebar** +(paste URLs and click "Extract & store to Memori"). This script is kept as a +convenience for rebuilding the Studio1 knowledge base from its public +docs/marketing site. + +It uses Firecrawl to crawl https://www.studio1hq.com/ and ingests the content +into Memori v3 as a searchable knowledge base. +""" + +import os +from typing import List + +from dotenv import load_dotenv +from firecrawl import FirecrawlApp +from memori import Memori +from openai import OpenAI +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker + + +load_dotenv() + +# Primary docs site root used for broad crawling +STUDIO1_URL = "https://docs.studio1hq.com/" + +# High-value pages we always want to ingest explicitly, even if the crawler +# misses them due to depth/links. This includes services, legal, and marketing +# "About" pages. +STUDIO1_STATIC_URLS = [ + "https://docs.studio1hq.com/about-us", + "https://docs.studio1hq.com/faq", + "https://docs.studio1hq.com/services/technical-content", + "https://docs.studio1hq.com/services/developer-advocacy", + "https://docs.studio1hq.com/services/tech-video-production", + "https://docs.studio1hq.com/services/audit-services", + "https://docs.studio1hq.com/services/organic-campaign", + "https://docs.studio1hq.com/services/product-launch", + "https://docs.studio1hq.com/services/influencer-management", + "https://docs.studio1hq.com/terms-of-use", + "https://docs.studio1hq.com/privacy-policy", + "https://www.studio1hq.com/about-us", +] + + +def _init_memori() -> tuple[Memori, OpenAI]: + """Initialize Memori v3 with SQLAlchemy + OpenAI, mirroring ai_consultant_agent. + + Returns (Memori instance, OpenAI client) so we can use the registered + OpenAI client to drive Memori's automatic ingestion. + """ + openai_key = os.getenv("OPENAI_API_KEY", "") + if not openai_key: + raise RuntimeError("OPENAI_API_KEY is not set.") + + db_path = os.getenv("SQLITE_DB_PATH", "./memori.sqlite") + database_url = f"sqlite:///{db_path}" + engine = create_engine( + database_url, + pool_pre_ping=True, + connect_args={"check_same_thread": False}, + ) + + # Optional connectivity check + with engine.connect() as conn: + conn.execute(text("SELECT 1")) + + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + client = OpenAI(api_key=openai_key) + mem = Memori(conn=SessionLocal).openai.register(client) + mem.attribution(entity_id="studio1-support-kb", process_id="studio1-ingest") + mem.config.storage.build() + return mem, client + + +def _crawl_studio1() -> List[dict]: + """Use Firecrawl to crawl the Studio1 docs site and return extracted pages.""" + firecrawl_key = os.getenv("FIRECRAWL_API_KEY", "") + if not firecrawl_key: + raise RuntimeError("FIRECRAWL_API_KEY is not set.") + + app = FirecrawlApp(api_key=firecrawl_key) + + # Basic crawl config; this can be tuned later. + # Modern Firecrawl Python SDK exposes a `crawl(url, *, limit, scrape_options, ...)` + # signature and returns a Pydantic model (e.g. CrawlStatus / CrawlJob). + limit = 50 + # Firecrawl v2 expects `scrape_options.formats` to be a ScrapeFormats model + # or a list of *supported* format literals. "text" is not a valid format, + # so we just request markdown + HTML and derive plain text ourselves later. + scrape_options = { + "formats": ["markdown", "html"], + "onlyMainContent": True, + } + + if hasattr(app, "crawl_url"): + # Some SDK variants expose `crawl_url`; keep this for backwards-compat. + job = app.crawl_url( + url=STUDIO1_URL, + limit=limit, + scrape_options=scrape_options, + ) + elif hasattr(app, "crawl"): + # Preferred modern API, matching docs.firecrawl.dev. + job = app.crawl( + STUDIO1_URL, + limit=limit, + scrape_options=scrape_options, + ) + else: + raise RuntimeError( + "Installed Firecrawl client has neither `crawl_url` nor `crawl` method. " + "Please check your `firecrawl-py` version and docs." + ) + + # Normalize Firecrawl response into a list of page dicts. + # - Newer SDKs return a Pydantic model (e.g. CrawlJob / CrawlStatus) + # - Older SDKs may return a plain dict with a 'data' key + if isinstance(job, dict): + pages = job.get("data") or job.get("pages") or job + else: + # Pydantic models don't support `.get`, but do support attributes and `.model_dump()`. + pages = ( + getattr(job, "data", None) + or getattr(job, "pages", None) + or getattr(job, "results", None) + ) + if pages is None: + if hasattr(job, "model_dump"): + # Pydantic v2 + data = job.model_dump() + elif hasattr(job, "dict"): + # Backwards compat with Pydantic v1 + data = job.dict() + else: + raise RuntimeError( + f"Unexpected Firecrawl response type (no data/pages/results): {type(job)}" + ) + pages = data.get("data") or data.get("pages") or data.get("results") or data + + if not isinstance(pages, list): + raise RuntimeError(f"Unexpected Firecrawl response format: {type(pages)}") + return pages + + +def _scrape_static_pages() -> List[dict]: + """Use Firecrawl to scrape a fixed list of high-value Studio1 URLs.""" + firecrawl_key = os.getenv("FIRECRAWL_API_KEY", "") + if not firecrawl_key: + raise RuntimeError("FIRECRAWL_API_KEY is not set.") + + app = FirecrawlApp(api_key=firecrawl_key) + results: List[dict] = [] + + for url in STUDIO1_STATIC_URLS: + try: + if hasattr(app, "scrape"): + doc = app.scrape( + url=url, + formats=["markdown", "html"], + onlyMainContent=True, + ) + elif hasattr(app, "scrape_url"): + # Backwards-compat for older SDKs + doc = app.scrape_url( + url=url, + scrape_options={ + "formats": ["markdown", "html"], + "onlyMainContent": True, + }, + ) + else: + raise RuntimeError( + "Installed Firecrawl client has neither `scrape` nor `scrape_url`." + ) + + # Normalise into a dict similar to crawl results + if isinstance(doc, dict): + data = doc.get("data") or doc + else: + if hasattr(doc, "model_dump"): + data = doc.model_dump() + elif hasattr(doc, "dict"): + data = doc.dict() + else: + data = doc + + # Some SDKs wrap the page document under "data" + if isinstance(data, dict) and "markdown" in data or "html" in data: + page = data + elif isinstance(data, dict) and isinstance(data.get("data"), dict): + page = data["data"] + else: + # Fallback – if it's already shaped like a page list, skip here + if isinstance(data, list): + # Append each if we somehow got multiple docs back + for p in data: + if isinstance(p, dict): + results.append(p) + continue + page = data + + if isinstance(page, dict): + # Ensure URL is set for deduplication later + page.setdefault("url", url) + results.append(page) + except Exception as e: + print(f"[Firecrawl] Warning: could not scrape {url}: {e}") + + return results + + +def ingest(): + mem, client = _init_memori() + crawled_pages = _crawl_studio1() + static_pages = _scrape_static_pages() + + # Deduplicate by URL to avoid double-ingesting the same page from crawl+scrape + pages: List[dict] = [] + seen_urls = set() + + def _add_pages(src_pages: List[dict]): + for p in src_pages: + url = None + if isinstance(p, dict): + meta = p.get("metadata") or {} + url = p.get("url") or meta.get("sourceURL") + if not url: + # Fallback to id of object to prevent accidental merge + key = id(p) + else: + key = url + if key in seen_urls: + continue + seen_urls.add(key) + pages.append(p) + + _add_pages(crawled_pages) + _add_pages(static_pages) + + print(f"Fetched {len(pages)} pages from Studio1 docs + static URLs") + + for idx, page in enumerate(pages, start=1): + # Firecrawl v2 returns Pydantic models (e.g. Document) rather than plain dicts. + # Normalize each item into a dict first. + if isinstance(page, dict): + page_dict = page + else: + if hasattr(page, "model_dump"): + page_dict = page.model_dump() + elif hasattr(page, "dict"): + page_dict = page.dict() + else: + raise RuntimeError(f"Unexpected page type from Firecrawl: {type(page)}") + + metadata = page_dict.get("metadata") or {} + url = page_dict.get("url") or metadata.get("sourceURL") or STUDIO1_URL + + markdown = ( + page_dict.get("markdown") + or page_dict.get("text") + or page_dict.get("content") + or "" + ) + if not markdown: + continue + + title = page_dict.get("title") or metadata.get("title") or f"Studio1 Page {idx}" + + doc_text = f"""Studio1 Documentation Page +Title: {title} +URL: {url} + +Content: +{markdown} +""" + print(f"Ingesting page {idx}: {url}") + + # Use the registered OpenAI client so Memori can automatically + # capture this as a "conversation" / memory. We keep the prompt + # lightweight and ask the model to simply acknowledge storage. + try: + _ = client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + { + "role": "user", + "content": ( + "Store the following Studio1 documentation page in " + "memory for future retrieval. Respond with a short " + "acknowledgement only.\n\n" + f"{doc_text}" + ), + } + ], + ) + except Exception as record_err: + # Don't abort ingestion if a single record fails; just log it. + print(f"[Memori] Could not index page {idx} ({url}): {record_err}") + + # Ensure any buffered writes are flushed to the backing store. + try: + adapter = getattr(mem.config.storage, "adapter", None) + if adapter is not None: + adapter.commit() + adapter.close() + except Exception as final_err: + print( + f"[Memori] Warning: issue committing/closing storage adapter: {final_err}" + ) + + print("Ingestion complete.") + + +if __name__ == "__main__": + ingest() diff --git a/demos/customer_support_voice_agent/pyproject.toml b/demos/customer_support_voice_agent/pyproject.toml new file mode 100644 index 00000000..16c4748e --- /dev/null +++ b/demos/customer_support_voice_agent/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "customer_support_voice_agent" +version = "0.1.0" +description = "Customer Support Voice Agent for Studio1 using Memori v3, Firecrawl, and OpenAI GPT-4o + TTS." +requires-python = ">=3.10" +dependencies = [ + "streamlit>=1.36.0", + "python-dotenv>=1.0.1", + "sqlalchemy>=2.0.25", + "memori>=0.3.0", # Adjust to the version you use elsewhere in this repo + "openai>=1.40.0", + "firecrawl-py>=0.1.0", # Firecrawl Python SDK + "pydantic>=2.7.0", +] + +[project.optional-dependencies] +dev = ["black", "ruff", "mypy"] + +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + + diff --git a/demos/customer_support_voice_agent/requirements.txt b/demos/customer_support_voice_agent/requirements.txt new file mode 100644 index 00000000..3fff1219 --- /dev/null +++ b/demos/customer_support_voice_agent/requirements.txt @@ -0,0 +1,9 @@ +streamlit>=1.36.0 +python-dotenv>=1.0.1 +sqlalchemy>=2.0.25 +memori>=0.3.0 +openai>=1.40.0 +firecrawl-py>=0.1.0 +pydantic>=2.7.0 + +