feat: add topic tagging, semantic duplicate detection, and per-provider vision toggle
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user