From 406164033da3c5a541138f7450ce13e8e65d9f3c Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Thu, 31 Jul 2025 15:29:29 -0600 Subject: [PATCH] feat: new summary using phi-4 and llama-index (#519) * feat: add litellm backend implementation * refactor: improve generate/completion methods for base LLM * refactor: remove tokenizer logic * style: apply code formatting * fix: remove hallucinations from LLM responses * refactor: comprehensive LLM and summarization rework * chore: remove debug code * feat: add structured output support to LiteLLM * refactor: apply self-review improvements * docs: add model structured output comments * docs: update model structured output comments * style: apply linting and formatting fixes * fix: resolve type logic bug * refactor: apply PR review feedback * refactor: apply additional PR review feedback * refactor: apply final PR review feedback * fix: improve schema passing for LLMs without structured output * feat: add PR comments and logger improvements * docs: update README and add HTTP logging * feat: improve HTTP logging * feat: add summary chunking functionality * fix: resolve title generation runtime issues * refactor: apply self-review improvements * style: apply linting and formatting * feat: implement LiteLLM class structure * style: apply linting and formatting fixes * docs: env template model name fix * chore: remove older litellm class * chore: format * refactor: simplify OpenAILLM * refactor: OpenAILLM tokenizer * refactor: self-review * refactor: self-review * refactor: self-review * chore: format * chore: remove LLM_USE_STRUCTURED_OUTPUT from envs * chore: roll back migration lint changes * chore: roll back migration lint changes * fix: make summary llm configuration optional for the tests * fix: missing f-string * fix: tweak the prompt for summary title * feat: try llamaindex for summarization * fix: complete refactor of summary builder using llamaindex and structured output when possible * fix: separate prompt as constant * fix: typings * fix: enhance prompt to prevent mentioning others subject while summarize one * fix: various changes after self-review * fix: from igor review --------- Co-authored-by: Igor Loskutov --- CLAUDE.md | 4 + README.md | 6 +- server/.env_template | 5 + server/README.md | 20 + server/env.example | 10 + server/gpu/modal_deployments/reflector_llm.py | 1 - .../modal_deployments/reflector_llm_zephyr.py | 1 - server/pyproject.toml | 2 + server/reflector/client.py | 2 +- server/reflector/llm/base.py | 16 +- server/reflector/llm/llm_modal.py | 5 +- server/reflector/llm/openai_llm.py | 117 +++ .../reflector/pipelines/main_live_pipeline.py | 19 +- .../processors/summary/summary_builder.py | 937 ++++++------------ .../processors/transcript_final_summary.py | 7 +- .../processors/transcript_final_title.py | 2 +- .../processors/transcript_translator.py | 1 + server/reflector/settings.py | 6 + server/reflector/stream_client.py | 2 +- server/reflector/tools/exportdb.py | 10 +- server/reflector/utils/retry.py | 25 +- server/reflector/utils/text_utils.py | 4 +- server/tests/conftest.py | 28 +- server/uv.lock | 557 +++++++++++ 24 files changed, 1115 insertions(+), 672 deletions(-) create mode 100644 server/reflector/llm/openai_llm.py diff --git a/CLAUDE.md b/CLAUDE.md index 3f256c93..3ffee5b5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -172,3 +172,7 @@ Modal.com integration for scalable ML processing: - **Audio Routing**: Use BlackHole (Mac) for merging multiple audio sources - **WebRTC**: Ensure proper CORS configuration for cross-origin streaming - **Database**: Run `uv run alembic upgrade head` after pulling schema changes + +## Pipeline/worker related info + +If you need to do any worker/pipeline related work, search for "Pipeline" classes and their "create" or "build" methods to find the main processor sequence. Look for task orchestration patterns (like "chord", "group", or "chain") to identify the post-processing flow with parallel execution chains. This will give you abstract vision on how processing pipeling is organized. diff --git a/README.md b/README.md index 41b47b67..ea3d96fc 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ Reflector Audio Management and Analysis is a cutting-edge web application under development by Monadical. It utilizes AI to record meetings, providing a permanent record with transcripts, translations, and automated summaries. -[![Tests](https://github.com/monadical-sas/cubbi/actions/workflows/pytests.yml/badge.svg?branch=main&event=push)](https://github.com/monadical-sas/cubbi/actions/workflows/pytests.yml) -[![License: MIT](https://img.shields.io/badge/license-AGPL--v3-green.svg)](https://opensource.org/licenses/AGPL-v3) +[![Tests](https://github.com/monadical-sas/reflector/actions/workflows/pytests.yml/badge.svg?branch=main&event=push)](https://github.com/monadical-sas/reflector/actions/workflows/pytests.yml) +[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://opensource.org/licenses/MIT) ## Screenshots @@ -74,7 +74,7 @@ Note: We currently do not have instructions for Windows users. ### Frontend -Start with `cd backend`. +Start with `cd www`. **Installation** diff --git a/server/.env_template b/server/.env_template index 579f80ca..f59c1ffe 100644 --- a/server/.env_template +++ b/server/.env_template @@ -14,3 +14,8 @@ BASE_URL=https://xxxxx.ngrok.app DIARIZATION_ENABLED=false SQS_POLLING_TIMEOUT_SECONDS=60 + +# Summary LLM configuration +SUMMARY_MODEL=monadical/private/smart +SUMMARY_LLM_URL= +SUMMARY_LLM_API_KEY= diff --git a/server/README.md b/server/README.md index 74675085..f027b753 100644 --- a/server/README.md +++ b/server/README.md @@ -20,3 +20,23 @@ Polls SQS every 60 seconds via /server/reflector/worker/process.py:24-62: # Every 60 seconds, check for new recordings sqs = boto3.client("sqs", ...) response = sqs.receive_message(QueueUrl=queue_url, ...) + +# Requeue + +```bash +uv run /app/requeue_uploaded_file.py TRANSCRIPT_ID +``` + +## Pipeline Management + +### Continue stuck pipeline from final summaries (identify_participants) step: + +```bash +uv run python -c "from reflector.pipelines.main_live_pipeline import task_pipeline_final_summaries; result = task_pipeline_final_summaries.delay(transcript_id='TRANSCRIPT_ID'); print(f'Task queued: {result.id}')" +``` + +### Run full post-processing pipeline (continues to completion): + +```bash +uv run python -c "from reflector.pipelines.main_live_pipeline import pipeline_post; pipeline_post(transcript_id='TRANSCRIPT_ID')" +``` diff --git a/server/env.example b/server/env.example index 0b2f2fde..ce70cfb4 100644 --- a/server/env.example +++ b/server/env.example @@ -70,6 +70,16 @@ ZEPHYR_LLM_URL=https://monadical-sas--reflector-llm-zephyr-web.modal.run ## Cache directory to store models CACHE_DIR=data +## ======================================================= +## Summary LLM configuration +## ======================================================= + +## Context size for summary generation (tokens) +SUMMARY_LLM_CONTEXT_SIZE_TOKENS=16000 +SUMMARY_LLM_URL= +SUMMARY_LLM_API_KEY=sk- +SUMMARY_MODEL= + ## ======================================================= ## Diarization ## diff --git a/server/gpu/modal_deployments/reflector_llm.py b/server/gpu/modal_deployments/reflector_llm.py index ea36a3ea..f3752f5d 100644 --- a/server/gpu/modal_deployments/reflector_llm.py +++ b/server/gpu/modal_deployments/reflector_llm.py @@ -9,7 +9,6 @@ import os import threading from typing import Optional -import modal from modal import App, Image, Secret, asgi_app, enter, exit, method # LLM diff --git a/server/gpu/modal_deployments/reflector_llm_zephyr.py b/server/gpu/modal_deployments/reflector_llm_zephyr.py index f5771738..5d9c0390 100644 --- a/server/gpu/modal_deployments/reflector_llm_zephyr.py +++ b/server/gpu/modal_deployments/reflector_llm_zephyr.py @@ -9,7 +9,6 @@ import os import threading from typing import Optional -import modal from modal import App, Image, Secret, asgi_app, enter, exit, method # LLM diff --git a/server/pyproject.toml b/server/pyproject.toml index b3d94b16..4ea9336d 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -38,6 +38,8 @@ dependencies = [ "jsonschema>=4.23.0", "openai>=1.59.7", "psycopg2-binary>=2.9.10", + "llama-index>=0.12.52", + "llama-index-llms-openai-like>=0.4.0", ] [dependency-groups] diff --git a/server/reflector/client.py b/server/reflector/client.py index 571da0a7..3e2d25de 100644 --- a/server/reflector/client.py +++ b/server/reflector/client.py @@ -51,7 +51,7 @@ async def main() -> NoReturn: logger.info(f"Cancelling {len(tasks)} outstanding tasks") await asyncio.gather(*tasks, return_exceptions=True) - logger.info(f'{"Flushing metrics"}') + logger.info(f"{'Flushing metrics'}") loop.stop() signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT) diff --git a/server/reflector/llm/base.py b/server/reflector/llm/base.py index 1e8cb1c9..1527ec63 100644 --- a/server/reflector/llm/base.py +++ b/server/reflector/llm/base.py @@ -17,6 +17,7 @@ T = TypeVar("T", bound="LLM") class LLM: _nltk_downloaded = False _registry = {} + model_name: str m_generate = Histogram( "llm_generate", "Time spent in LLM.generate", @@ -69,6 +70,7 @@ class LLM: module_name = f"reflector.llm.llm_{name}" importlib.import_module(module_name) cls.ensure_nltk() + return cls._registry[name](model_name) def get_model_name(self) -> str: @@ -121,6 +123,11 @@ class LLM: def _get_tokenizer(self): pass + def has_structured_output(self): + # whether implementation supports structured output + # on the model side (otherwise it's prompt engineering) + return False + async def generate( self, prompt: str, @@ -140,6 +147,7 @@ class LLM: prompt=prompt, gen_schema=gen_schema, gen_cfg=gen_cfg, + logger=logger, **kwargs, ) self.m_generate_success.inc() @@ -167,7 +175,9 @@ class LLM: try: with self.m_generate.time(): - result = await retry(self._completion)(messages=messages, **kwargs) + result = await retry(self._completion)( + messages=messages, **{**kwargs, "logger": logger} + ) self.m_generate_success.inc() except Exception: logger.exception("Failed to call llm after retrying") @@ -253,9 +263,7 @@ class LLM: ) -> str: raise NotImplementedError - async def _completion( - self, messages: list, logger: reflector_logger, **kwargs - ) -> dict: + async def _completion(self, messages: list, **kwargs) -> dict: raise NotImplementedError def _parse_json(self, result: str) -> dict: diff --git a/server/reflector/llm/llm_modal.py b/server/reflector/llm/llm_modal.py index 63eb4db4..65473102 100644 --- a/server/reflector/llm/llm_modal.py +++ b/server/reflector/llm/llm_modal.py @@ -31,7 +31,7 @@ class ModalLLM(LLM): async def _generate( self, prompt: str, gen_schema: dict | None, gen_cfg: dict | None, **kwargs - ): + ) -> str: json_payload = {"prompt": prompt} if gen_schema: json_payload["gen_schema"] = gen_schema @@ -52,12 +52,14 @@ class ModalLLM(LLM): timeout=self.timeout, retry_timeout=60 * 5, follow_redirects=True, + logger=kwargs.get("logger", reflector_logger), ) response.raise_for_status() text = response.json()["text"] return text async def _completion(self, messages: list, **kwargs) -> dict: + # returns full api response kwargs.setdefault("temperature", 0.3) kwargs.setdefault("max_tokens", 2048) kwargs.setdefault("stream", False) @@ -78,6 +80,7 @@ class ModalLLM(LLM): timeout=self.timeout, retry_timeout=60 * 5, follow_redirects=True, + logger=kwargs.get("logger", reflector_logger), ) response.raise_for_status() return response.json() diff --git a/server/reflector/llm/openai_llm.py b/server/reflector/llm/openai_llm.py new file mode 100644 index 00000000..14a6aa18 --- /dev/null +++ b/server/reflector/llm/openai_llm.py @@ -0,0 +1,117 @@ +import httpx +from transformers import AutoTokenizer +from reflector.logger import logger + + +def apply_gen_config(payload: dict, gen_cfg) -> None: + """Apply generation config overrides to the payload.""" + config_mapping = { + "temperature": "temperature", + "max_new_tokens": "max_tokens", + "max_tokens": "max_tokens", + "top_p": "top_p", + "frequency_penalty": "frequency_penalty", + "presence_penalty": "presence_penalty", + } + + for cfg_attr, payload_key in config_mapping.items(): + value = getattr(gen_cfg, cfg_attr, None) + if value is not None: + payload[payload_key] = value + if cfg_attr == "max_new_tokens": # Handle max_new_tokens taking precedence + break + + +class OpenAILLM: + def __init__(self, config_prefix: str, settings): + self.config_prefix = config_prefix + self.settings_obj = settings + self.model_name = getattr(settings, f"{config_prefix}_MODEL") + self.url = getattr(settings, f"{config_prefix}_LLM_URL") + self.api_key = getattr(settings, f"{config_prefix}_LLM_API_KEY") + + timeout = getattr(settings, f"{config_prefix}_LLM_TIMEOUT", 300) + self.temperature = getattr(settings, f"{config_prefix}_LLM_TEMPERATURE", 0.7) + self.max_tokens = getattr(settings, f"{config_prefix}_LLM_MAX_TOKENS", 1024) + self.client = httpx.AsyncClient(timeout=timeout) + + # Use a tokenizer that approximates OpenAI token counting + tokenizer_name = getattr(settings, f"{config_prefix}_TOKENIZER", "gpt2") + try: + self.tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) + except Exception: + logger.debug( + f"Failed to load tokenizer '{tokenizer_name}', falling back to default 'gpt2' tokenizer" + ) + self.tokenizer = AutoTokenizer.from_pretrained("gpt2") + + async def generate( + self, prompt: str, gen_schema=None, gen_cfg=None, logger=None + ) -> str: + if logger: + logger.debug( + "OpenAI LLM generate", + prompt=repr(prompt[:100] + "..." if len(prompt) > 100 else prompt), + ) + + messages = [{"role": "user", "content": prompt}] + result = await self.completion( + messages, gen_schema=gen_schema, gen_cfg=gen_cfg, logger=logger + ) + return result["choices"][0]["message"]["content"] + + async def completion( + self, messages: list, gen_schema=None, gen_cfg=None, logger=None, **kwargs + ) -> dict: + if logger: + logger.info("OpenAI LLM completion", messages_count=len(messages)) + + payload = { + "model": self.model_name, + "messages": messages, + "temperature": self.temperature, + "max_tokens": self.max_tokens, + } + + # Apply generation config overrides + if gen_cfg: + apply_gen_config(payload, gen_cfg) + + # Apply structured output schema + if gen_schema: + payload["response_format"] = { + "type": "json_schema", + "json_schema": {"name": "response", "schema": gen_schema}, + } + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.api_key}", + } + + url = f"{self.url.rstrip('/')}/chat/completions" + + if logger: + logger.debug( + "OpenAI API request", url=url, payload_keys=list(payload.keys()) + ) + + response = await self.client.post(url, json=payload, headers=headers) + response.raise_for_status() + + result = response.json() + + if logger: + logger.debug( + "OpenAI API response", + status_code=response.status_code, + choices_count=len(result.get("choices", [])), + ) + + return result + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.client.aclose() diff --git a/server/reflector/pipelines/main_live_pipeline.py b/server/reflector/pipelines/main_live_pipeline.py index 3a4c36be..8e491b70 100644 --- a/server/reflector/pipelines/main_live_pipeline.py +++ b/server/reflector/pipelines/main_live_pipeline.py @@ -16,7 +16,7 @@ import functools from contextlib import asynccontextmanager import boto3 -from celery import chord, group, shared_task +from celery import chord, group, shared_task, current_task from pydantic import BaseModel from reflector.db.meetings import meeting_consent_controller, meetings_controller from reflector.db.recordings import recordings_controller @@ -111,16 +111,29 @@ def get_transcript(func): Decorator to fetch the transcript from the database from the first argument """ + @functools.wraps(func) async def wrapper(**kwargs): transcript_id = kwargs.pop("transcript_id") transcript = await transcripts_controller.get_by_id(transcript_id=transcript_id) if not transcript: raise Exception("Transcript {transcript_id} not found") + + # Enhanced logger with Celery task context tlogger = logger.bind(transcript_id=transcript.id) + if current_task: + tlogger = tlogger.bind( + task_id=current_task.request.id, + task_name=current_task.name, + worker_hostname=current_task.request.hostname, + task_retries=current_task.request.retries, + transcript_id=transcript_id, + ) + try: - return await func(transcript=transcript, logger=tlogger, **kwargs) + result = await func(transcript=transcript, logger=tlogger, **kwargs) + return result except Exception as exc: - tlogger.error("Pipeline error", exc_info=exc) + tlogger.error("Pipeline error", function_name=func.__name__, exc_info=exc) raise return wrapper diff --git a/server/reflector/processors/summary/summary_builder.py b/server/reflector/processors/summary/summary_builder.py index 9120f0a7..195eda23 100644 --- a/server/reflector/processors/summary/summary_builder.py +++ b/server/reflector/processors/summary/summary_builder.py @@ -5,153 +5,186 @@ This script is used to generate a summary of a meeting notes transcript. """ import asyncio -import json -import re import sys from datetime import datetime from enum import Enum -from functools import partial +from textwrap import dedent +from typing import Type, TypeVar -import jsonschema import structlog +from llama_index.core import Settings +from llama_index.core.output_parsers import PydanticOutputParser +from llama_index.core.program import LLMTextCompletionProgram +from llama_index.core.response_synthesizers import TreeSummarize +from llama_index.llms.openai_like import OpenAILike +from pydantic import BaseModel, Field from reflector.llm.base import LLM -from transformers import AutoTokenizer +from reflector.llm.openai_llm import OpenAILLM +from reflector.settings import settings -JSON_SCHEMA_LIST_STRING = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "items": {"type": "string"}, -} +T = TypeVar("T", bound=BaseModel) -JSON_SCHEMA_ACTION_ITEMS = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "items": { - "type": "object", - "properties": { - "content": {"type": "string"}, - "assigned_to": { - "type": "array", - "items": {"type": "string", "minLength": 1}, - }, - }, - "required": ["content"], - }, -} +PARTICIPANTS_PROMPT = dedent( + """ + Identify all participants in this conversation. + Distinguish between people who actually spoke in the transcript and those who were only mentioned. + Each participant should only be listed once. + Do not include company names, only people's names. + """ +).strip() -JSON_SCHEMA_DECISIONS_OR_OPEN_QUESTIONS = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "items": { - "type": "object", - "properties": {"content": {"type": "string"}}, - "required": ["content"], - }, -} +TRANSCRIPTION_TYPE_PROMPT = dedent( + """ + Analyze the transcript to determine if it is a meeting, podcast, or interview. + A meeting typically involves severals participants engaging in discussions, + making decisions, and planning actions. A podcast often includes hosts + discussing topics or interviewing guests for an audience in a structured format. + An interview generally features one or more interviewer questioning one or + more interviewees, often for hiring, research, or journalism. Deliver your + classification with a confidence score and reasoning. + """ +).strip() -JSON_SCHEMA_TRANSCRIPTION_TYPE = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "transcription_type": {"type": "string", "enum": ["meeting", "podcast"]}, - }, - "required": ["transcription_type"], -} +SUBJECTS_PROMPT = dedent( + """ + What are the main / high level topic of the meeting. + Do not include direct quotes or unnecessary details. + Be concise and focused on the main ideas. + A subject briefly mentioned should not be included. + There should be maximum 6 subjects. + Do not write complete narrative sentences for the subject, + you must write a concise subject using noun phrases. + """ +).strip() +DETAILED_SUBJECT_PROMPT_TEMPLATE = dedent( + """ + Get me information about the topic "{subject}" -class ItemType(Enum): - ACTION_ITEM = "action-item" - DECISION = "decision" - OPEN_QUESTION = "open-question" + # RESPONSE GUIDELINES + Follow this structured approach to create the topic summary: + - Highlight important arguments, insights, or data presented. + - Outline decisions made. + - Indicate any decisions reached, including any rationale or key factors + that influenced these decisions. + - Detail action items and responsibilities. + - For each decision or unresolved issue, list specific action items agreed + upon, along with assigned individuals or teams responsible for each task. + - Specify deadlines or timelines if mentioned. For each action item, + include any deadlines or timeframes discussed for completion or follow-up. + - Mention unresolved issues or topics needing further discussion, aiding in + planning future meetings or follow-up actions. + - Do not include topic unrelated to {subject}. + + # OUTPUT + Your summary should be clear, concise, and structured, covering all major + points, decisions, and action items from the meeting. It should be easy to + understand for someone not present, providing a comprehensive understanding + of what transpired and what needs to be done next. The summary should not + exceed one page to ensure brevity and focus. + """ +).strip() + +PARAGRAPH_SUMMARY_PROMPT = dedent( + """ + Summarize the mentioned topic in 1 paragraph. + It will be integrated into the final summary, so just for this topic. + """ +).strip() + +RECAP_PROMPT = dedent( + """ + Provide a high-level quick recap of the following meeting, fitting in one paragraph. + Do not include decisions, action items or unresolved issue, just highlight the high moments. + Just dive into the meeting, be concise and do not include unnecessary details. + As we already know it is a meeting, do not start with 'During the meeting' or equivalent. + """ +).strip() + +STRUCTURED_RESPONSE_PROMPT_TEMPLATE = dedent( + """ + Based on the following analysis, provide the information in the requested JSON format: + + Analysis: + {analysis} + + {format_instructions} + """ +).strip() class TranscriptionType(Enum): MEETING = "meeting" PODCAST = "podcast" + INTERVIEW = "interview" -class Messages: - """ - Manage the LLM context for conversational messages, with roles (system, user, assistant). - """ +class TranscriptionTypeResponse(BaseModel): + """Pydantic model for transcription type classification""" - def __init__(self, messages=None, model_name=None, tokenizer=None, logger=None): - self.messages = messages or [] - self.model_name = model_name - self.tokenizer = tokenizer - self.logger = logger + transcription_type: str = Field( + description="The type of transcription - either 'meeting', 'podcast', or 'interview'" + ) + confidence: float = Field( + description="Confidence score between 0 and 1", ge=0.0, le=1.0 + ) + reasoning: str = Field(description="Brief explanation for the classification") - def set_model(self, model): - self.model_name = model - def set_logger(self, logger): - self.logger = logger +class ParticipantInfo(BaseModel): + """Information about a single participant""" - def copy(self): - m = Messages( - self.messages[:], - model_name=self.model_name, - tokenizer=self.tokenizer, - logger=self.logger, - ) - return m + name: str = Field(description="The name of the participant") + is_speaker: bool = Field( + default=True, description="Whether this person spoke in the transcript" + ) - def add_system(self, content: str): - self.add("system", content) - self.print_content("SYSTEM", content) - def add_user(self, content: str): - self.add("user", content) - self.print_content("USER", content) +class ParticipantsResponse(BaseModel): + """Pydantic model for participants identification""" - def add_assistant(self, content: str): - self.add("assistant", content) - self.print_content("ASSISTANT", content) + participants: list[ParticipantInfo] = Field( + description="List of all participants in the conversation" + ) + total_speakers: int = Field(description="Total number of people who spoke") + mentioned_only: list[str] = Field( + default_factory=list, description="Names mentioned but who didn't speak" + ) - def add(self, role: str, content: str): - self.messages.append({"role": role, "content": content}) - def get_tokenizer(self): - if not self.tokenizer: - self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) - return self.tokenizer +class SubjectsResponse(BaseModel): + """Pydantic model for extracted subjects/topics""" - def count_tokens(self): - tokenizer = self.get_tokenizer() - total_tokens = 0 - for message in self.messages: - total_tokens += len(tokenizer.tokenize(message["content"])) - return total_tokens - - def get_tokens_count(self, message): - tokenizer = self.get_tokenizer() - return len(tokenizer.tokenize(message)) - - def print_content(self, role, content): - if not self.logger: - return - for line in content.split("\n"): - self.logger.info(f">> {role}: {line}") + subjects: list[str] = Field( + description="List of main subjects/topics discussed, maximum 6 items", + ) class SummaryBuilder: - def __init__(self, llm, filename: str | None = None, logger=None): + def __init__(self, llm: LLM, filename: str | None = None, logger=None) -> None: self.transcript: str | None = None self.recap: str | None = None - self.summaries: list[dict] = [] + self.summaries: list[dict[str, str]] = [] self.subjects: list[str] = [] - self.items_action: list = [] - self.items_decision: list = [] - self.items_question: list = [] self.transcription_type: TranscriptionType | None = None self.llm_instance: LLM = llm self.model_name: str = llm.model_name self.logger = logger or structlog.get_logger() - self.m = Messages(model_name=self.model_name, logger=self.logger) if filename: self.read_transcript_from_file(filename) - def read_transcript_from_file(self, filename): + Settings.llm = OpenAILike( + model=llm.model_name, + api_base=llm.url, + api_key=llm.api_key, + context_window=settings.SUMMARY_LLM_CONTEXT_SIZE_TOKENS, + is_chat_model=True, + is_function_calling_model=llm.has_structured_output, + temperature=llm.temperature, + max_tokens=llm.max_tokens, + ) + + def read_transcript_from_file(self, filename: str) -> None: """ Load a transcript from a text file. Must be formatted as: @@ -163,461 +196,228 @@ class SummaryBuilder: with open(filename, "r", encoding="utf-8") as f: self.transcript = f.read().strip() - def set_transcript(self, transcript: str): + def set_transcript(self, transcript: str) -> None: assert isinstance(transcript, str) self.transcript = transcript - def set_llm_instance(self, llm): + def set_llm_instance(self, llm: LLM) -> None: self.llm_instance = llm + async def _get_structured_response( + self, prompt: str, output_cls: Type[T], tone_name: str | None = None + ) -> Type[T]: + """Generic function to get structured output from LLM for non-function-calling models.""" + # First, use TreeSummarize to get the response + summarizer = TreeSummarize(verbose=True) + + response = await summarizer.aget_response( + prompt, [self.transcript], tone_name=tone_name + ) + + # Then, use PydanticOutputParser to structure the response + output_parser = PydanticOutputParser(output_cls) + + prompt_template_str = STRUCTURED_RESPONSE_PROMPT_TEMPLATE + + program = LLMTextCompletionProgram.from_defaults( + output_parser=output_parser, + prompt_template_str=prompt_template_str, + verbose=False, + ) + + format_instructions = output_parser.format( + "Please structure the above information in the following JSON format:" + ) + + output = await program.acall( + analysis=str(response), format_instructions=format_instructions + ) + + return output + # ---------------------------------------------------------------------------- # Participants # ---------------------------------------------------------------------------- - async def identify_participants(self): + async def identify_participants(self) -> None: """ - From a transcript, try identify the participants. + From a transcript, try to identify the participants using TreeSummarize with structured output. This might not give the best result without good diarization, but it's a start. They are appended at the end of the transcript, providing more context for the assistant. """ - self.logger.debug("--- identify_participants") + self.logger.debug("--- identify_participants using TreeSummarize with Pydantic") - m = Messages(model_name=self.model_name) - m.add_system( - "You are an advanced note-taking assistant." - "You'll be given a transcript, and identify the participants." - ) - m.add_user( - f"# Transcript\n\n{self.transcript}\n\n" - "---\n\n" - "Please identify the participants in the conversation." - "Each participant should only be listed once, even if they are mentionned multiple times in the conversation." - "Participants are real people who are part of the conversation and the speakers." - "You can put participants that are mentioned by name." - "Do not put company name." - "Ensure that no duplicate names are included." - "Output the result in JSON format following the schema: " - f"\n```json-schema\n{JSON_SCHEMA_LIST_STRING}\n```" - ) - result = await self.llm( - m, - [ - self.validate_json, - partial(self.validate_json_schema, JSON_SCHEMA_LIST_STRING), - ], - ) + participants_prompt = PARTICIPANTS_PROMPT - # augment the transcript with the participants. - participants = self.format_list_md(result) - self.transcript += f"\n\n# Participants\n\n{participants}" + try: + response = await self._get_structured_response( + participants_prompt, + ParticipantsResponse, + tone_name="Participant identifier", + ) + + all_participants = [p.name for p in response.participants] + + self.logger.info( + "Participants analysis complete", + total_speakers=response.total_speakers, + speakers=[p.name for p in response.participants if p.is_speaker], + mentioned_only=response.mentioned_only, + total_identified=len(all_participants) + len(response.mentioned_only), + ) + + unique_participants = list(set(all_participants + response.mentioned_only)) + + if unique_participants: + participants_md = self.format_list_md(unique_participants) + self.transcript += f"\n\n# Participants\n\n{participants_md}" + else: + self.logger.warning("No participants identified in the transcript") + + except Exception as e: + self.logger.error(f"Error in participant identification: {e}") + self.logger.warning( + "Failed to identify participants, continuing without them" + ) # ---------------------------------------------------------------------------- # Transcription identification # ---------------------------------------------------------------------------- - async def identify_transcription_type(self): + async def identify_transcription_type(self) -> None: """ - Identify the type of transcription: meeting or podcast. + Identify the type of transcription: meeting or podcast using TreeSummarizer with structured output. """ - self.logger.debug("--- identify transcription type") - - m = Messages(model_name=self.model_name, logger=self.logger) - m.add_system( - "You are an advanced assistant specialize to recognize the type of an audio transcription." - "It could be a meeting or a podcast." - ) - m.add_user( - f"# Transcript\n\n{self.transcript}\n\n" - "---\n\n" - "Please identify the type of transcription (meeting or podcast). " - "Output the result in JSON format following the schema:" - f"\n```json-schema\n{JSON_SCHEMA_TRANSCRIPTION_TYPE}\n```" - ) - result = await self.llm( - m, - [ - self.validate_json, - partial(self.validate_json_schema, JSON_SCHEMA_TRANSCRIPTION_TYPE), - ], + self.logger.debug( + "--- identify transcription type using TreeSummarizer with Pydantic" ) - transcription_type = result["transcription_type"] - self.transcription_type = TranscriptionType(transcription_type) + transcription_type_prompt = TRANSCRIPTION_TYPE_PROMPT - # ---------------------------------------------------------------------------- - # Items - # ---------------------------------------------------------------------------- - - async def generate_items( - self, - search_action=False, - search_decision=False, - search_open_question=False, - ): - """ - Build a list of item about action, decision or question - """ - # require key subjects - if not self.subjects or not self.summaries: - await self.generate_summary() - - self.logger.debug("--- items") - - self.items_action = [] - self.items_decision = [] - self.items_question = [] - - item_types = [] - if search_action: - item_types.append(ItemType.ACTION_ITEM) - if search_decision: - item_types.append(ItemType.DECISION) - if search_open_question: - item_types.append(ItemType.OPEN_QUESTION) - - ## Version asking everything in one go - for item_type in item_types: - if item_type == ItemType.ACTION_ITEM: - json_schema = JSON_SCHEMA_ACTION_ITEMS - items = self.items_action - prompt_definition = ( - "An action item is a specific, actionable task designed to achieve a concrete outcome;" - "An action item scope is narrow, focused on short-term execution; " - "An action item is generally assigned to a specific person or team. " - "An action item is NOT a decision, a question, or a general topic. " - "For example: 'Gary, please send the report by Friday.' is an action item." - "But: 'I though Gary was here today. Anyway, somebody need to do an analysis.' is not an action item." - "The field assigned_to must contain a valid participant or person mentionned in the transcript." - ) - - elif item_type == ItemType.DECISION: - json_schema = JSON_SCHEMA_DECISIONS_OR_OPEN_QUESTIONS - items = self.items_decision - prompt_definition = ( - "A decision defines a broad or strategic direction or course of action;" - "It's more about setting the framework, high-level goals, or vision for what needs to happen;" - "A decision scope often affect multiple areas of the organization, and it's more about long-term impact." - ) - - elif item_type == ItemType.OPEN_QUESTION: - json_schema = JSON_SCHEMA_DECISIONS_OR_OPEN_QUESTIONS - items = self.items_question - prompt_definition = "" - - await self.build_items_type( - items, item_type, json_schema, prompt_definition + try: + response = await self._get_structured_response( + transcription_type_prompt, + TranscriptionTypeResponse, + tone_name="Transcription type classifier", ) - async def build_items_type( - self, - items: list, - item_type: ItemType, - json_schema: dict, - prompt_definition: str, - ): - m = Messages(model_name=self.model_name, logger=self.logger) - m.add_system( - "You are an advanced note-taking assistant." - f"You'll be given a transcript, and identify {item_type}." - + prompt_definition - ) - - if item_type in (ItemType.ACTION_ITEM, ItemType.DECISION): - # for both action_items and decision, break down per subject - for subject in self.subjects: - # find the summary of the subject - summary = "" - for entry in self.summaries: - if entry["subject"] == subject: - summary = entry["summary"] - break - - m2 = m.copy() - m2.add_user( - f"# Transcript\n\n{self.transcript}\n\n" - f"# Main subjects\n\n{self.format_list_md(self.subjects)}\n\n" - f"# Summary of {subject}\n\n{summary}\n\n" - "---\n\n" - f'What are the {item_type.value} only related to the main subject "{subject}" ? ' - f"Make sure the {item_type.value} do not overlap with other subjects. " - "To recall: " - + prompt_definition - + "If there are none, just return an empty list. " - "The result must be a list following this format: " - f"\n```json-schema\n{json_schema}\n```" - ) - result = await self.llm( - m2, - [ - self.validate_json, - partial(self.validate_json_schema, json_schema), - ], - ) - if not result: - self.logger.error( - f"Error: unable to identify {item_type.value} for {subject}" - ) - continue - else: - items.extend(result) - - # and for action-items and decision, we try do deduplicate - items = await self.deduplicate_items(item_type, items) - - elif item_type == ItemType.OPEN_QUESTION: - m2 = m.copy() - m2.add_user( - f"# Transcript\n\n{self.transcript}\n\n" - "---\n\n" - f"Identify the open questions unanswered during the meeting." - "If there are none, just return an empty list. " - "The result must be a list following this format:" - f"\n```json-schema\n{json_schema}\n```" + self.logger.info( + f"Transcription type identified: {response.transcription_type} " + f"(confidence: {response.confidence:.2f})" ) - result = await self.llm( - m2, - [ - self.validate_json, - partial(self.validate_json_schema, json_schema), - ], - ) - if not result: - self.logger.error("Error: unable to identify open questions") + self.logger.debug(f"Reasoning: {response.reasoning}") + + if response.transcription_type.lower() == "meeting": + self.transcription_type = TranscriptionType.MEETING + elif response.transcription_type.lower() == "podcast": + self.transcription_type = TranscriptionType.PODCAST + elif response.transcription_type.lower() == "interview": + self.transcription_type = TranscriptionType.INTERVIEW else: - items.extend(result) + self.logger.warning( + f"Unexpected transcription type: {response.transcription_type}, " + f"defaulting to meeting" + ) + self.transcription_type = TranscriptionType.MEETING - async def deduplicate_items(self, item_type: ItemType, items: list): - """ - Deduplicate items based on the transcript and the list of items gathered for all subjects - """ - m = Messages(model_name=self.model_name, logger=self.logger) - if item_type == ItemType.ACTION_ITEM: - json_schema = JSON_SCHEMA_ACTION_ITEMS - else: - json_schema = JSON_SCHEMA_DECISIONS_OR_OPEN_QUESTIONS - - title = item_type.value.replace("_", " ") - - m.add_system( - "You are an advanced assistant that correlate and consolidate information. " - f"Another agent found a list of {title}. However the list may be redundant. " - f"Your first step will be to give information about how theses {title} overlap. " - "In a second time, the user will ask you to consolidate according to your finding. " - f"Keep in mind that the same {title} can be written in different ways. " - ) - - md_items = [] - for item in items: - assigned_to = ", ".join(item.get("assigned_to", [])) - content = item["content"] - if assigned_to: - text = f"- **{assigned_to}**: {content}" - else: - text = f"- {content}" - md_items.append(text) - - md_text = "\n".join(md_items) - - m.add_user( - f"# Transcript\n\n{self.transcript}\n\n" - f"# {title}\n\n{md_text}\n\n--\n\n" - f"Here is a list of {title} identified by another agent. " - f"Some of the {title} seem to overlap or be redundant. " - "How can you effectively group or merge them into more consise list?" - ) - - await self.llm(m) - - m.add_user( - f"Consolidate the list of {title} according to your finding. " - f"The list must be shorter or equal than the original list. " - "Give the result using the following JSON schema:" - f"\n```json-schema\n{json_schema}\n```" - ) - - result = await self.llm( - m, - [ - self.validate_json, - partial(self.validate_json_schema, json_schema), - ], - ) - return result + except Exception as e: + self.logger.error(f"Error in transcription type identification: {e}") + self.transcription_type = TranscriptionType.MEETING # ---------------------------------------------------------------------------- # Summary # ---------------------------------------------------------------------------- - async def generate_summary(self, only_subjects=False): - """ - This is the main function to build the summary. + async def extract_subjects(self) -> None: + """Extract main subjects/topics from the transcript.""" + self.logger.info("--- extract main subjects using TreeSummarize") - It actually share the context with the different steps (subjects, quick recap) - which make it more sense to keep it in one function. + subjects_prompt = SUBJECTS_PROMPT - The process is: - - Extract the main subjects - - Generate a summary for all the main subjects - - Generate a quick recap - """ - self.logger.debug("--- extract main subjects") - - m = Messages(model_name=self.model_name, logger=self.logger) - m.add_system( - ( - "You are an advanced transcription summarization assistant." - "Your task is to summarize discussions by focusing only on the main ideas contributed by participants." - # Prevent generating another transcription - "Exclude direct quotes and unnecessary details." - # Do not mention others participants just because they didn't contributed - "Only include participant names if they actively contribute to the subject." - # Prevent generation of summary with "no others participants contributed" etc - "Keep summaries concise and focused on main subjects without adding conclusions such as 'no other participant contributed'. " - # Avoid: In the discussion, they talked about... - "Do not include contextual preface. " - # Prevention to have too long summary - "Summary should fit in a single paragraph. " - # Using other pronouns that the participants or the group - 'Mention the participants or the group using "they".' - # Avoid finishing the summary with "No conclusions were added by the summarizer" - "Do not mention conclusion if there is no conclusion" + try: + response = await self._get_structured_response( + subjects_prompt, + SubjectsResponse, + tone_name="Meeting assistant that talk only as list item", ) - ) - m.add_user( - f"# Transcript\n\n{self.transcript}\n\n" - + ( - "\n\n---\n\n" - "What are the main/key subjects discussed in this transcript ? " - "Do not include direct quotes or unnecessary details. " - "Be concise and focused on the main ideas. " - "A subject briefly mentionned should not be included. " - f"The result must follow the JSON schema: {JSON_SCHEMA_LIST_STRING}. " - ), - ) - # Note: Asking the model the key subject sometimes generate a lot of subjects - # We need to consolidate them to avoid redundancy when it happen. - m2 = m.copy() + self.subjects = response.subjects + self.logger.info(f"Extracted subjects: {self.subjects}") - subjects = await self.llm( - m2, + except Exception as e: + self.logger.error(f"Error extracting subjects: {e}") + self.subjects = [] + + async def generate_subject_summaries(self) -> None: + """Generate detailed summaries for each extracted subject.""" + assert self.transcript is not None + summarizer = TreeSummarize(verbose=False) + summaries = [] + + for subject in self.subjects: + detailed_prompt = DETAILED_SUBJECT_PROMPT_TEMPLATE.format(subject=subject) + + detailed_response = await summarizer.aget_response( + detailed_prompt, [self.transcript], tone_name="Topic assistant" + ) + + paragraph_prompt = PARAGRAPH_SUMMARY_PROMPT + + paragraph_response = await summarizer.aget_response( + paragraph_prompt, [str(detailed_response)], tone_name="Topic summarizer" + ) + + summaries.append({"subject": subject, "summary": str(paragraph_response)}) + self.logger.debug(f"Summary for {subject}: {paragraph_response}") + + self.summaries = summaries + + async def generate_recap(self) -> None: + """Generate a quick recap from the subject summaries.""" + summarizer = TreeSummarize(verbose=True) + + summaries_text = "\n\n".join( [ - self.validate_json, - partial(self.validate_json_schema, JSON_SCHEMA_LIST_STRING), - ], + f"{summary['subject']}: {summary['summary']}" + for summary in self.summaries + ] ) - if subjects: - self.subjects = subjects - if len(self.subjects) > 6: - # the model may bugged and generate a lot of subjects - m.add_user( - "No that may be too much. " - "Consolidate the subjects and remove any redundancy. " - "Keep the most importants. " - "Remember that the same subject can be written in different ways. " - "Do not consolidate subjects if they are worth keeping separate due to their importance or sensitivity. " - f"The result must follow the JSON schema: {JSON_SCHEMA_LIST_STRING}. " - ) - subjects = await self.llm( - m2, - [ - self.validate_json, - partial(self.validate_json_schema, JSON_SCHEMA_LIST_STRING), - ], - ) - if subjects: - self.subjects = subjects + recap_prompt = RECAP_PROMPT - # Write manually the assistants response to remove the redundancy if case somethign happen - m.add_assistant(self.format_list_md(self.subjects)) + recap_response = await summarizer.aget_response( + recap_prompt, [summaries_text], tone_name="Recap summarizer" + ) + + self.recap = str(recap_response) + self.logger.info(f"Quick recap: {self.recap}") + + async def generate_summary(self, only_subjects: bool = False) -> None: + """ + Generate summary by extracting subjects, creating summaries for each, and generating a recap. + """ + await self.extract_subjects() if only_subjects: return - summaries = [] - - # ---------------------------------------------------------------------------- - # Summarize per subject - # ---------------------------------------------------------------------------- - - m2 = m.copy() - for subject in subjects: - m2 = m # .copy() - prompt = ( - f"Summarize the main subject: '{subject}'. " - "Include only the main ideas contributed by participants. " - "Do not include direct quotes or unnecessary details. " - "Avoid introducing or restating the subject. " - "Focus on the core arguments without minor details. " - "Summarize in few sentences. " - ) - m2.add_user(prompt) - - summary = await self.llm(m2) - summaries.append( - { - "subject": subject, - "summary": summary, - } - ) - - self.summaries = summaries - - # ---------------------------------------------------------------------------- - # Quick recap - # ---------------------------------------------------------------------------- - - m3 = m # .copy() - m3.add_user( - "Provide a quick recap of the meeting, that fit into a small to medium paragraph." - ) - recap = await self.llm(m3) - self.recap = recap + await self.generate_subject_summaries() + await self.generate_recap() # ---------------------------------------------------------------------------- # Markdown # ---------------------------------------------------------------------------- - def as_markdown(self): - lines = [] + def as_markdown(self) -> str: + lines: list[str] = [] if self.recap: lines.append("# Quick recap") lines.append("") lines.append(self.recap) lines.append("") - if self.items_action: - lines.append("# Actions") - lines.append("") - for action in self.items_action: - assigned_to = ", ".join(action.get("assigned_to", [])) - content = action.get("content", "") - line = "-" - if assigned_to: - line += f" **{assigned_to}**:" - line += f" {content}" - lines.append(line) - lines.append("") - - if self.items_decision: - lines.append("") - lines.append("# Decisions") - for decision in self.items_decision: - content = decision.get("content", "") - lines.append(f"- {content}") - lines.append("") - - if self.items_question: - lines.append("") - lines.append("# Open questions") - for question in self.items_question: - content = question.get("content", "") - lines.append(f"- {content}") - lines.append("") - if self.summaries: lines.append("# Summary") lines.append("") @@ -625,122 +425,10 @@ class SummaryBuilder: lines.append(f"**{summary['subject']}**") lines.append(summary["summary"]) lines.append("") - lines.append("") return "\n".join(lines) - # ---------------------------------------------------------------------------- - # Validation API - # ---------------------------------------------------------------------------- - - def validate_list(self, result: str): - # does the list match 1. xxx\n2. xxx... ? - lines = result.split("\n") - firstline = lines[0].strip() - - if re.match(r"1\.\s.+", firstline): - # strip the numbers of the list - lines = [re.sub(r"^\d+\.\s", "", line).strip() for line in lines] - return lines - - if re.match(r"- ", firstline): - # strip the list markers - lines = [re.sub(r"^- ", "", line).strip() for line in lines] - return lines - - return result.split("\n") - - def validate_next_steps(self, result: str): - if result.lower().startswith("no"): - return None - - return result - - def validate_json(self, result): - # if result startswith ```json, strip begin/end - result = result.strip() - - # grab the json between ```json and ``` using regex if exist - match = re.search(r"```json(.*?)```", result, re.DOTALL) - if match: - result = match.group(1).strip() - - # try parsing json - try: - return json.loads(result) - except Exception: - self.logger.error(f"Unable to parse JSON: {result}") - return result.split("\n") - - def validate_json_schema(self, schema, result): - try: - jsonschema.validate(instance=result, schema=schema) - except Exception as e: - self.logger.exception(e) - raise - return result - - # ---------------------------------------------------------------------------- - # LLM API - # ---------------------------------------------------------------------------- - - async def llm( - self, - messages: Messages, - validate_func=None, - auto_append=True, - max_retries=3, - ): - """ - Perform a completion using the LLM model. - Automatically validate the result and retry maximum `max_retries` times if an error occurs. - Append the result to the message context if `auto_append` is True. - """ - - self.logger.debug( - f"--- messages ({len(messages.messages)} messages, " - f"{messages.count_tokens()} tokens)" - ) - - if validate_func and not isinstance(validate_func, list): - validate_func = [validate_func] - - while max_retries > 0: - try: - # do the llm completion - result = result_validated = await self.completion( - messages.messages, - logger=self.logger, - ) - self.logger.debug(f"--- result\n{result_validated}") - - # validate the result using the provided functions - if validate_func: - for func in validate_func: - result_validated = func(result_validated) - - self.logger.debug(f"--- validated\n{result_validated}") - - # add the result to the message context as an assistant response - # only if the response was not guided - if auto_append: - messages.add_assistant(result) - return result_validated - except Exception as e: - self.logger.error(f"Error: {e}") - max_retries -= 1 - - async def completion(self, messages: list, **kwargs) -> str: - """ - Complete the messages using the LLM model. - The request assume a /v1/chat/completions compatible endpoint. - `messages` are a list of dict with `role` and `content` keys. - """ - - result = await self.llm_instance.completion(messages=messages, **kwargs) - return result["choices"][0]["message"]["content"] - - def format_list_md(self, data: list): + def format_list_md(self, data: list[str]) -> str: return "\n".join([f"- {item}" for item in data]) @@ -777,12 +465,6 @@ if __name__ == "__main__": help="Generate a summary", ) - parser.add_argument( - "--items", - help="Generate a list of action items", - action="store_true", - ) - parser.add_argument( "--subjects", help="Generate a list of subjects", @@ -799,7 +481,8 @@ if __name__ == "__main__": async def main(): # build the summary - llm = LLM.get_instance(model_name="NousResearch/Hermes-3-Llama-3.1-8B") + + llm = OpenAILLM(config_prefix="SUMMARY", settings=settings) sm = SummaryBuilder(llm=llm, filename=args.transcript) if args.subjects: @@ -817,24 +500,14 @@ if __name__ == "__main__": await sm.identify_participants() sys.exit(0) - # if no summary or items is asked, ask for everything - if not args.summary and not args.items and not args.subjects: + # if no summary is asked, ask for everything + if not args.summary and not args.subjects: args.summary = True - args.items = True - - await sm.identify_participants() - await sm.identify_transcription_type() if args.summary: await sm.generate_summary() - if sm.transcription_type == TranscriptionType.MEETING: - if args.items: - await sm.generate_items( - search_action=True, - search_decision=True, - search_open_question=True, - ) + # Note: action items generation has been removed print("") print("-" * 80) diff --git a/server/reflector/processors/transcript_final_summary.py b/server/reflector/processors/transcript_final_summary.py index daa52e56..9cfc4a00 100644 --- a/server/reflector/processors/transcript_final_summary.py +++ b/server/reflector/processors/transcript_final_summary.py @@ -1,7 +1,8 @@ -from reflector.llm import LLM +from reflector.llm.openai_llm import OpenAILLM from reflector.processors.base import Processor from reflector.processors.summary.summary_builder import SummaryBuilder from reflector.processors.types import FinalLongSummary, FinalShortSummary, TitleSummary +from reflector.settings import settings class TranscriptFinalSummaryProcessor(Processor): @@ -16,14 +17,14 @@ class TranscriptFinalSummaryProcessor(Processor): super().__init__(**kwargs) self.transcript = transcript self.chunks: list[TitleSummary] = [] - self.llm = LLM.get_instance(model_name="NousResearch/Hermes-3-Llama-3.1-8B") + self.llm = OpenAILLM(config_prefix="SUMMARY", settings=settings) self.builder = None async def _push(self, data: TitleSummary): self.chunks.append(data) async def get_summary_builder(self, text) -> SummaryBuilder: - builder = SummaryBuilder(self.llm) + builder = SummaryBuilder(self.llm, logger=self.logger) builder.set_transcript(text) await builder.identify_participants() await builder.generate_summary() diff --git a/server/reflector/processors/transcript_final_title.py b/server/reflector/processors/transcript_final_title.py index 0a8aead8..4b486c08 100644 --- a/server/reflector/processors/transcript_final_title.py +++ b/server/reflector/processors/transcript_final_title.py @@ -49,7 +49,7 @@ class TranscriptFinalTitleProcessor(Processor): gen_cfg=self.params.gen_cfg, logger=self.logger, ) - accumulated_titles += title_result["summary"] + accumulated_titles += title_result["title"] return await self.get_title(accumulated_titles) diff --git a/server/reflector/processors/transcript_translator.py b/server/reflector/processors/transcript_translator.py index fbb07164..aee55580 100644 --- a/server/reflector/processors/transcript_translator.py +++ b/server/reflector/processors/transcript_translator.py @@ -52,6 +52,7 @@ class TranscriptTranslatorProcessor(Processor): params=json_payload, timeout=self.timeout, follow_redirects=True, + logger=self.logger, ) response.raise_for_status() result = response.json()["text"] diff --git a/server/reflector/settings.py b/server/reflector/settings.py index 5db27289..d300d449 100644 --- a/server/reflector/settings.py +++ b/server/reflector/settings.py @@ -82,6 +82,12 @@ class Settings(BaseSettings): # LLM Modal configuration LLM_MODAL_API_KEY: str | None = None + # per-task cases + SUMMARY_MODEL: str = "monadical/private/smart" + SUMMARY_LLM_URL: str | None = None + SUMMARY_LLM_API_KEY: str | None = None + SUMMARY_LLM_CONTEXT_SIZE_TOKENS: int = 16000 + # Diarization DIARIZATION_ENABLED: bool = True DIARIZATION_BACKEND: str = "modal" diff --git a/server/reflector/stream_client.py b/server/reflector/stream_client.py index b3e4d966..99534609 100644 --- a/server/reflector/stream_client.py +++ b/server/reflector/stream_client.py @@ -126,7 +126,7 @@ class StreamClient: answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) await pc.setRemoteDescription(answer) - self.reader = self.worker(f'{"worker"}', self.queue) + self.reader = self.worker(f"{'worker'}", self.queue) def get_reader(self): return self.reader diff --git a/server/reflector/tools/exportdb.py b/server/reflector/tools/exportdb.py index dabfcc10..dd257cce 100644 --- a/server/reflector/tools/exportdb.py +++ b/server/reflector/tools/exportdb.py @@ -36,9 +36,13 @@ async def export_db(filename: str) -> None: if entry["event"] == "TRANSCRIPT": yield tid, "event_transcript", idx, "text", entry["data"]["text"] if entry["data"].get("translation") is not None: - yield tid, "event_transcript", idx, "translation", entry[ - "data" - ].get("translation", None) + yield ( + tid, + "event_transcript", + idx, + "translation", + entry["data"].get("translation", None), + ) def export_transcripts(transcripts): for transcript in transcripts: diff --git a/server/reflector/utils/retry.py b/server/reflector/utils/retry.py index 90c0cadf..1e226119 100644 --- a/server/reflector/utils/retry.py +++ b/server/reflector/utils/retry.py @@ -34,6 +34,7 @@ def retry(fn): ), ) retry_ignore_exc_types = kwargs.pop("retry_ignore_exc_types", (Exception,)) + retry_logger = kwargs.pop("logger", logger) result = None last_exception = None @@ -58,17 +59,33 @@ def retry(fn): if result: return result except HTTPStatusError as e: - logger.exception(e) + retry_logger.exception(e) status_code = e.response.status_code - logger.debug(f"HTTP status {status_code} - {e}") + + # Log detailed error information including response body + try: + response_text = e.response.text + response_headers = dict(e.response.headers) + retry_logger.error( + f"HTTP {status_code} error for {e.request.method} {e.request.url}\n" + f"Response headers: {response_headers}\n" + f"Response body: {response_text}" + ) + + except Exception as log_error: + retry_logger.warning( + f"Failed to log detailed error info: {log_error}" + ) + retry_logger.debug(f"HTTP status {status_code} - {e}") + if status_code in retry_httpx_status_stop: message = f"HTTP status {status_code} is in retry_httpx_status_stop" raise RetryHTTPException(message) from e except retry_ignore_exc_types as e: - logger.exception(e) + retry_logger.exception(e) last_exception = e - logger.debug( + retry_logger.debug( f"Retrying {fn_name} - in {retry_backoff_interval:.1f}s " f"({monotonic() - start:.1f}s / {retry_timeout:.1f}s)" ) diff --git a/server/reflector/utils/text_utils.py b/server/reflector/utils/text_utils.py index 1ea5e8d4..da2260c5 100644 --- a/server/reflector/utils/text_utils.py +++ b/server/reflector/utils/text_utils.py @@ -253,9 +253,7 @@ def summarize( LOGGER.info("Breaking transcript into smaller chunks") chunks = chunk_text(transcript_text) - LOGGER.info( - f"Transcript broken into {len(chunks)} " f"chunks of at most 500 words" - ) + LOGGER.info(f"Transcript broken into {len(chunks)} chunks of at most 500 words") LOGGER.info(f"Writing summary text to: {output_file}") with open(output_file, "w") as f: diff --git a/server/tests/conftest.py b/server/tests/conftest.py index f161d028..eb242022 100644 --- a/server/tests/conftest.py +++ b/server/tests/conftest.py @@ -20,17 +20,23 @@ async def setup_database(): @pytest.fixture def dummy_processors(): - with patch( - "reflector.processors.transcript_topic_detector.TranscriptTopicDetectorProcessor.get_topic" - ) as mock_topic, patch( - "reflector.processors.transcript_final_title.TranscriptFinalTitleProcessor.get_title" - ) as mock_title, patch( - "reflector.processors.transcript_final_summary.TranscriptFinalSummaryProcessor.get_long_summary" - ) as mock_long_summary, patch( - "reflector.processors.transcript_final_summary.TranscriptFinalSummaryProcessor.get_short_summary" - ) as mock_short_summary, patch( - "reflector.processors.transcript_translator.TranscriptTranslatorProcessor.get_translation" - ) as mock_translate: + with ( + patch( + "reflector.processors.transcript_topic_detector.TranscriptTopicDetectorProcessor.get_topic" + ) as mock_topic, + patch( + "reflector.processors.transcript_final_title.TranscriptFinalTitleProcessor.get_title" + ) as mock_title, + patch( + "reflector.processors.transcript_final_summary.TranscriptFinalSummaryProcessor.get_long_summary" + ) as mock_long_summary, + patch( + "reflector.processors.transcript_final_summary.TranscriptFinalSummaryProcessor.get_short_summary" + ) as mock_short_summary, + patch( + "reflector.processors.transcript_translator.TranscriptTranslatorProcessor.get_translation" + ) as mock_translate, + ): mock_topic.return_value = {"title": "LLM TITLE", "summary": "LLM SUMMARY"} mock_title.return_value = {"title": "LLM TITLE"} mock_long_summary.return_value = "LLM LONG SUMMARY" diff --git a/server/uv.lock b/server/uv.lock index f1f920eb..23725ca8 100644 --- a/server/uv.lock +++ b/server/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 2 requires-python = ">=3.11, <3.13" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version < '3.12'", +] [[package]] name = "aioboto3" @@ -302,6 +306,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/76/3f1cf0568592f100fd68eb40ed8c491ce95ca3c1378cc2d4c1f6d1bd295d/av-14.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:fbbeef1f421a3461086853d6464ad5526b56ffe8ccb0ab3fd0a1f121dfbf26ad", size = 27925944, upload-time = "2025-05-16T19:10:16.485Z" }, ] +[[package]] +name = "banks" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "griffe" }, + { name = "jinja2" }, + { name = "platformdirs" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/f8/25ef24814f77f3fd7f0fd3bd1ef3749e38a9dbd23502fbb53034de49900c/banks-2.2.0.tar.gz", hash = "sha256:d1446280ce6e00301e3e952dd754fd8cee23ff277d29ed160994a84d0d7ffe62", size = 179052, upload-time = "2025-07-18T16:28:26.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/d6/f9168956276934162ec8d48232f9920f2985ee45aa7602e3c6b4bc203613/banks-2.2.0-py3-none-any.whl", hash = "sha256:963cd5c85a587b122abde4f4064078def35c50c688c1b9d36f43c92503854e7d", size = 29244, upload-time = "2025-07-18T16:28:27.835Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" }, +] + [[package]] name = "billiard" version = "4.2.1" @@ -650,6 +683,19 @@ asyncpg = [ { name = "asyncpg" }, ] +[[package]] +name = "dataclasses-json" +version = "0.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow" }, + { name = "typing-inspect" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, +] + [[package]] name = "debugpy" version = "1.8.15" @@ -667,6 +713,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/d5/98748d9860e767a1248b5e31ffa7ce8cb7006e97bf8abbf3d891d0a8ba4e/debugpy-1.8.15-py2.py3-none-any.whl", hash = "sha256:bce2e6c5ff4f2e00b98d45e7e01a49c7b489ff6df5f12d881c67d2f1ac635f3d", size = 5282697, upload-time = "2025-07-15T16:44:07.996Z" }, ] +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "deprecated" +version = "1.2.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" }, +] + +[[package]] +name = "dirtyjson" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/04/d24f6e645ad82ba0ef092fa17d9ef7a21953781663648a01c9371d9e8e98/dirtyjson-1.0.8.tar.gz", hash = "sha256:90ca4a18f3ff30ce849d100dcf4a003953c79d3a2348ef056f1d9c22231a25fd", size = 30782, upload-time = "2022-11-28T23:32:33.319Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/69/1bcf70f81de1b4a9f21b3a62ec0c83bdff991c88d6cc2267d02408457e88/dirtyjson-1.0.8-py3-none-any.whl", hash = "sha256:125e27248435a58acace26d5c2c4c11a1c0de0a9c5124c5a94ba78e517d74f53", size = 25197, upload-time = "2022-11-28T23:32:31.219Z" }, +] + [[package]] name = "distro" version = "1.9.0" @@ -826,6 +902,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, ] +[[package]] +name = "filetype" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, +] + [[package]] name = "flatbuffers" version = "25.2.10" @@ -933,6 +1018,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817, upload-time = "2025-06-05T16:29:49.244Z" }, ] +[[package]] +name = "griffe" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/72/10c5799440ce6f3001b7913988b50a99d7b156da71fe19be06178d5a2dd5/griffe-1.8.0.tar.gz", hash = "sha256:0b4658443858465c13b2de07ff5e15a1032bc889cfafad738a476b8b97bb28d7", size = 401098, upload-time = "2025-07-22T23:45:54.629Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/c4/a839fcc28bebfa72925d9121c4d39398f77f95bcba0cf26c972a0cfb1de7/griffe-1.8.0-py3-none-any.whl", hash = "sha256:110faa744b2c5c84dd432f4fa9aa3b14805dd9519777dd55e8db214320593b02", size = 132487, upload-time = "2025-07-22T23:45:52.778Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -1254,6 +1351,289 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/1e/408fd10217eac0e43aea0604be22b4851a09e03d761d44d4ea12089dd70e/levenshtein-0.27.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7987ef006a3cf56a4532bd4c90c2d3b7b4ca9ad3bf8ae1ee5713c4a3bdfda913", size = 98045, upload-time = "2025-03-02T19:44:44.527Z" }, ] +[[package]] +name = "llama-cloud" +version = "0.1.32" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/a4/1eed89c7820e273f74e80bc43ac7dd46953ecdc9e3dc5a1e30210beb400e/llama_cloud-0.1.32.tar.gz", hash = "sha256:cea98241127311ea91f191c3c006aa6558f01d16f9539ed93b24d716b888f10e", size = 99578, upload-time = "2025-07-08T14:23:41.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/54/08fc9ec16b483e57803309645c239ca72100d6e821849aa711971737ab17/llama_cloud-0.1.32-py3-none-any.whl", hash = "sha256:c42b2d5fb24acc8595bcc3626fb84c872909a16ab6d6879a1cb1101b21c238bd", size = 284617, upload-time = "2025-07-08T14:23:39.892Z" }, +] + +[[package]] +name = "llama-cloud-services" +version = "0.6.43" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "llama-cloud" }, + { name = "llama-index-core" }, + { name = "platformdirs" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "tenacity" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/30/82274bb0fc17430162a4f88f7ca0d25fd249767271d37af24b4a3e4cfcf7/llama_cloud_services-0.6.43.tar.gz", hash = "sha256:fa6be33bf54d467cace809efee8c2aeeb9de74ce66708513d37b40d738d3350f", size = 35169, upload-time = "2025-07-08T18:18:26.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/c3/1e82bd37f16e6d99988e9b53829303d3124504341862721f24c9b6f4f614/llama_cloud_services-0.6.43-py3-none-any.whl", hash = "sha256:2349195f501ba9151ea3ab384d20cae8b4dc4f335f60bd17607332626bdfa2e4", size = 40382, upload-time = "2025-07-08T18:18:25.517Z" }, +] + +[[package]] +name = "llama-index" +version = "0.12.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-agent-openai" }, + { name = "llama-index-cli" }, + { name = "llama-index-core" }, + { name = "llama-index-embeddings-openai" }, + { name = "llama-index-indices-managed-llama-cloud" }, + { name = "llama-index-llms-openai" }, + { name = "llama-index-multi-modal-llms-openai" }, + { name = "llama-index-program-openai" }, + { name = "llama-index-question-gen-openai" }, + { name = "llama-index-readers-file" }, + { name = "llama-index-readers-llama-parse" }, + { name = "nltk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/33/496f90bc77536c89b0b1266063977d99cf38e6a3458fd62a88c846f2c4f2/llama_index-0.12.52.tar.gz", hash = "sha256:3a81fa4fbf1a36e30502d2fb7da26d53bc1a1ab02db1db12e62f06bb014d5ad9", size = 8092, upload-time = "2025-07-23T18:11:59.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/ca/b1bb3edca7140b8d9e8957c95c6c59f2596071e89d5a10b0814976de9450/llama_index-0.12.52-py3-none-any.whl", hash = "sha256:21e05e5a02b3601e18358eeed8748384eac8d35d384fdcbe16d03f0ffb09ea61", size = 7090, upload-time = "2025-07-23T18:11:57.548Z" }, +] + +[[package]] +name = "llama-index-agent-openai" +version = "0.4.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "llama-index-llms-openai" }, + { name = "openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/94/69decc46d11e954c6a8c64999cc237af5932d116eeb7a06515856641a6d4/llama_index_agent_openai-0.4.12.tar.gz", hash = "sha256:d2fe53feb69cfe45752edb7328bf0d25f6a9071b3c056787e661b93e5b748a28", size = 12443, upload-time = "2025-06-29T00:52:03.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/f5/857ea1c136f422234e298e868af74094a71bf98687be40a365ad6551a660/llama_index_agent_openai-0.4.12-py3-none-any.whl", hash = "sha256:6dbb6276b2e5330032a726b28d5eef5140825f36d72d472b231f08ad3af99665", size = 14704, upload-time = "2025-06-29T00:52:02.528Z" }, +] + +[[package]] +name = "llama-index-cli" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "llama-index-embeddings-openai" }, + { name = "llama-index-llms-openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/44/6acba0b8425d15682def89a4dbba68c782fd74ce6e74a4fa48beb08632f6/llama_index_cli-0.4.4.tar.gz", hash = "sha256:c3af0cf1e2a7e5ef44d0bae5aa8e8872b54c5dd6b731afbae9f13ffeb4997be0", size = 25308, upload-time = "2025-07-07T05:17:40.556Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/21/89989b7fa8ce4b9bc6f0f7326ae7f959a887c1281f007a718eafd6ef614f/llama_index_cli-0.4.4-py3-none-any.whl", hash = "sha256:1070593cf79407054735ab7a23c5a65a26fc18d264661e42ef38fc549b4b7658", size = 28598, upload-time = "2025-07-07T05:17:39.522Z" }, +] + +[[package]] +name = "llama-index-core" +version = "0.12.52.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "aiosqlite" }, + { name = "banks" }, + { name = "dataclasses-json" }, + { name = "deprecated" }, + { name = "dirtyjson" }, + { name = "filetype" }, + { name = "fsspec" }, + { name = "httpx" }, + { name = "llama-index-workflows" }, + { name = "nest-asyncio" }, + { name = "networkx" }, + { name = "nltk" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "platformdirs" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "setuptools" }, + { name = "sqlalchemy", extra = ["asyncio"] }, + { name = "tenacity" }, + { name = "tiktoken" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "typing-inspect" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/3b/3937a1756a02e549a776272371dd6ec3a4541833b2dbb8ef58e61167f9c9/llama_index_core-0.12.52.post1.tar.gz", hash = "sha256:ac6f447271e5ac4c12e1901373ec4b5ac7814ea33bd1ad3c3c8e9ac9771834ab", size = 7279221, upload-time = "2025-07-23T17:32:33.961Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/48/8f6ea9f2a2f5a080166f0a45a252609df32cc1ad626836aaad2424e2c7ec/llama_index_core-0.12.52.post1-py3-none-any.whl", hash = "sha256:3e28d65d238bad8ec5ce372659ae0a3878851c6ba9c9447d6ddb4de138694b1f", size = 7649855, upload-time = "2025-07-23T17:32:28.271Z" }, +] + +[[package]] +name = "llama-index-embeddings-openai" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/02/a2604ef3a167131fdd701888f45f16c8efa6d523d02efe8c4e640238f4ea/llama_index_embeddings_openai-0.3.1.tar.gz", hash = "sha256:1368aad3ce24cbaed23d5ad251343cef1eb7b4a06d6563d6606d59cb347fef20", size = 5492, upload-time = "2024-11-27T16:04:17.017Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/45/ca55b91c4ac1b6251d4099fa44121a6c012129822906cadcc27b8cfb33a4/llama_index_embeddings_openai-0.3.1-py3-none-any.whl", hash = "sha256:f15a3d13da9b6b21b8bd51d337197879a453d1605e625a1c6d45e741756c0290", size = 6177, upload-time = "2024-11-27T16:04:15.981Z" }, +] + +[[package]] +name = "llama-index-indices-managed-llama-cloud" +version = "0.7.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-cloud" }, + { name = "llama-index-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/5a/7663c04257f2763a9b8089653b7e5d2d5aa241dcd8a21f2d0344f69c8ca1/llama_index_indices_managed_llama_cloud-0.7.10.tar.gz", hash = "sha256:53267907e23d8fbcbb97c7a96177a41446de18550ca6030276092e73b45ca880", size = 14769, upload-time = "2025-07-08T18:13:19.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ea/a0aef9ccb09bd6438fd99d7f9c547bc2eac06abc32852b9e70c807585559/llama_index_indices_managed_llama_cloud-0.7.10-py3-none-any.whl", hash = "sha256:f7edcfb8f694cab547cd9324be7835dc97470ce05150d0b8888fa3bf9d2f84a8", size = 16474, upload-time = "2025-07-08T18:13:18.703Z" }, +] + +[[package]] +name = "llama-index-instrumentation" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/57/76123657bf6f175382ceddee9af66507c37d603475cbf0968df8dfea9de2/llama_index_instrumentation-0.3.0.tar.gz", hash = "sha256:77741c1d9861ead080e6f98350625971488d1e046bede91cec9e0ce2f63ea34a", size = 42651, upload-time = "2025-07-17T17:41:20.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/d4/9377a53ea2f9bdd33f5ccff78ac863705657f422bb686cad4896b058ce46/llama_index_instrumentation-0.3.0-py3-none-any.whl", hash = "sha256:edfcd71aedc453dbdb4a7073a1e39ddef6ae2c13601a4cba6f2dfea38f48eeff", size = 15011, upload-time = "2025-07-17T17:41:19.723Z" }, +] + +[[package]] +name = "llama-index-llms-openai" +version = "0.4.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/39/a7ce514fb500951e9edb713ed918a9ffe49f1a76fccfc531a4ec5c7fe15a/llama_index_llms_openai-0.4.7.tar.gz", hash = "sha256:564af8ab39fb3f3adfeae73a59c0dca46c099ab844a28e725eee0c551d4869f8", size = 24251, upload-time = "2025-06-16T03:38:47.175Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/e9/391926dad180ced6bb37a62edddb8483fbecde411239bd5e726841bb77b4/llama_index_llms_openai-0.4.7-py3-none-any.whl", hash = "sha256:3b8d9d3c1bcadc2cff09724de70f074f43eafd5b7048a91247c9a41b7cd6216d", size = 25365, upload-time = "2025-06-16T03:38:45.72Z" }, +] + +[[package]] +name = "llama-index-llms-openai-like" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "llama-index-llms-openai" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/df/807ac6bb9470295769f950562f5f7252cb491166693ee877ff77d9022fbc/llama_index_llms_openai_like-0.4.0.tar.gz", hash = "sha256:15ae1c16b01ba0bfa822d53900f03e35c19ffe47b528958234bf1942a91f587c", size = 4898, upload-time = "2025-05-30T17:47:11.689Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/41/e080871437ec507377126165318f2da6713a4d6dc2767f2444a8bd818791/llama_index_llms_openai_like-0.4.0-py3-none-any.whl", hash = "sha256:52a3cb5ce78049fde5c9926898b90e02bc04e3d23adbc991842e9ff574df9ea1", size = 4593, upload-time = "2025-05-30T17:47:10.456Z" }, +] + +[[package]] +name = "llama-index-multi-modal-llms-openai" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "llama-index-llms-openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/5d/8a7ff14f5ac6844722152ba35ee4e1298b4665a3cf70eaeae6d6df938e37/llama_index_multi_modal_llms_openai-0.5.3.tar.gz", hash = "sha256:b755a8b47d8d2f34b5a3d249af81d9bfb69d3d2cf9ab539d3a42f7bfa3e2391a", size = 3760, upload-time = "2025-07-07T16:22:44.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/e5/bc4ec1f373cd2195e625af483eddc5b05a55f6c6db020746f6bb2c0fadde/llama_index_multi_modal_llms_openai-0.5.3-py3-none-any.whl", hash = "sha256:be6237df8f9caaa257f9beda5317287bbd2ec19473d777a30a34e41a7c5bddf8", size = 3434, upload-time = "2025-07-07T16:22:43.898Z" }, +] + +[[package]] +name = "llama-index-program-openai" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-agent-openai" }, + { name = "llama-index-core" }, + { name = "llama-index-llms-openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/81/9caa34e80adce1adb715ae083a54ad45c8fc0d9aef0f2d80d61c1b805ab6/llama_index_program_openai-0.3.2.tar.gz", hash = "sha256:04c959a2e616489894bd2eeebb99500d6f1c17d588c3da0ddc75ebd3eb7451ee", size = 6301, upload-time = "2025-05-30T23:00:27.872Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/80/d6ac8afafdd38115d61214891c36876e64f429809abff873660fe30862fe/llama_index_program_openai-0.3.2-py3-none-any.whl", hash = "sha256:451829ae53e074e7b47dcc60a9dd155fcf9d1dcbc1754074bdadd6aab4ceb9aa", size = 6129, upload-time = "2025-05-30T23:00:26.64Z" }, +] + +[[package]] +name = "llama-index-question-gen-openai" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "llama-index-llms-openai" }, + { name = "llama-index-program-openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6e/19c5051c81ef5fca597d13c6d41b863535521565b1414ab5ab0e5e8c1297/llama_index_question_gen_openai-0.3.1.tar.gz", hash = "sha256:5e9311b433cc2581ff8a531fa19fb3aa21815baff75aaacdef11760ac9522aa9", size = 4107, upload-time = "2025-05-30T23:00:31.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/2a/652593d0bd24f901776db0d1778a42363ea2656530da18215f413ce4f981/llama_index_question_gen_openai-0.3.1-py3-none-any.whl", hash = "sha256:1ce266f6c8373fc8d884ff83a44dfbacecde2301785db7144872db51b8b99429", size = 3733, upload-time = "2025-05-30T23:00:29.965Z" }, +] + +[[package]] +name = "llama-index-readers-file" +version = "0.4.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "defusedxml" }, + { name = "llama-index-core" }, + { name = "pandas" }, + { name = "pypdf" }, + { name = "striprtf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/13/529412fcd1789607e060168ccac347bc7f012ed98c1e34614d4fb443fe39/llama_index_readers_file-0.4.11.tar.gz", hash = "sha256:1b21cb66d78dd5f60e8716607d9a47ccd81bb39106d459665be1ca7799e9597b", size = 22765, upload-time = "2025-07-07T21:03:57.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/b3/4977330c49538b0252ca014c2ef10352ecc8f14c489ca6d95858bcb0cceb/llama_index_readers_file-0.4.11-py3-none-any.whl", hash = "sha256:e71192d8d6d0bf95131762da15fa205cf6e0cc248c90c76ee04d0fbfe160d464", size = 41046, upload-time = "2025-07-07T21:03:55.877Z" }, +] + +[[package]] +name = "llama-index-readers-llama-parse" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "llama-parse" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/30/4611821286f82ba7b5842295607baa876262db86f88b87d83595eed172bf/llama_index_readers_llama_parse-0.4.0.tar.gz", hash = "sha256:e99ec56f4f8546d7fda1a7c1ae26162fb9acb7ebcac343b5abdb4234b4644e0f", size = 2472, upload-time = "2024-11-18T00:00:08.893Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/4f/e30d4257fe9e4224f5612b77fe99aaceddae411b2e74ca30534491de3e6f/llama_index_readers_llama_parse-0.4.0-py3-none-any.whl", hash = "sha256:574e48386f28d2c86c3f961ca4a4906910312f3400dd0c53014465bfbc6b32bf", size = 2472, upload-time = "2024-11-18T00:00:07.293Z" }, +] + +[[package]] +name = "llama-index-workflows" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-instrumentation" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/9d/9dc7adc10d9976582bf50b074883986cb36b46f2fe45cf60550767300a29/llama_index_workflows-1.2.0.tar.gz", hash = "sha256:f6b19f01a340a1afb1d2fd2285c9dce346e304a3aae519e6103059f5afb2609f", size = 1019113, upload-time = "2025-07-23T18:32:47.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/c1/5190f102a042d36a6a495de27510c2d6e3aca98f892895bfacdcf9109c1d/llama_index_workflows-1.2.0-py3-none-any.whl", hash = "sha256:5722a7ce137e00361025768789e7e77720cd66f855791050183a3c540b6e5b8c", size = 37463, upload-time = "2025-07-23T18:32:46.294Z" }, +] + +[[package]] +name = "llama-parse" +version = "0.6.43" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-cloud-services" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/62/22e3f73a2b33b9db1523573611281010c8258bf1d17408913e8e46bdfe58/llama_parse-0.6.43.tar.gz", hash = "sha256:d88e91c97e37f77b2619111ef43c02b7da61125f821cf77f918996eb48200d78", size = 3536, upload-time = "2025-07-08T18:20:58.786Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/24/8497595be04a8a0209536e9ce70d4132f8f8e001986f4c700414b3777758/llama_parse-0.6.43-py3-none-any.whl", hash = "sha256:fe435309638c4fdec4fec31f97c5031b743c92268962d03b99bd76704f566c32", size = 4944, upload-time = "2025-07-08T18:20:57.089Z" }, +] + [[package]] name = "loguru" version = "0.7.3" @@ -1319,6 +1699,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, ] +[[package]] +name = "marshmallow" +version = "3.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825, upload-time = "2025-02-03T15:32:25.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878, upload-time = "2025-02-03T15:32:22.295Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -1391,6 +1783,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, +] + [[package]] name = "nltk" version = "3.9.1" @@ -1493,6 +1903,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222, upload-time = "2024-09-20T13:08:56.254Z" }, + { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274, upload-time = "2024-09-20T13:08:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836, upload-time = "2024-09-20T19:01:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505, upload-time = "2024-09-20T13:09:01.501Z" }, + { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420, upload-time = "2024-09-20T19:02:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457, upload-time = "2024-09-20T13:09:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166, upload-time = "2024-09-20T13:09:06.917Z" }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893, upload-time = "2024-09-20T13:09:09.655Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475, upload-time = "2024-09-20T13:09:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645, upload-time = "2024-09-20T19:02:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445, upload-time = "2024-09-20T13:09:17.621Z" }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235, upload-time = "2024-09-20T19:02:07.094Z" }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756, upload-time = "2024-09-20T13:09:20.474Z" }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248, upload-time = "2024-09-20T13:09:23.137Z" }, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -1502,6 +1940,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] +[[package]] +name = "pillow" +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, +] + [[package]] name = "platformdirs" version = "4.3.8" @@ -1847,6 +2322,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/28/2659c02301b9500751f8d42f9a6632e1508aa5120de5e43042b8b30f8d5d/pyopenssl-25.1.0-py3-none-any.whl", hash = "sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab", size = 56771, upload-time = "2025-05-17T16:28:29.197Z" }, ] +[[package]] +name = "pypdf" +version = "5.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/28/5a/139b1a3ec3789cc77a7cb9d5d3bc9e97e742e6d03708baeb7719f8ad0827/pypdf-5.8.0.tar.gz", hash = "sha256:f8332f80606913e6f0ce65488a870833c9d99ccdb988c17bb6c166f7c8e140cb", size = 5029494, upload-time = "2025-07-13T12:51:35.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/94/05d0310bfa92c26aa50a9d2dea2c6448a1febfdfcf98fb340a99d48a3078/pypdf-5.8.0-py3-none-any.whl", hash = "sha256:bfe861285cd2f79cceecefde2d46901e4ee992a9f4b42c56548c4a6e9236a0d1", size = 309718, upload-time = "2025-07-13T12:51:33.159Z" }, +] + [[package]] name = "pyreadline3" version = "3.5.4" @@ -2006,6 +2490,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, ] +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + [[package]] name = "pywin32" version = "311" @@ -2133,6 +2626,8 @@ dependencies = [ { name = "faster-whisper" }, { name = "httpx" }, { name = "jsonschema" }, + { name = "llama-index" }, + { name = "llama-index-llms-openai-like" }, { name = "loguru" }, { name = "nltk" }, { name = "openai" }, @@ -2194,6 +2689,8 @@ requires-dist = [ { name = "faster-whisper", specifier = ">=0.10.0" }, { name = "httpx", specifier = ">=0.24.1" }, { name = "jsonschema", specifier = ">=4.23.0" }, + { name = "llama-index", specifier = ">=0.12.52" }, + { name = "llama-index-llms-openai-like", specifier = ">=0.4.0" }, { name = "loguru", specifier = ">=0.7.0" }, { name = "nltk", specifier = ">=3.8.1" }, { name = "openai", specifier = ">=1.59.7" }, @@ -2544,6 +3041,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] +[[package]] +name = "soupsieve" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, +] + [[package]] name = "sqlalchemy" version = "1.4.54" @@ -2565,6 +3071,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/c3/c690d037be57efd3a69cde16a2ef1bd2a905dafe869434d33836de0983d0/SQLAlchemy-1.4.54-cp312-cp312-win_amd64.whl", hash = "sha256:f941aaf15f47f316123e1933f9ea91a6efda73a161a6ab6046d1cde37be62c88", size = 1593827, upload-time = "2024-09-05T17:52:07.454Z" }, ] +[package.optional-dependencies] +asyncio = [ + { name = "greenlet" }, +] + [[package]] name = "stamina" version = "25.1.0" @@ -2590,6 +3101,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/95/38ef0cd7fa11eaba6a99b3c4f5ac948d8bc6ff199aabd327a29cc000840c/starlette-0.47.1-py3-none-any.whl", hash = "sha256:5e11c9f5c7c3f24959edbf2dffdc01bba860228acf657129467d8a7468591527", size = 72747, upload-time = "2025-06-21T04:03:15.705Z" }, ] +[[package]] +name = "striprtf" +version = "0.0.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/20/3d419008265346452d09e5dadfd5d045b64b40d8fc31af40588e6c76997a/striprtf-0.0.26.tar.gz", hash = "sha256:fdb2bba7ac440072d1c41eab50d8d74ae88f60a8b6575c6e2c7805dc462093aa", size = 6258, upload-time = "2023-07-20T14:30:36.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/cf/0fea4f4ba3fc2772ac2419278aa9f6964124d4302117d61bc055758e000c/striprtf-0.0.26-py3-none-any.whl", hash = "sha256:8c8f9d32083cdc2e8bfb149455aa1cc5a4e0a035893bedc75db8b73becb3a1bb", size = 6914, upload-time = "2023-07-20T14:30:35.338Z" }, +] + [[package]] name = "structlog" version = "25.4.0" @@ -2620,6 +3140,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, ] +[[package]] +name = "tiktoken" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/cf/756fedf6981e82897f2d570dd25fa597eb3f4459068ae0572d7e888cfd6f/tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d", size = 35991, upload-time = "2025-02-14T06:03:01.003Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/ae/4613a59a2a48e761c5161237fc850eb470b4bb93696db89da51b79a871f1/tiktoken-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f32cc56168eac4851109e9b5d327637f15fd662aa30dd79f964b7c39fbadd26e", size = 1065987, upload-time = "2025-02-14T06:02:14.174Z" }, + { url = "https://files.pythonhosted.org/packages/3f/86/55d9d1f5b5a7e1164d0f1538a85529b5fcba2b105f92db3622e5d7de6522/tiktoken-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:45556bc41241e5294063508caf901bf92ba52d8ef9222023f83d2483a3055348", size = 1009155, upload-time = "2025-02-14T06:02:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/03/58/01fb6240df083b7c1916d1dcb024e2b761213c95d576e9f780dfb5625a76/tiktoken-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03935988a91d6d3216e2ec7c645afbb3d870b37bcb67ada1943ec48678e7ee33", size = 1142898, upload-time = "2025-02-14T06:02:16.666Z" }, + { url = "https://files.pythonhosted.org/packages/b1/73/41591c525680cd460a6becf56c9b17468d3711b1df242c53d2c7b2183d16/tiktoken-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b3d80aad8d2c6b9238fc1a5524542087c52b860b10cbf952429ffb714bc1136", size = 1197535, upload-time = "2025-02-14T06:02:18.595Z" }, + { url = "https://files.pythonhosted.org/packages/7d/7c/1069f25521c8f01a1a182f362e5c8e0337907fae91b368b7da9c3e39b810/tiktoken-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b2a21133be05dc116b1d0372af051cd2c6aa1d2188250c9b553f9fa49301b336", size = 1259548, upload-time = "2025-02-14T06:02:20.729Z" }, + { url = "https://files.pythonhosted.org/packages/6f/07/c67ad1724b8e14e2b4c8cca04b15da158733ac60136879131db05dda7c30/tiktoken-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:11a20e67fdf58b0e2dea7b8654a288e481bb4fc0289d3ad21291f8d0849915fb", size = 893895, upload-time = "2025-02-14T06:02:22.67Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e5/21ff33ecfa2101c1bb0f9b6df750553bd873b7fb532ce2cb276ff40b197f/tiktoken-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e88f121c1c22b726649ce67c089b90ddda8b9662545a8aeb03cfef15967ddd03", size = 1065073, upload-time = "2025-02-14T06:02:24.768Z" }, + { url = "https://files.pythonhosted.org/packages/8e/03/a95e7b4863ee9ceec1c55983e4cc9558bcfd8f4f80e19c4f8a99642f697d/tiktoken-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6600660f2f72369acb13a57fb3e212434ed38b045fd8cc6cdd74947b4b5d210", size = 1008075, upload-time = "2025-02-14T06:02:26.92Z" }, + { url = "https://files.pythonhosted.org/packages/40/10/1305bb02a561595088235a513ec73e50b32e74364fef4de519da69bc8010/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e811743b5dfa74f4b227927ed86cbc57cad4df859cb3b643be797914e41794", size = 1140754, upload-time = "2025-02-14T06:02:28.124Z" }, + { url = "https://files.pythonhosted.org/packages/1b/40/da42522018ca496432ffd02793c3a72a739ac04c3794a4914570c9bb2925/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99376e1370d59bcf6935c933cb9ba64adc29033b7e73f5f7569f3aad86552b22", size = 1196678, upload-time = "2025-02-14T06:02:29.845Z" }, + { url = "https://files.pythonhosted.org/packages/5c/41/1e59dddaae270ba20187ceb8aa52c75b24ffc09f547233991d5fd822838b/tiktoken-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:badb947c32739fb6ddde173e14885fb3de4d32ab9d8c591cbd013c22b4c31dd2", size = 1259283, upload-time = "2025-02-14T06:02:33.838Z" }, + { url = "https://files.pythonhosted.org/packages/5b/64/b16003419a1d7728d0d8c0d56a4c24325e7b10a21a9dd1fc0f7115c02f0a/tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16", size = 894897, upload-time = "2025-02-14T06:02:36.265Z" }, +] + [[package]] name = "tokenizers" version = "0.21.2" @@ -2731,6 +3275,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] +[[package]] +name = "typing-inspect" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, +] + [[package]] name = "typing-inspection" version = "0.4.1"