feat: add topic tagging, semantic duplicate detection, and per-provider vision toggle

This commit is contained in:
mamad
2026-08-28 20:45:42 +03:30
parent b12ddc1bfd
commit 6a5971bc08
11 changed files with 409 additions and 46 deletions
+2
View File
@@ -117,6 +117,7 @@ CREATE TABLE IF NOT EXISTS ai_providers (
reasoning_effort VARCHAR(32) DEFAULT '',
is_active BOOLEAN DEFAULT FALSE,
fallback_provider_id BIGINT REFERENCES ai_providers(id) ON DELETE SET NULL,
supports_vision BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
@@ -132,6 +133,7 @@ CREATE INDEX IF NOT EXISTS idx_ai_providers_active ON ai_providers(is_active);
-- Migration safety for existing tables
ALTER TABLE ai_providers ADD COLUMN IF NOT EXISTS fallback_provider_id BIGINT REFERENCES ai_providers(id) ON DELETE SET NULL;
ALTER TABLE ai_providers ADD COLUMN IF NOT EXISTS supports_vision BOOLEAN DEFAULT FALSE;
ALTER TABLE sources ADD COLUMN IF NOT EXISTS category_id INT REFERENCES channel_categories(id) ON DELETE SET NULL;
ALTER TABLE targets ADD COLUMN IF NOT EXISTS category_id INT REFERENCES channel_categories(id) ON DELETE SET NULL;
ALTER TABLE sources ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT TRUE;
+1
View File
@@ -99,6 +99,7 @@ class AIProviderProfile:
is_active: bool = False
fallback_provider_id: Optional[int] = None
fallback_provider_name: Optional[str] = None
supports_vision: bool = False
created_at: Optional[str] = None
+46 -9
View File
@@ -231,6 +231,8 @@ class Repository:
media_path: Optional[str] = None,
media_type: Optional[str] = None,
content_hash: Optional[str] = None,
tags: Optional[List[str]] = None,
subject: Optional[str] = None,
is_duplicate: bool = False,
duplicate_of_id: Optional[int] = None,
similarity_reason: Optional[str] = None,
@@ -242,9 +244,9 @@ class Repository:
"""
INSERT INTO posts (
source_channel_id, source_message_id, raw_text, media_path,
media_type, content_hash, is_duplicate, duplicate_of_id, similarity_reason, status
media_type, content_hash, tags, subject, is_duplicate, duplicate_of_id, similarity_reason, status
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'pending_review')
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'pending_review')
RETURNING id;
""",
source_channel_id,
@@ -253,6 +255,8 @@ class Repository:
media_path,
media_type,
content_hash,
tags or [],
subject,
is_duplicate,
duplicate_of_id,
similarity_reason,
@@ -267,6 +271,30 @@ class Repository:
row = await conn.fetchrow("SELECT * FROM posts WHERE id = $1;", post_id)
return _parse_post_row(row) if row else None
async def find_candidate_posts_by_tags(
self,
tags: List[str],
exclude_post_id: Optional[int] = None,
limit: int = 10
) -> List[Post]:
"""Find recent posts that share at least one tag."""
if not tags:
return []
pool = await self._get_pool()
async with pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT * FROM posts
WHERE tags && $1::text[]
AND is_deleted = FALSE
AND ($2::bigint IS NULL OR id <> $2)
ORDER BY id DESC
LIMIT $3;
""",
tags, exclude_post_id, limit
)
return [_parse_post_row(r) for r in rows]
async def find_duplicate_post(self, content_hash: Optional[str], exclude_post_id: Optional[int] = None) -> Optional[Post]:
"""Return the earliest post already carrying this content hash, if any."""
if not content_hash:
@@ -575,7 +603,8 @@ class Repository:
base_url: str = "",
api_key: str = "",
reasoning_effort: str = "",
is_active: bool = False
is_active: bool = False,
supports_vision: bool = False
) -> int:
pool = await self._get_pool()
async with pool.acquire() as conn:
@@ -583,11 +612,11 @@ class Repository:
await conn.execute("UPDATE ai_providers SET is_active = FALSE;")
return await conn.fetchval(
"""
INSERT INTO ai_providers (name, provider_type, model, base_url, api_key, reasoning_effort, is_active)
VALUES ($1, $2, $3, $4, $5, $6, $7)
INSERT INTO ai_providers (name, provider_type, model, base_url, api_key, reasoning_effort, is_active, supports_vision)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id;
""",
name, provider_type, model, base_url, api_key, reasoning_effort, is_active
name, provider_type, model, base_url, api_key, reasoning_effort, is_active, supports_vision
)
async def get_provider_profiles(self) -> List[AIProviderProfile]:
@@ -596,7 +625,7 @@ class Repository:
rows = await conn.fetch(
"""
SELECT p.id, p.name, p.provider_type, p.model, p.base_url, p.api_key, p.reasoning_effort, p.is_active,
p.fallback_provider_id,
p.fallback_provider_id, p.supports_vision,
fb.name as fallback_provider_name,
to_char(p.created_at, 'YYYY-MM-DD HH24:MI:SS') as created_at
FROM ai_providers p
@@ -612,7 +641,7 @@ class Repository:
row = await conn.fetchrow(
"""
SELECT p.id, p.name, p.provider_type, p.model, p.base_url, p.api_key, p.reasoning_effort, p.is_active,
p.fallback_provider_id,
p.fallback_provider_id, p.supports_vision,
fb.name as fallback_provider_name,
to_char(p.created_at, 'YYYY-MM-DD HH24:MI:SS') as created_at
FROM ai_providers p
@@ -629,7 +658,7 @@ class Repository:
row = await conn.fetchrow(
"""
SELECT p.id, p.name, p.provider_type, p.model, p.base_url, p.api_key, p.reasoning_effort, p.is_active,
p.fallback_provider_id,
p.fallback_provider_id, p.supports_vision,
fb.name as fallback_provider_name,
to_char(p.created_at, 'YYYY-MM-DD HH24:MI:SS') as created_at
FROM ai_providers p
@@ -656,6 +685,14 @@ class Repository:
fallback_provider_id, profile_id
)
async def update_provider_vision(self, profile_id: int, supports_vision: bool) -> None:
pool = await self._get_pool()
async with pool.acquire() as conn:
await conn.execute(
"UPDATE ai_providers SET supports_vision = $1 WHERE id = $2;",
supports_vision, profile_id
)
async def update_provider_profile(
self,
profile_id: int,