Compare commits

...

22 Commits

Author SHA1 Message Date
096b9868a7 rework concept 2025-12-17 22:08:13 +01:00
ea460864a6 readme for windows 2025-12-08 21:14:06 +01:00
58359294bc database 2025-12-03 15:09:30 +01:00
c7f949f69a persistent multithreading 2025-12-02 21:25:09 +01:00
e0627f5809 multithreading 2025-12-02 15:56:13 +01:00
92641bf863 video link 2025-12-01 11:25:07 +01:00
3b674f9cf6 adjust workflow info graphic 2025-12-01 10:28:17 +01:00
66e7d6e686 implement basic qdrant interface 2025-11-30 16:10:58 +01:00
0957027eed search object data model 2025-11-30 14:49:18 +01:00
c0e0325958 fixes to semeionSearchArtifact 2025-11-29 23:41:51 +01:00
f5219bf2e4 implement semeionArtifact 2025-11-29 23:18:55 +01:00
220d1b67b5 fix dependencies 2025-11-29 21:13:54 +01:00
c1826ebab6 adkust README 2025-11-29 19:02:40 +01:00
overcuriousity
48eeda1a2d license: BSD-3-Clause 2025-11-26 23:26:33 +01:00
overcuriousity
cb6c4a2b1b Merge branch 'main' of https://git.cc24.dev/mstoeck3/semeion 2025-11-26 23:15:42 +01:00
overcuriousity
c707fb59bd GUI first draft 2025-11-26 23:15:15 +01:00
1907996291 README.md aktualisiert 2025-11-26 21:14:34 +00:00
dbd1ea1ebf README.md aktualisiert 2025-11-26 21:12:37 +00:00
0782fb29d0 README.md aktualisiert 2025-11-26 21:10:05 +00:00
overcuriousity
fc23473569 rebranding 2025-11-26 22:06:33 +01:00
overcuriousity
79407eeb5c rename application to semeion 2025-11-26 21:41:05 +01:00
1162815134 typo 2025-11-26 10:51:22 +01:00
53 changed files with 2084 additions and 321 deletions

View File

@@ -0,0 +1,5 @@
Copyright (c) $years, $owner
All rights reserved.
This source code is licensed under the BSD-3-Clause license found in the
LICENSE file in the root directory of this source tree.

8
.env.example Normal file
View File

@@ -0,0 +1,8 @@
# LLM API Configuration (OpenAI-compatible endpoint)
LLM_API_KEY=your_llm_api_key_here
LLM_API_BASE=http://localhost:11434/v1
# Qdrant Vector Database Configuration
QDRANT_HOST=localhost
QDRANT_PORT=6333
QDRANT_API_KEY=your_qdrant_api_key_here

1
.gitignore vendored
View File

@@ -168,3 +168,4 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
app_database/

562
README.md
View File

