dobby-sdk 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dobby_sdk-0.2.3/.agent/rules/backend.md +395 -0
- dobby_sdk-0.2.3/.agent/rules/coding-standards.md +433 -0
- dobby_sdk-0.2.3/.agent/rules/commenting.md +95 -0
- dobby_sdk-0.2.3/.agent/rules/ultracite.md +289 -0
- dobby_sdk-0.2.3/.gitignore +14 -0
- dobby_sdk-0.2.3/.python-version +1 -0
- dobby_sdk-0.2.3/AGENTS.md +1 -0
- dobby_sdk-0.2.3/CLAUDE.md +1 -0
- dobby_sdk-0.2.3/LICENSE +21 -0
- dobby_sdk-0.2.3/PKG-INFO +89 -0
- dobby_sdk-0.2.3/README.md +56 -0
- dobby_sdk-0.2.3/TODO.md +11 -0
- dobby_sdk-0.2.3/dobby/__init__.py +18 -0
- dobby_sdk-0.2.3/dobby/_logging.py +35 -0
- dobby_sdk-0.2.3/dobby/common_tools/__init__.py +9 -0
- dobby_sdk-0.2.3/dobby/common_tools/tavily.py +104 -0
- dobby_sdk-0.2.3/dobby/exceptions/__init__.py +3 -0
- dobby_sdk-0.2.3/dobby/exceptions/tool.py +29 -0
- dobby_sdk-0.2.3/dobby/executor.py +344 -0
- dobby_sdk-0.2.3/dobby/providers/__init__.py +6 -0
- dobby_sdk-0.2.3/dobby/providers/anthropic/.gitkeep +0 -0
- dobby_sdk-0.2.3/dobby/providers/openai/__init__.py +6 -0
- dobby_sdk-0.2.3/dobby/providers/openai/completions.py +461 -0
- dobby_sdk-0.2.3/dobby/providers/openai/converters.py +78 -0
- dobby_sdk-0.2.3/dobby/providers/openai/provider.py +474 -0
- dobby_sdk-0.2.3/dobby/py.typed +0 -0
- dobby_sdk-0.2.3/dobby/retriever/__init__.py +1 -0
- dobby_sdk-0.2.3/dobby/retriever/base.py +45 -0
- dobby_sdk-0.2.3/dobby/retriever/hybrid_retriever.py +121 -0
- dobby_sdk-0.2.3/dobby/tools/__init__.py +6 -0
- dobby_sdk-0.2.3/dobby/tools/base.py +136 -0
- dobby_sdk-0.2.3/dobby/tools/injected.py +18 -0
- dobby_sdk-0.2.3/dobby/tools/schema_utils.py +414 -0
- dobby_sdk-0.2.3/dobby/tools/tool.py +141 -0
- dobby_sdk-0.2.3/dobby/types/__init__.py +52 -0
- dobby_sdk-0.2.3/dobby/types/document_part.py +54 -0
- dobby_sdk-0.2.3/dobby/types/image_part.py +34 -0
- dobby_sdk-0.2.3/dobby/types/message.py +49 -0
- dobby_sdk-0.2.3/dobby/types/reasoning_part.py +11 -0
- dobby_sdk-0.2.3/dobby/types/stream_events.py +108 -0
- dobby_sdk-0.2.3/dobby/types/text_part.py +11 -0
- dobby_sdk-0.2.3/dobby/types/tool_events.py +31 -0
- dobby_sdk-0.2.3/dobby/types/tool_part.py +15 -0
- dobby_sdk-0.2.3/dobby/types/tool_result_part.py +22 -0
- dobby_sdk-0.2.3/dobby/types/usage.py +25 -0
- dobby_sdk-0.2.3/dobby/vector_store/__init__.py +3 -0
- dobby_sdk-0.2.3/dobby/vector_store/azure_ai_search.py +423 -0
- dobby_sdk-0.2.3/dobby/vector_store/base.py +131 -0
- dobby_sdk-0.2.3/docs/executor.md +196 -0
- dobby_sdk-0.2.3/docs/getting-started.md +109 -0
- dobby_sdk-0.2.3/docs/providers/index.md +51 -0
- dobby_sdk-0.2.3/docs/providers/openai.md +128 -0
- dobby_sdk-0.2.3/docs/retrievers/hybrid-retriever.md +108 -0
- dobby_sdk-0.2.3/docs/tools/built-in-tools.md +77 -0
- dobby_sdk-0.2.3/docs/tools/creating-tools.md +199 -0
- dobby_sdk-0.2.3/docs/types/messages.md +209 -0
- dobby_sdk-0.2.3/docs/vector-stores/azure-ai-search.md +128 -0
- dobby_sdk-0.2.3/docs/vector-stores/index.md +64 -0
- dobby_sdk-0.2.3/examples/README.md +47 -0
- dobby_sdk-0.2.3/examples/multi_agent/README.md +40 -0
- dobby_sdk-0.2.3/examples/multi_agent/company_research_tool.py +161 -0
- dobby_sdk-0.2.3/examples/multi_agent/jd_analyzer.py +293 -0
- dobby_sdk-0.2.3/examples/web_search_agent.py +166 -0
- dobby_sdk-0.2.3/pyproject.toml +142 -0
- dobby_sdk-0.2.3/test.py +26 -0
- dobby_sdk-0.2.3/test_structured_output.py +333 -0
- dobby_sdk-0.2.3/uv.lock +963 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: Python Backend Coding Standards - FastAPI + SQLAlchemy
|
|
4
|
+
globs: "**/*.py"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Python Backend Coding Standards
|
|
8
|
+
|
|
9
|
+
FastAPI backend with SQLAlchemy 2.0, Pydantic, and Ruff linter. Follow these conventions for all Python code.
|
|
10
|
+
|
|
11
|
+
## Key Principles
|
|
12
|
+
- Python 3.12+ with type hints everywhere
|
|
13
|
+
- Google-style docstrings
|
|
14
|
+
- Ruff for linting and formatting
|
|
15
|
+
- Repository pattern for database access
|
|
16
|
+
- Service classes for business logic
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Technology Stack
|
|
21
|
+
|
|
22
|
+
- Python 3.12+
|
|
23
|
+
- FastAPI (latest stable)
|
|
24
|
+
- SQLAlchemy 2.0 with async support
|
|
25
|
+
- Pydantic and pydantic-settings for validation/config
|
|
26
|
+
- Alembic for migrations
|
|
27
|
+
- Ruff for linting/formatting
|
|
28
|
+
- uv for package management
|
|
29
|
+
- Loguru for logging
|
|
30
|
+
- Argon2 for password hashing
|
|
31
|
+
- python-jose for JWT tokens
|
|
32
|
+
- asyncpg for PostgreSQL async driver
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Folder Structure
|
|
37
|
+
|
|
38
|
+
server/
|
|
39
|
+
├── main.py # FastAPI app entry point with lifespan
|
|
40
|
+
├── pyproject.toml # Project dependencies (uv)
|
|
41
|
+
├── ruff.toml # Linter configuration
|
|
42
|
+
├── alembic/ # Database migrations
|
|
43
|
+
├── src/
|
|
44
|
+
│ ├── api/ # API layer (routes, schemas, services)
|
|
45
|
+
│ │ ├── __init__.py # Router aggregation
|
|
46
|
+
│ │ └── {feature}/ # Feature module
|
|
47
|
+
│ │ ├── __init__.py
|
|
48
|
+
│ │ ├── router.py # FastAPI routes
|
|
49
|
+
│ │ ├── schemas.py # Pydantic models
|
|
50
|
+
│ │ ├── service.py # Business logic
|
|
51
|
+
│ │ └── dependencies.py # Route dependencies (optional)
|
|
52
|
+
│ ├── core/ # Application core
|
|
53
|
+
│ │ ├── config.py # Settings via pydantic-settings
|
|
54
|
+
│ │ ├── exceptions.py # Custom HTTP exceptions
|
|
55
|
+
│ │ ├── security.py # JWT and password utilities
|
|
56
|
+
│ │ └── logger.py # Loguru configuration
|
|
57
|
+
│ └── lib/ # Shared libraries
|
|
58
|
+
│ ├── db/ # Database layer
|
|
59
|
+
│ │ ├── base.py # SQLAlchemy Base and mixins
|
|
60
|
+
│ │ ├── session.py # Async session management
|
|
61
|
+
│ │ ├── models/ # SQLAlchemy models
|
|
62
|
+
│ │ ├── repositories/ # Data access layer
|
|
63
|
+
│ │ ├── enums.py # Database enums
|
|
64
|
+
│ │ └── types.py # Custom types (UUID7)
|
|
65
|
+
│ └── {domain}/ # Domain-specific libraries
|
|
66
|
+
└── tests/ # Test files
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## File Conventions
|
|
71
|
+
|
|
72
|
+
### Module Docstrings
|
|
73
|
+
Every Python file starts with a docstring describing its purpose:
|
|
74
|
+
|
|
75
|
+
"""User model for authentication and ownership.
|
|
76
|
+
|
|
77
|
+
Users can be regular (password-authenticated) or guests.
|
|
78
|
+
Each user owns chats and their messages.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
### Router Files
|
|
82
|
+
Routes list endpoints in the module docstring:
|
|
83
|
+
|
|
84
|
+
"""Chat streaming API routes.
|
|
85
|
+
|
|
86
|
+
Endpoints:
|
|
87
|
+
- POST /chat/stream - Stream chat response from LLM via SSE
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
from fastapi import APIRouter, Depends
|
|
91
|
+
|
|
92
|
+
router = APIRouter(prefix="/chat", tags=["chat-stream"])
|
|
93
|
+
|
|
94
|
+
@router.post("/stream")
|
|
95
|
+
async def chat_stream(
|
|
96
|
+
request: ChatStreamRequest,
|
|
97
|
+
user: User = Depends(require_auth),
|
|
98
|
+
db: AsyncSession = Depends(get_db),
|
|
99
|
+
):
|
|
100
|
+
"""Stream chat response from LLM.
|
|
101
|
+
|
|
102
|
+
Delegates to ChatService for:
|
|
103
|
+
1. Save user message to DB
|
|
104
|
+
2. Get chat history
|
|
105
|
+
3. Stream LLM response
|
|
106
|
+
4. Save assistant message to DB
|
|
107
|
+
"""
|
|
108
|
+
service = ChatService(db)
|
|
109
|
+
return StreamingResponse(...)
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Docstring Format (Google Style)
|
|
114
|
+
|
|
115
|
+
Use Google-style docstrings for all functions and classes:
|
|
116
|
+
|
|
117
|
+
def process_data(input_data: str, max_length: int = 100) -> dict[str, Any]:
|
|
118
|
+
"""Process input data and return results.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
input_data: Raw input string to process.
|
|
122
|
+
max_length: Maximum allowed length (default 100).
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Dictionary containing processed results with keys:
|
|
126
|
+
- status: Processing status
|
|
127
|
+
- data: Processed content
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
ValueError: If input_data is empty.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
For classes, document attributes in the class docstring:
|
|
134
|
+
|
|
135
|
+
class User(Base, TimestampMixin):
|
|
136
|
+
"""User account model.
|
|
137
|
+
|
|
138
|
+
Attributes:
|
|
139
|
+
id: Unique UUID7 identifier
|
|
140
|
+
email: User's email address (unique, indexed)
|
|
141
|
+
password: Bcrypt hashed password (nullable for guest users)
|
|
142
|
+
is_active: Whether the account is enabled
|
|
143
|
+
user_type: Account type (regular or guest)
|
|
144
|
+
chats: User's chat sessions (relationship)
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Pydantic Schemas
|
|
150
|
+
|
|
151
|
+
Use Field with alias for camelCase JSON serialization:
|
|
152
|
+
|
|
153
|
+
from pydantic import BaseModel, Field
|
|
154
|
+
|
|
155
|
+
class ChatStreamRequest(BaseModel):
|
|
156
|
+
"""Request body for chat streaming endpoint."""
|
|
157
|
+
|
|
158
|
+
chat_id: UUID = Field(alias="chatId")
|
|
159
|
+
message: ChatMessage = Field(alias="message")
|
|
160
|
+
chat_model_config: ChatModelConfig = Field(alias="chatModelConfig")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class MessagePart(BaseModel):
|
|
164
|
+
"""A part of a message - text or file attachment."""
|
|
165
|
+
|
|
166
|
+
type: Literal["text", "file"]
|
|
167
|
+
text: str | None = None
|
|
168
|
+
url: str | None = None
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## SQLAlchemy Models
|
|
173
|
+
|
|
174
|
+
Use SQLAlchemy 2.0 style with mapped_column and Mapped type hints:
|
|
175
|
+
|
|
176
|
+
from sqlalchemy import String, Boolean
|
|
177
|
+
from sqlalchemy.dialects.postgresql import UUID
|
|
178
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
179
|
+
|
|
180
|
+
class User(Base, TimestampMixin):
|
|
181
|
+
__tablename__ = "users"
|
|
182
|
+
|
|
183
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
184
|
+
UUID(as_uuid=True),
|
|
185
|
+
primary_key=True,
|
|
186
|
+
default=generate_uuid7,
|
|
187
|
+
)
|
|
188
|
+
email: Mapped[str] = mapped_column(
|
|
189
|
+
String(64),
|
|
190
|
+
unique=True,
|
|
191
|
+
nullable=False,
|
|
192
|
+
index=True,
|
|
193
|
+
)
|
|
194
|
+
is_active: Mapped[bool] = mapped_column(
|
|
195
|
+
Boolean,
|
|
196
|
+
default=True,
|
|
197
|
+
nullable=False,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Relationships
|
|
201
|
+
chats: Mapped[list["Chat"]] = relationship(
|
|
202
|
+
back_populates="user",
|
|
203
|
+
cascade="all, delete-orphan",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def __repr__(self) -> str:
|
|
207
|
+
return f"<User id={self.id} email={self.email}>"
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Repository Pattern
|
|
212
|
+
|
|
213
|
+
Database access through repository classes with static methods:
|
|
214
|
+
|
|
215
|
+
class MessageRepository:
|
|
216
|
+
"""Repository for message persistence."""
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
async def get_by_chat_id(db: AsyncSession, chat_id: UUID) -> list[Message]:
|
|
220
|
+
"""Get all messages for a chat.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
db: Async database session.
|
|
224
|
+
chat_id: Chat UUID to filter by.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
List of messages ordered by creation time.
|
|
228
|
+
"""
|
|
229
|
+
result = await db.execute(
|
|
230
|
+
select(Message)
|
|
231
|
+
.where(Message.chat_id == chat_id)
|
|
232
|
+
.order_by(Message.created_at)
|
|
233
|
+
)
|
|
234
|
+
return list(result.scalars().all())
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
async def save(
|
|
238
|
+
db: AsyncSession,
|
|
239
|
+
chat_id: UUID,
|
|
240
|
+
parent_id: UUID,
|
|
241
|
+
role: str,
|
|
242
|
+
parts: list[dict],
|
|
243
|
+
) -> Message:
|
|
244
|
+
"""Save a new message."""
|
|
245
|
+
message = Message(
|
|
246
|
+
chat_id=chat_id,
|
|
247
|
+
parent_msg_id=parent_id,
|
|
248
|
+
role=role,
|
|
249
|
+
parts=parts,
|
|
250
|
+
)
|
|
251
|
+
db.add(message)
|
|
252
|
+
await db.commit()
|
|
253
|
+
await db.refresh(message)
|
|
254
|
+
return message
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Service Classes
|
|
259
|
+
|
|
260
|
+
Business logic in service classes initialized with database session:
|
|
261
|
+
|
|
262
|
+
class ChatService:
|
|
263
|
+
"""Chat business logic with agentic tool execution."""
|
|
264
|
+
|
|
265
|
+
def __init__(self, db: AsyncSession):
|
|
266
|
+
"""Initialize chat service.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
db: Async database session.
|
|
270
|
+
"""
|
|
271
|
+
self.db = db
|
|
272
|
+
|
|
273
|
+
async def stream_response(
|
|
274
|
+
self,
|
|
275
|
+
chat_id: UUID,
|
|
276
|
+
user_id: UUID,
|
|
277
|
+
user_message: ChatMessage,
|
|
278
|
+
model_config: ChatModelConfig,
|
|
279
|
+
) -> AsyncIterator[str]:
|
|
280
|
+
"""Process user message with agentic tool execution loop.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
chat_id: Chat UUID.
|
|
284
|
+
user_id: User UUID.
|
|
285
|
+
user_message: User's chat message.
|
|
286
|
+
model_config: Model configuration.
|
|
287
|
+
|
|
288
|
+
Yields:
|
|
289
|
+
SSE formatted event strings.
|
|
290
|
+
"""
|
|
291
|
+
...
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Custom Exceptions
|
|
296
|
+
|
|
297
|
+
HTTP exceptions with consistent structure:
|
|
298
|
+
|
|
299
|
+
from fastapi import HTTPException, status
|
|
300
|
+
|
|
301
|
+
class AuthException(HTTPException):
|
|
302
|
+
"""Base exception for authentication errors."""
|
|
303
|
+
|
|
304
|
+
def __init__(
|
|
305
|
+
self,
|
|
306
|
+
status_code: int = status.HTTP_401_UNAUTHORIZED,
|
|
307
|
+
detail: str = "Authentication failed",
|
|
308
|
+
headers: dict[str, str] | None = None,
|
|
309
|
+
) -> None:
|
|
310
|
+
if headers is None:
|
|
311
|
+
headers = {"WWW-Authenticate": "Bearer"}
|
|
312
|
+
super().__init__(status_code=status_code, detail=detail, headers=headers)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class InvalidCredentialsException(AuthException):
|
|
316
|
+
"""Raised when credentials are invalid."""
|
|
317
|
+
|
|
318
|
+
def __init__(self) -> None:
|
|
319
|
+
super().__init__(
|
|
320
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
321
|
+
detail="Invalid email or password",
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Configuration
|
|
327
|
+
|
|
328
|
+
Use pydantic-settings for environment configuration:
|
|
329
|
+
|
|
330
|
+
from pydantic import Field
|
|
331
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
332
|
+
|
|
333
|
+
class Settings(BaseSettings):
|
|
334
|
+
"""Application settings loaded from environment variables."""
|
|
335
|
+
|
|
336
|
+
model_config = SettingsConfigDict(
|
|
337
|
+
env_file=".env",
|
|
338
|
+
env_file_encoding="utf-8",
|
|
339
|
+
case_sensitive=False,
|
|
340
|
+
extra="ignore",
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
environment: Literal["development", "staging", "production"] = Field(
|
|
344
|
+
default="development",
|
|
345
|
+
description="Environment name",
|
|
346
|
+
)
|
|
347
|
+
debug: bool = False
|
|
348
|
+
database_url: str = "postgresql+asyncpg://localhost:5432/app"
|
|
349
|
+
|
|
350
|
+
settings = Settings()
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Type Hints
|
|
355
|
+
|
|
356
|
+
- Use modern Python 3.12+ syntax (list instead of List, dict instead of Dict)
|
|
357
|
+
- Use union syntax with pipe: str | None instead of Optional[str]
|
|
358
|
+
- Use Literal for fixed string values
|
|
359
|
+
- Use TYPE_CHECKING for circular import resolution
|
|
360
|
+
|
|
361
|
+
from typing import TYPE_CHECKING, Literal
|
|
362
|
+
from collections.abc import AsyncIterator
|
|
363
|
+
|
|
364
|
+
if TYPE_CHECKING:
|
|
365
|
+
from .chat import Chat
|
|
366
|
+
|
|
367
|
+
def process(data: str | None = None) -> dict[str, Any]:
|
|
368
|
+
...
|
|
369
|
+
|
|
370
|
+
async def stream() -> AsyncIterator[str]:
|
|
371
|
+
...
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Commands
|
|
376
|
+
|
|
377
|
+
uv sync # Install dependencies
|
|
378
|
+
uv run ruff check # Check linting
|
|
379
|
+
uv run ruff format # Format code
|
|
380
|
+
uv run alembic upgrade head # Run migrations
|
|
381
|
+
uv add - uv remove # to manage dependencies
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## Checklist for New Features
|
|
385
|
+
|
|
386
|
+
- Create feature folder in src/api/{feature}/
|
|
387
|
+
- Add router.py with endpoint docstrings
|
|
388
|
+
- Add schemas.py with Pydantic models (camelCase aliases)
|
|
389
|
+
- Add service.py for business logic
|
|
390
|
+
- Add to src/api/__init__.py router aggregation
|
|
391
|
+
- Create models in src/lib/db/models/ if needed
|
|
392
|
+
- Create repository in src/lib/db/repositories/ if needed
|
|
393
|
+
- Add Alembic migration if schema changes
|
|
394
|
+
- Use Google-style docstrings with Args/Returns
|
|
395
|
+
- Add type hints to all functions
|