Code Style
This document describes Vedana’s code style. If something isn’t covered, follow PEP 8 and general Python community practices.
Linter: ruff
Configuration is in the root pyproject.toml:
[tool.ruff]
line-length = 120
Run:
uv run ruff check .
uv run ruff format .
CI fails ruff on any violation. PRs don’t merge until green.
Type checker: mypy
Configuration:
[tool.mypy]
ignore_missing_imports = true
follow_imports = "silent"
namespace_packages = true
enable_incomplete_feature = ["NewGenericSyntax"]
Type annotations are required for:
- public functions and methods (anything not
_private); - API endpoint signatures;
- classes in
dataclass/pydantic.
Run:
uv run mypy libs/vedana-core/src
uv run mypy libs/jims-core/src
Python version
Target — 3.12. We use new features:
match/casewhere appropriate;type Foo = ...(PEP 695) for type aliases;- generic syntax
class Tool[T: BaseModel](PEP 695).
Don’t use the 3.9 from typing import Generic, TypeVar style — write the new way.
Import structure
# 1. Standard library
import asyncio
from datetime import datetime
# 2. Third-party packages
import httpx
from pydantic import BaseModel
# 3. Internal Vedana packages
from jims_core.thread.thread_context import ThreadContext
from vedana_core.graph import Graph
# 4. Local imports
from .util import some_helper
Ruff sorts automatically (isort mode).
Naming conventions
snake_casefor functions, methods, variables, modules.PascalCasefor classes, types, Enums.UPPER_SNAKEfor constants._leading_underscorefor private items.- Names that go into the LLM context or into Cypher (anchors, links, attributes) follow the style described in Data Model: anchor — lowercase singular, link sentence —
ANCHOR_verb_ANCHORUPPER_CASE.
Docstrings
Use Google-style docstrings for functions with non-trivial semantics:
async def process_rag_query(self, query: str, ctx: ThreadContext) -> tuple[str, list, dict]:
"""Run the full RAG pipeline for a single user query.
Args:
query: The user's question text.
ctx: Thread context containing history and LLM provider.
Returns:
A tuple of (answer, agent_query_events, technical_info).
Raises:
Exception: Any unhandled exception from data model loading or tool execution.
"""
Don’t duplicate type hints in prose (ruff/pydocstyle may complain).
For trivial functions (getters, simple wrappers) docstrings aren’t needed.
Async / sync
Vedana is async-first. Write functions as async def except when:
- the function is purely computational (no I/O);
- the function is called from a sync context (e.g.
Tool.fnsupports sync becauseTool.callwraps it inasyncio.to_thread).
In async functions:
- don’t block the loop. Long CPU work — through
asyncio.to_thread. - don’t use
time.sleep— useasyncio.sleep. - httpx async client (
httpx.AsyncClient), notrequests.
Error handling
- Outer pipeline boundaries (HTTP API, Telegram, widget) catch
Exceptionand return a generic message to the user, while writing the traceback to the log. - Internal modules raise specific exceptions (
ValueError,KeyError, or your subclasses), not a globalException. - Don’t swallow exceptions with
except: pass. If you really need to —except SpecificError: logger.warning(...).
Example from RagPipeline.__call__:
try:
answer, ... = await self.process_rag_query(user_query, ctx)
ctx.send_message(answer)
except Exception as e:
self.logger.exception(f"Error in RAG pipeline: {e}")
ctx.send_message("An error occurred while processing the request") # generic for the user
ctx.send_event("rag.error", {"query": user_query, "error": str(e), "traceback": traceback.format_exc()})
Logging
- Use the standard
loggingfor libraries (libs/). - Use
logurufor applications (CLI wrappers inapps/). - Levels:
DEBUG— detailed tool calls, data model selection, query parameters.INFO— normal events (thread created, ETL step done).WARNING— odd but non-fatal (filtering fell back to full DM, iteration limit).ERROR/EXCEPTION— actual errors.
- Never log secrets (tokens, keys).
Pydantic
- For configuration models —
pydantic_settings.BaseSettings. - For API / tool argument models —
pydantic.BaseModel. - Use
Field(description="...")for fields that go to the LLM (it’s part of the tool schema). - For strict validation —
pydantic.typesor your own validators.
SQL / SQLAlchemy
- Use the async API (
AsyncSession,async_sessionmaker). - Never build SQL via f-strings. Use parameterisation or the ORM.
- Transactions — through
async with sessionmaker() as session.
Cypher
Cypher strings are safe for read-only, but follow these rules:
- parameterise via
$param, not string interpolation; - escape labels via
escape_labels(seevedana_core/graph.py); - don’t do unbounded traversals (
MATCH (a)-[*]-(b)) without a limit.
Tests
See Testing. In short:
- unit tests are required for non-trivial logic;
- integration tests — where actual DBs / LLM are involved.
- LLM calls in tests — via VCR (
tests/cassettes) or LiteLLM router mocks.
What definitely not to do
- Use
print()instead of the logger. - Global state variables (except
LLMProviderusage counters that are specifically per-process). - Module-level imports that do I/O (e.g. read a file at import time).
- Magic numbers without a constant / comment.
- Long functions > 80 lines without decomposition.
- TODOs without an issue (if you leave a TODO — link it to an issue number).