@@ -1,309 +1,355 @@
# Gamayun
# semeion
![alt text](resources/title_image.png)
## Concept
## Overview
Desktop application for forensic investigators which allows to search for various types of artifacts via natural language.
The application uses a multi-stage hybrid search approach which accepts a query in natural language and then utilizes a GPT to generate a machine-readable search plan which can be adjusted by the user before execution.
The system then utilizes a combination of semantic search via pre-generated embeddings and filters the results as defined via the machine interface.
This enables the combination of semantic understanding with context and temporal relationships rather than traditional approaches of exact keyword matching.
Semeion is a **timeline-first digital forensics analysis platform** that solves the two biggest pain points in modern investigations: navigating massive communication datasets and correlating events across data sources, including filesystm artifacts and OS events.
## UX Example
> **Core Question Semeion Answers:** "What happened around this time—and how are these events connected?"
An investigator can ask "show me what happened after they discussed the payment" and the system will find relevant communication about payments, then correlate subsequent activities (file access, application launches, network traffic) in a temporal sequence, regardless of the specific applications or messaging platforms involved.
Traditional forensic tools often have unintuitiveur slow user experience (opinionated). Semeion in contrast aims to provide a fast, intelligent timeline that automatically clusters semantically-related events, enabling investigators to see patterns at a glance rather than clicking through endless individual entries.
## System Overview
An investigator opens Semeion alongside their primary forensic tool, either searches for relevant content (like "*discussions about data transfer*" in Russian messages) or navigates to a known timestamp (from incident logs), and immediately sees all correlated activity—messages, file access, network connections, system events—organized into meaningful sequences rather than flat lists.
### Core Concept
## Key Features
The system treats all digital artifacts as semantically-rich objects embedded in a multi-dimensional vector space. Instead of matching exact words, it finds artifacts with similar meaning. A local language model interprets natural language queries and decomposes them into structured search operations that can handle temporal reasoning ("after"), relationship detection ("both in"), and causal inference ("because of").
| Feature | Description |
|---------|-------------|
| **Timeline Visualization** | Render millions of events with <200ms response time; smooth zooming/panning |
| **Semantic Event Clustering** | Automatically group related events into patterns (file transfers, communication bursts, login sequences) |
| **Multilingual Semantic Search** | Find concepts across languages—search in English, find results in Russian/Chinese/Arabic with translation |
| **Two-Mode Operation** | Semantic content discovery → timeline analysis OR timestamp → content correlation |
| **Cross-Source Correlation** | Unified timeline across messages, files, network, browser, system events |
| **Event Grouping** | Collapse/expand semantically-related events; discover patterns |
| **Native Desktop Application** | PySide6-based—runs fully local, tailored for forensic-grade airgapped environments |
| **Open Source & Transparent** | Auditable methods, minimize "blackbox" AI, court-defensible results |
### Key Innovation
## The Problem Semeion Solves
Generic artifact understanding: The system doesn't need to "know" about TOX, WhatsApp, Signal, Telegram, or any specific application. During ingestion, it automatically classifies content as "chat message," "system event," "document," etc., based purely on semantic similarity to type descriptions. This means it works on artifacts from applications that don't even exist yet, or proprietary communication tools without public documentation.
### Problematic UX with existing forensic suites
### Architecture Philosophy
Existing forensic tools have timeline interfaces that:
Client-Server Separation: Compute-intensive operations (embedding generation, LLM inference, vector search) can run on powerful remote infrastructure, while the GUI client remains lightweight and runs on the investigator's local machine. This enables:
- Render slowly
- Show flat event lists with no context
- Require manual correlation across sources
- Have poor visualization and navigation
*This is highly opinionated from personal experiences of the maintainer with multiple large commercial suites which werent able to deliver a satisfying result. I wont name any here, and exceptions may exist, but improvements are required here.*
- Scaling compute resources independently of workstations
- Deployment in air-gapped labs
- Efficient resource utilization (centralized compute nodes can serve multiple investigators)
**Semeion's Solution:** Fast, interactive timeline with semantic clustering that shows patterns immediately.
### Large Communication Datasets
Modern investigations involve:
- Seized databases with millions of relevant artifacts
- Multilingual content
- Slang, code words, and poor machine translation
- Hours spent reading irrelevant conversations
**Semeion's Solution:** Semantic search that understands meaning, handles multiple languages, and jumps directly to timeline context.
## How It Works
### Two Entry Points
#### **Entry Point 1: Semantic content discovery → timeline analysis**
```bash
1. Investigator searches: "discussions about file transfers"
2. Semeion finds semantically relevant messages
3. Click any result → Timeline centers on that timestamp
4. See all activity ±2 hours: file access, USB connections, deletions
5. Semantic clustering highlights: "File Exfiltration Sequence" pattern
```
#### **Entry Point 2: timestamp → content correlation**
```bash
1. Incident log shows: "Ransomware encrypted files at 2024-03-15 14:22:00"
2. Navigate timeline to that timestamp
3. Semantic clustering reveals:
- Communication spike 30min before (cluster: "Coordination Activity")
- Suspicious file downloads (cluster: "Malware Delivery")
- Network connections (cluster: "C2 Communication")
- Process executions leading to encryption
4. Complete attack chain visible at a glance
```
## Semantic Event Clustering
**Traditional Timeline View (Overwhelming):**
```bash
14:33:15 - USB device connected
14:33:18 - Chrome visited facebook.com
14:33:22 - File modified: report.docx
14:33:30 - File copied to USB: project_data.zip (2.3 GB)
14:33:35 - File copied to USB: budget.xlsx
14:33:40 - File deleted: project_data.zip
14:33:45 - WhatsApp message sent
...50 more individual events...
```
**Semeion Clustered View (Clear Pattern):**
```bash
┌──────────────────────────────────────────────┐
│ 14:33:15-14:33:40 ⚠️ File Transfer (4) │ ← Click to expand
│ Pattern: Data Exfiltration Sequence │
│ USB connected → 2 files copied → source deleted │
│ Related: Files mentioned in chat 2min earlier │
└──────────────────────────────────────────────┘
```
### How Clustering Works
1. **Temporal Proximity**: Events within configurable time window evaluated together
2. **Semantic Similarity**: Vector embeddings detect conceptually-related events
3. **Entity Linking**: Shared file names, IPs, usernames connect events
4. **Pattern Templates**: Pre-defined sequences (USB exfiltration, login chains, etc.)
5. **LLM Summarization** (Optional): Natural language cluster descriptions
## Architecture
### Desktop Application (PySide6)
```bash
┌─────────────────────────────────────────────────────────┐
│ PySide6 Desktop UI │
│ ┌───────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ Timeline │ │ Semantic │ │ Artifact │ │
│ │ View │ │ Search │ │ Detail │ │
│ └───────────────┘ └──────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Analysis & Clustering Engine │
│ • Event clustering algorithm │
│ • Semantic similarity calculation │
│ • Cross-artifact correlation │
│ • Timeline rendering engine │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Data Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Qdrant │ │ SQLite │ │ LLM API │ │
│ │ (Vectors) │ │ (Metadata) │ │ (Optional) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
```
### Deployment Options
| Configuration | Qdrant | Embedding | LLM | Use Case |
|---------------|--------|-----------|-----|----------|
| **Fully Local** | localhost | Ollama/local model | Ollama (optional) | Single investigator, offline |
| **Airgapped Network** | Internal server | Internal server | Internal server | proper forensic environment |
| **Hybrid** | Local | Local | Cloud API (optional) | investigative environment without focus on confidentiality |
## Artifact Types
### Searchable & Timeline-Enabled
| Type | Examples | Features |
|------|----------|----------|
| **Messages** | WhatsApp, Telegram, Signal, SMS | Semantic search, multilingual, timeline clustering |
| **Browser** | Chrome, Firefox, Safari, Edge | History, downloads, searches—timeline correlation |
| **Email** | Any email format (EML, MBOX, PST) | Searchable content, timeline placement |
| **Documents** | PDF, Word, plain text | Content search, access time correlation |
### Timeline-Only (Context View)
| Type | Examples |
|------|----------|
| **File Events** | File creation, modification, deletion, access |
| **Process Events** | Application launches, process creation |
| **Network Events** | Connections, DNS queries |
| **System Events** | Logs, authentication events, USB connections |
## Technical Stack
| Component | Technology | Purpose |
|-----------|------------|---------|
| **UI Framework** | PySide6 (Qt6) | Native desktop application |
| **Timeline Rendering** | PyQtGraph or Plotly | High-performance visualization |
| **Vector Database** | Qdrant | Semantic search and similarity |
| **Metadata Storage** | SQLite | Fast local queries, metadata |
| **LLM Interface** | OpenAI-compatible API | Optional: cluster summarization, query interpretation |
| **Embedding** | Sentence Transformers / OpenAI | Multilingual vector generation |
| **Forensic Parsing** | pytsk3, pyewf | Disk image processing |
| **Language** | Python 3.13+ | Application logic |
## Data Model
### SemeionArtifact (Universal Schema, WiP)
Every artifact—regardless of source—conforms to a unified model:
```bash
┌─────────────────────────────────────────────────────────┐
│ SemeionArtifact │
├─────────────────────────────────────────────────────────┤
│ Identity: id, case_id │
│ Classification: artifact_class, source_platform │
│ Temporal: timestamp (UTC normalized)
│ Actors: [{identifier, display_name, role}]
│ Content: text, semantic_text │
│ Entities: indexed_entities[] (files, IPs, etc)
│ Hierarchy: parent_id, context_group │
│ Location: url, path, title │
│ Embeddings: semantic_vector (768-dim)
│ Source-Specific: message{}, browser{}, email{}, etc │
└─────────────────────────────────────────────────────────┘
```
### Cluster Model (WiP)
```bash
┌─────────────────────────────────────────────────────────┐
│ EventCluster │
├─────────────────────────────────────────────────────────┤
│ id: unique_cluster_id │
│ time_range: (start_timestamp, end_timestamp)
│ events: [artifact_ids]
│ pattern_type: "file_exfiltration" | "communication"
│ confidence: 0.0-1.0 │
│ summary: "USB transfer with deletion"
│ icon: "⚠️" | "💬" | "🌐" | etc │
│ semantic_links: [related_cluster_ids]
└─────────────────────────────────────────────────────────┘
```
## What Semeion Is NOT (yet)
| Not This | Why |
|----------|-----|
| **Forensic suite replacement** | Companion tool—use alongside Autopsy for acquisition |
| **Reporting tool** | Timeline export for reports, but documentation happens in primary suite |
| **AI evidence interpreter** | AI assists with search/clustering; investigator interprets evidence |
## Development Setup
## 🛠 Development Setup
This project uses [uv](https://github.com/astral-sh/uv) for fast dependency management and the modern "Src Layout" structure.
This project uses [uv](https://github.com/astral-sh/uv) for dependency management.
### Prerequisites
- Python 3.13+
- [uv](https://github.com/astral-sh/uv) installed (`curl -LsSf https://astral.sh/uv/install.sh | sh`)
- [uv](https://github.com/astral-sh/uv) installed
- requirements.txt
### Installation Steps
1. **Clone the repository**
```bash
git clone <your-repo-url>
cd gamayun
```
2. **Create a virtual environment**
This project requires Python 3.13.
```bash
uv venv --python 3.13
```
3. **Activate the environment**
- Linux/macOS:
```bash
source .venv/bin/activate
```
- Windows:
```powershell
.venv\Scripts\activate
```
4. **Install dependencies**
This command installs locked dependencies and links the local `gamayun` package in editable mode.
```bash
uv pip install -r requirements.txt -e .
```
### Running the Application
You can execute the module directly:
### Installation
```bash
python src/gamayun/main.py
git clone https://git.cc24.dev/mstoeck3/semeion
cd semeion
# Create virtual environment
uv venv --python 3.13
# Activate environment
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows
# Install dependencies
uv pip install -r requirements.txt -e .
# Configure environment
cp .env.example .env
# Edit .env with your Qdrant and embedding endpoint configurations
```
### Running Tests
### Running
```bash
pytest
semeion
```
## Data Flow
## System Requirements
### Ingestion Pipeline
### Minimum (Remote Processing)
```bash
Raw Evidence Sources
├─ Forensic Images (E01, DD, AFF4)
├─ Timeline CSV (Timesketch format)
└─ Loose Files (documents, logs, databases)
┌────────────────────────┐
│ Artifact Extraction │
│ • pytsk3 (images) │
│ • CSV parser │
│ • File processors │
└───────┬────────────────┘
┌────────────────────────┐
│ Content Extraction │
│ • PDF, DOCX, XLSX │
│ • SQLite databases │
│ • Text files │
│ • OCR for images │
└───────┬────────────────┘
┌────────────────────────┐
│ Semantic Enrichment │
│ • Classify type │
│ • Extract entities │
│ • Detect relationships │
│ • Add metadata │
└───────┬────────────────┘
┌────────────────────────┐
│ Embedding Generation │
│ → Remote/Local Service │
└───────┬────────────────┘
┌────────────────────────┐
│ Index in Qdrant │
│ • Vector + Payload │
│ • Create indexes │
│ • Snapshot for audit │
└────────────────────────┘
```
| Resource | Requirement |
|----------|-------------|
| CPU | 4 cores |
| RAM | 8 GB |
| Storage | Minimal (evidence stored elsewhere) |
| GPU | Not required |
| Network | Access to Qdrant and embedding endpoints (if remote) |
Reproducibility: Each ingestion run generates a manifest file containing:
### Recommended (Local Processing)
- Source hashes (MD5/SHA256 of evidence)
- Model versions (embedding model, LLM)
- Configuration parameters
- Processing statistics
- Timestamp and operator ID
| Resource | Requirement |
|----------|-------------|
| CPU | 8+ cores |
| RAM | 16 GB (32 GB for large cases) |
| Storage | SSD, sufficient for evidence & vectors |
| GPU | Optional (improves embedding speed with local models) |
| Network | Optional (fully offline capable) |
This manifest allows exact reproduction of the index from the same source data.
## Project Status
### Query Execution Pipeline
**Current Phase:** MVP Development - Timeline & Core Features
```bash
Natural Language Query
"bitcoin transaction after drug deal"
┌────────────────────────┐
│ LLM Query Parser │
│ → Remote/Local Service │
│ Returns: JSON Plan │
└───────┬────────────────┘
┌────────────────────────┐
│ Query Plan Editor (UI) │
│ • Review plan │
│ • Adjust parameters │
│ • Modify steps │
│ • User approves │
└───────┬────────────────┘
┌────────────────────────┐
│ Search Orchestrator │
│ • Execute Step 1 │
│ • Extract timestamps │
│ • Execute Step 2 │
│ • Apply temporal logic │
└───────┬────────────────┘
┌────────────────────────┐
│ Correlation Engine │
│ • Calculate proximity │
│ • Weight scores │
│ • Build relationships │
└───────┬────────────────┘
┌────────────────────────┐
│ Results Presentation │
│ • Timeline view │
│ • Correlation graph │
│ • Export options │
└────────────────────────┘
```
**Roadmap:**
---
1. ✅ Concept and architecture design
2. ⬜ Core infrastructure
- Unified artifact ingestion (WhatsApp, Chrome)
- SQLite + Qdrant integration
3. ⬜ Timeline visualization
- High-performance rendering (target: <200ms for 100k events)
- Multi-source swim lanes
- Zoom/pan navigation
4. ⬜ Semantic clustering
- Temporal proximity grouping
- Pattern template matching
- Semantic similarity detection
5. ⬜ Communication search
- Multilingual embedding
- Query interpretation
- Search → timeline jump
6. ⬜ Additional parsers (Telegram, Signal, etc.)
7. ⬜ Export and reporting
8. ⬜ Performance optimization & polish
## Technical Stack
## Why Open Source Matters
### Core Technologies
**Transparency for Court:**
| Component | Technology | Version | License | Purpose |
|----------------------|---------------------------|-----------|--------------------------------|--------------------------------|
| GUI Framework | PySide6 | TBD | LGPL | Desktop application interface |
| Vector Database | Qdrant | 1.10+ | Apache 2.0 | Semantic search and storage |
| LLM Inference | OpenAI-compatible API | various | various | Natural language understanding |
| LLM Models | various | various | various | Query parsing |
| Embeddings | OpenAI-compatible API | various | various | Semantic vectors |
| Embedding Model | TBD | TBD | TBD | Text to vector conversion |
| Forensic Parsing | pytsk3 | TBD | Apache 2.0 | Disk image processing |
| Image Handling | pyewf | TBD | LGPL | E01 image support |
| NLP | spaCy | TBD | MIT | Entity extraction |
| Programming Language | Python | 3.13+ | PSF | Application logic |
- Auditable algorithms—no "black box" analysis
- Reproducible results—scientific validation possible
- Peer-reviewed methods—community scrutiny
### Infrastructure Requirements
**Accessibility:**
#### Remote Processing
- Free for budget-constrained labs
- No vendor lock-in
- Community-driven development
- CPU: multi-Core
- RAM: ~4GiB
- Storage: insignificant, local client, possible evidence
- GPU: irrelevant
- remote infrastructure for compute
**Innovation:**
#### Small scale local processing (local workstation)
- Rapid feature development
- Specialized extensions possible
- Academic research enabled
- CPU: 8+ cores (modern Ryzen5/Ryzen7/Ryzen AI series)
- RAM: 32GB minimum (16GB for Qdrant, 8GB for LLM, 8GB for OS/app)
- Storage: 2TB SSD (1TB evidence + 1TB index)
- GPU: recommended, not required (speed considerations)
## Branding
#### Recommended Configuration (Remote Server Compute Node)
**Semeion** derives from the Greek σημεῖον (sēmeîon), meaning "sign," "signal," or "meaningful mark"—embodying the software's mission of revealing meaningful patterns within digital evidence through intelligent timeline analysis.
- CPU: 16+ cores (Ryzen7+)
- RAM: 64-128 GiB
- Storage: sufficient for index+evidence
- GPU: recommended, AMD Instinct MI50 32 GiB or better (Inference, Embeddings)
- Network: sufficient
#### Enterprise Configuration (Multi-User)
- TBD, out of scope
---
## Supported Ingestion Formats
### Primary: Specialized Data Objects
TBD
### Secondary: Conversion Engine (algorithmic)
Example:
- SQLite Parser for browser History -> Special Data Object
- Converter for TSK artifacts -> Metadata in Special Data Object (TBD)
## Use Case Scenarios
### Scenario 1: Drug Transaction Investigation
Query: "Find when the suspect made cryptocurrency payments after discussing deals" Process:
1. System finds chat messages about drug deals
2. Extracts timestamps of deal discussions
3. Searches for cryptocurrency-related activity after each discussion
4. Correlates wallet launches, browser activity, blockchain transactions
5. Presents timeline showing: discussion → wallet launch → transaction Evidence: Timeline of intent → action, strengthening case
### Scenario 2: Data Exfiltration
Query: "Show file access before large uploads to cloud storage" Process:
1. Identifies cloud storage upload events
2. Looks backward in time for file access
3. Correlates accessed files with uploaded data
4. Maps file paths to user actions Evidence: Demonstrates what data was taken and when
### Scenario 3: Coordinated Activity
Query: "Find people who communicated privately and are also in the same groups" Process:
1. Extracts participants from private messages
2. Extracts participants from group chats
3. Identifies overlap (intersection)
4. Shows sample conversations from each context Evidence: Demonstrates coordinated behavior across communication channels
### Scenario 4: Timeline Reconstruction
Query: "What happened between receiving the threatening email and deleting files?" Process:
1. Finds threatening email (semantic search)
2. Finds file deletion events (system logs)
3. Returns all artifacts between these timestamps
4. Visualizes complete timeline Evidence: Establishes sequence of events and potential motive
The mascot, **Koios** (Coeus), the Titan of intellect and inquiry in Greek mythology, represents the investigative mindset: the pursuit of hidden connections through analytical thought.
## License
BSD 3-Clause (subject to change during development)
BSD 3-Clause
## Contributing
Semeion is in active development. Contributions welcome, especially from:
- Digital forensics practitioners (workflow validation)
- Timeline visualization experts
- Multilingual NLP specialists
- Performance optimization engineers

View File

@@ -1,16 +1,19 @@
[project]
name = "gamayun"
name = "semeion"
version = "0.0.0"
description = "Scalable vector search engine with focus on post-mortem forensics"
authors = [
{name = "Mario Stöckl", email = "mstoeck3@hs-mittweida.de"}
]
readme = "README.md"
requires-python = "~=3.13"
requires-python = "~=3.13.0"
license = {text = "BSD-3-Clause"}
dependencies = [
"PySide6~=6.8.0",
"qdrant_client~=1.16.1",
"openai~=2.8.1",
"python-dotenv~=1.2.1",
]
[project.optional-dependencies]
@@ -23,7 +26,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/gamayun"]
packages = ["src/semeion"]
[project.scripts]
gamayun-cli = "gamayun.main:main"
semeion = "semeion.main:main"

View File

@@ -3,11 +3,15 @@
annotated-types==0.7.0
# via pydantic
anyio==4.11.0
# via httpx
# via
# httpx
# openai
certifi==2025.11.12
# via
# httpcore
# httpx
distro==1.9.0
# via openai
grpcio==1.76.0
# via qdrant-client
h11==0.16.0
@@ -19,7 +23,9 @@ hpack==4.1.0
httpcore==1.0.9
# via httpx
httpx==0.28.1
# via qdrant-client
# via
# openai
# qdrant-client
hyperframe==6.1.0
# via h2
idna==3.11
@@ -28,8 +34,12 @@ idna==3.11
# httpx
iniconfig==2.3.0
# via pytest
jiter==0.12.0
# via openai
numpy==2.3.5
# via qdrant-client
openai==2.8.1
# via semeion (pyproject.toml)
packaging==25.0
# via pytest
pluggy==1.6.0
@@ -39,13 +49,15 @@ portalocker==3.2.0
protobuf==6.33.1
# via qdrant-client
pydantic==2.12.4
# via qdrant-client
# via
# openai
# qdrant-client
pydantic-core==2.41.5
# via pydantic
pygments==2.19.2
# via pytest
pyside6==6.8.3
# via gamayun (pyproject.toml)
# via semeion (pyproject.toml)
pyside6-addons==6.8.3
# via pyside6
pyside6-essentials==6.8.3
@@ -53,19 +65,26 @@ pyside6-essentials==6.8.3
# pyside6
# pyside6-addons
pytest==9.0.1
# via gamayun (pyproject.toml)
# via semeion (pyproject.toml)
python-dotenv==1.2.1
# via semeion (pyproject.toml)
qdrant-client==1.16.1
# via gamayun (pyproject.toml)
# via semeion (pyproject.toml)
shiboken6==6.8.3
# via
# pyside6
# pyside6-addons
# pyside6-essentials
sniffio==1.3.1
# via anyio
# via
# anyio
# openai
tqdm==4.67.1
# via openai
typing-extensions==4.15.0
# via
# grpcio
# openai
# pydantic
# pydantic-core
# typing-inspection

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

After

Width:  |  Height:  |  Size: 6.6 MiB

BIN
resources/workflow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

View File

@@ -1,7 +0,0 @@
"""Gamayun semantic post-mortem forensics"""
__version__ = "0.0.0"
from .main import main
__all__ = ["main"]

View File

@@ -1,4 +0,0 @@
# from .browserSqlite import BrowserSqliteEngine
# from .forensicImage import ForensicImageEngine
__all__ = []

View File

@@ -1,4 +0,0 @@
# TODO: implement browser SQlite database converter -> gamayun object
# adapt browser2timesketch.py
__all__ = []

View File

@@ -1,3 +0,0 @@
# TODO: use pytsk to import forensic images and convert to gamayun objects
__all__ = []

View File

@@ -1,4 +0,0 @@
# from .llm import LLMClient
# from .qdrant import QdrantClient
__all__ = []

View File

@@ -1,3 +0,0 @@
# TODO: implement connection to embeddings provider
__all__ = []

View File

@@ -1,3 +0,0 @@
# TODO: implement connection to openAI compatible endpoint
__all__ = []

View File

@@ -1,3 +0,0 @@
# TODO: implement qdrant connector
__all__ = []

View File

@@ -1,13 +0,0 @@
from .ui import GUI
def main():
mainGui = GUI()
# TODO: Initialize logging here
# TODO: Initialize llm client and qdrant client
# TODO: start main window
exit()
if __name__ == "__main__":
main()

View File

@@ -1,3 +0,0 @@
from .main_window import GUI
__all__ = ["GUI"]

View File

@@ -1,5 +0,0 @@
from PySide6.QtWidgets import QMainWindow
class GUI(QMainWindow):
def __init__(self):
pass

14
src/semeion/__init__.py Normal file
View File

@@ -0,0 +1,14 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
"""semeion semantic post-mortem forensics"""
__version__ = "0.0.0"
from .main import main
__all__ = ["main"]

View File

@@ -0,0 +1,12 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
# from .browserSqlite import BrowserSqliteEngine
# from .forensicImage import ForensicImageEngine
__all__ = []

View File

@@ -0,0 +1,12 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
# TODO: implement browser SQlite database converter -> semeion object
# adapt browser2timesketch.py
__all__ = []

View File

@@ -0,0 +1,11 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
# TODO: use pytsk to import forensic images and convert to semeion objects
__all__ = []

View File

@@ -0,0 +1,12 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
# from .llm import LLMClient
# from .qdrant import QdrantClient
__all__ = []

View File

@@ -0,0 +1,11 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
# TODO: implement connection to embeddings provider
__all__ = []

View File

@@ -0,0 +1,11 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
# TODO: implement connection to openAI compatible endpoint
__all__ = []

View File

@@ -0,0 +1,47 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
import dotenv
import openai
class LLMInterface():
"""
provide a custom interface along with special methods relevant with the semeion application
"""
def __init__(self):
openai.api_key = dotenv.get_key(".env", "LLM_API_KEY")
openai.base_url = dotenv.get_key(".env", "LLM_API_BASE")
print("CLNT:LLMCLIENT INITIALIZED:", dotenv.get_key(".env", "LLM_MODEL"), "@", dotenv.get_key(".env", "LLM_API_BASE"))
def simple_query(self, prompt: str):
try:
print("CLNT:LLMCLIENT CALL LLM", prompt)
if dotenv.get_key(".env", "LLM_MODEL") is None:
raise ValueError("CLNT:LLMCLIENT ERR: LLM MODEL not set")
else:
model = dotenv.get_key(".env", "LLM_MODEL")
response = openai.chat.completions.create(
model=str(model),
messages=[
{
"role": "user",
"content": prompt
},
{
"role": "assistant",
"content": "Hello! How are you doing today? Is there something I can help you with?"
}
],
timeout=30
)
return response
except Exception as e:
print(f"CLNT:LLMCLIENT ERR: {e}")
raise

View File

@@ -0,0 +1,11 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
# TODO: implement qdrant connector
__all__ = ["QdrantInterface"]

View File

@@ -0,0 +1,41 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from qdrant_client import QdrantClient
import dotenv
class QdrantInterface():
"""
provide a custom interface along with special methods relevant with the semeion application
"""
standard_vector_size = 1536
def __init__(self):
host = dotenv.get_key(".env", "QDRANT_HOST")
port_str = dotenv.get_key(".env", "QDRANT_PORT")
port = int(port_str) if port_str is not None else None
api_key = dotenv.get_key(".env", "QDRANT_API_KEY")
self.client = QdrantClient(host=host, port=port, api_key=api_key)
def get_collections(self):
collections_obj = self.client.get_collections()
collections = []
for c in collections_obj.collections:
collections.append(c.name)
return collections
def create_semeion_collection(self, collection_name: str):
self.client.create_collection(collection_name, vector_size=self.standard_vector_size) if not self.client.collection_exists(collection_name) else None
return self.client.collection_exists(collection_name)
def submit(self, payload):
pass
def check_health(self):
pass

20
src/semeion/main.py Normal file
View File

@@ -0,0 +1,20 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from .ui import MainWindow
from PySide6.QtWidgets import QApplication
def main():
app = QApplication()
gui = MainWindow()
gui.show()
exit(app.exec())
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,41 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from .semeionArtifact import SemeionArtifact, Actors, ActorsRole, Content, ChunkInfo, ContextGroup, ContextGroupType, Location, Ingestion, ArtifactClass
from .semeionSearchObject import SemeionSearchObject, Filters, Options, Interpretation
from .source_specific_models import AuthenticationEventMetadata, BrowserEventMetadata, DocumentMetadata, EmailMetadata, FileEventMetadata, MessageMetadata, NetworkEventMetadata, ProcessEventMetadata, RegistryEventMetadata, ScheduledTaskMetadata, SystemEventMetadata
__all__ = ["SemeionArtifact",
"Actors",
"ActorsRole",
"Content",
"ChunkInfo",
"ContextGroup",
"ContextGroupType",
"Location",
"Ingestion",
"ArtifactClass",
"ContextGroupType",
"Location",
"Ingestion",
"ArtifactClass",
"SemeionSearchObject",
"AuthenticationEventMetadata",
"BrowserEventMetadata",
"DocumentMetadata",
"EmailMetadata",
"FileEventMetadata",
"MessageMetadata",
"NetworkEventMetadata",
"ProcessEventMetadata",
"RegistryEventMetadata",
"ScheduledTaskMetadata",
"SystemEventMetadata",
"Filters",
"Options",
"Interpretation"]

View File

@@ -0,0 +1,224 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from datetime import datetime
from enum import Enum
from typing import Annotated, Union
from uuid import UUID
from pydantic import BaseModel, Field
class ArtifactClass(str, Enum):
"""
the general class of the artifact which determines wether it can be searched for or not.
this also affects how the artifact is processed during ingestion and search.
currently only major artifact classes will have semantic meaning associated to them
and are searcheable, while others will only be stored for timeline context display.
"""
# searchable artifact classes:
MESSAGE = "message"
BROWSER_EVENT = "browser_event"
EMAIL = "email"
DOCUMENT = "document"
# non-searchable artifact classes:
FILE_EVENT = "file_event"
PROCESS_EVENT = "process_event"
REGISTRY_EVENT = "registry_event"
SYSTEM_EVENT = "system_event"
NETWORK_EVENT = "network_event"
AUTHENTICATION_EVENT = "authentication_event"
SCHEDULED_TASK = "scheduled_task"
class ActorsRole(str, Enum):
"""
this will determine the role of the actor, which is mainly important for message filtering and later behavioral analysis.
"""
SENDER = "sender" # message, email
RECEIVER = "receiver" # message, email
PARTICIPANT = "participant" # document collaboration, chat groups?
CREATOR = "creator" # document, file system
OWNER = "owner" # filesystem objects
INITIATOR = "initiator" # browser events, filesystem, network events
TARGET = "target" # network events, authentication events
class Actors(BaseModel):
"""
the actor is any entity associated with some behavioral pattern.
"""
identifier: str # some unique identifier which is consistent across artifacts
display_name: str # human readeable, needs to be parsed properly by ingestion module
role: ActorsRole # see class
class Content(BaseModel):
"""
the most important field, this is what gets embedded.
"""
text: str
truncated: bool = False # this should never happen, but in some scenario it might be useful for debugging or edge cases
# not chunked: the chunking is handled at ingestion time and linked via chunk_info
class ChunkInfo(BaseModel):
"""
if an artifact is chunked, this contains information about the chunking.
"""
index: int # zero-based index of the chunk
total: int # total number of chunks
class ContextGroupType(str, Enum):
"""
some artifacts can be aggregated into context groups, if they are semantically liked
"""
THREAD = "thread" # email threads, message threads
SESSION = "session" # browser sessions, network sessions
# not process trees: they are inherently linked by parent_id references
# not file system directories: they are inherently linked by path and parent_id
# no chunks: they are inherently linked by chunk_info
class ContextGroup(BaseModel):
"""
some artifacts can be aggregated into context groups, if they are semantically liked
"""
type: ContextGroupType
id: str
class Location(BaseModel):
"""
any information about where the artifact was located.
"""
host: str | None = None # hostname or machine identifier
path: str | None = None # file path or resource path
url: str | None = None # URL of the resource
title: str | None = None # mostly there to provide something human readable for browser events
physical: str | None = None # physical location, e.g., GPS coordinates, if applicable
class Ingestion(BaseModel):
"""
metadata about the ingestion process.
"""
ingested_at: datetime
source_file: str # the original source file from which this artifact was ingested
ingestor_id: str # identifier of the ingestor used
ingestor_version: str # version of the ingestor used
# source-specifi classes are imported from submodules
from .source_specific_models import *
source_specific_models = Union[
DocumentMetadata,
EmailMetadata,
MessageMetadata,
FileEventMetadata,
NetworkEventMetadata,
ProcessEventMetadata,
RegistryEventMetadata,
ScheduledTaskMetadata,
SystemEventMetadata,
AuthenticationEventMetadata,
BrowserEventMetadata
]
class SemeionArtifact(BaseModel):
"""
standard artifact structure for the semeion application.
JSON representation:
{
"_schema_version": "1.1.0",
"id": "string (uuid-v5)",
"case_id": "string",
"searchable": "bool",
"artifact_class": "string (enum)",
"source_platform": "string",
"timestamp": "string (ISO8601 UTC)",
"actors": [
{
"identifier": "string",
"display_name": "string | null",
"role": "string (enum)"
}
],
"content": {
"text": "string",
"truncated": "bool"
},
"display_text": "string",
"indexed_entities": ["string"],
"parent_id": "string (uuid) | null",
"chunk_info": {
"index": "int",
"total": "int"
} | null,
"context_group": {
"type": "string (enum)",
"id": "string"
} | null,
"location": {
"host": "string | null",
"path": "string | null",
"url": "string | null",
"title": "string | null"
} | null,
"source_specific": {} | null,
"marked_interesting": "bool",
"marked_not_interesting": "bool",
"ingestion": {
"ingested_at": "string (ISO8601)",
"source_file": "string",
"parser_id": "string",
"parser_version": "string"
}
}
"""
_schema_version: str = "1.1.1"
id: UUID = Field(description="deterministic UUID v5 based on case_id, source_file, and unique key")
case_id: str = Field(description="case identifier this artifact belongs to")
searchable: bool = Field(description="searchable or only for timeline context")
artifact_class: ArtifactClass = Field(description="general class of the artifact")
source_platform: str = Field(description="source platform from which this artifact was ingested (sleuthkit, chromium SQLite, gecko SQLite, etc.)")
timestamp: datetime = Field(description="timestamp of the artifact in ISO8601 UTC format - the main timestamp which will be used for searching and timeline ordering")
actors: list[Actors] = Field(description="of actors associated with this artifact")
content: Content = Field(description="main content of the artifact which will be embedded for semantic search")
display_text: str = Field(description="human readable representation of the artifact content for displaying in the UI")
indexed_entities: list[str] = Field(description="list of entities from the artifact content, might use sparse vectors for hybrid search")
parent_id: UUID | None = Field(default=None, description="serves for any nested relationships, e.g., file system hierarchies, process trees, etc.")
chunk_info: ChunkInfo | None = Field(default=None, description="if the artifact content is chunked, this contains information about the chunking")
context_group: ContextGroup | None = Field(default=None, description="a parameter which provides information for semantically linked artifacts, e.g., email threads, browser sessions, etc.")
location: Location | None = Field(default=None, description="information about where the artifact was located, e.g., file path, URL, host, etc.")
source_specific: source_specific_models | None = Field(default=None, description="source-specific metadata for the artifact")
marked_interesting: bool = Field(description="user feedback flag marking this artifact as interesting")
marked_not_interesting: bool = Field(description="user feedback flag marking this artifact as not interesting")
ingestion: Ingestion = Field(description="metadata about the ingestion process")
class Config:
"""
this is necessary for pydantic
"""
use_enum_values = True
populate_by_name = True
validate_assignment = True
def is_searchable(self) -> bool:
if self.artifact_class in [ArtifactClass.MESSAGE, ArtifactClass.BROWSER_EVENT, ArtifactClass.EMAIL, ArtifactClass.DOCUMENT] and self.searchable:
return True
elif self.artifact_class in [ArtifactClass.MESSAGE, ArtifactClass.BROWSER_EVENT, ArtifactClass.EMAIL, ArtifactClass.DOCUMENT] and not self.searchable:
exception = f"Artifact {self.id} of class {self.artifact_class} is marked as non-searchable, but this class should always be searchable."
raise ValueError(exception)
else:
return False
def get_vector_payload(self) -> dict:
"""
submit to qdrant
"""
payload = self.model_dump(mode="json", by_alias=True)
return payload

View File

@@ -0,0 +1,92 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
# from .llm import LLMClient
# from .qdrant import QdrantClient
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, Field
class Filters(BaseModel):
"""
filters which can be applied to the search query to limit the search space.
all filters are optional and can be combined.
"""
case_ids: list[str] | None = Field(default=None, description="list of case IDs to limit the search to")
artifact_classes: list[str] | None = Field(default=None, description="list of artifact classes to limit the search to")
source_platforms: list[str] | None = Field(default=None, description="list of source platforms to limit the search to")
actor_identifiers: list[str] | None = Field(default=None, description="list of actor identifiers to limit the search to")
indexed_entities_any: list[str] | None = Field(default=None, description="list of indexed entities, any of which must be present in the artifact")
time_after: datetime | None = Field(default=None, description="only return artifacts after this timestamp (inclusive)")
time_before: datetime | None = Field(default=None, description="only return artifacts before this timestamp (inclusive)")
context_group_ids: list[str] | None = Field(default=None, description="list of context group IDs to limit the search to")
hosts: list[str] | None = Field(default=None, description="list of hostnames or IPs to limit the search to")
class Options(BaseModel):
"""
options to modify the search behavior.
"""
limit: int = Field(default=100, description="maximum number of search results to return")
min_score: float | None = Field(default=None, description="minimum similarity score threshold for returned results")
use_hybrid: bool = Field(default=False, description="whether to use hybrid search (semantic dense vectors + keyword (sparse vectors)")
class Interpretation(BaseModel):
"""
this data structure holds information about how the semantic query was interpreted by the generative LLM
"""
original_query: str = Field(description="the original user query before any modifications or interpretations")
notes: list[str] = Field(default=[], description="any notes or clarifications made by the LLM regarding the query interpretation")
class SemeionSearchObject(BaseModel):
"""
standard search object structure for the semeion application.
example JSON representation:
{
"_schema_version": "1.1.0",
"query_id": "string (uuid-v4)",
"created_at": "string (ISO8601)",
"semantic_query": "string",
"filters": {
"case_ids": ["string"] | null,
"artifact_classes": ["string"] | null,
"source_platforms": ["string"] | null,
"actor_identifiers": ["string"] | null,
"indexed_entities_any": ["string"] | null,
"time_after": "string (ISO8601)" | null,
"time_before": "string (ISO8601)" | null,
"context_group_ids": ["string"] | null,
"hosts": ["string"] | null
},
"options": {
"limit": "int",
"min_score": "float" | null,
"use_hybrid": "bool"
},
"interpretation": {
"original_query": "string",
"notes": ["string"]
}
}
"""
_schema_version: str = "1.1.0"
query_id: UUID = Field(description="UUID v4 identifying this search query")
created_at: datetime = Field(description="timestamp when the search query was created for audit")
semantic_query: str = Field(description="the search string which gets embedded and used for semantic search")
filters: Filters = Field(description="filters to limit the search space")
options: Options = Field(description="options to modify the search behavior")
interpretation: Interpretation = Field(description="information about how the semantic query was interpreted by the generative LLM")

View File

@@ -0,0 +1,21 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from .document_metadata import DocumentMetadata
from .email import EmailMetadata
from .message import MessageMetadata
from .file_event import FileEventMetadata
from .network_event import NetworkEventMetadata
from .process_event import ProcessEventMetadata
from .registry_event import RegistryEventMetadata
from .scheduled_task import ScheduledTaskMetadata
from .system_event import SystemEventMetadata
from .authentication_event import AuthenticationEventMetadata
from .browser_event import BrowserEventMetadata
__all__ = ["DocumentMetadata", "EmailMetadata", "MessageMetadata", "FileEventMetadata", "NetworkEventMetadata", "ProcessEventMetadata", "RegistryEventMetadata", "ScheduledTaskMetadata", "SystemEventMetadata", "AuthenticationEventMetadata", "BrowserEventMetadata"]

View File

@@ -0,0 +1,15 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from pydantic import BaseModel
class AuthenticationEventMetadata(BaseModel):
"""
metadata for authentication events, e.g. ssh logins, user logins, etc.
"""
pass

View File

@@ -0,0 +1,15 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from pydantic import BaseModel
class BrowserEventMetadata(BaseModel):
"""
metadata for browser events, e.g. history, bookmarks, downloads, etc.
"""
pass

View File

@@ -0,0 +1,15 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from pydantic import BaseModel
class DocumentMetadata(BaseModel):
"""
metadata for documents, e.g. PDFs, Word files, etc.
"""
pass

View File

@@ -0,0 +1,15 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from pydantic import BaseModel
class EmailMetadata(BaseModel):
"""
metadata for emails
"""
pass

View File

@@ -0,0 +1,15 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from pydantic import BaseModel
class FileEventMetadata(BaseModel):
"""
metadata for file events, e.g. file creation, modification, deletion, filetype, etc.
"""
pass

View File

@@ -0,0 +1,15 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from pydantic import BaseModel
class MessageMetadata(BaseModel):
"""
metadata for messages (emails, chats, etc.)
"""
pass

View File

@@ -0,0 +1,15 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from pydantic import BaseModel
class NetworkEventMetadata(BaseModel):
"""
metadata for network events, e.g. connections, data transfers, etc.
"""
pass

View File

@@ -0,0 +1,15 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from pydantic import BaseModel
class ProcessEventMetadata(BaseModel):
"""
metadata for process events, e.g. process start, stop, fork, etc.
"""
pass

View File

@@ -0,0 +1,15 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from pydantic import BaseModel
class RegistryEventMetadata(BaseModel):
"""
metadata for registry events, e.g. key creation, modification, deletion, etc.
"""
pass

View File

@@ -0,0 +1,15 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from pydantic import BaseModel
class ScheduledTaskMetadata(BaseModel):
"""
metadata for scheduled tasks, e.g. cron jobs, automated scripts, etc.
"""
pass

View File

@@ -0,0 +1,15 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from pydantic import BaseModel
class SystemEventMetadata(BaseModel):
"""
metadata for system events, e.g. system start, shutdown, errors, etc.
"""
pass

View File

@@ -0,0 +1,13 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from semeion.services.llm_service import LLMService
from semeion.services.qdrant_service import QdrantService
from semeion.services.localdb import DatabaseService, AppSettings
__all__ = ["LLMService", "QdrantService", "DatabaseService", "AppSettings"]

View File

@@ -0,0 +1,76 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from semeion.interfaces.llm.llm_client import LLMInterface
from PySide6.QtCore import QObject, Signal, Slot, QThread
class LLMWorker(QObject): # initializes once and is queried repeatedly
result = Signal(object)
error = Signal(str)
status = Signal(str)
def __init__(self):
super().__init__()
self.client = None
@Slot()
def initialize(self):
self.client = LLMInterface()
self.status.emit("LLM: ready")
@Slot(str)
def run_query(self, prompt: str):
if self.client is None:
self.error.emit("LLM client not initialized")
return
try:
self.status.emit("LLM: started")
raw_response = self.client.simple_query(prompt)
response = raw_response.choices[0].message.content or ""
self.status.emit("LLM: finished")
self.result.emit(response)
except Exception as e:
self.error.emit(str(e))
class LLMService(QObject):
"""persistent service"""
query_finished = Signal(object)
query_error = Signal(str)
query_status = Signal(str)
# signal to send to active worker
_do_query = Signal(str)
def __init__(self):
super().__init__()
self._thread = QThread()
self.worker = LLMWorker()
self.worker.moveToThread(self._thread)
self._thread.started.connect(self.worker.initialize)
self._do_query.connect(self.worker.run_query)
self.worker.result.connect(self.query_finished.emit)
self.worker.error.connect(self.query_error.emit)
self.worker.status.connect(self.query_status.emit)
self._thread.start()
@Slot(str)
def query(self, prompt: str):
self._do_query.emit(prompt) # start query via signal
def stop(self):
self._thread.quit()
self._thread.wait()
self.worker.deleteLater()
self._thread.deleteLater()

View File

@@ -0,0 +1,84 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
import sqlite3
import os
from dotenv import dotenv_values
DB_DIR = "app_database"
DB_FILE = os.path.join(DB_DIR, "management_db.sqlite")
class DatabaseService():
def __init__(self) -> None:
print("DBSV: INITIALIZE DATABASE")
self.conn = self.connect_db()
def connect_db(self) -> sqlite3.Connection:
os.makedirs(DB_DIR, exist_ok=True)
try:
conn = sqlite3.connect(DB_FILE)
print("DBSV: DATABASE AVAILABLE")
return conn
except sqlite3.Error as e:
raise
def check_exist(self) -> bool:
if os.path.exists(DB_FILE):
print("DBSV: DATABASE EXISTS")
return True
else:
return False
class AppSettings(DatabaseService):
def __init__(self)->None:
print("DBSV:APPSETTINGS: SETTINGS AVAILABLE")
self.conn = self.connect_db()
self.create_initial_configs()
def check_exist(self):
try:
with self.conn as conn:
cur = conn.cursor()
statement = """
SELECT key,val FROM settings;
"""
cur.execute(statement)
rows = cur.fetchall()
if len(rows)==6: # expecting 6 initial settings
print("DBSV:APPSETTINGS: SETTINGS EXIST")
return True
else:
return False
except sqlite3.DatabaseError as e:
print("DBSV: DB NOT FOUND. CREATING...")
return e
def create_initial_configs(self)->None:
if self.check_exist()==True:
return
else:
print("DBSV:APPSETTINGS: CREATING INITIAL SETTINGS")
initial_config =dotenv_values()
with self.conn as conn:
cur = conn.cursor()
statement = """
CREATE TABLE IF NOT EXISTS settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key text NOT NULL,
val text NOT NULL
);
"""
cur.execute(statement)
for key,val in initial_config.items():
task = (key,val)
statement = f"""
INSERT INTO settings(key,val) VALUES(?,?);
"""
cur.execute(statement, task)
conn.commit()

View File

@@ -0,0 +1,67 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from semeion.interfaces.qdrant.qdrant_client import QdrantInterface
from PySide6.QtCore import QObject, Signal, Slot, QThread
class QdrantWorker(QObject):
result = Signal(object)
error = Signal(str)
status = Signal(str)
def __init__(self):
super().__init__()
self.client = None
@Slot()
def initialize(self):
self.client = QdrantInterface()
self.status.emit("Qdrant: ready")
@Slot()
def get_collections(self):
try:
if self.client is None:
self.error.emit("Qdrant: Failed to establish connection to QDrant")
return
self.status.emit("Qdrant: fetching collections")
collections = self.client.get_collections()
self.status.emit("Qdrant: Successfully fetched collections")
self.result.emit(collections)
except Exception as e:
self.error.emit(str(e))
# Service = manages Worker + QThread, UI connects to this
class QdrantService(QObject):
query_finished = Signal(object)
query_error = Signal(str)
query_status = Signal(str)
_do_discover = Signal()
def __init__(self):
super().__init__()
self._thread = QThread()
self.worker =QdrantWorker()
self.worker.moveToThread(self._thread)
self._thread.started.connect(self.worker.initialize)
self._do_discover.connect(self.worker.get_collections)
self.worker.result.connect(self.query_finished.emit)
self.worker.error.connect(self.query_error.emit)
self.worker.status.connect(self.query_status.emit)
self._thread.start()
@Slot()
def discover_collections(self):
self._do_discover.emit()

View File

@@ -0,0 +1,10 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from .main_window import MainWindow
__all__ = ["MainWindow"]

View File

@@ -0,0 +1,145 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QLabel, QPushButton, QLineEdit, QMenu, QHBoxLayout
from PySide6.QtCore import Qt, Signal
from . import styling
from semeion.services import LLMService, QdrantService, DatabaseService, AppSettings
class MainWindow(QMainWindow):
main_exec_start = Signal(str)
def __init__(self):
super().__init__()
self.db = DatabaseService()
self.settings = AppSettings()
print("MAIN: CREATING SERVICES")
self.create_services()
print("MAIN: INITIALIZING GUI")
self.create_ui()
self.connect_ui_elements()
def create_ui(self):
self.setWindowTitle("Semeion Interface")
self.setGeometry(100, 100, 800, 600)
self.setStyleSheet(styling.main_window_style)
central_widget = QWidget()
self.setCentralWidget(central_widget)
self.main_layout = QVBoxLayout()
central_widget.setLayout(self.main_layout)
# Toolbar
self.create_menuBar()
self.conn_status_layout = QHBoxLayout()
conn_widget_qdrant = QLabel("Qdrant Interface available")
conn_widget_llm = QLabel("LLM Interface available")
conn_widget_embeddings = QLabel("EMB_PLACEHOLDER")
status_widgets = []
status_widgets.append(conn_widget_qdrant)
status_widgets.append(conn_widget_llm)
status_widgets.append(conn_widget_embeddings)
for w in status_widgets:
w.setStyleSheet(styling.status_widget_style)
self.conn_status_layout.addWidget(w)
self.main_layout.addLayout(self.conn_status_layout)
# spacing
self.main_layout.addStretch(1)
# branding
self.brandingLabel = QLabel("S E M E I O N\n")
self.brandingLabel.setStyleSheet("font-size: 32px; font-weight: bold; font-family: Cantarell;")
self.main_layout.addWidget(self.brandingLabel, alignment=Qt.AlignmentFlag.AlignCenter)
# search Input
self.searchInput = QLineEdit()
self.searchInput.setPlaceholderText("Natural Language Search...")
self.searchInput.setFixedWidth(400)
self.searchInput.setMinimumHeight(30)
self.searchInput.setStyleSheet(styling.input_style)
self.main_layout.addWidget(self.searchInput, alignment=Qt.AlignmentFlag.AlignCenter)
self.main_layout.addSpacing(10)
self.executeButton = QPushButton("Execute")
self.executeButton.setAttribute(Qt.WidgetAttribute.WA_Hover)
self.executeButton.setStyleSheet(styling.button_style)
self.main_layout.addWidget(self.executeButton, alignment=Qt.AlignmentFlag.AlignCenter)
self.main_layout.addStretch(1)
def create_menuBar(self):
menuBar = self.menuBar()
action_caseManager = menuBar.addAction("&Case Manager", self.open_case_manager)
action_caseManager.setStatusTip("Manage Cases")
action_ingestionManager = menuBar.addAction("&Ingestion Manager", self.open_ingestion_manager)
action_ingestionManager.setStatusTip("Ingest various Artifacts into dataset")
menuBar_settingsMenu = QMenu("&Settings", self)
action_connectionManager = menuBar_settingsMenu.addAction("&Manage Server Connections", self.open_connection_manager)
menuBar.addMenu(menuBar_settingsMenu)
menuBar_settingsMenu.setStatusTip("Application Settings")
menuBar_about = QMenu("&About", self)
action_aboutModal = menuBar_about.addAction("&About...", self.open_about_window)
menuBar.addMenu(menuBar_about)
menuBar.setStyleSheet(styling.toolbar_style)
def create_services(self):
self.llm_service = LLMService()
self.qdrant_service = QdrantService()
# Button -> executes LLM Service
self.main_exec_start.connect(self.llm_service.query)
# response handling
self.llm_service.query_finished.connect(self.handle_llm_response)
# connect qdrant signals
self.qdrant_service.query_finished.connect(self.handle_qdrant_response)
# Qdrant: execute collection discovery on startup
self.qdrant_service.discover_collections()
def connect_ui_elements(self):
self.searchInput.returnPressed.connect(self.execute_from_input)
self.executeButton.clicked.connect(self.execute_from_input)
def execute_from_input(self) -> str|None:
query = self.searchInput.text().strip()
if not query:
return
self.main_exec_start.emit(query)
self.executeButton.setDisabled(True)
self.searchInput.setDisabled(True)
self.reenable_list = [self.executeButton, self.searchInput]
print("MAIN: LLM QUERY SUBMIT:", query)
def handle_llm_response(self, response):
self.enable_input(self.reenable_list)
print("MAIN: LLM RESPONSE:", response)
def handle_qdrant_response(self, response):
print("MAIN: QDRANT COLLECTIONS:", response)
def enable_input(self, elements: list):
for e in elements:
e.setEnabled(True)
def open_case_manager(self):
print("clicked case manager")
def open_ingestion_manager(self):
print("clicked ingestion manager")
def open_connection_manager(self):
print("clicked open connection manager")
def open_about_window(self):
print("clicked open about")

View File

@@ -0,0 +1,75 @@
#
# Copyright (c) 2025, mstoeck3
# All rights reserved.
#
# This source code is licensed under the BSD-3-Clause license found in the
# LICENSE file in the root directory of this source tree.
#
## DISCLAIMER: Styles are mostly AI-generated
main_window_style = """
QWidget {
background-color: #f0f0f0;
font-family: Arial, sans-serif;
font-size: 14px;
color: #333;
}
"""
button_style = """
QPushButton {
padding: 8px 16px;
font-size: 16px;
border-radius: 12px;
background-color: #808080;
color: white;
border: 1px solid #6e6e6e;
}
QPushButton:hover {
background-color: #6e6e6e;
}
QPushButton:pressed {
background-color: #5e5e5e;
}
"""
input_style = """
QLineEdit {
border: 1px solid #ddd;
border-radius: 24px;
padding: 10px 20px;
font-size: 16px;
}
QLineEdit:focus {
border: 1px solid #aaa;
outline: none;
}
"""
toolbar_style = """
QMenuBar::item {
padding: 6px 12px;
border-radius: 4px;
}
QMenuBar::item:selected {
background-color: #e8e8e8;
}
QMenu {
border: 2px solid #c0c0c0;
border-radius: 6px;
padding: 6px 4px;
}
QMenu::item {
padding: 8px 24px 8px 12px;
border-radius: 4px;
margin: 2px;
}
QMenu::item:selected {
background-color: #e8e8e8;
}
"""
status_widget_style = """
"""

518
uv.lock generated Normal file
View File

@@ -0,0 +1,518 @@
version = 1
revision = 3
requires-python = "==3.13.*"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
name = "anyio"
version = "4.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" },
]
[[package]]
name = "certifi"
version = "2025.11.12"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "distro"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
]
[[package]]
name = "grpcio"
version = "1.76.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" },
{ url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" },
{ url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" },
{ url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" },
{ url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" },
{ url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" },
{ url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" },
{ url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" },
{ url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" },
{ url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "h2"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "hpack" },
{ name = "hyperframe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" },
]
[[package]]
name = "hpack"
version = "4.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[package.optional-dependencies]
http2 = [
{ name = "h2" },
]
[[package]]
name = "hyperframe"
version = "6.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" },
]
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "jiter"
version = "0.12.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" },
{ url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" },
{ url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" },
{ url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" },
{ url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" },
{ url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" },
{ url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" },
{ url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" },
{ url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" },
{ url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" },
{ url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" },
{ url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" },
{ url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" },
{ url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" },
{ url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" },
{ url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" },
{ url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" },
{ url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" },
]
[[package]]
name = "numpy"
version = "2.3.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" },
{ url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" },
{ url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" },
{ url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" },
{ url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" },
{ url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" },
{ url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" },
{ url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" },
{ url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" },
{ url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" },
{ url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" },
{ url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" },
{ url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" },
{ url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" },
{ url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" },
{ url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" },
{ url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" },
{ url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" },
{ url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" },
{ url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" },
{ url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" },
{ url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" },
]
[[package]]
name = "openai"
version = "2.8.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
{ name = "httpx" },
{ name = "jiter" },
{ name = "pydantic" },
{ name = "sniffio" },
{ name = "tqdm" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d5/e4/42591e356f1d53c568418dc7e30dcda7be31dd5a4d570bca22acb0525862/openai-2.8.1.tar.gz", hash = "sha256:cb1b79eef6e809f6da326a7ef6038719e35aa944c42d081807bfa1be8060f15f", size = 602490, upload-time = "2025-11-17T22:39:59.549Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/55/4f/dbc0c124c40cb390508a82770fb9f6e3ed162560181a85089191a851c59a/openai-2.8.1-py3-none-any.whl", hash = "sha256:c6c3b5a04994734386e8dad3c00a393f56d3b68a27cd2e8acae91a59e4122463", size = 1022688, upload-time = "2025-11-17T22:39:57.675Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
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 = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "portalocker"
version = "3.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pywin32", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" },
]
[[package]]
name = "protobuf"
version = "6.33.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0a/03/a1440979a3f74f16cab3b75b0da1a1a7f922d56a8ddea96092391998edc0/protobuf-6.33.1.tar.gz", hash = "sha256:97f65757e8d09870de6fd973aeddb92f85435607235d20b2dfed93405d00c85b", size = 443432, upload-time = "2025-11-13T16:44:18.895Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/06/f1/446a9bbd2c60772ca36556bac8bfde40eceb28d9cc7838755bc41e001d8f/protobuf-6.33.1-cp310-abi3-win32.whl", hash = "sha256:f8d3fdbc966aaab1d05046d0240dd94d40f2a8c62856d41eaa141ff64a79de6b", size = 425593, upload-time = "2025-11-13T16:44:06.275Z" },
{ url = "https://files.pythonhosted.org/packages/a6/79/8780a378c650e3df849b73de8b13cf5412f521ca2ff9b78a45c247029440/protobuf-6.33.1-cp310-abi3-win_amd64.whl", hash = "sha256:923aa6d27a92bf44394f6abf7ea0500f38769d4b07f4be41cb52bd8b1123b9ed", size = 436883, upload-time = "2025-11-13T16:44:09.222Z" },
{ url = "https://files.pythonhosted.org/packages/cd/93/26213ff72b103ae55bb0d73e7fb91ea570ef407c3ab4fd2f1f27cac16044/protobuf-6.33.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:fe34575f2bdde76ac429ec7b570235bf0c788883e70aee90068e9981806f2490", size = 427522, upload-time = "2025-11-13T16:44:10.475Z" },
{ url = "https://files.pythonhosted.org/packages/c2/32/df4a35247923393aa6b887c3b3244a8c941c32a25681775f96e2b418f90e/protobuf-6.33.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:f8adba2e44cde2d7618996b3fc02341f03f5bc3f2748be72dc7b063319276178", size = 324445, upload-time = "2025-11-13T16:44:11.869Z" },
{ url = "https://files.pythonhosted.org/packages/8e/d0/d796e419e2ec93d2f3fa44888861c3f88f722cde02b7c3488fcc6a166820/protobuf-6.33.1-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:0f4cf01222c0d959c2b399142deb526de420be8236f22c71356e2a544e153c53", size = 339161, upload-time = "2025-11-13T16:44:12.778Z" },
{ url = "https://files.pythonhosted.org/packages/1d/2a/3c5f05a4af06649547027d288747f68525755de692a26a7720dced3652c0/protobuf-6.33.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:8fd7d5e0eb08cd5b87fd3df49bc193f5cfd778701f47e11d127d0afc6c39f1d1", size = 323171, upload-time = "2025-11-13T16:44:14.035Z" },
{ url = "https://files.pythonhosted.org/packages/08/b4/46310463b4f6ceef310f8348786f3cff181cea671578e3d9743ba61a459e/protobuf-6.33.1-py3-none-any.whl", hash = "sha256:d595a9fd694fdeb061a62fbe10eb039cc1e444df81ec9bb70c7fc59ebcb1eafa", size = 170477, upload-time = "2025-11-13T16:44:17.633Z" },
]
[[package]]
name = "pydantic"
version = "2.12.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
]
[[package]]
name = "pydantic-core"
version = "2.41.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pyside6"
version = "6.8.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyside6-addons" },
{ name = "pyside6-essentials" },
{ name = "shiboken6" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/5f/dc408fb62a99d20e30f930f4de60c7d5c257d25a97baab600dc430f859bf/PySide6-6.8.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:31f390c961b54067ae41360e5ea3b340ce0e0e5feadea2236c28226d3b37edcc", size = 553886, upload-time = "2025-03-27T12:10:10.147Z" },
{ url = "https://files.pythonhosted.org/packages/f0/00/67c41f7280ed9d1c53a50bdaa5a6050134875341f0be96a58d329fe71ade/PySide6-6.8.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:8e53e2357bfbdee1fa86c48312bf637460a2c26d49e7af0b3fae2e179ccc7052", size = 554788, upload-time = "2025-03-27T12:10:12.514Z" },
{ url = "https://files.pythonhosted.org/packages/99/80/79df5af0f309f75297054caf127e82c6c4e6c3cd237a8abd428d18fe3a16/PySide6-6.8.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:5bf5153cab9484629315f57c56a9ad4b7d075b4dd275f828f7549abf712c590b", size = 554785, upload-time = "2025-03-27T12:10:14.054Z" },
{ url = "https://files.pythonhosted.org/packages/ce/b0/238abab3e005fff130a455ba07e8f1323e624b14e41f70f2f62c6bdc2601/PySide6-6.8.3-cp39-abi3-win_amd64.whl", hash = "sha256:722dc0061d8ef6dbb8c0b99864f21e83a5b49ece1ecb2d0b890840d969e1e461", size = 561080, upload-time = "2025-03-27T12:10:15.467Z" },
]
[[package]]
name = "pyside6-addons"
version = "6.8.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyside6-essentials" },
{ name = "shiboken6" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/23/fcd006128ecac40284fc218f804a46c52ebc1ad5995f3edb548d296f0d39/PySide6_Addons-6.8.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:ea46649e40b9e6ab11a0da2da054d3914bff5607a5882885e9c3bc2eef200036", size = 302447038, upload-time = "2025-03-27T12:11:29.671Z" },
{ url = "https://files.pythonhosted.org/packages/49/93/e7c743e7a01e66f22cac4133c320832700b9d15a43d9a46dbd067cc1877e/PySide6_Addons-6.8.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6983d3b01fad53637bad5360930d5923509c744cc39704f9c1190eb9934e33da", size = 160547187, upload-time = "2025-03-27T12:12:13.027Z" },
{ url = "https://files.pythonhosted.org/packages/c7/5b/51217c90f9292651ef39ec3179bcb7797db1481a8f61138378720b1b91bc/PySide6_Addons-6.8.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:7949a844a40ee10998eb2734e2c06c4c7182dfcd4c21cc4108a6b96655ebe59f", size = 156291508, upload-time = "2025-03-27T12:12:52.071Z" },
{ url = "https://files.pythonhosted.org/packages/77/e8/bbe84519355814166bbb7e867dfc8c7ea0044827d64b6b11c29a7b1a7424/PySide6_Addons-6.8.3-cp39-abi3-win_amd64.whl", hash = "sha256:67548f6db11f4e1b7e4b6efd9c3fc2e8d275188a7b2feac388961128572a6955", size = 127865208, upload-time = "2025-03-27T12:13:24.828Z" },
]
[[package]]
name = "pyside6-essentials"
version = "6.8.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "shiboken6" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/ad/74/082381f996baea3d6716d542c2184cb031e63f1cb6d4edca871fe82dd787/PySide6_Essentials-6.8.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:aa56c135db924ecfaf50088baf32f737d28027419ca5fee67c0c7141b29184e3", size = 134189733, upload-time = "2025-03-27T12:14:00.369Z" },
{ url = "https://files.pythonhosted.org/packages/f0/b2/3205336262bf88d57f01503df81ede2a0b1eecbb2a7d58978a5e5625f7c1/PySide6_Essentials-6.8.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fd57fa0c886ef99b3844173322c0023ec77cc946a0c9a0cdfbc2ac5c511053c1", size = 95088226, upload-time = "2025-03-27T12:14:25.965Z" },
{ url = "https://files.pythonhosted.org/packages/c9/e9/c5c48563c0436465356dbef44ef0396713c1194c37c7bb4d75c83414b90c/PySide6_Essentials-6.8.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:b4f4823f870b5bed477d6f7b6a3041839b859f70abfd703cf53208c73c2fe4cd", size = 92966444, upload-time = "2025-03-27T12:14:50.946Z" },
{ url = "https://files.pythonhosted.org/packages/8e/0f/5d8c6da7586e57ee032643e0c0e62335ef1a1add1a980160ddd1654f1d8d/PySide6_Essentials-6.8.3-cp39-abi3-win_amd64.whl", hash = "sha256:3c0fae5550aff69f2166f46476c36e0ef56ce73d84829eac4559770b0c034b07", size = 72191029, upload-time = "2025-03-27T12:15:10.425Z" },
]
[[package]]
name = "pytest"
version = "9.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" },
]
[[package]]
name = "python-dotenv"
version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
]
[[package]]
name = "pywin32"
version = "311"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
{ url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
{ url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
]
[[package]]
name = "qdrant-client"
version = "1.16.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "grpcio" },
{ name = "httpx", extra = ["http2"] },
{ name = "numpy" },
{ name = "portalocker" },
{ name = "protobuf" },
{ name = "pydantic" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d9/68/fec3816a223c0b73b0e0036460be45c61ce2770ffb9197ac371e4f615ddc/qdrant_client-1.16.1.tar.gz", hash = "sha256:676c7c10fd4d4cb2981b8fcb32fd764f5f661b04b7334d024034d07212f971fd", size = 332130, upload-time = "2025-11-25T04:31:54.212Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/e2/60a20d04b0595c641516463168909c5bbcc192d3d6eacb637c1677109c6a/qdrant_client-1.16.1-py3-none-any.whl", hash = "sha256:1eefe89f66e8a468ba0de1680e28b441e69825cfb62e8fb2e457c15e24ce5e3b", size = 378481, upload-time = "2025-11-25T04:31:52.629Z" },
]
[[package]]
name = "semeion"
version = "0.0.0"
source = { editable = "." }
dependencies = [
{ name = "openai" },
{ name = "pyside6" },
{ name = "python-dotenv" },
{ name = "qdrant-client" },
]
[package.optional-dependencies]
dev = [
{ name = "pytest" },
]
[package.metadata]
requires-dist = [
{ name = "openai", specifier = "~=2.8.1" },
{ name = "pyside6", specifier = "~=6.8.0" },
{ name = "pytest", marker = "extra == 'dev'" },
{ name = "python-dotenv", specifier = "~=1.2.1" },
{ name = "qdrant-client", specifier = "~=1.16.1" },
]
provides-extras = ["dev"]
[[package]]
name = "shiboken6"
version = "6.8.3"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/89/162c763f2799786e16f9045aa599dcf126e0f5f6a517f1e0251ba0be17e6/shiboken6-6.8.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:483efc7dd53c69147b8a8ade71f7619c79ffc683efcb1dc4f4cb6c40bb23d29b", size = 402402, upload-time = "2025-03-27T12:07:27.796Z" },
{ url = "https://files.pythonhosted.org/packages/76/a1/f1958c9d00176044ab00464cd89b6969ef3a7d2ed12d316ff1eda3dec88f/shiboken6-6.8.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:295a003466ca2cccf6660e2f2ceb5e6cef4af192a48a196a32d46b6f0c9ec5cb", size = 204730, upload-time = "2025-03-27T12:07:29.773Z" },
{ url = "https://files.pythonhosted.org/packages/a8/8d/48ba0b33953592e54b688f2dceb06274f419f7743a3ea0f4e48faf837652/shiboken6-6.8.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:2b1a41348102952d2a5fbf3630bddd4d44112e18058b5e4cf505e51f2812429d", size = 201025, upload-time = "2025-03-27T12:07:31.272Z" },
{ url = "https://files.pythonhosted.org/packages/aa/29/e77f3fba5337f2d98f977eece0fd24cda6cf7d83be2699514d2ca1d34f6c/shiboken6-6.8.3-cp39-abi3-win_amd64.whl", hash = "sha256:bca3a94513ce9242f7d4bbdca902072a1631888e0aa3a8711a52cc5dbe93588f", size = 1150998, upload-time = "2025-03-27T12:07:33.38Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "tqdm"
version = "4.67.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
name = "urllib3"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]