diff --git a/8-application-demos/6-kalshi-bet-predictor/.gitignore b/8-application-demos/6-kalshi-bet-predictor/.gitignore new file mode 100644 index 00000000..01d392ed --- /dev/null +++ b/8-application-demos/6-kalshi-bet-predictor/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +.env +.venv/ +.vscode/ \ No newline at end of file diff --git a/8-application-demos/6-kalshi-bet-predictor/README.md b/8-application-demos/6-kalshi-bet-predictor/README.md new file mode 100644 index 00000000..80bd5e78 --- /dev/null +++ b/8-application-demos/6-kalshi-bet-predictor/README.md @@ -0,0 +1,55 @@ +# Kalshi Bet Predictor +This repository contains a set of Python scripts designed to find equivalent binary markets across Kalshi and Polymarket, use an LLM via OpenAI and web search via Exa to generate an independent prediction, and then calculate the trading "edge" on both platforms. + +Core Components +--------------- + +The project is structured around three main scripts: + +**`find_equiv_markets.py`**: A utility script to automatically search Kalshi and Polymarket APIs, use a Sentence Transformer and FAISS vector index to find markets with similar titles (i.e., equivalent questions), and save potential matches to a CSV file for manual review. + +**`analyst.py`** and **`main.py`**: These scripts form the core prediction engine. + +* `analyst.py` handles API interactions with OpenAI (for prediction and question generation) and Exa (for web information retrieval). + +* `main.py` fetches the current prices from both Kalshi and Polymarket, runs the prediction via `BetAnalyst`, and calculates the trading edge against the model's prediction. This logic is intended to be hosted on Cerebrium + +* **`compare.py`**: This script reads the market pairs from the CSV, asynchronously calls the hosted prediction endpoint for each pair, and compiles statistics on the trading edge and which platform offers a better opportunity more frequently. + + + +Prerequisites +------------- + +You will need API keys for the following services: + +* **OpenAI**: For the large language model (`BetAnalyst` class). + +* **Exa**: For semantic search/information retrieval (`BetAnalyst` class). + +* **Cerebrium** (or similar hosting platform): To deploy the `main.py` and `analyst.py` logic as a prediction endpoint. + + +Create a `.env` file in your project root to store your keys: + +``` OPENAI_API_KEY="your_openai_key" EXA_API_KEY="your_exa_key" ``` + +Setup and Installation +---------------------- + +### Dependencies + +Install the required Python packages: + +```bash +pip install -r requirements.txt +``` + + +Workflow +-------- + +1. Host the prediction service by deploying `main.py` and `analyst.py` on Cerebrium to expose a `predict` endpoint that runs the `BetAnalyst` logic. +2. Run `find_equiv_markets.py` to identify equivalent Kalshi and Polymarket markets and export the candidate pairs to a CSV file. +3. Execute `compare.py`, which loads the CSV pairs, calls the hosted prediction endpoint for each pair, and aggregates the edge statistics to highlight the most favorable markets. + diff --git a/8-application-demos/6-kalshi-bet-predictor/analyst.py b/8-application-demos/6-kalshi-bet-predictor/analyst.py new file mode 100644 index 00000000..2cfaa146 --- /dev/null +++ b/8-application-demos/6-kalshi-bet-predictor/analyst.py @@ -0,0 +1,120 @@ +from typing import Tuple +from dotenv import load_dotenv +import os +import json +from exa_py import Exa +from openai import OpenAI +from pydantic import BaseModel + +class BetAnalyst: + def __init__(self, model_name: str = "gpt-5-nano"): + + load_dotenv() + + exa_api_key = os.environ.get("EXA_API_KEY") + openai_api_key = os.environ.get("OPENAI_API_KEY") + + if not exa_api_key: + raise EnvironmentError("Missing EXA_API_KEY in environment variables") + if not openai_api_key: + raise EnvironmentError("Missing OPENAI_API_KEY in environment variables") + + self.exa = Exa(exa_api_key) + self.client = OpenAI(api_key=openai_api_key) + self.model_name = model_name + + print(f"Using model: {model_name}") + + def _generate_response(self, prompt: str, text_format = None): + try: + response = self.client.responses.create( + model=self.model_name, + input=prompt, + ) + + output_text = response.output_text.strip() + print(f"Generated raw response: {output_text}") + + if text_format is not None: + parsed = self.client.responses.parse( + model=self.model_name, + input=[ + { + "role": "user", + "content": output_text + }, + ], + text_format=text_format, + ) + print(f"Parsed structured response: {parsed.output_parsed}") + return parsed.output_parsed + + return output_text + + except Exception as e: + raise RuntimeError(f"Error during API call: {e}") from e + + def get_relevant_questions(self, question: str) -> list[str]: + prompt = ( + "Based on the following question, generate a list of 5 relevant questions " + "that one could search online to gather more information. " + "These questions should yield information that would be helpful to answering " + "the following question in an objective manner.\n\n" + "Your response SHOULD ONLY BE the following lines, in this exact format:\n" + "1. \n" + "2. \n" + "3. \n" + "4. \n" + "5. \n" + "Do not add ANY preamble, conclusion, or extra text.\n\n" + f"Question: \"{question}\"" + ) + + raw_response = self._generate_response(prompt) + + relevant_questions = [] + for line in raw_response.split('\n'): + line = line.strip() + if line and line[0].isdigit(): + clean_question = line.split('.', 1)[-1].strip() + relevant_questions.append(clean_question) + + print(f"Generated relevant questions: {relevant_questions}") + + return relevant_questions + + + def get_web_info(self, questions): + results = [self.exa.answer(q, text=True) for q in questions] + answers = [r.answer for r in results] + return answers + + def get_binary_answer_with_percentage(self, information: str, question: str) -> Tuple[str, str, str]: + prompt = ( + "Analyze the provided information below to answer the given binary question. " + "Based on the information, determine the probability that the answer is 'Yes' or 'No'.\n\n" + "--- Information ---\n" + f"{information}\n\n" + "--- Question ---\n" + f"{question}\n\n" + "IMPORTANT INSTRUCTIONS:\n" + "1. Your response MUST ONLY be a single line in THIS EXACT FORMAT:\n" + " Yes: %, No: %, Explanation: \n" + "2. Percentages must sum to 100%.\n" + "3. Do NOT include any preamble, summary, or additional text.\n" + "4. Provide a brief but clear explanation supporting your probabilities.\n\n" + ) + + class Response(BaseModel): + yes_percentage: str + no_percentage: str + explanation: str + + response = self._generate_response(prompt, Response) + print(f"HELLO {response}") + + try: + return response.yes_percentage, response.no_percentage, response.explanation + except json.JSONDecodeError: + raise RuntimeError(f"Failed to parse output as JSON: {response}") + \ No newline at end of file diff --git a/8-application-demos/6-kalshi-bet-predictor/cerebrium.toml b/8-application-demos/6-kalshi-bet-predictor/cerebrium.toml new file mode 100644 index 00000000..704861f3 --- /dev/null +++ b/8-application-demos/6-kalshi-bet-predictor/cerebrium.toml @@ -0,0 +1,23 @@ +[cerebrium.deployment] +name = "kalshi-bet-predictor" +python_version = "3.11" +docker_base_image_url = "debian:bookworm-slim" +disable_auth = true +include = ['./*', 'main.py', 'cerebrium.toml'] +exclude = ['.*'] + +[cerebrium.dependencies.paths] +pip = "cerebrium_requirements.txt" + +[cerebrium.hardware] +cpu = 4 +memory = 16 +compute = "CPU" + +[cerebrium.scaling] +min_replicas = 0 +max_replicas = 100 +cooldown = 30 +replica_concurrency = 1 +scaling_metric = "concurrency_utilization" + diff --git a/8-application-demos/6-kalshi-bet-predictor/cerebrium_requirements.txt b/8-application-demos/6-kalshi-bet-predictor/cerebrium_requirements.txt new file mode 100644 index 00000000..dfebee10 --- /dev/null +++ b/8-application-demos/6-kalshi-bet-predictor/cerebrium_requirements.txt @@ -0,0 +1,22 @@ +annotated-types==0.7.0 +anyio==4.11.0 +certifi==2025.10.5 +charset-normalizer==3.4.4 +distro==1.9.0 +dotenv==0.9.9 +exa-py==1.16.1 +h11==0.16.0 +httpcore==1.0.9 +httpx==0.28.1 +idna==3.11 +jiter==0.11.1 +openai==2.6.0 +pydantic==2.12.3 +pydantic_core==2.41.4 +python-dotenv==1.1.1 +requests==2.32.5 +sniffio==1.3.1 +tqdm==4.67.1 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +urllib3==2.5.0 diff --git a/8-application-demos/6-kalshi-bet-predictor/compare.py b/8-application-demos/6-kalshi-bet-predictor/compare.py new file mode 100644 index 00000000..c8a184ce --- /dev/null +++ b/8-application-demos/6-kalshi-bet-predictor/compare.py @@ -0,0 +1,127 @@ +import csv +import json +from typing import Dict, List, Tuple +import asyncio +import aiohttp + +def load_markets(csv_path: str) -> List[Tuple[str, str]]: + markets = [] + with open(csv_path, 'r') as f: + reader = csv.reader(f) + next(reader) # skip header + for row in reader: + if len(row) >= 2: + markets.append((row[0], row[1])) + return markets + +async def get_market_data(session: aiohttp.ClientSession, kalshi_ticker: str, + poly_slug: str, endpoint_url: str) -> Dict: + + payload = json.dumps({ + 'kalshi_ticker': kalshi_ticker, + 'poly_slug': poly_slug + }) + + headers = { + 'Authorization': '', + 'Content-Type': 'application/json' + } + + try: + async with session.post(endpoint_url, headers=headers, data=payload) as response: + response.raise_for_status() + data = await response.json() + print(data) + data = data['result'] + + kalshi_data = data['kalshi'] + poly_data = data['polymarket'] + + return { + 'kalshi_ticker': kalshi_ticker, + 'poly_slug': poly_slug, + 'kalshi_edge_value': kalshi_data['edge'], + 'poly_edge_value': poly_data['edge'], + 'kalshi_is_buy_yes': kalshi_data['buy_yes'], + 'kalshi_is_buy_no': kalshi_data['buy_no'], + 'poly_is_buy_yes': poly_data['buy_yes'], + 'poly_is_buy_no': poly_data['buy_no'], + } + except Exception as e: + print(f"Error fetching data for {kalshi_ticker}/{poly_slug}: {e}") + return None + +async def analyze_markets_async(csv_path: str, endpoint_url: str) -> List[Dict]: + markets = load_markets(csv_path) + + print(f"Fetching data for {len(markets)} markets all at once...") + + async with aiohttp.ClientSession() as session: + tasks = [get_market_data(session, kalshi_ticker, poly_slug, endpoint_url) + for kalshi_ticker, poly_slug in markets] + + results = await asyncio.gather(*tasks) + + return [r for r in results if r is not None] + +def compute_statistics(results: List[Dict]) -> None: + print("\n" + "="*80) + print("STATISTICS") + print("="*80) + + if not results: + print("No results to analyze") + return + + total_markets = len(results) + + kalshi_edges_values = [r['kalshi_edge_value'] for r in results] + kalshi_edge_sum = sum(kalshi_edges_values) + + poly_edges_values = [r['poly_edge_value'] for r in results] + poly_edge_sum = sum(poly_edges_values) + + kalshi_better_count = sum(1 for r in results if r['kalshi_edge_value'] > r['poly_edge_value']) + poly_better_count = sum(1 for r in results if r['poly_edge_value'] > r['kalshi_edge_value']) + equal_count = total_markets - kalshi_better_count - poly_better_count + + edge_differences = [abs(r['kalshi_edge_value'] - r['poly_edge_value']) for r in results] + avg_edge_difference = sum(edge_differences) / total_markets + max_edge_difference = max(edge_differences) + + print(f"\nTotal markets analyzed: {total_markets}") + print("\n" + "-"*80) + print("COMPARISON") + print("-"*80) + print(f"Markets with greater Kalshi edge: {kalshi_better_count} ({kalshi_better_count/total_markets*100:.1f}%)") + print(f"Markets with greater Polymarket edge: {poly_better_count} ({poly_better_count/total_markets*100:.1f}%)") + print(f"Markets with equal edge: {equal_count} ({equal_count/total_markets*100:.1f}%)") + print(f"\nAverage edge difference: {avg_edge_difference:.4f} cents") + print(f"Max edge difference: {max_edge_difference:.4f} cents") + + print("\n" + "="*80) + if kalshi_edge_sum > poly_edge_sum: + advantage = kalshi_edge_sum - poly_edge_sum + print(f"OVERALL: Kalshi has greater total edge (+{advantage:.4f}) cents") + print(f"OVERALL: Kalshi has an average edge of (+{advantage/total_markets:.4f}) cents per market") + elif poly_edge_sum > kalshi_edge_sum: + advantage = poly_edge_sum - kalshi_edge_sum + print(f"OVERALL: Polymarket has greater total edge (+{advantage:.4f}) cents") + print(f"OVERALL: Polymarket has an average edge of (+{advantage/total_markets:.4f}) cents per market") + else: + print("OVERALL: Both platforms have equal total edge") + print("="*80) + +def main(): + CSV_PATH = "" + ENDPOINT_URL = "" + + print("Starting async market analysis...") + results = asyncio.run(analyze_markets_async(CSV_PATH, ENDPOINT_URL)) + + print(f"\nSuccessfully fetched {len(results)} markets") + + compute_statistics(results) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/8-application-demos/6-kalshi-bet-predictor/find_equiv_markets.py b/8-application-demos/6-kalshi-bet-predictor/find_equiv_markets.py new file mode 100644 index 00000000..e2806174 --- /dev/null +++ b/8-application-demos/6-kalshi-bet-predictor/find_equiv_markets.py @@ -0,0 +1,198 @@ +import csv +import os +import requests +import faiss +from sentence_transformers import SentenceTransformer +from typing import List, Dict, Any + +# --- Config --- +SIMILARITY_THRESHOLD = 0.70 # threshold for cosine simlarity +MAX_MARKET_LIMIT = 40000 # max number of active & open markets to gather +TOP_K = 5 # number of top Polymarket markets to check for each Kalshi market +KALSHI_API_URL = "https://api.elections.kalshi.com/trade-api/v2/markets" +POLYMARKET_API_URL = "https://clob.polymarket.com/markets" +OUTPUT_FILE = "markets.csv" + +def get_kalshi_markets() -> List[Dict[str, Any]]: + print("Fetching Kalshi markets...") + markets_list = [] + cursor = "" + try: + while True: + params = {'limit': 1000} + if cursor: + params['cursor'] = cursor + + response = requests.get(KALSHI_API_URL, params=params) + response.raise_for_status() + data = response.json() + + if 'markets' not in data: + print("Error: 'markets' key not in Kalshi response.") + break + + for market in data['markets']: + if market['status'] == 'active' and market['market_type'] == 'binary': + + markets_list.append({ + 'platform': 'Kalshi', + 'title': market['title'], + 'ticker': market['ticker'], + 'url': f"https://kalshi.com/markets/{market['ticker']}", + 'event_url': f"https://kalshi.com/markets/{market['event_ticker']}", + 'close_date': market['close_time'] + }) + + cursor = data['cursor'] + print(f"Found {len(markets_list)} active and open markets") + + if len(markets_list) > MAX_MARKET_LIMIT or not cursor: + break + + print(f"Found {len(markets_list)} open binary markets on Kalshi.") + return markets_list + + except requests.exceptions.RequestException as e: + print(f"Error fetching Kalshi markets: {e}") + return [] + +def get_kalshi_market(ticker): + title = requests.get(f"{KALSHI_API_URL}/{ticker}") + title = title.json() + return title['market']['title'] + +def get_polymarket_markets() -> List[Dict[str, Any]]: + print("Fetching Polymarket markets...") + markets_list = [] + next_cursor = None + + try: + while True: + params = {} + if next_cursor: + params['next_cursor'] = next_cursor + + response = requests.get(POLYMARKET_API_URL, params=params) + response.raise_for_status() + data = response.json() + + market_list_page = data['data'] + if not market_list_page: + break + + for market in market_list_page: + if market.get('active') and not market.get('closed'): + markets_list.append({ + 'platform': 'Polymarket', + 'title': market.get('question'), + 'id': market.get('condition_id'), + 'url': f"https://polymarket.com/event/{market.get('market_slug')}", + 'close_date': market.get('end_date_iso') + }) + + next_cursor = data.get('next_cursor') + print(f"Found {len(markets_list)} active and open markets") + + if len(markets_list) > MAX_MARKET_LIMIT or not next_cursor or next_cursor == 'LTE=': + break + + print(f"Found {len(markets_list)} open markets on Polymarket.") + return markets_list + + except requests.exceptions.RequestException as e: + print(f"Error fetching Polymarket markets: {e}") + return [] + + +def find_similar_markets(kalshi_markets, poly_markets, threshold=0.9, top_k=TOP_K): + print("\nLoading NLP model...") + model = SentenceTransformer('all-MiniLM-L6-v2') + + kalshi_titles = [m['title'] for m in kalshi_markets] + poly_titles = [m['title'] for m in poly_markets] + + if not kalshi_titles or not poly_titles: + print("Not enough market data to compare.") + return [] + + print("Encoding titles into embeddings...") + kalshi_embeddings = model.encode(kalshi_titles, convert_to_numpy=True, normalize_embeddings=True) + poly_embeddings = model.encode(poly_titles, convert_to_numpy=True, normalize_embeddings=True) + + print(f"Building vector index for {len(poly_embeddings)} Polymarket markets...") + dim = poly_embeddings.shape[1] + index = faiss.IndexFlatIP(dim) # Inner product for cosine similarity + index.add(poly_embeddings) + + print(f"Querying top {top_k} nearest Polymarket markets for each Kalshi market...") + scores, indices = index.search(kalshi_embeddings, top_k) + + potential_matches = [] + for i, kalshi_market in enumerate(kalshi_markets): + for j in range(top_k): + score = float(scores[i][j]) + if score >= threshold: + poly_market = poly_markets[indices[i][j]] + potential_matches.append({ + 'score': score, + 'kalshi_market': kalshi_market, + 'poly_market': poly_market + }) + if i % 100 == 0: + print(f"Processed {i}/{len(kalshi_markets)} Kalshi markets...") + + + return potential_matches + +def interactive_save(matches: List[Dict[str, Any]]): + print("\n--- Review Mode ---") + print("Press 'y' to save a match, anything else to skip.\n") + + file_exists = os.path.exists(OUTPUT_FILE) + with open(OUTPUT_FILE, "a", newline='', encoding="utf-8") as csvfile: + writer = csv.writer(csvfile) + if not file_exists: + writer.writerow(["kalshi_ticker", "poly_slug"]) + + for i, match in enumerate(matches): + kalshi_ticker = match['kalshi_market']['ticker'] + poly_slug = match['poly_market']['url'].split("event/")[1] + kalshi_title = get_kalshi_market(kalshi_ticker) + poly_title = match['poly_market']['title'] + score = match['score'] + + print(f"\nMatch #{i+1} (Score: {score:.4f})") + print(f"[KALSHI] {kalshi_title}") + print(f"[POLYMARKET] {poly_title}") + print(f" > Kalshi URL: {match['kalshi_market']['url']}") + print(f" > Polymarket URL:{match['poly_market']['url']}") + + choice = input("Save this match? (y/n): ").strip().lower() + if choice == 'y': + writer.writerow([kalshi_ticker, poly_slug]) + print("Saved.") + else: + print("Skipped.") + + print(f"\nDone. Saved matches to '{OUTPUT_FILE}'.") + +def main(): + kalshi_markets = get_kalshi_markets() + poly_markets = get_polymarket_markets() + + if not kalshi_markets or not poly_markets: + print("\nCould not fetch markets from one or both platforms. Exiting.") + return + + matches = find_similar_markets(kalshi_markets, poly_markets, SIMILARITY_THRESHOLD) + print(f"\n--- Found {len(matches)} Potential Matches ---") + + if not matches: + print("No strong matches found.") + return + + matches.sort(key=lambda x: x['score'], reverse=True) + interactive_save(matches) + +if __name__ == "__main__": + main() diff --git a/8-application-demos/6-kalshi-bet-predictor/main.py b/8-application-demos/6-kalshi-bet-predictor/main.py new file mode 100644 index 00000000..e32a3b29 --- /dev/null +++ b/8-application-demos/6-kalshi-bet-predictor/main.py @@ -0,0 +1,120 @@ +import json +import requests +import re +from dataclasses import dataclass +from analyst import BetAnalyst + +@dataclass +class MarketData: + question: str + yes_price: str + no_price: str + +def _fetch_api_data(url: str): + try: + res = requests.get(url) + res.raise_for_status() + obj = res.json() + return obj + except requests.exceptions.RequestException as e: + raise RuntimeError(f"Error fetching Kalshi market data: {e}") + +def get_kalshi_market(ticker: str) -> MarketData: + url = f"https://api.elections.kalshi.com/trade-api/v2/markets/{ticker}" + raw_data = _fetch_api_data(url) + + try: + market = raw_data['market'] + return MarketData( + question = market['title'], + yes_price=float(market['yes_ask']), + no_price=float(market['no_ask']) + ) + except (KeyError, TypeError, ValueError) as e: + raise RuntimeError(f"Error parsing Kalshi data structure: {e}") from e + +def get_polymarket_market(slug: str) -> MarketData: + url = f"https://gamma-api.polymarket.com/markets/slug/{slug}" # slug + raw_data = _fetch_api_data(url) + + try: + poly_values = json.loads(raw_data['outcomePrices']) + yes_price, no_price = [float(v) for v in poly_values] + + return MarketData( + question = raw_data['question'], + yes_price=yes_price, + no_price=no_price + ) + except (KeyError, TypeError, ValueError) as e: + raise RuntimeError(f"Error parsing Kalshi data structure: {e}") from e + + +def getMarket(is_kalshi, ticker): + if is_kalshi: + url = f"https://api.elections.kalshi.com/trade-api/v2/markets/{ticker}" # market ticker + else: + url = f"https://gamma-api.polymarket.com/markets/slug/{ticker}" # slug + try: + res = requests.get(url) + res.raise_for_status() + obj = res.json() + return obj + except requests.exceptions.RequestException as e: + raise RuntimeError(f"Error fetching Kalshi market data: {e}") + + +def evaluate(analyst, question): + # Generate questions using OpenAI API + relevant_questions = analyst.get_relevant_questions(question) + # Use Exa semantic search to retrieve answers to questions + answers = analyst.get_web_info(relevant_questions) + + information = "" + for i, v in enumerate(relevant_questions): + information += f"INFORMATION {i+1}: \n" + information += f"QUESTION {i+1}: {v}\n" + information += f"ANSWER {i+1}: {answers[i]} \n\n" + + information.rstrip("\n") + + # Passes relevant Q&As to OpenAI API and generates Y/N percentage with explanation + yes, no, explanation = analyst.get_binary_answer_with_percentage(information, question) + return yes, no, explanation + +def predict(kalshi_ticker, poly_slug): + kalshi_market = get_kalshi_market(kalshi_ticker) + poly_market = get_polymarket_market(poly_slug) + question = poly_market.question # we use polymarket because they have direct question + + print(f"Question: {question}") + + analyst = BetAnalyst() + pred_yes, pred_no, explanation = evaluate(analyst, question) + + match_yes = re.search(r"(\d+)%", pred_yes) + match_no = re.search(r"(\d+)%", pred_no) + pred_yes = float(match_yes.group(1)) + pred_no = float(match_no.group(1)) + + kalshi_buy_yes = kalshi_market.yes_price < pred_yes + kalshi_buy_no = kalshi_market.no_price < pred_no + + poly_buy_yes = poly_market.yes_price < pred_yes + poly_buy_no = poly_market.no_price < pred_no + + return { + "kalshi": { + "buy_yes":kalshi_buy_yes, + "buy_no": kalshi_buy_no, + "edge": max(pred_yes-kalshi_market.yes_price, pred_no-kalshi_market.no_price), + }, + "polymarket": { + "buy_yes":poly_buy_yes, + "buy_no": poly_buy_no, + "edge": max(pred_yes-poly_market.yes_price, pred_no-poly_market.no_price), + }, + "yes": pred_yes, + "no": pred_no, + "explanation": explanation + } diff --git a/8-application-demos/6-kalshi-bet-predictor/requirements.txt b/8-application-demos/6-kalshi-bet-predictor/requirements.txt new file mode 100644 index 00000000..6f68e701 --- /dev/null +++ b/8-application-demos/6-kalshi-bet-predictor/requirements.txt @@ -0,0 +1,54 @@ +aiohappyeyeballs==2.6.1 +aiohttp==3.13.1 +aiosignal==1.4.0 +annotated-types==0.7.0 +anyio==4.11.0 +attrs==25.4.0 +certifi==2025.10.5 +charset-normalizer==3.4.4 +distro==1.9.0 +dotenv==0.9.9 +exa-py==1.16.1 +faiss-cpu==1.12.0 +filelock==3.20.0 +frozenlist==1.8.0 +fsspec==2025.9.0 +h11==0.16.0 +hf-xet==1.1.10 +httpcore==1.0.9 +httpx==0.28.1 +huggingface-hub==0.35.3 +idna==3.11 +Jinja2==3.1.6 +jiter==0.11.1 +joblib==1.5.2 +MarkupSafe==3.0.3 +mpmath==1.3.0 +multidict==6.7.0 +networkx==3.5 +numpy==2.3.4 +openai==2.6.0 +packaging==25.0 +pillow==12.0.0 +propcache==0.4.1 +pydantic==2.12.3 +pydantic_core==2.41.4 +python-dotenv==1.1.1 +PyYAML==6.0.3 +regex==2025.10.23 +requests==2.32.5 +safetensors==0.6.2 +scikit-learn==1.7.2 +scipy==1.16.2 +sentence-transformers==5.1.2 +sniffio==1.3.1 +sympy==1.14.0 +threadpoolctl==3.6.0 +tokenizers==0.22.1 +torch==2.9.0 +tqdm==4.67.1 +transformers==4.57.1 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +urllib3==2.5.0 +yarl==1.22.0