feat: ollama, embedings and more
This commit is contained in:
+2
-6
@@ -3,18 +3,14 @@
|
|||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
.tmp/
|
||||||
.venv/
|
.venv/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
# Agent runtime
|
# Agent runtime
|
||||||
koboldcpp.exe
|
|
||||||
model.gguf
|
|
||||||
*.gguf
|
|
||||||
data/
|
data/
|
||||||
src/LocalDiplomacy.Agent/data/
|
|
||||||
src/LocalDiplomacy.Agent/koboldcpp.exe
|
|
||||||
src/LocalDiplomacy.Agent/model.gguf
|
|
||||||
src/LocalDiplomacy.Agent/config.yaml
|
src/LocalDiplomacy.Agent/config.yaml
|
||||||
|
src/LocalDiplomacy.Agent/data/
|
||||||
|
|
||||||
# .NET/Bannerlord build output
|
# .NET/Bannerlord build output
|
||||||
bin/
|
bin/
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
22432
|
||||||
@@ -2,31 +2,30 @@ server:
|
|||||||
host: "127.0.0.1"
|
host: "127.0.0.1"
|
||||||
port: 8766
|
port: 8766
|
||||||
|
|
||||||
koboldcpp:
|
ollama:
|
||||||
autostart: true
|
base_url: "http://127.0.0.1:11434"
|
||||||
executable_path: "./koboldcpp.exe"
|
|
||||||
model_path: "./model.gguf"
|
|
||||||
base_url: "http://127.0.0.1:5001"
|
|
||||||
chat_path: "/v1/chat/completions"
|
chat_path: "/v1/chat/completions"
|
||||||
model: "local-model"
|
model: "llama3.1:8b"
|
||||||
port: 5001
|
|
||||||
context_size: 8192
|
|
||||||
extra_args:
|
|
||||||
- "--jinja"
|
|
||||||
- "--jinjatools"
|
|
||||||
startup_timeout_seconds: 180
|
|
||||||
timeout_seconds: 120
|
timeout_seconds: 120
|
||||||
tool_mode: "openai_tools"
|
auto_pull_models: true
|
||||||
json_repair_retry: true
|
|
||||||
|
|
||||||
memory:
|
memory:
|
||||||
provider: "disabled"
|
provider: "sqlite"
|
||||||
vector_store: "qdrant"
|
sqlite_path: "./data/localdiplomacy.sqlite3"
|
||||||
qdrant_host: "127.0.0.1"
|
embedding_provider: "ollama"
|
||||||
qdrant_port: 6333
|
embedding_model: "nomic-embed-text"
|
||||||
collection: "localdiplomacy_memories"
|
embedding_auto_pull: true
|
||||||
embedder_provider: "ollama"
|
max_prompt_memories: 8
|
||||||
embedder_model: "nomic-embed-text"
|
|
||||||
|
vector_index:
|
||||||
|
mode: "embedded"
|
||||||
|
path: "./data/qdrant"
|
||||||
|
host: "127.0.0.1"
|
||||||
|
port: 6333
|
||||||
|
executable_path: "./qdrant/qdrant.exe"
|
||||||
|
autostart: false
|
||||||
|
startup_timeout_seconds: 30
|
||||||
|
fallback_mode: "disabled"
|
||||||
|
|
||||||
event_log:
|
event_log:
|
||||||
sqlite_path: "./data/localdiplomacy_events.sqlite3"
|
sqlite_path: "./data/localdiplomacy_events.sqlite3"
|
||||||
@@ -35,5 +34,5 @@ generation:
|
|||||||
temperature: 0.7
|
temperature: 0.7
|
||||||
max_tokens: 800
|
max_tokens: 800
|
||||||
# Useful for Qwen3-style thinking models when tool calls must be machine-readable.
|
# Useful for Qwen3-style thinking models when tool calls must be machine-readable.
|
||||||
suppress_thinking: false
|
suppress_thinking: true
|
||||||
suppress_thinking_token: "/no_think"
|
suppress_thinking_token: "/no_think"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
@@ -24,19 +24,27 @@ from .contracts import (
|
|||||||
WorldTickResponse,
|
WorldTickResponse,
|
||||||
)
|
)
|
||||||
from .event_log import EventLog
|
from .event_log import EventLog
|
||||||
from .koboldcpp_client import KoboldCppClient
|
from .embeddings import create_embedder
|
||||||
from .koboldcpp_process import KoboldCppProcess
|
|
||||||
from .memory import MemoryStore
|
from .memory import MemoryStore
|
||||||
|
from .ollama_client import OllamaClient
|
||||||
from .tools import ToolRegistry
|
from .tools import ToolRegistry
|
||||||
|
from .vector_index import VectorIndex
|
||||||
|
|
||||||
|
|
||||||
class AppState:
|
class AppState:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.config = load_config()
|
self.config = load_config()
|
||||||
self.event_log = EventLog(resolve_runtime_path(self.config.event_log.sqlite_path))
|
self.event_log = EventLog(resolve_runtime_path(self.config.event_log.sqlite_path))
|
||||||
self.memory = MemoryStore(self.config.memory)
|
self.embedder = create_embedder(
|
||||||
self.kobold_process = KoboldCppProcess(self.config.koboldcpp, Path.cwd())
|
self.config.memory.embedding_provider,
|
||||||
self.kobold_client = KoboldCppClient(self.config)
|
self.config.memory.embedding_model,
|
||||||
|
base_url=self.config.ollama.base_url,
|
||||||
|
timeout_seconds=self.config.ollama.timeout_seconds,
|
||||||
|
auto_pull=self.config.memory.embedding_auto_pull,
|
||||||
|
)
|
||||||
|
self.vector_index = VectorIndex(self.config.vector_index, self.embedder)
|
||||||
|
self.memory = MemoryStore(self.config.memory, self.vector_index)
|
||||||
|
self.llm_client = OllamaClient(self.config)
|
||||||
self.recent_errors: list[str] = []
|
self.recent_errors: list[str] = []
|
||||||
self.ui_logs: list[dict[str, str]] = []
|
self.ui_logs: list[dict[str, str]] = []
|
||||||
|
|
||||||
@@ -56,17 +64,18 @@ state = AppState()
|
|||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(_: FastAPI):
|
async def lifespan(_: FastAPI):
|
||||||
|
state.vector_index.initialize()
|
||||||
state.memory.initialize()
|
state.memory.initialize()
|
||||||
state.log("info", "LocalDiplomacy.Agent starting.")
|
state.log("info", "LocalDiplomacy.Agent starting.")
|
||||||
try:
|
if state.llm_client.is_reachable():
|
||||||
state.kobold_process.ensure_started()
|
state.log("info", f"Ollama endpoint configured as {state.config.ollama.base_url}.")
|
||||||
state.log("info", f"KoboldCpp endpoint configured as {state.config.koboldcpp.base_url}.")
|
else:
|
||||||
except Exception as exc: # Keep service up so /health can explain the problem.
|
message = state.llm_client.last_error or f"Ollama is unreachable at {state.config.ollama.base_url}."
|
||||||
state.recent_errors.append(str(exc))
|
state.recent_errors.append(message)
|
||||||
state.log("error", str(exc))
|
state.log("error", message)
|
||||||
yield
|
yield
|
||||||
state.log("info", "LocalDiplomacy.Agent stopping.")
|
state.log("info", "LocalDiplomacy.Agent stopping.")
|
||||||
state.kobold_process.stop()
|
state.vector_index.stop()
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(title="LocalDiplomacy.Agent", version=__version__, lifespan=lifespan)
|
app = FastAPI(title="LocalDiplomacy.Agent", version=__version__, lifespan=lifespan)
|
||||||
@@ -91,12 +100,12 @@ def dashboard_state() -> JSONResponse:
|
|||||||
"version": __version__,
|
"version": __version__,
|
||||||
"health": health_data,
|
"health": health_data,
|
||||||
"debug": debug_data,
|
"debug": debug_data,
|
||||||
"koboldcpp": {
|
"llm": {
|
||||||
"base_url": state.config.koboldcpp.base_url,
|
"provider": "ollama",
|
||||||
"chat_path": state.config.koboldcpp.chat_path,
|
"base_url": state.config.ollama.base_url,
|
||||||
"model": state.config.koboldcpp.model,
|
"chat_path": state.config.ollama.chat_path,
|
||||||
"autostart": state.config.koboldcpp.autostart,
|
"model": state.config.ollama.model,
|
||||||
"timeout_seconds": state.config.koboldcpp.timeout_seconds,
|
"timeout_seconds": state.config.ollama.timeout_seconds,
|
||||||
},
|
},
|
||||||
"generation": state.config.generation.model_dump(mode="json"),
|
"generation": state.config.generation.model_dump(mode="json"),
|
||||||
"logs": state.ui_logs[-100:],
|
"logs": state.ui_logs[-100:],
|
||||||
@@ -104,48 +113,45 @@ def dashboard_state() -> JSONResponse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/koboldcpp")
|
@app.post("/api/ollama")
|
||||||
async def update_koboldcpp_settings(request: Request) -> JSONResponse:
|
async def update_ollama_settings(request: Request) -> JSONResponse:
|
||||||
payload = await request.json()
|
payload = await request.json()
|
||||||
base_url = str(payload.get("base_url") or "").strip().rstrip("/")
|
base_url = str(payload.get("base_url") or "").strip().rstrip("/")
|
||||||
model = str(payload.get("model") or "").strip()
|
model = str(payload.get("model") or "").strip()
|
||||||
timeout_seconds = payload.get("timeout_seconds")
|
timeout_seconds = payload.get("timeout_seconds")
|
||||||
autostart = payload.get("autostart")
|
|
||||||
|
|
||||||
if not base_url.startswith(("http://", "https://")):
|
if not base_url.startswith(("http://", "https://")):
|
||||||
return JSONResponse({"ok": False, "error": "KoboldCpp API URL must start with http:// or https://."}, status_code=400)
|
return JSONResponse({"ok": False, "error": "Ollama API URL must start with http:// or https://."}, status_code=400)
|
||||||
|
|
||||||
state.config.koboldcpp.base_url = base_url
|
state.config.ollama.base_url = base_url
|
||||||
if model:
|
if model:
|
||||||
state.config.koboldcpp.model = model
|
state.config.ollama.model = model
|
||||||
if isinstance(timeout_seconds, (int, float)) and timeout_seconds > 0:
|
if isinstance(timeout_seconds, (int, float)) and timeout_seconds > 0:
|
||||||
state.config.koboldcpp.timeout_seconds = int(timeout_seconds)
|
state.config.ollama.timeout_seconds = int(timeout_seconds)
|
||||||
if isinstance(autostart, bool):
|
|
||||||
state.config.koboldcpp.autostart = autostart
|
|
||||||
|
|
||||||
save_config(state.config)
|
save_config(state.config)
|
||||||
state.log("info", f"KoboldCpp API settings updated: {state.config.koboldcpp.base_url}, model {state.config.koboldcpp.model}.")
|
state.log("info", f"Ollama API settings updated: {state.config.ollama.base_url}, model {state.config.ollama.model}.")
|
||||||
return JSONResponse({"ok": True})
|
return JSONResponse({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/koboldcpp/ping")
|
@app.post("/api/ollama/ping")
|
||||||
def ping_koboldcpp() -> JSONResponse:
|
def ping_ollama() -> JSONResponse:
|
||||||
reachable = state.kobold_process.is_reachable()
|
reachable = state.llm_client.is_reachable()
|
||||||
state.log("info" if reachable else "warning", f"KoboldCpp ping {'succeeded' if reachable else 'failed'} for {state.config.koboldcpp.base_url}.")
|
state.log("info" if reachable else "warning", f"Ollama ping {'succeeded' if reachable else 'failed'} for {state.config.ollama.base_url}.")
|
||||||
return JSONResponse({"ok": reachable, "base_url": state.config.koboldcpp.base_url})
|
return JSONResponse({"ok": reachable, "base_url": state.config.ollama.base_url})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health", response_model=HealthResponse)
|
@app.get("/health", response_model=HealthResponse)
|
||||||
def health() -> HealthResponse:
|
def health() -> HealthResponse:
|
||||||
errors = list(state.recent_errors[-5:])
|
errors = list(state.recent_errors[-5:])
|
||||||
kobold = "reachable" if state.kobold_process.is_reachable() else "unreachable"
|
llm = "reachable" if state.llm_client.is_reachable() else "unreachable"
|
||||||
if kobold == "unreachable":
|
if llm == "unreachable" and state.llm_client.last_error:
|
||||||
errors.extend(state.kobold_process.validate_runtime_files())
|
errors.append(state.llm_client.last_error)
|
||||||
status = "ok" if not errors and kobold == "reachable" else "degraded"
|
status = "ok" if not errors and llm == "reachable" else "degraded"
|
||||||
return HealthResponse(
|
return HealthResponse(
|
||||||
status=status,
|
status=status,
|
||||||
agent_version=__version__,
|
agent_version=__version__,
|
||||||
koboldcpp=kobold,
|
llm=llm,
|
||||||
memory=state.memory.status,
|
memory=state.memory.status,
|
||||||
event_log="ok",
|
event_log="ok",
|
||||||
errors=errors,
|
errors=errors,
|
||||||
@@ -155,7 +161,7 @@ def health() -> HealthResponse:
|
|||||||
@app.get("/debug/status", response_model=DebugStatusResponse)
|
@app.get("/debug/status", response_model=DebugStatusResponse)
|
||||||
def debug_status() -> DebugStatusResponse:
|
def debug_status() -> DebugStatusResponse:
|
||||||
return DebugStatusResponse(
|
return DebugStatusResponse(
|
||||||
last_koboldcpp_latency_ms=state.kobold_client.last_latency_ms,
|
last_llm_latency_ms=state.llm_client.last_latency_ms,
|
||||||
memory_count_estimate=state.memory.count_estimate(),
|
memory_count_estimate=state.memory.count_estimate(),
|
||||||
queued_action_count=0,
|
queued_action_count=0,
|
||||||
recent_errors=state.recent_errors[-10:],
|
recent_errors=state.recent_errors[-10:],
|
||||||
@@ -171,7 +177,17 @@ async def conversation_respond(request: ConversationRequest) -> ConversationResp
|
|||||||
campaign_id=request.campaign_id,
|
campaign_id=request.campaign_id,
|
||||||
turn_id=request.turn_id,
|
turn_id=request.turn_id,
|
||||||
)
|
)
|
||||||
registry = ToolRegistry(state.memory, state.event_log)
|
registry = ToolRegistry(
|
||||||
|
state.memory,
|
||||||
|
state.event_log,
|
||||||
|
{
|
||||||
|
"save_id": request.save_id,
|
||||||
|
"campaign_id": request.campaign_id,
|
||||||
|
"character_id": request.npc.id,
|
||||||
|
"kingdom_id": request.npc.kingdom_id,
|
||||||
|
"player_id": request.player.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
tool_schemas = _select_tool_schemas(request, registry)
|
tool_schemas = _select_tool_schemas(request, registry)
|
||||||
forced_tool_name = _forced_tool_name(tool_schemas)
|
forced_tool_name = _forced_tool_name(tool_schemas)
|
||||||
messages = _build_action_messages(request) if forced_tool_name else _build_messages(request)
|
messages = _build_action_messages(request) if forced_tool_name else _build_messages(request)
|
||||||
@@ -235,6 +251,9 @@ def _build_messages(request: ConversationRequest) -> list[dict[str, str]]:
|
|||||||
"Stay grounded in the supplied game state. Do not invent entity IDs. "
|
"Stay grounded in the supplied game state. Do not invent entity IDs. "
|
||||||
"Use the provided tools only through the API tool-calling mechanism. "
|
"Use the provided tools only through the API tool-calling mechanism. "
|
||||||
"Never write JSON arrays, XML tags, pseudo-functions, or tool calls in your visible message content. "
|
"Never write JSON arrays, XML tags, pseudo-functions, or tool calls in your visible message content. "
|
||||||
|
"Never explain your reasoning, compare candidate replies, or mention the game state packet. "
|
||||||
|
"Write exactly one concise line of NPC dialogue in the NPC's voice. "
|
||||||
|
"Do not write the player's response. "
|
||||||
"If an action is needed, call propose_game_action. Otherwise, answer as the NPC in natural language. "
|
"If an action is needed, call propose_game_action. Otherwise, answer as the NPC in natural language. "
|
||||||
"Proposed actions are requests only; the game validates them before anything happens."
|
"Proposed actions are requests only; the game validates them before anything happens."
|
||||||
)
|
)
|
||||||
@@ -256,7 +275,7 @@ def _build_messages(request: ConversationRequest) -> list[dict[str, str]]:
|
|||||||
"role": "user",
|
"role": "user",
|
||||||
"content": (
|
"content": (
|
||||||
"Game state packet follows as JSON. Use it as context, then respond to the player. "
|
"Game state packet follows as JSON. Use it as context, then respond to the player. "
|
||||||
"Visible content must be ordinary NPC dialogue, not JSON.\n\n"
|
"Visible content must be exactly one ordinary NPC dialogue line, not JSON, analysis, or alternatives.\n\n"
|
||||||
f"{json.dumps(packet, ensure_ascii=False)}"
|
f"{json.dumps(packet, ensure_ascii=False)}"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -394,7 +413,7 @@ async def _run_tool_loop(
|
|||||||
tool_choice: str | dict[str, Any] | None = None
|
tool_choice: str | dict[str, Any] | None = None
|
||||||
if index == 0 and forced_tool_name:
|
if index == 0 and forced_tool_name:
|
||||||
tool_choice = {"type": "function", "function": {"name": forced_tool_name}}
|
tool_choice = {"type": "function", "function": {"name": forced_tool_name}}
|
||||||
data = await state.kobold_client.chat(messages, tool_schemas, tool_choice=tool_choice) or {}
|
data = await state.llm_client.chat(messages, tool_schemas, tool_choice=tool_choice) or {}
|
||||||
choices = data.get("choices") or [{}]
|
choices = data.get("choices") or [{}]
|
||||||
choice = choices[0] or {}
|
choice = choices[0] or {}
|
||||||
message = choice.get("message") or {}
|
message = choice.get("message") or {}
|
||||||
@@ -403,7 +422,7 @@ async def _run_tool_loop(
|
|||||||
content = str(message.get("content") or "").strip()
|
content = str(message.get("content") or "").strip()
|
||||||
if _looks_like_pseudo_tool_content(content):
|
if _looks_like_pseudo_tool_content(content):
|
||||||
return await _repair_visible_response(messages, content)
|
return await _repair_visible_response(messages, content)
|
||||||
return content
|
return _clean_visible_response(content)
|
||||||
|
|
||||||
messages.append(message)
|
messages.append(message)
|
||||||
for call in tool_calls:
|
for call in tool_calls:
|
||||||
@@ -431,6 +450,59 @@ def _looks_like_pseudo_tool_content(content: str) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_visible_response(content: str) -> str:
|
||||||
|
cleaned = _strip_reasoning_markers(content).strip()
|
||||||
|
quoted = re.findall(r'"([^"\n]{1,320})"', cleaned)
|
||||||
|
if quoted:
|
||||||
|
cleaned = quoted[-1].strip()
|
||||||
|
|
||||||
|
lines = [line.strip() for line in cleaned.splitlines() if line.strip()]
|
||||||
|
rejected_prefixes = (
|
||||||
|
"okay",
|
||||||
|
"the user",
|
||||||
|
"my task",
|
||||||
|
"let me",
|
||||||
|
"looking at",
|
||||||
|
"the correct",
|
||||||
|
"the most",
|
||||||
|
"this response",
|
||||||
|
"therefore",
|
||||||
|
"i need to",
|
||||||
|
"i should",
|
||||||
|
)
|
||||||
|
dialogue_lines = [
|
||||||
|
line.strip('"')
|
||||||
|
for line in lines
|
||||||
|
if not line.lower().startswith(rejected_prefixes)
|
||||||
|
and "json" not in line.lower()
|
||||||
|
and "game state" not in line.lower()
|
||||||
|
]
|
||||||
|
if dialogue_lines:
|
||||||
|
cleaned = dialogue_lines[0]
|
||||||
|
|
||||||
|
sentences = re.split(r"(?<=[.!?])\s+", cleaned)
|
||||||
|
if sentences and len(cleaned) > 240:
|
||||||
|
cleaned = sentences[0]
|
||||||
|
return cleaned.strip().strip('"') or "Well met."
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_reasoning_markers(content: str) -> str:
|
||||||
|
markers = (
|
||||||
|
"<think>",
|
||||||
|
"</think>",
|
||||||
|
"Okay,",
|
||||||
|
"The most appropriate response is:",
|
||||||
|
"The correct response",
|
||||||
|
"Therefore,",
|
||||||
|
)
|
||||||
|
cleaned = content
|
||||||
|
if "</think>" in cleaned:
|
||||||
|
cleaned = cleaned.split("</think>", 1)[1]
|
||||||
|
for marker in markers[:2]:
|
||||||
|
cleaned = cleaned.replace(marker, "")
|
||||||
|
return cleaned.strip()
|
||||||
|
|
||||||
|
|
||||||
async def _repair_visible_response(messages: list[dict[str, Any]], bad_content: str) -> str:
|
async def _repair_visible_response(messages: list[dict[str, Any]], bad_content: str) -> str:
|
||||||
repair_messages = [
|
repair_messages = [
|
||||||
*messages,
|
*messages,
|
||||||
@@ -447,14 +519,14 @@ async def _repair_visible_response(messages: list[dict[str, Any]], bad_content:
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
data = await state.kobold_client.chat(repair_messages, [])
|
data = await state.llm_client.chat(repair_messages, [])
|
||||||
choices = data.get("choices") or [{}]
|
choices = data.get("choices") or [{}]
|
||||||
choice = choices[0] or {}
|
choice = choices[0] or {}
|
||||||
message = choice.get("message") or {}
|
message = choice.get("message") or {}
|
||||||
repaired = str(message.get("content") or "").strip()
|
repaired = str(message.get("content") or "").strip()
|
||||||
if _looks_like_pseudo_tool_content(repaired):
|
if _looks_like_pseudo_tool_content(repaired):
|
||||||
return "I understand the matter, but I will not move until the terms are made plain."
|
return "I understand the matter, but I will not move until the terms are made plain."
|
||||||
return repaired
|
return _clean_visible_response(repaired)
|
||||||
|
|
||||||
|
|
||||||
DASHBOARD_HTML = """
|
DASHBOARD_HTML = """
|
||||||
@@ -609,22 +681,22 @@ DASHBOARD_HTML = """
|
|||||||
<h2>Status</h2>
|
<h2>Status</h2>
|
||||||
<div class="status">
|
<div class="status">
|
||||||
<div class="row"><span>Agent</span><span class="value" id="agentStatus">...</span></div>
|
<div class="row"><span>Agent</span><span class="value" id="agentStatus">...</span></div>
|
||||||
<div class="row"><span>KoboldCpp</span><span class="value" id="koboldStatus">...</span></div>
|
<div class="row"><span>Ollama</span><span class="value" id="llmStatus">...</span></div>
|
||||||
<div class="row"><span>Memory</span><span class="value" id="memoryStatus">...</span></div>
|
<div class="row"><span>Memory</span><span class="value" id="memoryStatus">...</span></div>
|
||||||
<div class="row"><span>Last latency</span><span class="value" id="latency">...</span></div>
|
<div class="row"><span>Last latency</span><span class="value" id="latency">...</span></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section style="margin-top:18px">
|
<section style="margin-top:18px">
|
||||||
<h2>KoboldCpp API</h2>
|
<h2>Ollama API</h2>
|
||||||
<label for="baseUrl">API address</label>
|
<label for="baseUrl">API address</label>
|
||||||
<input id="baseUrl" placeholder="http://127.0.0.1:5001">
|
<input id="baseUrl" placeholder="http://127.0.0.1:11434">
|
||||||
<label for="model">Model id</label>
|
<label for="model">Model id</label>
|
||||||
<input id="model" placeholder="local-model">
|
<input id="model" placeholder="local-model">
|
||||||
<label for="timeout">Timeout seconds</label>
|
<label for="timeout">Timeout seconds</label>
|
||||||
<input id="timeout" type="number" min="1" step="1">
|
<input id="timeout" type="number" min="1" step="1">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button class="primary" onclick="saveSettings()">Save</button>
|
<button class="primary" onclick="saveSettings()">Save</button>
|
||||||
<button onclick="pingKobold()">Ping</button>
|
<button onclick="pingOllama()">Ping</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -640,19 +712,19 @@ DASHBOARD_HTML = """
|
|||||||
const response = await fetch('/api/dashboard');
|
const response = await fetch('/api/dashboard');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const agentOk = data.health.status === 'ok';
|
const agentOk = data.health.status === 'ok';
|
||||||
const koboldOk = data.health.koboldcpp === 'reachable';
|
const llmOk = data.health.llm === 'reachable';
|
||||||
document.getElementById('headline').textContent = agentOk ? 'Ready for Bannerlord' : 'Needs attention';
|
document.getElementById('headline').textContent = agentOk ? 'Ready for Bannerlord' : 'Needs attention';
|
||||||
document.getElementById('headline').className = agentOk ? 'ok' : 'bad';
|
document.getElementById('headline').className = agentOk ? 'ok' : 'bad';
|
||||||
document.getElementById('agentStatus').textContent = data.health.status;
|
document.getElementById('agentStatus').textContent = data.health.status;
|
||||||
document.getElementById('agentStatus').className = 'value ' + (agentOk ? 'ok' : 'bad');
|
document.getElementById('agentStatus').className = 'value ' + (agentOk ? 'ok' : 'bad');
|
||||||
document.getElementById('koboldStatus').textContent = data.health.koboldcpp;
|
document.getElementById('llmStatus').textContent = data.health.llm;
|
||||||
document.getElementById('koboldStatus').className = 'value ' + (koboldOk ? 'ok' : 'bad');
|
document.getElementById('llmStatus').className = 'value ' + (llmOk ? 'ok' : 'bad');
|
||||||
document.getElementById('memoryStatus').textContent = data.health.memory;
|
document.getElementById('memoryStatus').textContent = data.health.memory;
|
||||||
document.getElementById('latency').textContent = data.debug.last_koboldcpp_latency_ms === null ? 'none' : data.debug.last_koboldcpp_latency_ms + ' ms';
|
document.getElementById('latency').textContent = data.debug.last_llm_latency_ms === null ? 'none' : data.debug.last_llm_latency_ms + ' ms';
|
||||||
if (!loadedConfig) {
|
if (!loadedConfig) {
|
||||||
document.getElementById('baseUrl').value = data.koboldcpp.base_url;
|
document.getElementById('baseUrl').value = data.llm.base_url;
|
||||||
document.getElementById('model').value = data.koboldcpp.model;
|
document.getElementById('model').value = data.llm.model;
|
||||||
document.getElementById('timeout').value = data.koboldcpp.timeout_seconds;
|
document.getElementById('timeout').value = data.llm.timeout_seconds;
|
||||||
loadedConfig = true;
|
loadedConfig = true;
|
||||||
}
|
}
|
||||||
const logs = document.getElementById('logs');
|
const logs = document.getElementById('logs');
|
||||||
@@ -668,22 +740,21 @@ DASHBOARD_HTML = """
|
|||||||
base_url: document.getElementById('baseUrl').value,
|
base_url: document.getElementById('baseUrl').value,
|
||||||
model: document.getElementById('model').value,
|
model: document.getElementById('model').value,
|
||||||
timeout_seconds: Number(document.getElementById('timeout').value),
|
timeout_seconds: Number(document.getElementById('timeout').value),
|
||||||
autostart: false
|
|
||||||
};
|
};
|
||||||
const response = await fetch('/api/koboldcpp', {
|
const response = await fetch('/api/ollama', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
alert(data.error || 'Failed to save KoboldCpp settings.');
|
alert(data.error || 'Failed to save Ollama settings.');
|
||||||
}
|
}
|
||||||
await refresh();
|
await refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pingKobold() {
|
async function pingOllama() {
|
||||||
await fetch('/api/koboldcpp/ping', {method: 'POST'});
|
await fetch('/api/ollama/ping', {method: 'POST'});
|
||||||
await refresh();
|
await refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,30 +15,32 @@ class ServerConfig(BaseModel):
|
|||||||
port: int = 8766
|
port: int = 8766
|
||||||
|
|
||||||
|
|
||||||
class KoboldCppConfig(BaseModel):
|
class OllamaConfig(BaseModel):
|
||||||
autostart: bool = True
|
base_url: str = "http://127.0.0.1:11434"
|
||||||
executable_path: str = "./koboldcpp.exe"
|
|
||||||
model_path: str = "./model.gguf"
|
|
||||||
base_url: str = "http://127.0.0.1:5001"
|
|
||||||
chat_path: str = "/v1/chat/completions"
|
chat_path: str = "/v1/chat/completions"
|
||||||
model: str = "local-model"
|
model: str = "llama3.1:8b"
|
||||||
port: int = 5001
|
|
||||||
context_size: int = 8192
|
|
||||||
extra_args: list[str] = Field(default_factory=lambda: ["--jinja", "--jinjatools"])
|
|
||||||
startup_timeout_seconds: int = 180
|
|
||||||
timeout_seconds: int = 120
|
timeout_seconds: int = 120
|
||||||
tool_mode: str = "openai_tools"
|
auto_pull_models: bool = True
|
||||||
json_repair_retry: bool = True
|
|
||||||
|
|
||||||
|
|
||||||
class MemoryConfig(BaseModel):
|
class MemoryConfig(BaseModel):
|
||||||
provider: str = "disabled"
|
provider: str = "sqlite"
|
||||||
vector_store: str = "qdrant"
|
sqlite_path: str = "./data/localdiplomacy.sqlite3"
|
||||||
qdrant_host: str = "127.0.0.1"
|
embedding_provider: str = "ollama"
|
||||||
qdrant_port: int = 6333
|
embedding_model: str = "nomic-embed-text"
|
||||||
collection: str = "localdiplomacy_memories"
|
embedding_auto_pull: bool = True
|
||||||
embedder_provider: str = "ollama"
|
max_prompt_memories: int = 8
|
||||||
embedder_model: str = "nomic-embed-text"
|
|
||||||
|
|
||||||
|
class VectorIndexConfig(BaseModel):
|
||||||
|
mode: str = "embedded"
|
||||||
|
path: str = "./data/qdrant"
|
||||||
|
host: str = "127.0.0.1"
|
||||||
|
port: int = 6333
|
||||||
|
executable_path: str = "./qdrant/qdrant.exe"
|
||||||
|
autostart: bool = False
|
||||||
|
startup_timeout_seconds: int = 30
|
||||||
|
fallback_mode: str = "disabled"
|
||||||
|
|
||||||
|
|
||||||
class EventLogConfig(BaseModel):
|
class EventLogConfig(BaseModel):
|
||||||
@@ -48,14 +50,15 @@ class EventLogConfig(BaseModel):
|
|||||||
class GenerationConfig(BaseModel):
|
class GenerationConfig(BaseModel):
|
||||||
temperature: float = 0.7
|
temperature: float = 0.7
|
||||||
max_tokens: int = 800
|
max_tokens: int = 800
|
||||||
suppress_thinking: bool = False
|
suppress_thinking: bool = True
|
||||||
suppress_thinking_token: str = "/no_think"
|
suppress_thinking_token: str = "/no_think"
|
||||||
|
|
||||||
|
|
||||||
class AppConfig(BaseModel):
|
class AppConfig(BaseModel):
|
||||||
server: ServerConfig = Field(default_factory=ServerConfig)
|
server: ServerConfig = Field(default_factory=ServerConfig)
|
||||||
koboldcpp: KoboldCppConfig = Field(default_factory=KoboldCppConfig)
|
ollama: OllamaConfig = Field(default_factory=OllamaConfig)
|
||||||
memory: MemoryConfig = Field(default_factory=MemoryConfig)
|
memory: MemoryConfig = Field(default_factory=MemoryConfig)
|
||||||
|
vector_index: VectorIndexConfig = Field(default_factory=VectorIndexConfig)
|
||||||
event_log: EventLogConfig = Field(default_factory=EventLogConfig)
|
event_log: EventLogConfig = Field(default_factory=EventLogConfig)
|
||||||
generation: GenerationConfig = Field(default_factory=GenerationConfig)
|
generation: GenerationConfig = Field(default_factory=GenerationConfig)
|
||||||
|
|
||||||
|
|||||||
@@ -115,14 +115,14 @@ class ActionResultResponse(BaseModel):
|
|||||||
class HealthResponse(BaseModel):
|
class HealthResponse(BaseModel):
|
||||||
status: Literal["ok", "degraded", "error"]
|
status: Literal["ok", "degraded", "error"]
|
||||||
agent_version: str
|
agent_version: str
|
||||||
koboldcpp: str
|
llm: str
|
||||||
memory: str
|
memory: str
|
||||||
event_log: str
|
event_log: str
|
||||||
errors: list[str] = Field(default_factory=list)
|
errors: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class DebugStatusResponse(BaseModel):
|
class DebugStatusResponse(BaseModel):
|
||||||
last_koboldcpp_latency_ms: int | None = None
|
last_llm_latency_ms: int | None = None
|
||||||
memory_count_estimate: int | None = None
|
memory_count_estimate: int | None = None
|
||||||
queued_action_count: int = 0
|
queued_action_count: int = 0
|
||||||
recent_errors: list[str] = Field(default_factory=list)
|
recent_errors: list[str] = Field(default_factory=list)
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
_TOKEN_PATTERN = re.compile(r"[A-Za-z0-9_]+")
|
||||||
|
|
||||||
|
|
||||||
|
class Embedder(Protocol):
|
||||||
|
@property
|
||||||
|
def dimensions(self) -> int:
|
||||||
|
...
|
||||||
|
|
||||||
|
def embed(self, text: str) -> list[float]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class HashingEmbedder:
|
||||||
|
"""Deterministic local embedding fallback used until a real local embedder is configured."""
|
||||||
|
|
||||||
|
dimensions: int = 384
|
||||||
|
|
||||||
|
def embed(self, text: str) -> list[float]:
|
||||||
|
vector = [0.0] * self.dimensions
|
||||||
|
for token in _TOKEN_PATTERN.findall(text.lower()):
|
||||||
|
digest = hashlib.blake2b(token.encode("utf-8"), digest_size=8).digest()
|
||||||
|
bucket = int.from_bytes(digest[:4], "little") % self.dimensions
|
||||||
|
sign = 1.0 if digest[4] & 1 else -1.0
|
||||||
|
vector[bucket] += sign
|
||||||
|
|
||||||
|
magnitude = math.sqrt(sum(value * value for value in vector))
|
||||||
|
if magnitude == 0:
|
||||||
|
return vector
|
||||||
|
return [value / magnitude for value in vector]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class OllamaEmbedder:
|
||||||
|
model: str
|
||||||
|
base_url: str = "http://127.0.0.1:11434"
|
||||||
|
timeout_seconds: int = 60
|
||||||
|
auto_pull: bool = True
|
||||||
|
_model_checked: bool = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dimensions(self) -> int:
|
||||||
|
return len(self.embed("dimension probe"))
|
||||||
|
|
||||||
|
def embed(self, text: str) -> list[float]:
|
||||||
|
self._ensure_model()
|
||||||
|
response = httpx.post(
|
||||||
|
f"{self.base_url}/api/embed",
|
||||||
|
json={"model": self.model, "input": text},
|
||||||
|
timeout=self.timeout_seconds,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
embeddings = data.get("embeddings")
|
||||||
|
if isinstance(embeddings, list) and embeddings:
|
||||||
|
return [float(value) for value in embeddings[0]]
|
||||||
|
embedding = data.get("embedding")
|
||||||
|
if isinstance(embedding, list):
|
||||||
|
return [float(value) for value in embedding]
|
||||||
|
raise ValueError("Ollama embed response did not include an embedding.")
|
||||||
|
|
||||||
|
def _ensure_model(self) -> None:
|
||||||
|
if self._model_checked:
|
||||||
|
return
|
||||||
|
object.__setattr__(self, "_model_checked", True)
|
||||||
|
if not self.auto_pull:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
tags = httpx.get(f"{self.base_url}/api/tags", timeout=2)
|
||||||
|
tags.raise_for_status()
|
||||||
|
models = tags.json().get("models") or []
|
||||||
|
names = {str(model.get("name") or model.get("model")) for model in models if model.get("name") or model.get("model")}
|
||||||
|
if self.model in names:
|
||||||
|
return
|
||||||
|
except httpx.HTTPError:
|
||||||
|
return
|
||||||
|
|
||||||
|
response = httpx.post(
|
||||||
|
f"{self.base_url}/api/pull",
|
||||||
|
json={"model": self.model, "stream": False},
|
||||||
|
timeout=None,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FallbackEmbedder:
|
||||||
|
primary: Embedder
|
||||||
|
fallback: Embedder
|
||||||
|
_using_fallback: bool = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dimensions(self) -> int:
|
||||||
|
if self._using_fallback:
|
||||||
|
return self.fallback.dimensions
|
||||||
|
try:
|
||||||
|
return self.primary.dimensions
|
||||||
|
except Exception:
|
||||||
|
self._using_fallback = True
|
||||||
|
return self.fallback.dimensions
|
||||||
|
|
||||||
|
def embed(self, text: str) -> list[float]:
|
||||||
|
if self._using_fallback:
|
||||||
|
return self.fallback.embed(text)
|
||||||
|
try:
|
||||||
|
return self.primary.embed(text)
|
||||||
|
except Exception:
|
||||||
|
self._using_fallback = True
|
||||||
|
return self.fallback.embed(text)
|
||||||
|
|
||||||
|
|
||||||
|
def create_embedder(
|
||||||
|
provider: str,
|
||||||
|
model: str,
|
||||||
|
base_url: str = "http://127.0.0.1:11434",
|
||||||
|
timeout_seconds: int = 60,
|
||||||
|
auto_pull: bool = True,
|
||||||
|
) -> Embedder:
|
||||||
|
normalized = provider.lower().strip()
|
||||||
|
if normalized in {"hashing", "local_hash", "deterministic"}:
|
||||||
|
return HashingEmbedder()
|
||||||
|
if normalized == "ollama":
|
||||||
|
return FallbackEmbedder(
|
||||||
|
primary=OllamaEmbedder(model=model, base_url=base_url, timeout_seconds=timeout_seconds, auto_pull=auto_pull),
|
||||||
|
fallback=HashingEmbedder(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return HashingEmbedder()
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import time
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
|
|
||||||
from .config import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class KoboldCppClient:
|
|
||||||
def __init__(self, config: AppConfig):
|
|
||||||
self.config = config
|
|
||||||
self.last_latency_ms: int | None = None
|
|
||||||
|
|
||||||
async def chat(
|
|
||||||
self,
|
|
||||||
messages: list[dict[str, Any]],
|
|
||||||
tools: list[dict[str, Any]],
|
|
||||||
tool_choice: str | dict[str, Any] | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
payload: dict[str, Any] = {
|
|
||||||
"model": self.config.koboldcpp.model,
|
|
||||||
"messages": self._prepare_messages(messages),
|
|
||||||
"temperature": self.config.generation.temperature,
|
|
||||||
"max_tokens": self.config.generation.max_tokens,
|
|
||||||
}
|
|
||||||
if tools:
|
|
||||||
payload["tools"] = tools
|
|
||||||
payload["tool_choice"] = tool_choice or "auto"
|
|
||||||
|
|
||||||
url = f"{self.config.koboldcpp.base_url}{self.config.koboldcpp.chat_path}"
|
|
||||||
started = time.perf_counter()
|
|
||||||
async with httpx.AsyncClient(timeout=self.config.koboldcpp.timeout_seconds) as client:
|
|
||||||
response = await client.post(url, json=payload)
|
|
||||||
response.raise_for_status()
|
|
||||||
self.last_latency_ms = int((time.perf_counter() - started) * 1000)
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def _prepare_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
||||||
if not self.config.generation.suppress_thinking:
|
|
||||||
return messages
|
|
||||||
|
|
||||||
token = self.config.generation.suppress_thinking_token.strip()
|
|
||||||
if not token:
|
|
||||||
return messages
|
|
||||||
|
|
||||||
prepared: list[dict[str, Any]] = []
|
|
||||||
token_prefix = f"{token} "
|
|
||||||
for message in messages:
|
|
||||||
copied = dict(message)
|
|
||||||
content = copied.get("content")
|
|
||||||
role = copied.get("role")
|
|
||||||
if role in {"system", "user"} and isinstance(content, str) and not content.lstrip().startswith(token):
|
|
||||||
copied["content"] = f"{token_prefix}{content}"
|
|
||||||
prepared.append(copied)
|
|
||||||
return prepared
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
|
|
||||||
from .config import KoboldCppConfig, resolve_runtime_path
|
|
||||||
|
|
||||||
|
|
||||||
class KoboldCppProcess:
|
|
||||||
def __init__(self, config: KoboldCppConfig, cwd: Path | None = None):
|
|
||||||
self.config = config
|
|
||||||
self.cwd = cwd or Path.cwd()
|
|
||||||
self.process: subprocess.Popen[str] | None = None
|
|
||||||
self.last_error: str | None = None
|
|
||||||
|
|
||||||
def validate_runtime_files(self) -> list[str]:
|
|
||||||
errors: list[str] = []
|
|
||||||
executable = resolve_runtime_path(self.config.executable_path, self.cwd)
|
|
||||||
model = resolve_runtime_path(self.config.model_path, self.cwd)
|
|
||||||
if not executable.exists():
|
|
||||||
errors.append(f"Missing KoboldCpp executable: {executable}")
|
|
||||||
if not model.exists():
|
|
||||||
errors.append(f"Missing KoboldCpp model: {model}")
|
|
||||||
return errors
|
|
||||||
|
|
||||||
def is_reachable(self) -> bool:
|
|
||||||
try:
|
|
||||||
with httpx.Client(timeout=2) as client:
|
|
||||||
response = client.get(f"{self.config.base_url}/v1/models")
|
|
||||||
return response.status_code < 500
|
|
||||||
except httpx.HTTPError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def ensure_started(self) -> None:
|
|
||||||
if not self.config.autostart or self.is_reachable():
|
|
||||||
return
|
|
||||||
|
|
||||||
errors = self.validate_runtime_files()
|
|
||||||
if errors:
|
|
||||||
self.last_error = "; ".join(errors)
|
|
||||||
raise RuntimeError(self.last_error)
|
|
||||||
|
|
||||||
executable = resolve_runtime_path(self.config.executable_path, self.cwd)
|
|
||||||
model = resolve_runtime_path(self.config.model_path, self.cwd)
|
|
||||||
command = [
|
|
||||||
str(executable),
|
|
||||||
"--model",
|
|
||||||
str(model),
|
|
||||||
"--port",
|
|
||||||
str(self.config.port),
|
|
||||||
"--contextsize",
|
|
||||||
str(self.config.context_size),
|
|
||||||
*self.config.extra_args,
|
|
||||||
]
|
|
||||||
|
|
||||||
self.process = subprocess.Popen(
|
|
||||||
command,
|
|
||||||
cwd=self.cwd,
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
self._wait_until_ready()
|
|
||||||
|
|
||||||
def _wait_until_ready(self) -> None:
|
|
||||||
deadline = time.monotonic() + self.config.startup_timeout_seconds
|
|
||||||
while time.monotonic() < deadline:
|
|
||||||
if self.process is not None and self.process.poll() is not None:
|
|
||||||
self.last_error = f"KoboldCpp exited early with code {self.process.returncode}"
|
|
||||||
raise RuntimeError(self.last_error)
|
|
||||||
if self.is_reachable():
|
|
||||||
return
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
self.last_error = "Timed out waiting for KoboldCpp API readiness"
|
|
||||||
raise TimeoutError(self.last_error)
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
|
||||||
if self.process is None or self.process.poll() is not None:
|
|
||||||
return
|
|
||||||
self.process.terminate()
|
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
import sqlite3
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .config import resolve_runtime_path
|
||||||
|
from .vector_index import VectorIndex
|
||||||
|
|
||||||
|
|
||||||
|
_HEADING_PATTERN = re.compile(r"^(#{1,6})\s+(.+?)\s*$", re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LoreStore:
|
||||||
|
sqlite_path: str
|
||||||
|
vector_index: VectorIndex | None = None
|
||||||
|
_initialized: bool = False
|
||||||
|
_path: Path = field(init=False)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self._path = resolve_runtime_path(self.sqlite_path)
|
||||||
|
|
||||||
|
def initialize(self) -> None:
|
||||||
|
self._path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with self._connect() as connection:
|
||||||
|
connection.execute("PRAGMA journal_mode=WAL")
|
||||||
|
self._create_schema(connection)
|
||||||
|
connection.commit()
|
||||||
|
self._initialized = True
|
||||||
|
|
||||||
|
def import_markdown(self, source_key: str, name: str, markdown: str) -> int:
|
||||||
|
self._ensure_initialized()
|
||||||
|
content_hash = hashlib.sha256(markdown.encode("utf-8")).hexdigest()
|
||||||
|
chunks = self._chunk_markdown(markdown)
|
||||||
|
with self._connect() as connection:
|
||||||
|
cursor = connection.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO lore_sources (source_key, name, content_hash)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(source_key) DO UPDATE SET
|
||||||
|
name = excluded.name,
|
||||||
|
content_hash = excluded.content_hash,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
RETURNING id
|
||||||
|
""",
|
||||||
|
(source_key, name, content_hash),
|
||||||
|
)
|
||||||
|
source_id = int(cursor.fetchone()["id"])
|
||||||
|
connection.execute("DELETE FROM lore_chunks WHERE lore_source_id = ?", (source_id,))
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for index, chunk in enumerate(chunks):
|
||||||
|
chunk_key = f"{source_key}:{index}"
|
||||||
|
cursor = connection.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO lore_chunks (
|
||||||
|
lore_source_id,
|
||||||
|
source_key,
|
||||||
|
chunk_key,
|
||||||
|
heading_path,
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
summary,
|
||||||
|
tags_json
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, '[]')
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
source_id,
|
||||||
|
source_key,
|
||||||
|
chunk_key,
|
||||||
|
chunk["heading_path"],
|
||||||
|
chunk["title"],
|
||||||
|
chunk["text"],
|
||||||
|
chunk["summary"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
chunk_id = int(cursor.lastrowid)
|
||||||
|
point_id = self._upsert_vector(chunk_id, source_id, source_key, chunk)
|
||||||
|
if point_id:
|
||||||
|
connection.execute(
|
||||||
|
"UPDATE lore_chunks SET qdrant_point_id = ? WHERE id = ?",
|
||||||
|
(point_id, chunk_id),
|
||||||
|
)
|
||||||
|
count += 1
|
||||||
|
connection.commit()
|
||||||
|
return count
|
||||||
|
|
||||||
|
def search(self, query: str, source_key: str | None = None, limit: int = 5) -> list[dict[str, object]]:
|
||||||
|
self._ensure_initialized()
|
||||||
|
if self.vector_index and self.vector_index.enabled:
|
||||||
|
filters = {"source_key": source_key} if source_key else {}
|
||||||
|
results = self.vector_index.search_lore(query, filters, limit)
|
||||||
|
if results:
|
||||||
|
ids = [result.sqlite_id for result in results]
|
||||||
|
rows_by_id = {int(row["id"]): row for row in self._fetch_rows_by_ids(ids)}
|
||||||
|
return [self._row_to_chunk(rows_by_id[chunk_id]) for chunk_id in ids if chunk_id in rows_by_id]
|
||||||
|
|
||||||
|
params: list[object] = []
|
||||||
|
where = ""
|
||||||
|
if source_key:
|
||||||
|
where = "AND source_key = ?"
|
||||||
|
params.append(source_key)
|
||||||
|
needle = f"%{query.lower()}%"
|
||||||
|
rows = self._fetch_rows(
|
||||||
|
f"""
|
||||||
|
SELECT *
|
||||||
|
FROM lore_chunks
|
||||||
|
WHERE (LOWER(text) LIKE ? OR LOWER(summary) LIKE ? OR LOWER(title) LIKE ?)
|
||||||
|
{where}
|
||||||
|
ORDER BY id
|
||||||
|
LIMIT ?
|
||||||
|
""",
|
||||||
|
[needle, needle, needle, *params, limit],
|
||||||
|
)
|
||||||
|
return [self._row_to_chunk(row) for row in rows]
|
||||||
|
|
||||||
|
def _connect(self) -> sqlite3.Connection:
|
||||||
|
connection = sqlite3.connect(self._path)
|
||||||
|
connection.row_factory = sqlite3.Row
|
||||||
|
return connection
|
||||||
|
|
||||||
|
def _ensure_initialized(self) -> None:
|
||||||
|
if not self._initialized:
|
||||||
|
self.initialize()
|
||||||
|
|
||||||
|
def _create_schema(self, connection: sqlite3.Connection) -> None:
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS lore_sources (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
source_key TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
content_hash TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS lore_chunks (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
lore_source_id INTEGER NOT NULL,
|
||||||
|
source_key TEXT NOT NULL,
|
||||||
|
chunk_key TEXT NOT NULL,
|
||||||
|
heading_path TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
summary TEXT NOT NULL,
|
||||||
|
tags_json TEXT NOT NULL DEFAULT '[]',
|
||||||
|
qdrant_point_id TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY(lore_source_id) REFERENCES lore_sources(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_lore_chunks_source
|
||||||
|
ON lore_chunks(source_key)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def _upsert_vector(self, chunk_id: int, source_id: int, source_key: str, chunk: dict[str, str]) -> str | None:
|
||||||
|
if not self.vector_index or not self.vector_index.enabled:
|
||||||
|
return None
|
||||||
|
return self.vector_index.upsert_lore_chunk(
|
||||||
|
chunk_id,
|
||||||
|
"\n".join([chunk["title"], chunk["summary"], chunk["text"]]),
|
||||||
|
{
|
||||||
|
"lore_source_id": source_id,
|
||||||
|
"source_key": source_key,
|
||||||
|
"title": chunk["title"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _fetch_rows(self, statement: str, params: list[object]) -> list[sqlite3.Row]:
|
||||||
|
with self._connect() as connection:
|
||||||
|
return list(connection.execute(statement, params).fetchall())
|
||||||
|
|
||||||
|
def _fetch_rows_by_ids(self, chunk_ids: list[int]) -> list[sqlite3.Row]:
|
||||||
|
if not chunk_ids:
|
||||||
|
return []
|
||||||
|
placeholders = ",".join("?" for _ in chunk_ids)
|
||||||
|
return self._fetch_rows(f"SELECT * FROM lore_chunks WHERE id IN ({placeholders})", list(chunk_ids))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _chunk_markdown(markdown: str) -> list[dict[str, str]]:
|
||||||
|
matches = list(_HEADING_PATTERN.finditer(markdown))
|
||||||
|
if not matches:
|
||||||
|
text = markdown.strip()
|
||||||
|
return [{"title": "Lore", "heading_path": "Lore", "text": text, "summary": text[:240]}] if text else []
|
||||||
|
|
||||||
|
chunks: list[dict[str, str]] = []
|
||||||
|
heading_stack: list[tuple[int, str]] = []
|
||||||
|
for index, match in enumerate(matches):
|
||||||
|
level = len(match.group(1))
|
||||||
|
title = match.group(2).strip()
|
||||||
|
start = match.end()
|
||||||
|
end = matches[index + 1].start() if index + 1 < len(matches) else len(markdown)
|
||||||
|
body = markdown[start:end].strip()
|
||||||
|
heading_stack = [(existing_level, existing_title) for existing_level, existing_title in heading_stack if existing_level < level]
|
||||||
|
heading_stack.append((level, title))
|
||||||
|
if not body:
|
||||||
|
continue
|
||||||
|
heading_path = " > ".join(existing_title for _, existing_title in heading_stack)
|
||||||
|
chunks.append(
|
||||||
|
{
|
||||||
|
"title": title,
|
||||||
|
"heading_path": heading_path,
|
||||||
|
"text": body,
|
||||||
|
"summary": body[:240],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return chunks
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _row_to_chunk(row: sqlite3.Row) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"id": row["id"],
|
||||||
|
"source_key": row["source_key"],
|
||||||
|
"title": row["title"],
|
||||||
|
"heading_path": row["heading_path"],
|
||||||
|
"text": row["text"],
|
||||||
|
"summary": row["summary"],
|
||||||
|
"qdrant_point_id": row["qdrant_point_id"],
|
||||||
|
}
|
||||||
@@ -1,70 +1,476 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sqlite3
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .config import MemoryConfig
|
from .config import MemoryConfig, resolve_runtime_path
|
||||||
|
from .vector_index import VectorIndex
|
||||||
|
|
||||||
|
|
||||||
|
_TOKEN_PATTERN = re.compile(r"[A-Za-z0-9_]+")
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MemoryStore:
|
class MemoryStore:
|
||||||
config: MemoryConfig
|
config: MemoryConfig
|
||||||
_fallback: list[dict[str, Any]] = field(default_factory=list)
|
vector_index: VectorIndex | None = None
|
||||||
_mem0: Any = None
|
_initialized: bool = False
|
||||||
|
_fts_available: bool = False
|
||||||
|
_last_error: str | None = None
|
||||||
|
_path: Path = field(init=False)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self._path = resolve_runtime_path(self.config.sqlite_path)
|
||||||
|
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
if self.config.provider.lower() != "mem0":
|
self._path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
return
|
with self._connect() as connection:
|
||||||
|
connection.execute("PRAGMA journal_mode=WAL")
|
||||||
try:
|
connection.execute("PRAGMA foreign_keys=ON")
|
||||||
from mem0 import Memory # type: ignore
|
self._create_schema(connection)
|
||||||
except ImportError:
|
self._fts_available = self._create_fts_schema(connection)
|
||||||
self._mem0 = None
|
connection.commit()
|
||||||
return
|
self._initialized = True
|
||||||
|
self._last_error = None
|
||||||
mem0_config = {
|
|
||||||
"vector_store": {
|
|
||||||
"provider": "qdrant",
|
|
||||||
"config": {
|
|
||||||
"host": self.config.qdrant_host,
|
|
||||||
"port": self.config.qdrant_port,
|
|
||||||
"collection_name": self.config.collection,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"embedder": {
|
|
||||||
"provider": self.config.embedder_provider,
|
|
||||||
"config": {"model": self.config.embedder_model},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
self._mem0 = Memory.from_config(mem0_config)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self) -> str:
|
def status(self) -> str:
|
||||||
if self.config.provider.lower() == "disabled":
|
if self._last_error:
|
||||||
return "disabled"
|
return "error"
|
||||||
return "reachable" if self._mem0 is not None else "fallback"
|
if not self._initialized:
|
||||||
|
return "not_initialized"
|
||||||
|
sqlite_status = "sqlite_fts" if self._fts_available else "sqlite"
|
||||||
|
if self.vector_index:
|
||||||
|
return f"{sqlite_status}+vector:{self.vector_index.status}"
|
||||||
|
return sqlite_status
|
||||||
|
|
||||||
def remember(self, text: str, metadata: dict[str, Any]) -> None:
|
def remember(self, text: str, metadata: dict[str, Any]) -> None:
|
||||||
if self._mem0 is not None:
|
self._ensure_initialized()
|
||||||
user_id = metadata.get("campaign_id", "default")
|
normalized = self._normalize_metadata(metadata)
|
||||||
self._mem0.add(text, user_id=user_id, metadata=metadata)
|
tags = normalized.pop("tags")
|
||||||
return
|
summary = normalized.pop("summary", None) or text
|
||||||
self._fallback.append({"text": text, "metadata": metadata})
|
extra_metadata = {
|
||||||
|
key: value
|
||||||
|
for key, value in normalized.items()
|
||||||
|
if key
|
||||||
|
not in {
|
||||||
|
"save_id",
|
||||||
|
"campaign_id",
|
||||||
|
"character_id",
|
||||||
|
"related_character_id",
|
||||||
|
"player_id",
|
||||||
|
"kingdom_id",
|
||||||
|
"location_id",
|
||||||
|
"category",
|
||||||
|
"importance",
|
||||||
|
"confidence",
|
||||||
|
"visibility",
|
||||||
|
"created_day",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with self._connect() as connection:
|
||||||
|
cursor = connection.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO memories (
|
||||||
|
save_id,
|
||||||
|
campaign_id,
|
||||||
|
subject_character_id,
|
||||||
|
related_character_id,
|
||||||
|
player_id,
|
||||||
|
kingdom_id,
|
||||||
|
location_id,
|
||||||
|
category,
|
||||||
|
importance,
|
||||||
|
confidence,
|
||||||
|
visibility,
|
||||||
|
text,
|
||||||
|
summary,
|
||||||
|
tags_json,
|
||||||
|
created_day,
|
||||||
|
metadata_json
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
normalized.get("save_id"),
|
||||||
|
normalized.get("campaign_id"),
|
||||||
|
normalized.get("character_id"),
|
||||||
|
normalized.get("related_character_id"),
|
||||||
|
normalized.get("player_id"),
|
||||||
|
normalized.get("kingdom_id"),
|
||||||
|
normalized.get("location_id"),
|
||||||
|
normalized.get("category"),
|
||||||
|
int(normalized.get("importance") or 3),
|
||||||
|
float(normalized.get("confidence") or 1.0),
|
||||||
|
normalized.get("visibility") or "private",
|
||||||
|
text,
|
||||||
|
summary,
|
||||||
|
json.dumps(tags, ensure_ascii=False),
|
||||||
|
normalized.get("created_day"),
|
||||||
|
json.dumps(extra_metadata, ensure_ascii=False),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
memory_id = int(cursor.lastrowid)
|
||||||
|
if self._fts_available:
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO memories_fts (memory_id, text, summary, tags)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(memory_id, text, summary, " ".join(tags)),
|
||||||
|
)
|
||||||
|
qdrant_point_id = self._upsert_vector(memory_id, text, normalized, tags)
|
||||||
|
if qdrant_point_id:
|
||||||
|
connection.execute(
|
||||||
|
"UPDATE memories SET qdrant_point_id = ? WHERE id = ?",
|
||||||
|
(qdrant_point_id, memory_id),
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
def search(self, query: str, metadata: dict[str, Any], limit: int = 5) -> list[dict[str, Any]]:
|
def search(self, query: str, metadata: dict[str, Any], limit: int = 5) -> list[dict[str, Any]]:
|
||||||
if self._mem0 is not None:
|
self._ensure_initialized()
|
||||||
user_id = metadata.get("campaign_id", "default")
|
normalized = self._normalize_metadata(metadata)
|
||||||
result = self._mem0.search(query, user_id=user_id, limit=limit)
|
limit = max(1, min(int(limit), self.config.max_prompt_memories))
|
||||||
return result if isinstance(result, list) else [result]
|
vector_matches = self._search_vector(query, normalized, limit)
|
||||||
|
if vector_matches:
|
||||||
campaign_id = metadata.get("campaign_id")
|
return vector_matches
|
||||||
matches = [
|
if self._fts_available and query.strip():
|
||||||
item
|
try:
|
||||||
for item in self._fallback
|
matches = self._search_fts(query, normalized, limit)
|
||||||
if item["metadata"].get("campaign_id") == campaign_id
|
if matches:
|
||||||
and query.lower() in item["text"].lower()
|
return matches
|
||||||
]
|
except sqlite3.OperationalError as exc:
|
||||||
return matches[:limit]
|
self._last_error = str(exc)
|
||||||
|
return self._search_like(query, normalized, limit)
|
||||||
|
|
||||||
def count_estimate(self) -> int:
|
def count_estimate(self) -> int:
|
||||||
return len(self._fallback)
|
self._ensure_initialized()
|
||||||
|
with self._connect() as connection:
|
||||||
|
row = connection.execute("SELECT COUNT(*) FROM memories").fetchone()
|
||||||
|
return int(row[0] if row else 0)
|
||||||
|
|
||||||
|
def rebuild_vector_index(self) -> int:
|
||||||
|
self._ensure_initialized()
|
||||||
|
if not self.vector_index or not self.vector_index.enabled:
|
||||||
|
return 0
|
||||||
|
count = 0
|
||||||
|
with self._connect() as connection:
|
||||||
|
rows = connection.execute("SELECT * FROM memories ORDER BY id").fetchall()
|
||||||
|
for row in rows:
|
||||||
|
tags = json.loads(row["tags_json"] or "[]")
|
||||||
|
payload = self._row_to_vector_payload(row, tags)
|
||||||
|
point_id = self.vector_index.upsert_memory(
|
||||||
|
int(row["id"]),
|
||||||
|
self._index_text(row["text"], row["summary"], tags),
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
if point_id:
|
||||||
|
connection.execute(
|
||||||
|
"UPDATE memories SET qdrant_point_id = ? WHERE id = ?",
|
||||||
|
(point_id, row["id"]),
|
||||||
|
)
|
||||||
|
count += 1
|
||||||
|
connection.commit()
|
||||||
|
return count
|
||||||
|
|
||||||
|
def _connect(self) -> sqlite3.Connection:
|
||||||
|
connection = sqlite3.connect(self._path)
|
||||||
|
connection.row_factory = sqlite3.Row
|
||||||
|
return connection
|
||||||
|
|
||||||
|
def _ensure_initialized(self) -> None:
|
||||||
|
if not self._initialized:
|
||||||
|
self.initialize()
|
||||||
|
|
||||||
|
def _create_schema(self, connection: sqlite3.Connection) -> None:
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS memory_schema (
|
||||||
|
version INTEGER PRIMARY KEY,
|
||||||
|
applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS memories (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
save_id TEXT,
|
||||||
|
campaign_id TEXT NOT NULL,
|
||||||
|
subject_character_id TEXT,
|
||||||
|
related_character_id TEXT,
|
||||||
|
player_id TEXT,
|
||||||
|
kingdom_id TEXT,
|
||||||
|
location_id TEXT,
|
||||||
|
category TEXT,
|
||||||
|
importance INTEGER NOT NULL DEFAULT 3,
|
||||||
|
confidence REAL NOT NULL DEFAULT 1.0,
|
||||||
|
visibility TEXT NOT NULL DEFAULT 'private',
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
summary TEXT NOT NULL,
|
||||||
|
tags_json TEXT NOT NULL DEFAULT '[]',
|
||||||
|
created_day REAL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_accessed_at TEXT,
|
||||||
|
qdrant_point_id TEXT,
|
||||||
|
metadata_json TEXT NOT NULL DEFAULT '{}'
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_memories_scope
|
||||||
|
ON memories(save_id, campaign_id, subject_character_id)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_memories_campaign
|
||||||
|
ON memories(campaign_id)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_memories_faction
|
||||||
|
ON memories(save_id, kingdom_id)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_memories_location
|
||||||
|
ON memories(save_id, location_id)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_memories_category
|
||||||
|
ON memories(save_id, category)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
connection.execute("INSERT OR IGNORE INTO memory_schema (version) VALUES (1)")
|
||||||
|
|
||||||
|
def _create_fts_schema(self, connection: sqlite3.Connection) -> bool:
|
||||||
|
try:
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
|
||||||
|
USING fts5(memory_id UNINDEXED, text, summary, tags)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except sqlite3.OperationalError as exc:
|
||||||
|
self._last_error = str(exc)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _search_fts(
|
||||||
|
self,
|
||||||
|
query: str,
|
||||||
|
metadata: dict[str, Any],
|
||||||
|
limit: int,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
match_query = self._fts_query(query)
|
||||||
|
if not match_query:
|
||||||
|
return []
|
||||||
|
where, params = self._scope_clause(metadata, "m")
|
||||||
|
rows = self._fetch_rows(
|
||||||
|
f"""
|
||||||
|
SELECT m.*, bm25(memories_fts) AS rank
|
||||||
|
FROM memories_fts
|
||||||
|
JOIN memories m ON m.id = memories_fts.memory_id
|
||||||
|
WHERE memories_fts MATCH ?
|
||||||
|
{where}
|
||||||
|
ORDER BY rank, m.importance DESC, m.id DESC
|
||||||
|
LIMIT ?
|
||||||
|
""",
|
||||||
|
[match_query, *params, limit],
|
||||||
|
)
|
||||||
|
self._mark_accessed([int(row["id"]) for row in rows])
|
||||||
|
return [self._row_to_memory(row) for row in rows]
|
||||||
|
|
||||||
|
def _search_like(
|
||||||
|
self,
|
||||||
|
query: str,
|
||||||
|
metadata: dict[str, Any],
|
||||||
|
limit: int,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
where, params = self._scope_clause(metadata, "m")
|
||||||
|
text_clause = ""
|
||||||
|
if query.strip():
|
||||||
|
text_clause = "AND (LOWER(m.text) LIKE ? OR LOWER(m.summary) LIKE ? OR LOWER(m.tags_json) LIKE ?)"
|
||||||
|
needle = f"%{query.lower()}%"
|
||||||
|
params.extend([needle, needle, needle])
|
||||||
|
rows = self._fetch_rows(
|
||||||
|
f"""
|
||||||
|
SELECT m.*
|
||||||
|
FROM memories m
|
||||||
|
WHERE 1 = 1
|
||||||
|
{where}
|
||||||
|
{text_clause}
|
||||||
|
ORDER BY m.importance DESC, m.id DESC
|
||||||
|
LIMIT ?
|
||||||
|
""",
|
||||||
|
[*params, limit],
|
||||||
|
)
|
||||||
|
self._mark_accessed([int(row["id"]) for row in rows])
|
||||||
|
return [self._row_to_memory(row) for row in rows]
|
||||||
|
|
||||||
|
def _search_vector(
|
||||||
|
self,
|
||||||
|
query: str,
|
||||||
|
metadata: dict[str, Any],
|
||||||
|
limit: int,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
if not self.vector_index or not self.vector_index.enabled:
|
||||||
|
return []
|
||||||
|
results = self.vector_index.search_memories(query, metadata, limit)
|
||||||
|
if not results:
|
||||||
|
return []
|
||||||
|
ids = [result.sqlite_id for result in results]
|
||||||
|
rows_by_id = {int(row["id"]): row for row in self._fetch_rows_by_ids(ids)}
|
||||||
|
ordered_rows = [rows_by_id[memory_id] for memory_id in ids if memory_id in rows_by_id]
|
||||||
|
self._mark_accessed([int(row["id"]) for row in ordered_rows])
|
||||||
|
return [self._row_to_memory(row) for row in ordered_rows]
|
||||||
|
|
||||||
|
def _fetch_rows(self, statement: str, params: list[Any]) -> list[sqlite3.Row]:
|
||||||
|
with self._connect() as connection:
|
||||||
|
return list(connection.execute(statement, params).fetchall())
|
||||||
|
|
||||||
|
def _fetch_rows_by_ids(self, memory_ids: list[int]) -> list[sqlite3.Row]:
|
||||||
|
if not memory_ids:
|
||||||
|
return []
|
||||||
|
placeholders = ",".join("?" for _ in memory_ids)
|
||||||
|
return self._fetch_rows(f"SELECT * FROM memories WHERE id IN ({placeholders})", list(memory_ids))
|
||||||
|
|
||||||
|
def _upsert_vector(
|
||||||
|
self,
|
||||||
|
memory_id: int,
|
||||||
|
text: str,
|
||||||
|
metadata: dict[str, Any],
|
||||||
|
tags: list[str],
|
||||||
|
) -> str | None:
|
||||||
|
if not self.vector_index or not self.vector_index.enabled:
|
||||||
|
return None
|
||||||
|
payload = {
|
||||||
|
"save_id": metadata.get("save_id"),
|
||||||
|
"campaign_id": metadata.get("campaign_id"),
|
||||||
|
"character_id": metadata.get("character_id"),
|
||||||
|
"related_character_id": metadata.get("related_character_id"),
|
||||||
|
"player_id": metadata.get("player_id"),
|
||||||
|
"kingdom_id": metadata.get("kingdom_id"),
|
||||||
|
"location_id": metadata.get("location_id"),
|
||||||
|
"category": metadata.get("category"),
|
||||||
|
"importance": int(metadata.get("importance") or 3),
|
||||||
|
"visibility": metadata.get("visibility") or "private",
|
||||||
|
"tags": tags,
|
||||||
|
}
|
||||||
|
return self.vector_index.upsert_memory(
|
||||||
|
memory_id,
|
||||||
|
self._index_text(text, metadata.get("summary") or text, tags),
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _mark_accessed(self, memory_ids: list[int]) -> None:
|
||||||
|
if not memory_ids:
|
||||||
|
return
|
||||||
|
placeholders = ",".join("?" for _ in memory_ids)
|
||||||
|
with self._connect() as connection:
|
||||||
|
connection.execute(
|
||||||
|
f"UPDATE memories SET last_accessed_at = CURRENT_TIMESTAMP WHERE id IN ({placeholders})",
|
||||||
|
memory_ids,
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
def _scope_clause(self, metadata: dict[str, Any], alias: str) -> tuple[str, list[Any]]:
|
||||||
|
clauses: list[str] = []
|
||||||
|
params: list[Any] = []
|
||||||
|
for field, column in (
|
||||||
|
("save_id", "save_id"),
|
||||||
|
("campaign_id", "campaign_id"),
|
||||||
|
("character_id", "subject_character_id"),
|
||||||
|
("related_character_id", "related_character_id"),
|
||||||
|
("player_id", "player_id"),
|
||||||
|
("kingdom_id", "kingdom_id"),
|
||||||
|
("location_id", "location_id"),
|
||||||
|
("category", "category"),
|
||||||
|
):
|
||||||
|
value = metadata.get(field)
|
||||||
|
if value is None or value == "":
|
||||||
|
continue
|
||||||
|
if field in {"save_id", "campaign_id"}:
|
||||||
|
clauses.append(f"AND {alias}.{column} = ?")
|
||||||
|
else:
|
||||||
|
clauses.append(f"AND ({alias}.{column} = ? OR {alias}.{column} IS NULL)")
|
||||||
|
params.append(value)
|
||||||
|
return "\n".join(clauses), params
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_metadata(metadata: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
normalized = dict(metadata)
|
||||||
|
tags = normalized.get("tags") or []
|
||||||
|
if isinstance(tags, str):
|
||||||
|
tags = [tags]
|
||||||
|
normalized["tags"] = [str(tag) for tag in tags if str(tag).strip()]
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _fts_query(query: str) -> str:
|
||||||
|
tokens = [token.replace("'", "''") for token in _TOKEN_PATTERN.findall(query.lower())]
|
||||||
|
return " OR ".join(tokens[:12])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _row_to_memory(row: sqlite3.Row) -> dict[str, Any]:
|
||||||
|
metadata = json.loads(row["metadata_json"] or "{}")
|
||||||
|
tags = json.loads(row["tags_json"] or "[]")
|
||||||
|
scope = {
|
||||||
|
"save_id": row["save_id"],
|
||||||
|
"campaign_id": row["campaign_id"],
|
||||||
|
"character_id": row["subject_character_id"],
|
||||||
|
"related_character_id": row["related_character_id"],
|
||||||
|
"player_id": row["player_id"],
|
||||||
|
"kingdom_id": row["kingdom_id"],
|
||||||
|
"location_id": row["location_id"],
|
||||||
|
}
|
||||||
|
metadata.update(
|
||||||
|
{
|
||||||
|
**scope,
|
||||||
|
"category": row["category"],
|
||||||
|
"importance": row["importance"],
|
||||||
|
"confidence": row["confidence"],
|
||||||
|
"visibility": row["visibility"],
|
||||||
|
"tags": tags,
|
||||||
|
"created_day": row["created_day"],
|
||||||
|
"created_at": row["created_at"],
|
||||||
|
"last_accessed_at": row["last_accessed_at"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"id": row["id"],
|
||||||
|
"text": row["text"],
|
||||||
|
"summary": row["summary"],
|
||||||
|
"metadata": metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _index_text(text: str, summary: str, tags: list[str]) -> str:
|
||||||
|
return "\n".join(part for part in (summary, text, " ".join(tags)) if part)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _row_to_vector_payload(row: sqlite3.Row, tags: list[str]) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"save_id": row["save_id"],
|
||||||
|
"campaign_id": row["campaign_id"],
|
||||||
|
"character_id": row["subject_character_id"],
|
||||||
|
"related_character_id": row["related_character_id"],
|
||||||
|
"player_id": row["player_id"],
|
||||||
|
"kingdom_id": row["kingdom_id"],
|
||||||
|
"location_id": row["location_id"],
|
||||||
|
"category": row["category"],
|
||||||
|
"importance": row["importance"],
|
||||||
|
"visibility": row["visibility"],
|
||||||
|
"tags": tags,
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from .contracts import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MockKoboldClient:
|
class MockLlmClient:
|
||||||
"""Deterministic local LLM stand-in for connector testing."""
|
"""Deterministic local LLM stand-in for connector testing."""
|
||||||
|
|
||||||
last_latency_ms: int | None = 1
|
last_latency_ms: int | None = 1
|
||||||
@@ -168,7 +168,7 @@ def _extract_packet(content: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
async def run_local_mock(command: str, args: argparse.Namespace) -> dict[str, Any]:
|
async def run_local_mock(command: str, args: argparse.Namespace) -> dict[str, Any]:
|
||||||
state.kobold_client = MockKoboldClient() # type: ignore[assignment]
|
state.llm_client = MockLlmClient() # type: ignore[assignment]
|
||||||
|
|
||||||
if command == "health":
|
if command == "health":
|
||||||
return agent_health().model_dump(mode="json")
|
return agent_health().model_dump(mode="json")
|
||||||
@@ -243,11 +243,38 @@ async def run_http(command: str, args: argparse.Namespace) -> dict[str, Any]:
|
|||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
async def run_interactive_chat(args: argparse.Namespace) -> None:
|
||||||
|
print("LocalDiplomacy chat. Type /quit to exit.")
|
||||||
|
print(f"Mode: {'mock in-process' if args.mock_agent else args.url}")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
message = input("You> ").strip()
|
||||||
|
except EOFError:
|
||||||
|
print()
|
||||||
|
return
|
||||||
|
if not message:
|
||||||
|
continue
|
||||||
|
if message.lower() in {"/q", "/quit", "quit", "exit"}:
|
||||||
|
return
|
||||||
|
|
||||||
|
args.message = message
|
||||||
|
result = await run_local_mock("say", args) if args.mock_agent else await run_http("say", args)
|
||||||
|
print(f"Derthert> {result.get('assistant_text', '')}")
|
||||||
|
if result.get("game_actions"):
|
||||||
|
print("Actions:")
|
||||||
|
for action in result["game_actions"]:
|
||||||
|
print(f" - {action.get('action_type')} ({action.get('reason')})")
|
||||||
|
if result.get("memory_writes"):
|
||||||
|
print("Memory writes:")
|
||||||
|
for memory in result["memory_writes"]:
|
||||||
|
print(f" - {memory.get('text')}")
|
||||||
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(description="Mock Bannerlord connector for LocalDiplomacy.Agent.")
|
parser = argparse.ArgumentParser(description="Mock Bannerlord connector for LocalDiplomacy.Agent.")
|
||||||
parser.add_argument("--url", default="http://127.0.0.1:8766", help="Agent URL for HTTP mode.")
|
parser.add_argument("--url", default="http://127.0.0.1:8766", help="Agent URL for HTTP mode.")
|
||||||
parser.add_argument("--timeout", type=float, default=120.0, help="HTTP timeout in seconds.")
|
parser.add_argument("--timeout", type=float, default=120.0, help="HTTP timeout in seconds.")
|
||||||
parser.add_argument("--mock-agent", action="store_true", help="Run against the in-process agent with a fake Kobold client.")
|
parser.add_argument("--mock-agent", action="store_true", help="Run against the in-process agent with a fake LLM client.")
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
subparsers.add_parser("health")
|
subparsers.add_parser("health")
|
||||||
@@ -255,6 +282,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
say = subparsers.add_parser("say")
|
say = subparsers.add_parser("say")
|
||||||
say.add_argument("message")
|
say.add_argument("message")
|
||||||
|
|
||||||
|
subparsers.add_parser("chat")
|
||||||
|
|
||||||
tick = subparsers.add_parser("tick")
|
tick = subparsers.add_parser("tick")
|
||||||
tick.add_argument("--day", type=float, default=1.0)
|
tick.add_argument("--day", type=float, default=1.0)
|
||||||
|
|
||||||
@@ -270,6 +299,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
async def async_main() -> int:
|
async def async_main() -> int:
|
||||||
parser = build_parser()
|
parser = build_parser()
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
if args.command == "chat":
|
||||||
|
await run_interactive_chat(args)
|
||||||
|
return 0
|
||||||
if args.mock_agent:
|
if args.mock_agent:
|
||||||
result = await run_local_mock(args.command, args)
|
result = await run_local_mock(args.command, args)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from .config import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class OllamaClient:
|
||||||
|
def __init__(self, config: AppConfig):
|
||||||
|
self.config = config
|
||||||
|
self.last_latency_ms: int | None = None
|
||||||
|
self.last_error: str | None = None
|
||||||
|
self._effective_model: str | None = None
|
||||||
|
|
||||||
|
def is_reachable(self) -> bool:
|
||||||
|
try:
|
||||||
|
with httpx.Client(timeout=2) as client:
|
||||||
|
response = client.get(f"{self.config.ollama.base_url}/api/tags")
|
||||||
|
return response.status_code < 500
|
||||||
|
except httpx.HTTPError as exc:
|
||||||
|
self.last_error = str(exc)
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def chat(
|
||||||
|
self,
|
||||||
|
messages: list[dict[str, Any]],
|
||||||
|
tools: list[dict[str, Any]],
|
||||||
|
tool_choice: str | dict[str, Any] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
payload: dict[str, Any] = {
|
||||||
|
"model": self._model(),
|
||||||
|
"messages": self._prepare_messages(messages),
|
||||||
|
"temperature": self.config.generation.temperature,
|
||||||
|
"max_tokens": self.config.generation.max_tokens,
|
||||||
|
}
|
||||||
|
if tools:
|
||||||
|
payload["tools"] = tools
|
||||||
|
payload["tool_choice"] = tool_choice or "auto"
|
||||||
|
|
||||||
|
url = f"{self.config.ollama.base_url}{self.config.ollama.chat_path}"
|
||||||
|
started = time.perf_counter()
|
||||||
|
async with httpx.AsyncClient(timeout=self.config.ollama.timeout_seconds) as client:
|
||||||
|
response = await client.post(url, json=payload)
|
||||||
|
if response.status_code == 400 and tools and "does not support tools" in response.text:
|
||||||
|
payload.pop("tools", None)
|
||||||
|
payload.pop("tool_choice", None)
|
||||||
|
response = await client.post(url, json=payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
self.last_latency_ms = int((time.perf_counter() - started) * 1000)
|
||||||
|
self.last_error = None
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def _model(self) -> str:
|
||||||
|
if self._effective_model:
|
||||||
|
return self._effective_model
|
||||||
|
|
||||||
|
configured = self.config.ollama.model
|
||||||
|
installed = self._installed_models()
|
||||||
|
if configured.lower().strip() == "auto":
|
||||||
|
self._effective_model = installed[0] if installed else configured
|
||||||
|
return self._effective_model
|
||||||
|
if configured in installed:
|
||||||
|
self._effective_model = configured
|
||||||
|
return configured
|
||||||
|
|
||||||
|
if self.config.ollama.auto_pull_models and self._pull_model(configured):
|
||||||
|
self._effective_model = configured
|
||||||
|
return configured
|
||||||
|
|
||||||
|
self._effective_model = installed[0] if installed else configured
|
||||||
|
return self._effective_model
|
||||||
|
|
||||||
|
def _installed_models(self) -> list[str]:
|
||||||
|
try:
|
||||||
|
with httpx.Client(timeout=2) as client:
|
||||||
|
response = client.get(f"{self.config.ollama.base_url}/api/tags")
|
||||||
|
response.raise_for_status()
|
||||||
|
models = response.json().get("models") or []
|
||||||
|
except httpx.HTTPError as exc:
|
||||||
|
self.last_error = str(exc)
|
||||||
|
return []
|
||||||
|
|
||||||
|
names: list[str] = []
|
||||||
|
for model in models:
|
||||||
|
name = str(model.get("name") or model.get("model") or "")
|
||||||
|
if not name or self._looks_like_embedding_model(model):
|
||||||
|
continue
|
||||||
|
names.append(name)
|
||||||
|
return names
|
||||||
|
|
||||||
|
def _pull_model(self, model: str) -> bool:
|
||||||
|
try:
|
||||||
|
with httpx.Client(timeout=None) as client:
|
||||||
|
response = client.post(
|
||||||
|
f"{self.config.ollama.base_url}/api/pull",
|
||||||
|
json={"model": model, "stream": False},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
self.last_error = None
|
||||||
|
return True
|
||||||
|
except httpx.HTTPError as exc:
|
||||||
|
self.last_error = str(exc)
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _looks_like_embedding_model(model: dict[str, Any]) -> bool:
|
||||||
|
name = str(model.get("name") or model.get("model") or "").lower()
|
||||||
|
details = model.get("details") or {}
|
||||||
|
families = [str(family).lower() for family in details.get("families") or []]
|
||||||
|
family = str(details.get("family") or "").lower()
|
||||||
|
return (
|
||||||
|
"embed" in name
|
||||||
|
or "embedding" in name
|
||||||
|
or family in {"bert", "nomic-bert"}
|
||||||
|
or any(family_name in {"bert", "nomic-bert"} or "embed" in family_name for family_name in families)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _prepare_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
|
if not self.config.generation.suppress_thinking:
|
||||||
|
return messages
|
||||||
|
|
||||||
|
token = self.config.generation.suppress_thinking_token.strip()
|
||||||
|
if not token:
|
||||||
|
return messages
|
||||||
|
|
||||||
|
prepared: list[dict[str, Any]] = []
|
||||||
|
token_prefix = f"{token} "
|
||||||
|
for message in messages:
|
||||||
|
copied = dict(message)
|
||||||
|
content = copied.get("content")
|
||||||
|
role = copied.get("role")
|
||||||
|
if role in {"system", "user"} and isinstance(content, str) and not content.lstrip().startswith(token):
|
||||||
|
copied["content"] = f"{token_prefix}{content}"
|
||||||
|
prepared.append(copied)
|
||||||
|
return prepared
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from .config import VectorIndexConfig, resolve_runtime_path
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class QdrantManagedProcess:
|
||||||
|
config: VectorIndexConfig
|
||||||
|
_process: subprocess.Popen[bytes] | None = None
|
||||||
|
|
||||||
|
def is_reachable(self) -> bool:
|
||||||
|
try:
|
||||||
|
with httpx.Client(timeout=1.0) as client:
|
||||||
|
response = client.get(f"http://{self.config.host}:{self.config.port}/healthz")
|
||||||
|
return response.status_code < 500
|
||||||
|
except httpx.HTTPError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def ensure_started(self) -> None:
|
||||||
|
if self.is_reachable():
|
||||||
|
return
|
||||||
|
if not self.config.autostart:
|
||||||
|
raise RuntimeError("Qdrant server is not reachable and autostart is disabled.")
|
||||||
|
|
||||||
|
executable = resolve_runtime_path(self.config.executable_path)
|
||||||
|
if not executable.exists():
|
||||||
|
raise FileNotFoundError(f"Qdrant executable not found: {executable}")
|
||||||
|
|
||||||
|
storage_path = resolve_runtime_path(self.config.path)
|
||||||
|
storage_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
env = {
|
||||||
|
**os.environ,
|
||||||
|
"QDRANT__SERVICE__HOST": self.config.host,
|
||||||
|
"QDRANT__SERVICE__HTTP_PORT": str(self.config.port),
|
||||||
|
"QDRANT__STORAGE__STORAGE_PATH": str(storage_path),
|
||||||
|
}
|
||||||
|
creation_flags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||||||
|
self._process = subprocess.Popen(
|
||||||
|
[str(executable)],
|
||||||
|
cwd=str(executable.parent),
|
||||||
|
env=env,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
creationflags=creation_flags,
|
||||||
|
)
|
||||||
|
self._wait_until_reachable()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
if self._process is None or self._process.poll() is not None:
|
||||||
|
return
|
||||||
|
self._process.terminate()
|
||||||
|
try:
|
||||||
|
self._process.wait(timeout=10)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
self._process.kill()
|
||||||
|
self._process.wait(timeout=5)
|
||||||
|
|
||||||
|
def _wait_until_reachable(self) -> None:
|
||||||
|
deadline = time.monotonic() + self.config.startup_timeout_seconds
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
if self.is_reachable():
|
||||||
|
return
|
||||||
|
if self._process is not None and self._process.poll() is not None:
|
||||||
|
raise RuntimeError(f"Qdrant exited early with code {self._process.returncode}.")
|
||||||
|
time.sleep(0.25)
|
||||||
|
raise TimeoutError("Timed out waiting for Qdrant to become reachable.")
|
||||||
@@ -14,9 +14,10 @@ ToolHandler = Callable[[dict[str, Any]], dict[str, Any]]
|
|||||||
|
|
||||||
|
|
||||||
class ToolRegistry:
|
class ToolRegistry:
|
||||||
def __init__(self, memory: MemoryStore, event_log: EventLog):
|
def __init__(self, memory: MemoryStore, event_log: EventLog, default_scope: dict[str, Any] | None = None):
|
||||||
self.memory = memory
|
self.memory = memory
|
||||||
self.event_log = event_log
|
self.event_log = event_log
|
||||||
|
self.default_scope = default_scope or {}
|
||||||
self.queued_actions: list[GameAction] = []
|
self.queued_actions: list[GameAction] = []
|
||||||
self.memory_writes: list[MemoryWrite] = []
|
self.memory_writes: list[MemoryWrite] = []
|
||||||
|
|
||||||
@@ -138,6 +139,7 @@ class ToolRegistry:
|
|||||||
"Search local long-term memory for relevant facts.",
|
"Search local long-term memory for relevant facts.",
|
||||||
{
|
{
|
||||||
"query": {"type": "string"},
|
"query": {"type": "string"},
|
||||||
|
"save_id": {"type": "string"},
|
||||||
"campaign_id": {"type": "string"},
|
"campaign_id": {"type": "string"},
|
||||||
"character_id": {"type": "string"},
|
"character_id": {"type": "string"},
|
||||||
"kingdom_id": {"type": "string"},
|
"kingdom_id": {"type": "string"},
|
||||||
@@ -150,6 +152,7 @@ class ToolRegistry:
|
|||||||
"Store an atomic long-term memory fact: conversations, secrets, known info, relationships, visits, events, promises, or personality changes.",
|
"Store an atomic long-term memory fact: conversations, secrets, known info, relationships, visits, events, promises, or personality changes.",
|
||||||
{
|
{
|
||||||
"text": {"type": "string"},
|
"text": {"type": "string"},
|
||||||
|
"save_id": {"type": "string"},
|
||||||
"campaign_id": {"type": "string"},
|
"campaign_id": {"type": "string"},
|
||||||
"character_id": {"type": "string"},
|
"character_id": {"type": "string"},
|
||||||
"kingdom_id": {"type": "string"},
|
"kingdom_id": {"type": "string"},
|
||||||
@@ -180,6 +183,7 @@ class ToolRegistry:
|
|||||||
"analyze_lie",
|
"analyze_lie",
|
||||||
"Analyze whether a player statement conflicts with known memories or live state; returns a recommendation, not a game mutation.",
|
"Analyze whether a player statement conflicts with known memories or live state; returns a recommendation, not a game mutation.",
|
||||||
{
|
{
|
||||||
|
"save_id": {"type": "string"},
|
||||||
"campaign_id": {"type": "string"},
|
"campaign_id": {"type": "string"},
|
||||||
"speaker_id": {"type": "string"},
|
"speaker_id": {"type": "string"},
|
||||||
"listener_id": {"type": "string"},
|
"listener_id": {"type": "string"},
|
||||||
@@ -330,9 +334,10 @@ class ToolRegistry:
|
|||||||
|
|
||||||
def _search_memory(self, args: dict[str, Any]) -> dict[str, Any]:
|
def _search_memory(self, args: dict[str, Any]) -> dict[str, Any]:
|
||||||
metadata = {
|
metadata = {
|
||||||
"campaign_id": args.get("campaign_id"),
|
"save_id": args.get("save_id") or self.default_scope.get("save_id"),
|
||||||
"character_id": args.get("character_id"),
|
"campaign_id": args.get("campaign_id") or self.default_scope.get("campaign_id"),
|
||||||
"kingdom_id": args.get("kingdom_id"),
|
"character_id": args.get("character_id") or self.default_scope.get("character_id"),
|
||||||
|
"kingdom_id": args.get("kingdom_id") or self.default_scope.get("kingdom_id"),
|
||||||
}
|
}
|
||||||
memories = self.memory.search(args["query"], metadata, int(args.get("limit", 5)))
|
memories = self.memory.search(args["query"], metadata, int(args.get("limit", 5)))
|
||||||
return {"ok": True, "memories": memories}
|
return {"ok": True, "memories": memories}
|
||||||
@@ -348,9 +353,10 @@ class ToolRegistry:
|
|||||||
|
|
||||||
def _remember_fact(self, args: dict[str, Any]) -> dict[str, Any]:
|
def _remember_fact(self, args: dict[str, Any]) -> dict[str, Any]:
|
||||||
scope = {
|
scope = {
|
||||||
"campaign_id": args.get("campaign_id"),
|
"save_id": args.get("save_id") or self.default_scope.get("save_id"),
|
||||||
"character_id": args.get("character_id"),
|
"campaign_id": args.get("campaign_id") or self.default_scope.get("campaign_id"),
|
||||||
"kingdom_id": args.get("kingdom_id"),
|
"character_id": args.get("character_id") or self.default_scope.get("character_id"),
|
||||||
|
"kingdom_id": args.get("kingdom_id") or self.default_scope.get("kingdom_id"),
|
||||||
}
|
}
|
||||||
write = MemoryWrite(
|
write = MemoryWrite(
|
||||||
scope=scope,
|
scope=scope,
|
||||||
@@ -371,7 +377,11 @@ class ToolRegistry:
|
|||||||
def _analyze_lie(self, args: dict[str, Any]) -> dict[str, Any]:
|
def _analyze_lie(self, args: dict[str, Any]) -> dict[str, Any]:
|
||||||
memories = self.memory.search(
|
memories = self.memory.search(
|
||||||
args["claim"],
|
args["claim"],
|
||||||
{"campaign_id": args.get("campaign_id"), "character_id": args.get("listener_id")},
|
{
|
||||||
|
"save_id": args.get("save_id") or self.default_scope.get("save_id"),
|
||||||
|
"campaign_id": args.get("campaign_id") or self.default_scope.get("campaign_id"),
|
||||||
|
"character_id": args.get("listener_id"),
|
||||||
|
},
|
||||||
5,
|
5,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .config import VectorIndexConfig, resolve_runtime_path
|
||||||
|
from .embeddings import Embedder
|
||||||
|
from .qdrant_process import QdrantManagedProcess
|
||||||
|
|
||||||
|
|
||||||
|
MEMORY_COLLECTION = "localdiplomacy_memories"
|
||||||
|
LORE_COLLECTION = "localdiplomacy_lore"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VectorSearchResult:
|
||||||
|
sqlite_id: int
|
||||||
|
score: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VectorIndex:
|
||||||
|
config: VectorIndexConfig
|
||||||
|
embedder: Embedder
|
||||||
|
_client: Any = None
|
||||||
|
_managed_process: QdrantManagedProcess | None = None
|
||||||
|
_status: str = "disabled"
|
||||||
|
_last_error: str | None = None
|
||||||
|
_path: Path = field(init=False)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self._path = resolve_runtime_path(self.config.path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self) -> str:
|
||||||
|
if self._last_error:
|
||||||
|
return f"error:{self._last_error}"
|
||||||
|
return self._status
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enabled(self) -> bool:
|
||||||
|
return self._client is not None
|
||||||
|
|
||||||
|
def initialize(self) -> None:
|
||||||
|
mode = self.config.mode.lower().strip()
|
||||||
|
if mode == "disabled":
|
||||||
|
self._status = "disabled"
|
||||||
|
return
|
||||||
|
if mode == "embedded":
|
||||||
|
self._initialize_embedded()
|
||||||
|
return
|
||||||
|
if mode == "managed_server":
|
||||||
|
self._initialize_managed_server()
|
||||||
|
return
|
||||||
|
self._last_error = f"unknown_mode:{self.config.mode}"
|
||||||
|
self._status = "disabled"
|
||||||
|
|
||||||
|
def upsert_memory(self, sqlite_id: int, text: str, payload: dict[str, Any]) -> str | None:
|
||||||
|
return self._upsert(MEMORY_COLLECTION, "memories", sqlite_id, text, payload)
|
||||||
|
|
||||||
|
def upsert_lore_chunk(self, sqlite_id: int, text: str, payload: dict[str, Any]) -> str | None:
|
||||||
|
return self._upsert(LORE_COLLECTION, "lore_chunks", sqlite_id, text, payload)
|
||||||
|
|
||||||
|
def _upsert(
|
||||||
|
self,
|
||||||
|
collection_name: str,
|
||||||
|
sqlite_table: str,
|
||||||
|
sqlite_id: int,
|
||||||
|
text: str,
|
||||||
|
payload: dict[str, Any],
|
||||||
|
) -> str | None:
|
||||||
|
if not self._client:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
vector = self.embedder.embed(text)
|
||||||
|
models = self._models()
|
||||||
|
self._client.upsert(
|
||||||
|
collection_name=collection_name,
|
||||||
|
points=[
|
||||||
|
models.PointStruct(
|
||||||
|
id=sqlite_id,
|
||||||
|
vector=vector,
|
||||||
|
payload={
|
||||||
|
**{key: value for key, value in payload.items() if value is not None},
|
||||||
|
"sqlite_table": sqlite_table,
|
||||||
|
"sqlite_id": sqlite_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
except Exception as exc: # qdrant-client exceptions vary by backend.
|
||||||
|
self._last_error = str(exc)
|
||||||
|
return None
|
||||||
|
return str(sqlite_id)
|
||||||
|
|
||||||
|
def search_memories(self, query: str, filters: dict[str, Any], limit: int) -> list[VectorSearchResult]:
|
||||||
|
return self._search(MEMORY_COLLECTION, query, filters, limit)
|
||||||
|
|
||||||
|
def search_lore(self, query: str, filters: dict[str, Any], limit: int) -> list[VectorSearchResult]:
|
||||||
|
return self._search(LORE_COLLECTION, query, filters, limit)
|
||||||
|
|
||||||
|
def _search(
|
||||||
|
self,
|
||||||
|
collection_name: str,
|
||||||
|
query: str,
|
||||||
|
filters: dict[str, Any],
|
||||||
|
limit: int,
|
||||||
|
) -> list[VectorSearchResult]:
|
||||||
|
if not self._client or not query.strip():
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
vector = self.embedder.embed(query)
|
||||||
|
result = self._query_points(collection_name, vector, filters, limit)
|
||||||
|
except Exception as exc:
|
||||||
|
self._last_error = str(exc)
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
VectorSearchResult(
|
||||||
|
sqlite_id=int(point.payload.get("sqlite_id")),
|
||||||
|
score=float(getattr(point, "score", 0.0)),
|
||||||
|
)
|
||||||
|
for point in result
|
||||||
|
if getattr(point, "payload", None) and point.payload.get("sqlite_id") is not None
|
||||||
|
]
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
if self._managed_process is not None:
|
||||||
|
self._managed_process.stop()
|
||||||
|
|
||||||
|
def _initialize_embedded(self) -> None:
|
||||||
|
try:
|
||||||
|
from qdrant_client import QdrantClient
|
||||||
|
except ImportError:
|
||||||
|
self._last_error = "qdrant_client_not_installed"
|
||||||
|
self._status = "disabled"
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._client = QdrantClient(path=str(self._path))
|
||||||
|
self._ensure_collections()
|
||||||
|
self._status = "embedded"
|
||||||
|
self._last_error = None
|
||||||
|
except Exception as exc:
|
||||||
|
self._client = None
|
||||||
|
self._last_error = str(exc)
|
||||||
|
self._status = "disabled"
|
||||||
|
|
||||||
|
def _initialize_managed_server(self) -> None:
|
||||||
|
try:
|
||||||
|
from qdrant_client import QdrantClient
|
||||||
|
except ImportError:
|
||||||
|
self._last_error = "qdrant_client_not_installed"
|
||||||
|
self._status = "disabled"
|
||||||
|
return
|
||||||
|
|
||||||
|
self._managed_process = QdrantManagedProcess(self.config)
|
||||||
|
try:
|
||||||
|
self._managed_process.ensure_started()
|
||||||
|
self._client = QdrantClient(host=self.config.host, port=self.config.port)
|
||||||
|
self._ensure_collections()
|
||||||
|
self._status = "managed_server"
|
||||||
|
self._last_error = None
|
||||||
|
except Exception as exc:
|
||||||
|
self._client = None
|
||||||
|
self._last_error = str(exc)
|
||||||
|
if self.config.fallback_mode.lower().strip() == "embedded":
|
||||||
|
self._initialize_embedded()
|
||||||
|
else:
|
||||||
|
self._status = "disabled"
|
||||||
|
|
||||||
|
def _ensure_collections(self) -> None:
|
||||||
|
models = self._models()
|
||||||
|
for collection_name in (MEMORY_COLLECTION, LORE_COLLECTION):
|
||||||
|
if not self._client.collection_exists(collection_name):
|
||||||
|
self._client.create_collection(
|
||||||
|
collection_name=collection_name,
|
||||||
|
vectors_config=models.VectorParams(
|
||||||
|
size=self.embedder.dimensions,
|
||||||
|
distance=models.Distance.COSINE,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _query_points(self, collection_name: str, vector: list[float], filters: dict[str, Any], limit: int) -> list[Any]:
|
||||||
|
query_filter = self._build_filter(filters)
|
||||||
|
if hasattr(self._client, "query_points"):
|
||||||
|
response = self._client.query_points(
|
||||||
|
collection_name=collection_name,
|
||||||
|
query=vector,
|
||||||
|
query_filter=query_filter,
|
||||||
|
limit=limit,
|
||||||
|
with_payload=True,
|
||||||
|
)
|
||||||
|
return list(response.points)
|
||||||
|
return list(
|
||||||
|
self._client.search(
|
||||||
|
collection_name=collection_name,
|
||||||
|
query_vector=vector,
|
||||||
|
query_filter=query_filter,
|
||||||
|
limit=limit,
|
||||||
|
with_payload=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _models() -> Any:
|
||||||
|
from qdrant_client import models
|
||||||
|
|
||||||
|
return models
|
||||||
|
|
||||||
|
def _build_filter(self, filters: dict[str, Any]) -> Any:
|
||||||
|
must = []
|
||||||
|
models = self._models()
|
||||||
|
for key in (
|
||||||
|
"save_id",
|
||||||
|
"campaign_id",
|
||||||
|
"character_id",
|
||||||
|
"kingdom_id",
|
||||||
|
"location_id",
|
||||||
|
"category",
|
||||||
|
"lore_source_id",
|
||||||
|
"source_key",
|
||||||
|
):
|
||||||
|
value = filters.get(key)
|
||||||
|
if value is None or value == "":
|
||||||
|
continue
|
||||||
|
must.append(models.FieldCondition(key=key, match=models.MatchValue(value=value)))
|
||||||
|
if not must:
|
||||||
|
return None
|
||||||
|
return models.Filter(must=must)
|
||||||
@@ -7,15 +7,12 @@ dependencies = [
|
|||||||
"fastapi>=0.115.0",
|
"fastapi>=0.115.0",
|
||||||
"httpx>=0.27.0",
|
"httpx>=0.27.0",
|
||||||
"pydantic>=2.8.0",
|
"pydantic>=2.8.0",
|
||||||
|
"qdrant-client>=1.10.0",
|
||||||
"pyyaml>=6.0.0",
|
"pyyaml>=6.0.0",
|
||||||
"uvicorn[standard]>=0.30.0",
|
"uvicorn[standard]>=0.30.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
memory = [
|
|
||||||
"mem0ai>=0.1.0",
|
|
||||||
"qdrant-client>=1.10.0",
|
|
||||||
]
|
|
||||||
test = [
|
test = [
|
||||||
"pytest>=8.2.0",
|
"pytest>=8.2.0",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
from localdiplomacy_agent.config import VectorIndexConfig
|
||||||
|
from localdiplomacy_agent.embeddings import HashingEmbedder
|
||||||
|
from localdiplomacy_agent.lore import LoreStore
|
||||||
|
from localdiplomacy_agent.vector_index import VectorIndex
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleDemoAi:
|
||||||
|
def answer(self, npc_name: str, player_question: str, lore_chunks: list[dict[str, object]]) -> str:
|
||||||
|
lore_context = "\n\n".join(
|
||||||
|
f"{chunk['heading_path']}\n{chunk['text']}"
|
||||||
|
for chunk in lore_chunks
|
||||||
|
)
|
||||||
|
print("\n--- PROMPT LORE CONTEXT GIVEN TO AI ---")
|
||||||
|
print(lore_context)
|
||||||
|
print("--- END PROMPT LORE CONTEXT ---\n")
|
||||||
|
|
||||||
|
assert "Moonlit Compact" in lore_context
|
||||||
|
assert "silver bells" in lore_context
|
||||||
|
return (
|
||||||
|
f"{npc_name}: You ask why our envoys wear silver bells? "
|
||||||
|
"It is the sign of the Moonlit Compact. In Aurelian custom, "
|
||||||
|
"no envoy bearing those bells may be harmed before moonset."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_dummy_lore_execution_prints_console_trace(tmp_path):
|
||||||
|
dummy_world_lore = """
|
||||||
|
# Aurelian Marches
|
||||||
|
|
||||||
|
## Moonlit Compact
|
||||||
|
|
||||||
|
The Moonlit Compact is the oldest diplomatic law of the Aurelian Marches. Envoys who wear silver bells are protected until moonset, even when they cross enemy roads during wartime.
|
||||||
|
|
||||||
|
## Ember Host
|
||||||
|
|
||||||
|
The Ember Host is a militant order that guards mountain passes. Its captains mark treaties with red wax and broken arrowheads.
|
||||||
|
|
||||||
|
## Glass Abbey
|
||||||
|
|
||||||
|
The Glass Abbey records noble bloodlines and refuses entry to anyone carrying an unsheathed blade.
|
||||||
|
"""
|
||||||
|
print("\n=== FULL LORE EXECUTION DEMO ===")
|
||||||
|
print(f"SQLite path: {tmp_path / 'lore.sqlite3'}")
|
||||||
|
print(f"Qdrant path: {tmp_path / 'qdrant'}")
|
||||||
|
|
||||||
|
vector_index = VectorIndex(
|
||||||
|
VectorIndexConfig(mode="embedded", path=str(tmp_path / "qdrant")),
|
||||||
|
HashingEmbedder(),
|
||||||
|
)
|
||||||
|
vector_index.initialize()
|
||||||
|
print(f"Vector index status: {vector_index.status}")
|
||||||
|
|
||||||
|
lore = LoreStore(str(tmp_path / "lore.sqlite3"), vector_index)
|
||||||
|
lore.initialize()
|
||||||
|
imported_count = lore.import_markdown(
|
||||||
|
"aurelian_marches",
|
||||||
|
"Aurelian Marches Demo Lore",
|
||||||
|
dummy_world_lore,
|
||||||
|
)
|
||||||
|
print(f"Imported lore chunks: {imported_count}")
|
||||||
|
|
||||||
|
player_question = "Why does this envoy wear silver bells during war?"
|
||||||
|
retrieved = lore.search(player_question, source_key="aurelian_marches", limit=2)
|
||||||
|
print(f"Query: {player_question}")
|
||||||
|
print(f"Retrieved chunks: {len(retrieved)}")
|
||||||
|
for index, chunk in enumerate(retrieved, start=1):
|
||||||
|
print(f"\nRetrieved #{index}")
|
||||||
|
print(f" id: {chunk['id']}")
|
||||||
|
print(f" title: {chunk['title']}")
|
||||||
|
print(f" heading_path: {chunk['heading_path']}")
|
||||||
|
print(f" qdrant_point_id: {chunk['qdrant_point_id']}")
|
||||||
|
print(f" summary: {chunk['summary']}")
|
||||||
|
|
||||||
|
answer = ConsoleDemoAi().answer(
|
||||||
|
npc_name="Lady Maravel",
|
||||||
|
player_question=player_question,
|
||||||
|
lore_chunks=retrieved,
|
||||||
|
)
|
||||||
|
print("--- FINAL AI-STYLE ANSWER ---")
|
||||||
|
print(answer)
|
||||||
|
print("=== END FULL LORE EXECUTION DEMO ===\n")
|
||||||
|
|
||||||
|
assert imported_count == 3
|
||||||
|
assert retrieved[0]["title"] == "Moonlit Compact"
|
||||||
|
assert "silver bells" in answer
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
from localdiplomacy_agent.config import AppConfig, GenerationConfig
|
|
||||||
from localdiplomacy_agent.koboldcpp_client import KoboldCppClient
|
|
||||||
|
|
||||||
|
|
||||||
def test_suppress_thinking_prefixes_text_messages_once():
|
|
||||||
client = KoboldCppClient(
|
|
||||||
AppConfig(generation=GenerationConfig(suppress_thinking=True))
|
|
||||||
)
|
|
||||||
|
|
||||||
messages = [
|
|
||||||
{"role": "system", "content": "Use tools when needed."},
|
|
||||||
{"role": "user", "content": "/no_think Already prefixed."},
|
|
||||||
{"role": "tool", "tool_call_id": "call_1", "content": "{}"},
|
|
||||||
]
|
|
||||||
|
|
||||||
prepared = client._prepare_messages(messages)
|
|
||||||
|
|
||||||
assert prepared[0]["content"] == "/no_think Use tools when needed."
|
|
||||||
assert prepared[1]["content"] == "/no_think Already prefixed."
|
|
||||||
assert prepared[2]["content"] == "{}"
|
|
||||||
assert messages[0]["content"] == "Use tools when needed."
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from localdiplomacy_agent.config import KoboldCppConfig
|
|
||||||
from localdiplomacy_agent.koboldcpp_process import KoboldCppProcess
|
|
||||||
|
|
||||||
|
|
||||||
def test_runtime_file_validation_reports_missing_files(tmp_path: Path):
|
|
||||||
process = KoboldCppProcess(KoboldCppConfig(), cwd=tmp_path)
|
|
||||||
|
|
||||||
errors = process.validate_runtime_files()
|
|
||||||
|
|
||||||
assert any("koboldcpp.exe" in error for error in errors)
|
|
||||||
assert any("model.gguf" in error for error in errors)
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
from localdiplomacy_agent.config import VectorIndexConfig
|
||||||
|
from localdiplomacy_agent.embeddings import HashingEmbedder
|
||||||
|
from localdiplomacy_agent.lore import LoreStore
|
||||||
|
from localdiplomacy_agent.vector_index import VectorIndex
|
||||||
|
|
||||||
|
|
||||||
|
class LoreAwareFakeAi:
|
||||||
|
def answer(self, player_question: str, lore_chunks: list[dict[str, object]]) -> str:
|
||||||
|
context = "\n".join(str(chunk["text"]) for chunk in lore_chunks)
|
||||||
|
assert "Dawn Court" in context
|
||||||
|
assert "sun oath" in context
|
||||||
|
return "The Dawn Court binds its knights by the sun oath before they ride from Auric Gate."
|
||||||
|
|
||||||
|
|
||||||
|
def test_dummy_world_lore_retrieved_from_vector_database_and_used_by_ai(tmp_path):
|
||||||
|
dummy_lore = """
|
||||||
|
# Kingdoms of Testoria
|
||||||
|
|
||||||
|
## Dawn Court
|
||||||
|
|
||||||
|
The Dawn Court rules from Auric Gate. Its knights swear the sun oath, a vow to protect travelers at first light and never draw steel after sunset unless defending a guest.
|
||||||
|
|
||||||
|
## Ashen League
|
||||||
|
|
||||||
|
The Ashen League is a merchant republic that settles disputes through masked arbitration and river tolls.
|
||||||
|
"""
|
||||||
|
vector_index = VectorIndex(
|
||||||
|
VectorIndexConfig(mode="embedded", path=str(tmp_path / "qdrant")),
|
||||||
|
HashingEmbedder(),
|
||||||
|
)
|
||||||
|
vector_index.initialize()
|
||||||
|
lore = LoreStore(str(tmp_path / "lore.sqlite3"), vector_index)
|
||||||
|
lore.initialize()
|
||||||
|
|
||||||
|
imported = lore.import_markdown("dummy_testoria", "Testoria Dummy Lore", dummy_lore)
|
||||||
|
chunks = lore.search("Who swears the sun oath at Auric Gate?", source_key="dummy_testoria", limit=1)
|
||||||
|
answer = LoreAwareFakeAi().answer("Who swears the sun oath?", chunks)
|
||||||
|
|
||||||
|
assert imported == 2
|
||||||
|
assert vector_index.status == "embedded"
|
||||||
|
assert chunks[0]["title"] == "Dawn Court"
|
||||||
|
assert chunks[0]["qdrant_point_id"] == "1"
|
||||||
|
assert "Dawn Court" in answer
|
||||||
|
assert "sun oath" in answer
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
from localdiplomacy_agent.config import MemoryConfig
|
||||||
|
from localdiplomacy_agent.config import VectorIndexConfig
|
||||||
|
from localdiplomacy_agent.embeddings import HashingEmbedder
|
||||||
|
from localdiplomacy_agent.event_log import EventLog
|
||||||
|
from localdiplomacy_agent.memory import MemoryStore
|
||||||
|
from localdiplomacy_agent.tools import ToolRegistry
|
||||||
|
from localdiplomacy_agent.vector_index import VectorIndex
|
||||||
|
|
||||||
|
|
||||||
|
def test_memory_store_persists_across_instances(tmp_path):
|
||||||
|
sqlite_path = tmp_path / "memory.sqlite3"
|
||||||
|
first = MemoryStore(MemoryConfig(sqlite_path=str(sqlite_path)))
|
||||||
|
first.initialize()
|
||||||
|
first.remember(
|
||||||
|
"The player promised Derthert they would defend Sargot from Battania.",
|
||||||
|
{
|
||||||
|
"save_id": "save-a",
|
||||||
|
"campaign_id": "campaign",
|
||||||
|
"character_id": "lord_derthert",
|
||||||
|
"kingdom_id": "kingdom_vlandia",
|
||||||
|
"category": "promise",
|
||||||
|
"importance": 8,
|
||||||
|
"tags": ["sargot", "battania"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
second = MemoryStore(MemoryConfig(sqlite_path=str(sqlite_path)))
|
||||||
|
second.initialize()
|
||||||
|
|
||||||
|
matches = second.search(
|
||||||
|
"defend Sargot",
|
||||||
|
{
|
||||||
|
"save_id": "save-a",
|
||||||
|
"campaign_id": "campaign",
|
||||||
|
"character_id": "lord_derthert",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert second.count_estimate() == 1
|
||||||
|
assert matches[0]["text"] == "The player promised Derthert they would defend Sargot from Battania."
|
||||||
|
assert matches[0]["metadata"]["category"] == "promise"
|
||||||
|
assert matches[0]["metadata"]["importance"] == 8
|
||||||
|
|
||||||
|
|
||||||
|
def test_memory_store_isolates_saves(tmp_path):
|
||||||
|
store = MemoryStore(MemoryConfig(sqlite_path=str(tmp_path / "memory.sqlite3")))
|
||||||
|
store.initialize()
|
||||||
|
store.remember(
|
||||||
|
"The player promised Derthert they would defend Sargot.",
|
||||||
|
{"save_id": "save-a", "campaign_id": "campaign", "character_id": "lord_derthert"},
|
||||||
|
)
|
||||||
|
store.remember(
|
||||||
|
"The player betrayed Derthert near Sargot.",
|
||||||
|
{"save_id": "save-b", "campaign_id": "campaign", "character_id": "lord_derthert"},
|
||||||
|
)
|
||||||
|
|
||||||
|
matches = store.search(
|
||||||
|
"Derthert Sargot",
|
||||||
|
{"save_id": "save-a", "campaign_id": "campaign", "character_id": "lord_derthert"},
|
||||||
|
limit=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(matches) == 1
|
||||||
|
assert "promised" in matches[0]["text"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_tool_registry_injects_default_save_scope(tmp_path):
|
||||||
|
memory = MemoryStore(MemoryConfig(sqlite_path=str(tmp_path / "memory.sqlite3")))
|
||||||
|
registry = ToolRegistry(
|
||||||
|
memory,
|
||||||
|
EventLog(tmp_path / "events.sqlite3"),
|
||||||
|
{
|
||||||
|
"save_id": "save-a",
|
||||||
|
"campaign_id": "campaign",
|
||||||
|
"character_id": "lord_derthert",
|
||||||
|
"kingdom_id": "kingdom_vlandia",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
registry.dispatch(
|
||||||
|
"remember_fact",
|
||||||
|
'{"text":"The player promised to defend Sargot.","category":"promise"}',
|
||||||
|
)
|
||||||
|
|
||||||
|
matches = memory.search(
|
||||||
|
"defend Sargot",
|
||||||
|
{"save_id": "save-a", "campaign_id": "campaign", "character_id": "lord_derthert"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(matches) == 1
|
||||||
|
assert matches[0]["metadata"]["save_id"] == "save-a"
|
||||||
|
assert matches[0]["metadata"]["campaign_id"] == "campaign"
|
||||||
|
|
||||||
|
|
||||||
|
def test_memory_store_upserts_embedded_vector_index(tmp_path):
|
||||||
|
sqlite_path = tmp_path / "memory.sqlite3"
|
||||||
|
vector_index = VectorIndex(
|
||||||
|
VectorIndexConfig(mode="embedded", path=str(tmp_path / "qdrant")),
|
||||||
|
HashingEmbedder(),
|
||||||
|
)
|
||||||
|
vector_index.initialize()
|
||||||
|
memory = MemoryStore(MemoryConfig(sqlite_path=str(sqlite_path)), vector_index)
|
||||||
|
memory.initialize()
|
||||||
|
|
||||||
|
memory.remember(
|
||||||
|
"The player promised Derthert they would defend Sargot from Battania.",
|
||||||
|
{
|
||||||
|
"save_id": "save-a",
|
||||||
|
"campaign_id": "campaign",
|
||||||
|
"character_id": "lord_derthert",
|
||||||
|
"kingdom_id": "kingdom_vlandia",
|
||||||
|
"category": "promise",
|
||||||
|
"tags": ["sargot", "battania"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
matches = memory.search(
|
||||||
|
"promise defend Sargot",
|
||||||
|
{
|
||||||
|
"save_id": "save-a",
|
||||||
|
"campaign_id": "campaign",
|
||||||
|
"character_id": "lord_derthert",
|
||||||
|
"category": "promise",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with sqlite3.connect(sqlite_path) as connection:
|
||||||
|
row = connection.execute("SELECT qdrant_point_id FROM memories").fetchone()
|
||||||
|
|
||||||
|
assert vector_index.status == "embedded"
|
||||||
|
assert row[0] == "1"
|
||||||
|
assert matches[0]["id"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_managed_qdrant_falls_back_to_embedded_when_unavailable(tmp_path):
|
||||||
|
vector_index = VectorIndex(
|
||||||
|
VectorIndexConfig(
|
||||||
|
mode="managed_server",
|
||||||
|
path=str(tmp_path / "qdrant"),
|
||||||
|
port=65333,
|
||||||
|
autostart=False,
|
||||||
|
fallback_mode="embedded",
|
||||||
|
),
|
||||||
|
HashingEmbedder(),
|
||||||
|
)
|
||||||
|
|
||||||
|
vector_index.initialize()
|
||||||
|
|
||||||
|
assert vector_index.status == "embedded"
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
from localdiplomacy_agent.config import AppConfig, GenerationConfig
|
||||||
|
from localdiplomacy_agent.config import OllamaConfig
|
||||||
|
from localdiplomacy_agent.ollama_client import OllamaClient
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def test_suppress_thinking_prefixes_text_messages_once():
|
||||||
|
client = OllamaClient(
|
||||||
|
AppConfig(generation=GenerationConfig(suppress_thinking=True))
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": "Use tools when needed."},
|
||||||
|
{"role": "user", "content": "/no_think Already prefixed."},
|
||||||
|
{"role": "tool", "tool_call_id": "call_1", "content": "{}"},
|
||||||
|
]
|
||||||
|
|
||||||
|
prepared = client._prepare_messages(messages)
|
||||||
|
|
||||||
|
assert prepared[0]["content"] == "/no_think Use tools when needed."
|
||||||
|
assert prepared[1]["content"] == "/no_think Already prefixed."
|
||||||
|
assert prepared[2]["content"] == "{}"
|
||||||
|
assert messages[0]["content"] == "Use tools when needed."
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_falls_back_to_first_installed_ollama_model(monkeypatch):
|
||||||
|
class FakeResponse:
|
||||||
|
def raise_for_status(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {"models": [{"name": "installed-model:latest"}]}
|
||||||
|
|
||||||
|
class FakeClient:
|
||||||
|
def __init__(self, timeout):
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc, tb):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get(self, url):
|
||||||
|
return FakeResponse()
|
||||||
|
|
||||||
|
monkeypatch.setattr("localdiplomacy_agent.ollama_client.httpx.Client", FakeClient)
|
||||||
|
|
||||||
|
client = OllamaClient(AppConfig(ollama=OllamaConfig(model="missing-model", auto_pull_models=False)))
|
||||||
|
|
||||||
|
assert client._model() == "installed-model:latest"
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_model_uses_first_installed_ollama_model(monkeypatch):
|
||||||
|
class FakeResponse:
|
||||||
|
def raise_for_status(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {"models": [{"name": "installed-model:latest"}]}
|
||||||
|
|
||||||
|
class FakeClient:
|
||||||
|
def __init__(self, timeout):
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc, tb):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get(self, url):
|
||||||
|
return FakeResponse()
|
||||||
|
|
||||||
|
monkeypatch.setattr("localdiplomacy_agent.ollama_client.httpx.Client", FakeClient)
|
||||||
|
|
||||||
|
client = OllamaClient(AppConfig(ollama=OllamaConfig(model="auto", auto_pull_models=True)))
|
||||||
|
|
||||||
|
assert client._model() == "installed-model:latest"
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_model_skips_embedding_models(monkeypatch):
|
||||||
|
class FakeResponse:
|
||||||
|
def raise_for_status(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {
|
||||||
|
"models": [
|
||||||
|
{"name": "nomic-embed-text:latest", "details": {"family": "bert", "families": ["bert"]}},
|
||||||
|
{"name": "chat-model:latest", "details": {"family": "qwen2", "families": ["qwen2"]}},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeClient:
|
||||||
|
def __init__(self, timeout):
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc, tb):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get(self, url):
|
||||||
|
return FakeResponse()
|
||||||
|
|
||||||
|
monkeypatch.setattr("localdiplomacy_agent.ollama_client.httpx.Client", FakeClient)
|
||||||
|
|
||||||
|
client = OllamaClient(AppConfig(ollama=OllamaConfig(model="auto", auto_pull_models=True)))
|
||||||
|
|
||||||
|
assert client._model() == "chat-model:latest"
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_pulls_missing_configured_model(monkeypatch):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
class FakeResponse:
|
||||||
|
def __init__(self, data):
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
def raise_for_status(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return self.data
|
||||||
|
|
||||||
|
class FakeClient:
|
||||||
|
def __init__(self, timeout):
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc, tb):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get(self, url):
|
||||||
|
return FakeResponse({"models": []})
|
||||||
|
|
||||||
|
def post(self, url, json):
|
||||||
|
calls.append((url, json))
|
||||||
|
return FakeResponse({"status": "success"})
|
||||||
|
|
||||||
|
monkeypatch.setattr("localdiplomacy_agent.ollama_client.httpx.Client", FakeClient)
|
||||||
|
|
||||||
|
client = OllamaClient(AppConfig(ollama=OllamaConfig(model="llama3.1:8b", auto_pull_models=True)))
|
||||||
|
|
||||||
|
assert client._model() == "llama3.1:8b"
|
||||||
|
assert calls == [
|
||||||
|
(
|
||||||
|
"http://127.0.0.1:11434/api/pull",
|
||||||
|
{"model": "llama3.1:8b", "stream": False},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_chat_retries_without_tools_when_model_does_not_support_tools(monkeypatch):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
class FakeResponse:
|
||||||
|
def __init__(self, status_code, text, data):
|
||||||
|
self.status_code = status_code
|
||||||
|
self.text = text
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
def raise_for_status(self):
|
||||||
|
if self.status_code >= 400:
|
||||||
|
raise AssertionError("fallback response should be successful")
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return self.data
|
||||||
|
|
||||||
|
class FakeAsyncClient:
|
||||||
|
def __init__(self, timeout):
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def post(self, url, json):
|
||||||
|
calls.append(dict(json))
|
||||||
|
if len(calls) == 1:
|
||||||
|
return FakeResponse(400, "model does not support tools", {})
|
||||||
|
return FakeResponse(200, "ok", {"choices": [{"message": {"content": "Hello."}}]})
|
||||||
|
|
||||||
|
monkeypatch.setattr("localdiplomacy_agent.ollama_client.httpx.AsyncClient", FakeAsyncClient)
|
||||||
|
|
||||||
|
client = OllamaClient(AppConfig(ollama=OllamaConfig(model="Qwen3-4B-abliterated-q4_k_m")))
|
||||||
|
client._effective_model = "Qwen3-4B-abliterated-q4_k_m"
|
||||||
|
result = await client.chat(
|
||||||
|
[{"role": "user", "content": "Hello"}],
|
||||||
|
[{"type": "function", "function": {"name": "remember_fact", "parameters": {"type": "object"}}}],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["choices"][0]["message"]["content"] == "Hello."
|
||||||
|
assert "tools" in calls[0]
|
||||||
|
assert "tools" not in calls[1]
|
||||||
Generated
+3
-433
@@ -39,15 +39,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "backoff"
|
|
||||||
version = "2.2.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2026.4.22"
|
version = "2026.4.22"
|
||||||
@@ -57,95 +48,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
|
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "charset-normalizer"
|
|
||||||
version = "3.4.7"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.3.3"
|
version = "8.3.3"
|
||||||
@@ -167,15 +69,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.136.1"
|
version = "0.136.1"
|
||||||
@@ -192,63 +85,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" },
|
{ url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "greenlet"
|
|
||||||
version = "3.5.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/3c/3f/dbf99fb14bfeb88c28f16729215478c0e265cacd6dc22270c8f31bb6892f/greenlet-3.5.0.tar.gz", hash = "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", size = 196995, upload-time = "2026-04-27T13:37:15.544Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/0f/a91f143f356523ff682309732b175765a9bc2836fd7c081c2c67fedc1ad4/greenlet-3.5.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082", size = 284726, upload-time = "2026-04-27T12:20:51.402Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/82/800646c7ffc5dbabd75ddd2f6b519bb898c0c9c969e5d0473bfe5d20bcce/greenlet-3.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3", size = 604264, upload-time = "2026-04-27T12:52:39.494Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/ac/354867c0bba812fc33b15bc55aedafedd0aee3c7dd91dfca22444157dc0c/greenlet-3.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c", size = 616099, upload-time = "2026-04-27T12:59:39.623Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/ab/192090c4a5b30df148c22bf4b8895457d739a7c7c5a7b9c41e5dd7f537f2/greenlet-3.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa94cb2288681e3a11645958f1871d48ee9211bd2f66628fdace505927d6e564", size = 623976, upload-time = "2026-04-27T13:02:37.363Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/b0/815bece7399e01cadb69014219eebd0042339875c59a59b0820a46ece356/greenlet-3.5.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662", size = 615198, upload-time = "2026-04-27T12:25:25.928Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/24/11/05eb2b9b188c6df7d68a89c99134d644a7af616a40b9808e8e6ced315d5d/greenlet-3.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:64d6ac45f7271f48e45f67c95b54ef73534c52ec041fcda8edf520c6d811f4bc", size = 418379, upload-time = "2026-04-27T13:05:12.755Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/80/3b2c0a895d6698f6ddb31b07942ebfa982f3e30888bc5546a5b5990de8b2/greenlet-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b", size = 1574927, upload-time = "2026-04-27T12:53:25.81Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/0e/f354af514a4c61454dbc68e44d47544a5a4d6317e30b77ddfa3a09f4c5f3/greenlet-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4", size = 1642683, upload-time = "2026-04-27T12:25:23.9Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/6a/87f38255201e993a1915265ebb80cd7c2c78b04a45744995abbf6b259fd8/greenlet-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8", size = 238115, upload-time = "2026-04-27T12:21:48.845Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/f8/450fe3c5938fa737ea4d22699772e6e34e8e24431a47bf4e8a1ceed4a98e/greenlet-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:6c18dfb59c70f5a94acd271c72e90128c3c776e41e5f07767908c8c1b74ad339", size = 235017, upload-time = "2026-04-27T12:22:26.768Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ef/32/f2ce6d4cac3e55bc6173f92dbe627e782e1850f89d986c3606feb63aafa7/greenlet-3.5.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", size = 286228, upload-time = "2026-04-27T12:20:34.421Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/aa/caed9e5adf742315fc7be2a84196373aab4816e540e38ba0d76cb7584d68/greenlet-3.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", size = 601775, upload-time = "2026-04-27T12:52:41.045Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/af/90ae08497400a941595d12774447f752d3dfe0fbb012e35b76bc5c0ff37e/greenlet-3.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", size = 614436, upload-time = "2026-04-27T12:59:41.595Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/e9/4eeadf8cb3403ac274245ba75f07844abc7fa5f6787583fc9156ba741e0f/greenlet-3.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:41353ec2ecedf7aa8f682753a41919f8718031a6edac46b8d3dc7ed9e1ceb136", size = 620610, upload-time = "2026-04-27T13:02:39.194Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/e0/2e13df68f367e2f9960616927d60857dd7e56aaadd59a47c644216b2f920/greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", size = 611388, upload-time = "2026-04-27T12:25:28.008Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/ef/f913b3c0eb7d26d86a2401c5e1546c9d46b657efee724b06f6f4ac5d8824/greenlet-3.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:58c1c374fe2b3d852f9b6b11a7dff4c85404e51b9a596fd9e89cf904eb09866d", size = 422775, upload-time = "2026-04-27T13:05:14.261Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/82/f7/393c64055132ac0d488ef6be549253b7e6274194863967ddc0bc8f5b87b8/greenlet-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", size = 1570768, upload-time = "2026-04-27T12:53:28.099Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/4b/eaf7735253522cf56d1b74d672a58f54fc114702ceaf05def59aae72f6e1/greenlet-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", size = 1635983, upload-time = "2026-04-27T12:25:26.903Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4c/fe/4fb3a0805bd5165da5ebf858da7cc01cce8061674106d2cf5bdab32cbfde/greenlet-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", size = 238840, upload-time = "2026-04-27T12:23:54.806Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/cb/baa584cb00532126ffe12d9787db0a60c5a4f55c27bfe2666df5d4c30a32/greenlet-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2", size = 235615, upload-time = "2026-04-27T12:21:38.57Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/58/fc576f99037ce19c5aa16628e4c3226b6d1419f72a62c79f5f40576e6eb3/greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", size = 285066, upload-time = "2026-04-27T12:23:05.033Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4a/ba/b28ddbe6bfad6a8ac196ef0e8cff37bc65b79735995b9e410923fffeeb70/greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", size = 604414, upload-time = "2026-04-27T12:52:42.358Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/09/06/4b69f8f0b67603a8be2790e55107a190b376f2627fe0eaf5695d85ffb3cd/greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", size = 617349, upload-time = "2026-04-27T12:59:43.32Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/15/a643b4ecd09969e30b8a150d5919960caae0abe4f5af75ab040b1ab85e78/greenlet-3.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d", size = 623234, upload-time = "2026-04-27T13:02:40.611Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", size = 613927, upload-time = "2026-04-27T12:25:30.28Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/18/3b13d5ef1275b0ffaf933b05efa21408ac4ca95823c7411d79682e4fdcff/greenlet-3.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae", size = 425243, upload-time = "2026-04-27T13:05:15.689Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/e1/bd0af6213c7dd33175d8a462d4c1fe1175124ebed4855bc1475a5b5242c2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", size = 1570893, upload-time = "2026-04-27T12:53:29.483Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9b/2a/0789702f864f5382cb476b93d7a9c823c10472658102ccd65f415747d2e2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", size = 1636060, upload-time = "2026-04-27T12:25:28.845Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", size = 238740, upload-time = "2026-04-27T12:24:01.341Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/b7/9c5c3d653bd4ff614277c049ac676422e2c557db47b4fe43e6313fc005dc/greenlet-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", size = 235525, upload-time = "2026-04-27T12:23:12.308Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/5e/a70f31e3e8d961c4ce589c15b28e4225d63704e431a23932a3808cbcc867/greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", size = 285564, upload-time = "2026-04-27T12:23:08.555Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/af/a6/046c0a28e21833e4086918218cfb3d8bed51c075a1b700f20b9d7861c0f4/greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", size = 651166, upload-time = "2026-04-27T12:52:43.644Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/f8/4af27f71c5ff32a7fbc516adb46370d9c4ae2bc7bd3dc7d066ac542b4b15/greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", size = 663792, upload-time = "2026-04-27T12:59:44.93Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/89/2dadb89793c37ee8b4c237857188293e9060dc085f19845c292e00f8e091/greenlet-3.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf2d8a80bec89ab46221ae45c5373d5ba0bd36c19aa8508e85c6cd7e5106cd37", size = 668086, upload-time = "2026-04-27T13:02:42.314Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", size = 660933, upload-time = "2026-04-27T12:25:33.276Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/82/35/75722be7e26a2af4cbd2dc35b0ed382dacf9394b7e75551f76ed1abe87f2/greenlet-3.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:1bae92a1dd94c5f9d9493c3a212dd874c202442047cf96446412c862feca83a2", size = 470799, upload-time = "2026-04-27T13:05:17.094Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/83/e4/b903e5a5fae1e8a28cdd32a0cfbfd560b668c25b692f67768822ddc5f40f/greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", size = 1618401, upload-time = "2026-04-27T12:53:31.062Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/e3/5ec408a329acb854fb607a122e1ee5fb3ff649f9a97952948a90803c0d8e/greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", size = 1682038, upload-time = "2026-04-27T12:25:31.838Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", size = 239835, upload-time = "2026-04-27T12:24:54.136Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4e/62/1c498375cee177b55d980c1db319f26470e5309e54698c8f8fc06c0fd539/greenlet-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", size = 236862, upload-time = "2026-04-27T12:23:24.957Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/a8/4522939255bb5409af4e87132f915446bf3622c2c292d14d3c38d128ae82/greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", size = 293614, upload-time = "2026-04-27T12:24:12.874Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/5e/8744c52e2c027b5a8772a01561934c8835f869733e101f62075c60430340/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", size = 650723, upload-time = "2026-04-27T12:52:45.412Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/ef/7b4c39c03cf46ceca512c5d3f914afd85aa30b2cc9a93015b0dd73e4be6c/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", size = 656529, upload-time = "2026-04-27T12:59:46.295Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/5c/0602239503b124b70e39355cbdb39361ecfe65b87a5f2f63752c32f5286f/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1aa4ce8debcd4ea7fb2e150f3036588c41493d1d52c43538924ae1819003f4ce", size = 657015, upload-time = "2026-04-27T13:02:43.973Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/b5/c7768f352f5c010f92064d0063f987e7dc0cd290a6d92a34109015ce4aa1/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", size = 654364, upload-time = "2026-04-27T12:25:35.64Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/51/8699f865f125dc952384cb432b0f7138aa4d8f2969a7d12d0df5b94d054d/greenlet-3.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:728a73687e39ae9ca34e4694cbf2f049d3fbc7174639468d0f67200a97d8f9e2", size = 488275, upload-time = "2026-04-27T13:05:18.28Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ef/d0/079ebe12e4b1fc758857ce5be1a5e73f06870f2101e52611d1e71925ce54/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", size = 1614204, upload-time = "2026-04-27T12:53:32.618Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/89/6c2fb63df3596552d20e58fb4d96669243388cf680cff222758812c7bfaa/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", size = 1675480, upload-time = "2026-04-27T12:25:34.168Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "grpcio"
|
name = "grpcio"
|
||||||
version = "1.80.0"
|
version = "1.80.0"
|
||||||
@@ -427,96 +263,6 @@ 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" },
|
{ 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.14.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/1f/198ae537fccb7080a0ed655eb56abf64a92f79489dfbf79f40fa34225bcd/jiter-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7e791e247b8044512e070bd1f3633dc08350d32776d2d6e7473309d0edf256a2", size = 316896, upload-time = "2026-04-10T14:26:01.986Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/34/da67cff3fce964a36d03c3e365fb0f8726ade2a6cfd4d3c70107e216ead6/jiter-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71527ce13fd5a0c4e40ad37331f8c547177dbb2dd0a93e5278b6a5eecf748804", size = 321085, upload-time = "2026-04-10T14:26:03.364Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/36/4c72e67180d4e71a4f5dcf7886d0840e83c49ab11788172177a77570326e/jiter-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c4a7ab56f746014874f2c525584c0daca1dec37f66fd707ecef3b7e5c2228c", size = 347393, upload-time = "2026-04-10T14:26:05.314Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/db/9b39e09ceafa9878235c0fc29e3e3f9b12a4c6a98ea3085b998cadf3accc/jiter-0.14.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:376e9dafff914253bb9d46cdc5f7965607fbe7feb0a491c34e35f92b2770702e", size = 372937, upload-time = "2026-04-10T14:26:06.884Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/96/0dcba1d7a82c1b720774b48ef239376addbaf30df24c34742ac4a57b67b2/jiter-0.14.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23ad2a7a9da1935575c820428dd8d2490ce4d23189691ce33da1fc0a58e14e1c", size = 463646, upload-time = "2026-04-10T14:26:08.345Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/e3/f61b71543e746e6b8b805e7755814fc242715c16f1dba58e1cbccb8032c2/jiter-0.14.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54b3ddf5786bc7732d293bba3411ac637ecfa200a39983166d1df86a59a43c9f", size = 380225, upload-time = "2026-04-10T14:26:10.161Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/5e/0ddeb7096aca099114abe36c4921016e8d251e6f35f5890240b31f1f60ae/jiter-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c001d5a646c2a50dc055dd526dad5d5245969e8234d2b1131d0451e81f3a373", size = 358682, upload-time = "2026-04-10T14:26:11.574Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/d1/fe0c46cd7fda9cad8f1ff9ad217dc61f1e4280b21052ec6dfe88c1446ef2/jiter-0.14.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:834bb5bdabca2e91592a03d373838a8d0a1b8bbde7077ae6913fd2fc51812d00", size = 359973, upload-time = "2026-04-10T14:26:13.316Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/21/f5317f91729b501019184771c80d60abd89907009e7bfa6c7e348c5bdd44/jiter-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4e9178be60e229b1b2b0710f61b9e24d1f4f8556985a83ff4c4f95920eea7314", size = 397568, upload-time = "2026-04-10T14:26:15.212Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/05/79d8f33fb2bf168db0df5c9cd16fe440a8ada57e929d3677b22712c2568f/jiter-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7e4ccff04ec03614e62c613e976a3a5860dc9714ce8266f44328bdc8b1cab2c", size = 522535, upload-time = "2026-04-10T14:26:16.956Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/00/d1e3ff3d2a465e67f08507d74bafb2dcd29eba91dc939820e39e8dea38b8/jiter-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:69539d936fb5d55caf6ecd33e2e884de083ff0ea28579780d56c4403094bb8d9", size = 556709, upload-time = "2026-04-10T14:26:18.5Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/5b/bbb2189f62ace8d95e869aa4c84c9946616f301e2d02895a6f20dcc3bba3/jiter-0.14.0-cp311-cp311-win32.whl", hash = "sha256:4927d09b3e572787cc5e0a5318601448e1ab9391bcef95677f5840c2d00eaa6d", size = 208660, upload-time = "2026-04-10T14:26:20.511Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/86/c500b53dcbf08575f5963e536ebd757a1f7c568272ba5d180b212c9a87fb/jiter-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:42d6ed359ac49eb922fdd565f209c57340aa06d589c84c8413e42a0f9ae1b842", size = 204659, upload-time = "2026-04-10T14:26:22.152Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/4a/a676249049d42cb29bef82233e4fe0524d414cbe3606c7a4b311193c2f77/jiter-0.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:6dd689f5f4a5a33747b28686e051095beb214fe28cfda5e9fe58a295a788f593", size = 194772, upload-time = "2026-04-10T14:26:23.458Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/68/7390a418f10897da93b158f2d5a8bd0bcd73a0f9ec3bb36917085bb759ef/jiter-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb2ce3a7bc331256dfb14cefc34832366bb28a9aca81deaf43bbf2a5659e607", size = 316295, upload-time = "2026-04-10T14:26:24.887Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/a0/5854ac00ff63551c52c6c89534ec6aba4b93474e7924d64e860b1c94165b/jiter-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5252a7ca23785cef5d02d4ece6077a1b556a410c591b379f82091c3001e14844", size = 315898, upload-time = "2026-04-10T14:26:26.601Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/a1/4f44832650a16b18e8391f1bf1d6ca4909bc738351826bcc198bba4357f4/jiter-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c409578cbd77c338975670ada777add4efd53379667edf0aceea730cabede6fb", size = 343730, upload-time = "2026-04-10T14:26:28.326Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/64/a329e9d469f86307203594b1707e11ae51c3348d03bfd514a5f997870012/jiter-0.14.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ede4331a1899d604463369c730dbb961ffdc5312bc7f16c41c2896415b1304a", size = 370102, upload-time = "2026-04-10T14:26:30.089Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/c1/5e3dfc59635aa4d4c7bd20a820ac1d09b8ed851568356802cf1c08edb3cf/jiter-0.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92cd8b6025981a041f5310430310b55b25ca593972c16407af8837d3d7d2ca01", size = 461335, upload-time = "2026-04-10T14:26:31.911Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/1b/dd157009dbc058f7b00108f545ccb72a2d56461395c4fc7b9cfdccb00af4/jiter-0.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:351bf6eda4e3a7ceb876377840c702e9a3e4ecc4624dbfb2d6463c67ae52637d", size = 378536, upload-time = "2026-04-10T14:26:33.595Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/78/256013667b7c10b8834f8e6e54cd3e562d4c6e34227a1596addccc05e38c/jiter-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfbeb93d9ecd9ca128bbf8910120367777973fa193fb9a39c31237d8df165", size = 353859, upload-time = "2026-04-10T14:26:35.098Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/d9/137d65ade9093a409fe80955ce60b12bb753722c986467aeda47faf450ad/jiter-0.14.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ae039aaef8de3f8157ecc1fdd4d85043ac4f57538c245a0afaecb8321ec951c3", size = 357626, upload-time = "2026-04-10T14:26:36.685Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/48/76750835b87029342727c1a268bea8878ab988caf81ee4e7b880900eeb5a/jiter-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d9d51eb96c82a9652933bd769fe6de66877d6eb2b2440e281f2938c51b5643e", size = 393172, upload-time = "2026-04-10T14:26:38.097Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/60/456c4e81d5c8045279aefe60e9e483be08793828800a4e64add8fdde7f2a/jiter-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d824ca4148b705970bf4e120924a212fdfca9859a73e42bd7889a63a4ea6bb98", size = 520300, upload-time = "2026-04-10T14:26:39.532Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/9f/2020e0984c235f678dced38fe4eec3058cf528e6af36ebf969b410305941/jiter-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3", size = 553059, upload-time = "2026-04-10T14:26:40.991Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ef/32/e2d298e1a22a4bbe6062136d1c7192db7dba003a6975e51d9a9eecabc4c2/jiter-0.14.0-cp312-cp312-win32.whl", hash = "sha256:5dec7c0a3e98d2a3f8a2e67382d0d7c3ac60c69103a4b271da889b4e8bb1e129", size = 206030, upload-time = "2026-04-10T14:26:42.517Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/ac/96369141b3d8a4a8e4590e983085efe1c436f35c0cda940dd76d942e3e40/jiter-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc7e37b4b8bc7e80a63ad6cfa5fc11fab27dbfea4cc4ae644b1ab3f273dc348f", size = 201603, upload-time = "2026-04-10T14:26:44.328Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/01/c3/75d847f264647017d7e3052bbcc8b1e24b95fa139c320c5f5066fa7a0bdd/jiter-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:ee4a72f12847ef29b072aee9ad5474041ab2924106bdca9fcf5d7d965853e057", size = 191525, upload-time = "2026-04-10T14:26:46Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/2a/09f70020898507a89279659a1afe3364d57fc1b2c89949081975d135f6f5/jiter-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af72f204cf4d44258e5b4c1745130ac45ddab0e71a06333b01de660ab4187a94", size = 315502, upload-time = "2026-04-10T14:26:47.697Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/be/080c96a45cd74f9fce5db4fd68510b88087fb37ffe2541ff73c12db92535/jiter-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b77da71f6e819be5fbcec11a453fde5b1d0267ef6ed487e2a392fd8e14e4e3a", size = 314870, upload-time = "2026-04-10T14:26:49.149Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/5e/2d0fee155826a968a832cc32438de5e2a193292c8721ca70d0b53e58245b/jiter-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f4ea612fe8b84b8b04e51d0e78029ecf3466348e25973f953de6e6a59aa4c1", size = 343406, upload-time = "2026-04-10T14:26:50.762Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/af/bf9ee0d3a4f8dc0d679fc1337f874fe60cdbf841ebbb304b374e1c9aaceb/jiter-0.14.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62fe2451f8fcc0240261e6a4df18ecbcd58327857e61e625b2393ea3b468aac9", size = 369415, upload-time = "2026-04-10T14:26:52.188Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0f/83/8e8561eadba31f4d3948a5b712fb0447ec71c3560b57a855449e7b8ddc98/jiter-0.14.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6112f26f5afc75bcb475787d29da3aa92f9d09c7858f632f4be6ffe607be82e9", size = 461456, upload-time = "2026-04-10T14:26:53.611Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/c9/c5299e826a5fe6108d172b344033f61c69b1bb979dd8d9ddd4278a160971/jiter-0.14.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:215a6cb8fb7dc702aa35d475cc00ddc7f970e5c0b1417fb4b4ac5d82fa2a29db", size = 378488, upload-time = "2026-04-10T14:26:55.211Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/37/c16d9d15c0a471b8644b1abe3c82668092a707d9bedcf076f24ff2e380cd/jiter-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ab96a30fb3cb2c7e0cd33f7616c8860da5f5674438988a54ac717caccdbaa", size = 353242, upload-time = "2026-04-10T14:26:56.705Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/ea/8050cb0dc654e728e1bfacbc0c640772f2181af5dedd13ae70145743a439/jiter-0.14.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3a99c1387b1f2928f799a9de899193484d66206a50e98233b6b088a7f0c1edb2", size = 356823, upload-time = "2026-04-10T14:26:58.281Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/3b/cf71506d270e5f84d97326bf220e47aed9b95e9a4a060758fb07772170ab/jiter-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab18d11074485438695f8d34a1b6da61db9754248f96d51341956607a8f39985", size = 392564, upload-time = "2026-04-10T14:27:00.018Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/cc/8c6c74a3efb5bd671bfd14f51e8a73375464ca914b1551bc3b40e26ac2c9/jiter-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:801028dcfc26ac0895e4964cbc0fd62c73be9fd4a7d7b1aaf6e5790033a719b7", size = 520322, upload-time = "2026-04-10T14:27:01.664Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/24/68d7b883ec959884ddf00d019b2e0e82ba81b167e1253684fa90519ce33c/jiter-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ad425b087aafb4a1c7e1e98a279200743b9aaf30c3e0ba723aec93f061bd9bc8", size = 552619, upload-time = "2026-04-10T14:27:03.316Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/89/b1a0985223bbf3150ff9e8f46f98fc9360c1de94f48abe271bbe1b465682/jiter-0.14.0-cp313-cp313-win32.whl", hash = "sha256:882bcb9b334318e233950b8be366fe5f92c86b66a7e449e76975dfd6d776a01f", size = 205699, upload-time = "2026-04-10T14:27:04.662Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4c/19/3f339a5a7f14a11730e67f6be34f9d5105751d547b615ef593fa122a5ded/jiter-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:9b8c571a5dba09b98bd3462b5a53f27209a5cbbe85670391692ede71974e979f", size = 201323, upload-time = "2026-04-10T14:27:06.139Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/56/752dd89c84be0e022a8ea3720bcfa0a8431db79a962578544812ce061739/jiter-0.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:34f19dcc35cb1abe7c369b3756babf8c7f04595c0807a848df8f26ef8298ef92", size = 191099, upload-time = "2026-04-10T14:27:07.564Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/28/292916f354f25a1fe8cf2c918d1415c699a4a659ae00be0430e1c5d9ffea/jiter-0.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e89bcd7d426a75bb4952c696b267075790d854a07aad4c9894551a82c5b574ab", size = 320880, upload-time = "2026-04-10T14:27:09.326Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/c7/b002a7d8b8957ac3d469bd59c18ef4b1595a5216ae0de639a287b9816023/jiter-0.14.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b25beaa0d4447ea8c7ae0c18c688905d34840d7d0b937f2f7bdd52162c98a40", size = 346563, upload-time = "2026-04-10T14:27:11.287Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/3b/f8d07580d8706021d255a6356b8fab13ee4c869412995550ce6ed4ddf97d/jiter-0.14.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a8758dd413c51e3b7f6557cdc6921faf70b14106f45f969f091f5cda990ea", size = 357928, upload-time = "2026-04-10T14:27:12.729Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/5b/ac1a974da29e35507230383110ffec59998b290a8732585d04e19a9eb5ba/jiter-0.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e1a7eead856a5038a8d291f1447176ab0b525c77a279a058121b5fccee257f6f", size = 203519, upload-time = "2026-04-10T14:27:14.125Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/6d/9fc8433d667d2454271378a79747d8c76c10b51b482b454e6190e511f244/jiter-0.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e692633a12cda97e352fdcd1c4acc971b1c28707e1e33aeef782b0cbf051975", size = 190113, upload-time = "2026-04-10T14:27:16.638Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/1e/354ed92461b165bd581f9ef5150971a572c873ec3b68a916d5aa91da3cc2/jiter-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6f396837fc7577871ca8c12edaf239ed9ccef3bbe39904ae9b8b63ce0a48b140", size = 315277, upload-time = "2026-04-10T14:27:18.109Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/95/8c7c7028aa8636ac21b7a55faef3e34215e6ed0cbf5ae58258427f621aa3/jiter-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a4d50ea3d8ba4176f79754333bd35f1bbcd28e91adc13eb9b7ca91bc52a6cef9", size = 315923, upload-time = "2026-04-10T14:27:19.603Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/40/e2a852a44c4a089f2681a16611b7ce113224a80fd8504c46d78491b47220/jiter-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce17f8a050447d1b4153bda4fb7d26e6a9e74eb4f4a41913f30934c5075bf615", size = 344943, upload-time = "2026-04-10T14:27:21.262Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fc/1f/670f92adee1e9895eac41e8a4d623b6da68c4d46249d8b556b60b63f949e/jiter-0.14.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4f1c4b125e1652aefbc2e2c1617b60a160ab789d180e3d423c41439e5f32850", size = 369725, upload-time = "2026-04-10T14:27:22.766Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/01/2f/541c9ba567d05de1c4874a0f8f8c5e3fd78e2b874266623da9a775cf46e0/jiter-0.14.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be808176a6a3a14321d18c603f2d40741858a7c4fc982f83232842689fe86dd9", size = 461210, upload-time = "2026-04-10T14:27:24.315Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/a9/c31cbec09627e0d5de7aeaec7690dba03e090caa808fefd8133137cf45bc/jiter-0.14.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26679d58ba816f88c3849306dd58cb863a90a1cf352cdd4ef67e30ccf8a77994", size = 380002, upload-time = "2026-04-10T14:27:26.155Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/02/3c05c1666c41904a2f607475a73e7a4763d1cbde2d18229c4f85b22dc253/jiter-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80381f5a19af8fa9aef743f080e34f6b25ebd89656475f8cf0470ec6157052aa", size = 354678, upload-time = "2026-04-10T14:27:27.701Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/97/e15b33545c2b13518f560d695f974b9891b311641bdcf178d63177e8801e/jiter-0.14.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:004df5fdb8ecbd6d99f3227df18ba1a259254c4359736a2e6f036c944e02d7c5", size = 358920, upload-time = "2026-04-10T14:27:29.256Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/d2/8b1461def6b96ba44530df20d07ef7a1c7da22f3f9bf1727e2d611077bf1/jiter-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cff5708f7ed0fa098f2b53446c6fa74c48469118e5cd7497b4f1cd569ab06928", size = 394512, upload-time = "2026-04-10T14:27:31.344Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/88/837566dd6ed6e452e8d3205355afd484ce44b2533edfa4ed73a298ea893e/jiter-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:2492e5f06c36a976d25c7cc347a60e26d5470178d44cde1b9b75e60b4e519f28", size = 521120, upload-time = "2026-04-10T14:27:33.299Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/6b/b00b45c4d1b4c031777fe161d620b755b5b02cdade1e316dcb46e4471d63/jiter-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7609cfbe3a03d37bfdbf5052012d5a879e72b83168a363deae7b3a26564d57de", size = 553668, upload-time = "2026-04-10T14:27:34.868Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/d8/6fe5b42011d19397433d345716eac16728ac241862a2aac9c91923c7509a/jiter-0.14.0-cp314-cp314-win32.whl", hash = "sha256:7282342d32e357543565286b6450378c3cd402eea333fc1ebe146f1fabb306fc", size = 207001, upload-time = "2026-04-10T14:27:36.455Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/43/5c2e08da1efad5e410f0eaaabeadd954812612c33fbbd8fd5328b489139d/jiter-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd77945f38866a448e73b0b7637366afa814d4617790ecd88a18ca74377e6c02", size = 202187, upload-time = "2026-04-10T14:27:38Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/1f/6e39ac0b4cdfa23e606af5b245df5f9adaa76f35e0c5096790da430ca506/jiter-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:f2d4c61da0821ee42e0cdf5489da60a6d074306313a377c2b35af464955a3611", size = 192257, upload-time = "2026-04-10T14:27:39.504Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/57/7dbc0ffbbb5176a27e3518716608aa464aee2e2887dc938f0b900a120449/jiter-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf7ff85517dd2f20a5750081d2b75083c1b269cf75afc7511bdf1f9548beb3b", size = 323441, upload-time = "2026-04-10T14:27:41.039Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/83/6e/7b3314398d8983f06b557aa21b670511ec72d3b79a68ee5e4d9bff972286/jiter-0.14.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ef8791c3e78d6c6b157c6d360fbb5c715bebb8113bc6a9303c5caff012754a", size = 348109, upload-time = "2026-04-10T14:27:42.552Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/4f/8dc674bcd7db6dba566de73c08c763c337058baff1dbeb34567045b27cdc/jiter-0.14.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e74663b8b10da1fe0f4e4703fd7980d24ad17174b6bb35d8498d6e3ebce2ae6a", size = 368328, upload-time = "2026-04-10T14:27:44.574Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3b/5f/188e09a1f20906f98bbdec44ed820e19f4e8eb8aff88b9d1a5a497587ff3/jiter-0.14.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1aca29ba52913f78362ec9c2da62f22cdc4c3083313403f90c15460979b84d9b", size = 463301, upload-time = "2026-04-10T14:27:46.717Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/f0/19046ef965ed8f349e8554775bb12ff4352f443fbe12b95d31f575891256/jiter-0.14.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b39b7d87a952b79949af5fef44d2544e58c21a28da7f1bae3ef166455c61746", size = 378891, upload-time = "2026-04-10T14:27:48.32Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d918a68b26e9fab068c2b5453577ef04943ab2807b9a6275df2a812599a310", size = 360749, upload-time = "2026-04-10T14:27:49.88Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/72/26/e054771be889707c6161dbdec9c23d33a9ec70945395d70f07cfea1e9a6f/jiter-0.14.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:b08997c35aee1201c1a5361466a8fb9162d03ae7bf6568df70b6c859f1e654a4", size = 358526, upload-time = "2026-04-10T14:27:51.504Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/0f/7bea65ea2a6d91f2bf989ff11a18136644392bf2b0497a1fa50934c30a9c/jiter-0.14.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:260bf7ca20704d58d41f669e5e9fe7fe2fa72901a6b324e79056f5d52e9c9be2", size = 393926, upload-time = "2026-04-10T14:27:53.368Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/a1/b1ff7d70deef61ac0b7c6c2f12d2ace950cdeecb4fdc94500a0926802857/jiter-0.14.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:37826e3df29e60f30a382f9294348d0238ef127f4b5d7f5f8da78b5b9e050560", size = 521052, upload-time = "2026-04-10T14:27:55.058Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/7b/3b0649983cbaf15eda26a414b5b1982e910c67bd6f7b1b490f3cfc76896a/jiter-0.14.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:645be49c46f2900937ba0eaf871ad5183c96858c0af74b6becc7f4e367e36e06", size = 553716, upload-time = "2026-04-10T14:27:57.269Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/f8/33d78c83bd93ae0c0af05293a6660f88a1977caef39a6d72a84afab94ce0/jiter-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:2f7877ed45118de283786178eceaf877110abacd04fde31efff3940ae9672674", size = 207957, upload-time = "2026-04-10T14:27:59.285Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/ac/2b760516c03e2227826d1f7025d89bf6bf6357a28fe75c2a2800873c50bf/jiter-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:14c0cb10337c49f5eafe8e7364daca5e29a020ea03580b8f8e6c597fed4e1588", size = 204690, upload-time = "2026-04-10T14:28:00.962Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/2e/a44c20c58aeed0355f2d326969a181696aeb551a25195f47563908a815be/jiter-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5419d4aa2024961da9fe12a9cfe7484996735dca99e8e090b5c88595ef1951ff", size = 191338, upload-time = "2026-04-10T14:28:02.853Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/32/a1/ef34ca2cab2962598591636a1804b93645821201cc0095d4a93a9a329c9d/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a25ffa2dbbdf8721855612f6dca15c108224b12d0c4024d0ac3d7902132b4211", size = 311366, upload-time = "2026-04-10T14:28:27.943Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/bb/520576a532a6b8a6f42747afed289c8448c879a34d7802fe2c832d4fd38f/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ac9cbaa86c10996b92bd12c91659b60f939f8e28fcfa6bc11a0e90a774ce95b", size = 309873, upload-time = "2026-04-10T14:28:29.688Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b2/7c/c16db114ea1f2f532f198aa8dc39585026af45af362c69a0492f31bc4821/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:844e73b6c56b505e9e169234ea3bdea2ea43f769f847f47ac559ba1d2361ebea", size = 344816, upload-time = "2026-04-10T14:28:31.348Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/8f/15e7741ff19e9bcd4d753f7ff22f988fd54592f134ca13701c13ea8c20e0/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52c076f187405fc21523c746c04399c9af8ece566077ed147b2126f2bcba577", size = 351445, upload-time = "2026-04-10T14:28:33.093Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/21/42/9042c3f3019de4adcb8c16591c325ec7255beea9fcd33a42a43f3b0b1000/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:fbd9e482663ca9d005d051330e4d2d8150bb208a209409c10f7e7dfdf7c49da9", size = 308810, upload-time = "2026-04-10T14:28:34.673Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/cf/a7e19b308bd86bb04776803b1f01a5f9a287a4c55205f4708827ee487fbf/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:33a20d838b91ef376b3a56896d5b04e725c7df5bc4864cc6569cf046a8d73b6d", size = 308443, upload-time = "2026-04-10T14:28:36.658Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/44/e26ede3f0caeff93f222559cb0cc4ca68579f07d009d7b6010c5b586f9b1/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:432c4db5255d86a259efde91e55cb4c8d18c0521d844c9e2e7efcce3899fb016", size = 343039, upload-time = "2026-04-10T14:28:38.356Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "localdiplomacy-agent"
|
name = "localdiplomacy-agent"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -526,14 +272,11 @@ dependencies = [
|
|||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
|
{ name = "qdrant-client" },
|
||||||
{ name = "uvicorn", extra = ["standard"] },
|
{ name = "uvicorn", extra = ["standard"] },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
memory = [
|
|
||||||
{ name = "mem0ai" },
|
|
||||||
{ name = "qdrant-client" },
|
|
||||||
]
|
|
||||||
test = [
|
test = [
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
]
|
]
|
||||||
@@ -542,32 +285,13 @@ test = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "fastapi", specifier = ">=0.115.0" },
|
{ name = "fastapi", specifier = ">=0.115.0" },
|
||||||
{ name = "httpx", specifier = ">=0.27.0" },
|
{ name = "httpx", specifier = ">=0.27.0" },
|
||||||
{ name = "mem0ai", marker = "extra == 'memory'", specifier = ">=0.1.0" },
|
|
||||||
{ name = "pydantic", specifier = ">=2.8.0" },
|
{ name = "pydantic", specifier = ">=2.8.0" },
|
||||||
{ name = "pytest", marker = "extra == 'test'", specifier = ">=8.2.0" },
|
{ name = "pytest", marker = "extra == 'test'", specifier = ">=8.2.0" },
|
||||||
{ name = "pyyaml", specifier = ">=6.0.0" },
|
{ name = "pyyaml", specifier = ">=6.0.0" },
|
||||||
{ name = "qdrant-client", marker = "extra == 'memory'", specifier = ">=1.10.0" },
|
{ name = "qdrant-client", specifier = ">=1.10.0" },
|
||||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0" },
|
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0" },
|
||||||
]
|
]
|
||||||
provides-extras = ["memory", "test"]
|
provides-extras = ["test"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mem0ai"
|
|
||||||
version = "2.0.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "openai" },
|
|
||||||
{ name = "posthog" },
|
|
||||||
{ name = "protobuf" },
|
|
||||||
{ name = "pydantic" },
|
|
||||||
{ name = "pytz" },
|
|
||||||
{ name = "qdrant-client" },
|
|
||||||
{ name = "sqlalchemy" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ef/03/3dc535b98310912e4f10083acdbbca2c5e2dfccb3921230a460464f9f4d0/mem0ai-2.0.1.tar.gz", hash = "sha256:070dbc3f1f332c8908379b42a81ab3a96ab169f2f9fa537e6ac719df02478f9c", size = 211820, upload-time = "2026-04-25T17:39:06.744Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a0/96/e6153262f1464f4d412208732fea31496d9983ade155dd2c5c5492f8f8a4/mem0ai-2.0.1-py3-none-any.whl", hash = "sha256:63da5f50ad0c2514e27c2f380ef03f2ceea47c97873096ddfd997785b58043ec", size = 299461, upload-time = "2026-04-25T17:39:04.143Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "numpy"
|
name = "numpy"
|
||||||
@@ -648,25 +372,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" },
|
{ url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "openai"
|
|
||||||
version = "2.33.0"
|
|
||||||
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/f0/ee/d056c82f63c05f06baac0cffb4a90952d8274f90c49dfe244f20497b9bbd/openai-2.33.0.tar.gz", hash = "sha256:f850c435e2a4685bba3295bd54912dd26315d9c1b7733068186134d6e0599f9a", size = 693254, upload-time = "2026-04-28T14:04:42.428Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/32/37734d769bc8b42e4938785313cc05aade6cb0fa72479d3220a0d61a4e78/openai-2.33.0-py3-none-any.whl", hash = "sha256:03ac37d70e8c9e3a8124214e3afa785e2cbc12e627fbd98177a086ef2fd87ad5", size = 1162695, upload-time = "2026-04-28T14:04:40.482Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "26.2"
|
version = "26.2"
|
||||||
@@ -697,22 +402,6 @@ 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" },
|
{ 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 = "posthog"
|
|
||||||
version = "7.13.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "backoff" },
|
|
||||||
{ name = "distro" },
|
|
||||||
{ name = "python-dateutil" },
|
|
||||||
{ name = "requests" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/09/ecc82b5ba5876164a3807adcc5101466da1e4416600075bdbd2071327457/posthog-7.13.1.tar.gz", hash = "sha256:5e53c57db076807530bbec5634c96673ceae8e8e58b99c983af26f02bb4759aa", size = 194124, upload-time = "2026-04-24T19:08:32.56Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/83/bf/eafd5e7508b03264b7deb4db6563c4a2830de7114e01ccbf369756b779d1/posthog-7.13.1-py3-none-any.whl", hash = "sha256:fc0f4b4a8878957e1ea8d319b2e4038b66a19625837f59b020cddaaf59fce982", size = 228291, upload-time = "2026-04-24T19:08:30.822Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "protobuf"
|
name = "protobuf"
|
||||||
version = "6.33.6"
|
version = "6.33.6"
|
||||||
@@ -870,18 +559,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "python-dateutil"
|
|
||||||
version = "2.9.0.post0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "six" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@@ -891,15 +568,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pytz"
|
|
||||||
version = "2026.1.post1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pywin32"
|
name = "pywin32"
|
||||||
version = "311"
|
version = "311"
|
||||||
@@ -992,92 +660,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/68/69/77d1a971c4b933e8c79403e99bcbb790463da5e48333cc4fd5d412c63c98/qdrant_client-1.17.1-py3-none-any.whl", hash = "sha256:6cda4064adfeaf211c751f3fbc00edbbdb499850918c7aff4855a9a759d56cbd", size = 389947, upload-time = "2026-03-13T17:13:43.156Z" },
|
{ url = "https://files.pythonhosted.org/packages/68/69/77d1a971c4b933e8c79403e99bcbb790463da5e48333cc4fd5d412c63c98/qdrant_client-1.17.1-py3-none-any.whl", hash = "sha256:6cda4064adfeaf211c751f3fbc00edbbdb499850918c7aff4855a9a759d56cbd", size = 389947, upload-time = "2026-03-13T17:13:43.156Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "requests"
|
|
||||||
version = "2.33.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "certifi" },
|
|
||||||
{ name = "charset-normalizer" },
|
|
||||||
{ name = "idna" },
|
|
||||||
{ name = "urllib3" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "six"
|
|
||||||
version = "1.17.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[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 = "sqlalchemy"
|
|
||||||
version = "2.0.49"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/b5/e3617cc67420f8f403efebd7b043128f94775e57e5b84e7255203390ceae/sqlalchemy-2.0.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5070135e1b7409c4161133aa525419b0062088ed77c92b1da95366ec5cbebbe", size = 2159126, upload-time = "2026-04-03T16:50:13.242Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/9b/91ca80403b17cd389622a642699e5f6564096b698e7cdcbcbb6409898bc4/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac7a3e245fd0310fd31495eb61af772e637bdf7d88ee81e7f10a3f271bff014", size = 3315509, upload-time = "2026-04-03T16:54:49.332Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/61/0722511d98c54de95acb327824cb759e8653789af2b1944ab1cc69d32565/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d4e5a0ceba319942fa6b585cf82539288a61e314ef006c1209f734551ab9536", size = 3315014, upload-time = "2026-04-03T16:56:56.376Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/55/d514a653ffeb4cebf4b54c47bec32ee28ad89d39fafba16eeed1d81dccd5/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ddcb27fb39171de36e207600116ac9dfd4ae46f86c82a9bf3934043e80ebb88", size = 3267388, upload-time = "2026-04-03T16:54:51.272Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2f/16/0dcc56cb6d3335c1671a2258f5d2cb8267c9a2260e27fde53cbfb1b3540a/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32fe6a41ad97302db2931f05bb91abbcc65b5ce4c675cd44b972428dd2947700", size = 3289602, upload-time = "2026-04-03T16:56:57.63Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/6c/f8ab6fb04470a133cd80608db40aa292e6bae5f162c3a3d4ab19544a67af/sqlalchemy-2.0.49-cp311-cp311-win32.whl", hash = "sha256:46d51518d53edfbe0563662c96954dc8fcace9832332b914375f45a99b77cc9a", size = 2119044, upload-time = "2026-04-03T17:00:53.455Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/59/55a6d627d04b6ebb290693681d7683c7da001eddf90b60cfcc41ee907978/sqlalchemy-2.0.49-cp311-cp311-win_amd64.whl", hash = "sha256:951d4a210744813be63019f3df343bf233b7432aadf0db54c75802247330d3af", size = 2143642, upload-time = "2026-04-03T17:00:54.769Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "starlette"
|
name = "starlette"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -1091,18 +673,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
|
{ url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tqdm"
|
|
||||||
version = "4.67.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.15.0"
|
version = "4.15.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user