-
Notifications
You must be signed in to change notification settings - Fork 1
feat(ask): verify typed public semantic claims #682
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7148067
ba1f0e2
0fa544d
59c3ee6
bf86f74
b9cf3f0
35cf27c
aae2b4b
0aa3113
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,10 +29,22 @@ | |
| from fastapi import HTTPException, status | ||
|
|
||
| from lineageweave.ask_delivery import build_ask_delivery | ||
| from lineageweave.claim_verification import ( | ||
| CLAIM_NOT_ENOUGH_INFORMATION, | ||
| VERIFICATION_COMPLETED, | ||
| VERIFICATION_NO_PUBLIC_CLAIMS, | ||
| VERIFICATION_SKIPPED, | ||
| VERIFICATION_UNAVAILABLE, | ||
| ClaimVerificationClient, | ||
| ClaimVerificationResult, | ||
| NullClaimVerificationClient, | ||
| public_claim_candidates, | ||
| ) | ||
| from lineageweave.embedding_client import EmbeddingClient, NullEmbeddingClient | ||
| from lineageweave.http_client import HttpClientError | ||
| from lineageweave.observability import record_server_failure | ||
| from lineageweave.post_chat import ( | ||
| ChatSourceDocument, | ||
| PostChatClient, | ||
| cited_post_evidence, | ||
| cited_post_summaries, | ||
|
|
@@ -92,6 +104,7 @@ async def enqueue_global_ask_job( | |
| *, | ||
| requesting_account_id: str, | ||
| question_text: str, | ||
| verify_external_requested: bool, | ||
| corporate_entity_ids: frozenset[str], | ||
| process_unit_ids: frozenset[str], | ||
| ) -> str: | ||
|
|
@@ -104,11 +117,13 @@ async def enqueue_global_ask_job( | |
| async with conn.transaction(): | ||
| job_id = await conn.fetchval( | ||
| """ | ||
| insert into global_ask_job (requesting_account_id, question_text) | ||
| values ($1, $2) returning global_ask_job_id | ||
| insert into global_ask_job | ||
| (requesting_account_id, question_text, verify_external_requested) | ||
| values ($1, $2, $3) returning global_ask_job_id | ||
| """, | ||
| requesting_account_id, | ||
| question_text, | ||
| verify_external_requested, | ||
| ) | ||
| await conn.executemany( | ||
| """ | ||
|
|
@@ -141,6 +156,55 @@ async def enqueue_global_ask_job( | |
| return str(job_id) | ||
|
|
||
|
|
||
| def _verification_next_action(status_code: str) -> str | None: | ||
| """Name the next evidence action without promoting web results to authority.""" | ||
|
|
||
| return { | ||
| VERIFICATION_SKIPPED: None, | ||
| VERIFICATION_UNAVAILABLE: "Review the cited posts or try the public information check again later.", | ||
| VERIFICATION_NO_PUBLIC_CLAIMS: "Open the cited posts to review the available evidence.", | ||
| VERIFICATION_COMPLETED: "Compare the public sources with the cited posts before deciding what to do next.", | ||
| CLAIM_NOT_ENOUGH_INFORMATION: "Find another reliable source before relying on this claim.", | ||
| }.get(status_code, "Open the cited posts and review their evidence.") | ||
|
|
||
|
|
||
| async def _verify_public_claims( | ||
| sources: list[ChatSourceDocument], | ||
| cited_post_ids: list[str], | ||
| *, | ||
| verify_external: bool, | ||
| client: ClaimVerificationClient, | ||
| ) -> tuple[str, tuple[ClaimVerificationResult, ...]]: | ||
| """Verify only cited claims explicitly marked safe for public egress.""" | ||
|
|
||
| if not verify_external: | ||
| return VERIFICATION_SKIPPED, () | ||
| cited_ids = frozenset(cited_post_ids) | ||
| claims = tuple( | ||
| claim | ||
| for claim in public_claim_candidates( | ||
| sources, allowed_source_post_ids=cited_ids | ||
| ) | ||
| ) | ||
|
seonghobae marked this conversation as resolved.
|
||
| if not claims: | ||
| return VERIFICATION_NO_PUBLIC_CLAIMS, () | ||
| if not client.available: | ||
| return VERIFICATION_UNAVAILABLE, () | ||
| try: | ||
| results = tuple( | ||
| await asyncio.gather( | ||
| *(asyncio.to_thread(client.verify, claim) for claim in claims) | ||
| ) | ||
| ) | ||
| except (HttpClientError, IndexError, KeyError, OSError, TypeError, ValueError): | ||
| return VERIFICATION_UNAVAILABLE, () | ||
|
Comment on lines
+193
to
+200
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 One failed claim discards all verification, or fails the job
Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| return VERIFICATION_COMPLETED, tuple( | ||
| result | ||
| for result in results | ||
| if set(result.source_post_ids).issubset(cited_ids) | ||
| ) | ||
|
|
||
|
|
||
| async def load_job_visibility( | ||
| conn: asyncpg.Connection, job_id: str, account_id: str | ||
| ) -> tuple[set[str], set[str], bool, bool]: | ||
|
|
@@ -215,6 +279,8 @@ async def compute_global_ask_answer( | |
| process_scope_limited: bool, | ||
| chat_client: PostChatClient, | ||
| embedding_client: EmbeddingClient | None = None, | ||
| verify_external: bool = False, | ||
| claim_verification_client: ClaimVerificationClient | None = None, | ||
| ) -> dict[str, Any]: | ||
| """Assemble one complete Ask answer payload from authorized evidence. | ||
|
|
||
|
|
@@ -254,7 +320,14 @@ def can_see(row: asyncpg.Record) -> bool: | |
| status.HTTP_503_SERVICE_UNAVAILABLE, | ||
| "Ask Agent is unavailable: authorized evidence could not be assembled", | ||
| ) from exc | ||
| verification_client = claim_verification_client or NullClaimVerificationClient() | ||
| if not sources: | ||
| verification_status, external_claims = await _verify_public_claims( | ||
| sources, | ||
| [], | ||
| verify_external=verify_external, | ||
| client=verification_client, | ||
| ) | ||
| delivery = build_ask_delivery("", (), ()) | ||
| return { | ||
| "answer_text": "", | ||
|
|
@@ -264,6 +337,8 @@ def can_see(row: asyncpg.Record) -> bool: | |
| "cited_post_evidence": [], | ||
| "lineage_graph": {"nodes": [], "edges": [], "truncated": False}, | ||
| "cited_post_images": [], | ||
| "external_verification_status": verification_status, | ||
| "external_claims": [claim.to_payload() for claim in external_claims], | ||
| "next_action": "No authorized source posts are available for this question.", | ||
| "delivery": delivery, | ||
| } | ||
|
|
@@ -300,6 +375,12 @@ def can_see(row: asyncpg.Record) -> bool: | |
| "Ask Agent is unavailable: contextual-orchestrator could not complete the answer", | ||
| ) from exc | ||
| cited_ids = list(answer.cited_post_ids) | ||
| verification_status, external_claims = await _verify_public_claims( | ||
| sources, | ||
| cited_ids, | ||
| verify_external=verify_external, | ||
| client=verification_client, | ||
| ) | ||
|
seonghobae marked this conversation as resolved.
|
||
| async with pool.acquire() as conn: | ||
| lineage_graph = await lineage_graphs_for_posts(conn, can_see, cited_ids) | ||
| images = await cited_post_images(conn, cited_ids) | ||
|
|
@@ -314,6 +395,11 @@ def can_see(row: asyncpg.Record) -> bool: | |
| "source_post_ids": [source.post_id for source in sources], | ||
| "lineage_graph": lineage_graph, | ||
| "delivery": build_ask_delivery(answer.answer_text, cited_posts, cited_evidence), | ||
| "external_verification_status": verification_status, | ||
| "external_claims": [claim.to_payload() for claim in external_claims], | ||
| "next_action": ( | ||
| _verification_next_action(verification_status) if verify_external else None | ||
| ), | ||
| } | ||
|
|
||
|
|
||
|
|
@@ -350,6 +436,9 @@ async def process_global_ask_job( | |
| job_id: str, | ||
| chat_factory: Callable[[], PostChatClient], | ||
| embedding_factory: Callable[[], EmbeddingClient] = NullEmbeddingClient, | ||
| claim_verification_factory: Callable[ | ||
| [], ClaimVerificationClient | ||
| ] = NullClaimVerificationClient, | ||
| ) -> None: | ||
| """Claim, answer, and settle one Ask job. | ||
|
|
||
|
|
@@ -363,7 +452,7 @@ async def process_global_ask_job( | |
| """ | ||
| update global_ask_job set job_status_code = $2, updated_at = now() | ||
| where global_ask_job_id = $1 and job_status_code = $3 | ||
| returning requesting_account_id, question_text | ||
| returning requesting_account_id, question_text, verify_external_requested | ||
| """, | ||
| job_id, | ||
| RUNNING, | ||
|
|
@@ -397,6 +486,8 @@ async def process_global_ask_job( | |
| process_scope_limited=process_scope_limited, | ||
| chat_client=chat_client, | ||
| embedding_client=embedding_factory(), | ||
| verify_external=bool(row["verify_external_requested"]), | ||
| claim_verification_client=claim_verification_factory(), | ||
| ), | ||
| timeout=JOB_DEADLINE_SECONDS, | ||
| ) | ||
|
|
@@ -511,6 +602,7 @@ async def consume_global_ask_stream_once( | |
| last_id: str, | ||
| chat_factory: Callable[[], PostChatClient], | ||
| embedding_factory: Callable[[], EmbeddingClient] = NullEmbeddingClient, | ||
| claim_verification_factory: Callable[[], ClaimVerificationClient] = NullClaimVerificationClient, | ||
| limiter: asyncio.Semaphore | None = None, | ||
| tasks: set[asyncio.Task] | None = None, | ||
| ) -> str: | ||
|
|
@@ -535,6 +627,7 @@ async def consume_global_ask_stream_once( | |
| job_id=job_id, | ||
| chat_factory=chat_factory, | ||
| embedding_factory=embedding_factory, | ||
| claim_verification_factory=claim_verification_factory, | ||
| ) | ||
| else: | ||
| await limiter.acquire() | ||
|
|
@@ -544,6 +637,7 @@ async def consume_global_ask_stream_once( | |
| job_id=job_id, | ||
| chat_factory=chat_factory, | ||
| embedding_factory=embedding_factory, | ||
| claim_verification_factory=claim_verification_factory, | ||
| limiter=limiter, | ||
| ) | ||
| ) | ||
|
|
@@ -560,6 +654,7 @@ async def _process_and_release( | |
| job_id: str, | ||
| chat_factory: Callable[[], PostChatClient], | ||
| embedding_factory: Callable[[], EmbeddingClient], | ||
| claim_verification_factory: Callable[[], ClaimVerificationClient], | ||
| limiter: asyncio.Semaphore, | ||
| ) -> None: | ||
| """Run one dispatched job and free its concurrency slot afterwards.""" | ||
|
|
@@ -569,6 +664,7 @@ async def _process_and_release( | |
| job_id=job_id, | ||
| chat_factory=chat_factory, | ||
| embedding_factory=embedding_factory, | ||
| claim_verification_factory=claim_verification_factory, | ||
| ) | ||
| finally: | ||
| limiter.release() | ||
|
|
@@ -590,6 +686,7 @@ async def run_global_ask_worker( | |
| *, | ||
| chat_factory: Callable[[], PostChatClient], | ||
| embedding_factory: Callable[[], EmbeddingClient] = NullEmbeddingClient, | ||
| claim_verification_factory: Callable[[], ClaimVerificationClient] = NullClaimVerificationClient, | ||
| ) -> None: | ||
| """Run the at-least-once Ask consumer with periodic queued-row recovery.""" | ||
| last_id = await _stream_tail(client) | ||
|
|
@@ -609,6 +706,7 @@ async def run_global_ask_worker( | |
| last_id=last_id, | ||
| chat_factory=chat_factory, | ||
| embedding_factory=embedding_factory, | ||
| claim_verification_factory=claim_verification_factory, | ||
| limiter=limiter, | ||
| tasks=tasks, | ||
| ) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 Info: Unreachable next-action mapping entry
_verification_next_actionmapsCLAIM_NOT_ENOUGH_INFORMATION, but the function only receives whole-verification statuses (VERIFICATION_*), never a per-claim status. That entry never fires, so a run whose claims are all not-enough-information still shows the generic COMPLETED next action.Was this helpful? React with 👍 or 👎 to provide feedback.