mirror of
https://github.com/hanzoai/insights.git
synced 2026-11-02 14:32:45 +00:00
Every generation so far began with a person POSTing to the conversation API.
This is the unattended half: a standing question the project keeps, asked on a
cadence, delivered by email to whoever can see the project.
Question and Run are the two durable halves. The Run row is written before the
gateway is called and settled after, so a worker that dies mid-ask leaves a row
saying so rather than no trace; it is also the claim that stops a second ask
starting, taken under a nowait row lock on the question. A claim expires at the
wall clock, or one crash would silence a question permanently.
Nobody is waiting for these answers, so nothing about them is self-limiting.
Three caps, all settings: wall clock per ask (120s, which also expires a dead
claim), asks per team per rolling day (50, checked before the claim so a refusal
costs nothing at the gateway), and standing questions per project (20).
Delivery reuses the alerting path — EmailMessage with a template — rather than
the activity log, whose bell only shows a reader their own edits, or Slack,
which is inert unless CLOUD_DEPLOYMENT is set.
Read surface is /v1/projects/{id}/assistant/questions, scoped by team alone:
a standing question belongs to the project, not to whoever typed it.
35 lines
1.2 KiB
Python
35 lines
1.2 KiB
Python
"""The scheduler's end of the standing questions.
|
|
|
|
Two tasks, because one asking must not decide the fate of the others: the tick
|
|
only picks what is due and hands each to its own task, so a question that sits at
|
|
its wall clock delays nothing but itself.
|
|
|
|
Neither task decides whether it is allowed to run — `runner.claim` does, once,
|
|
for every caller. A task that raced another, or a team out of budget, simply
|
|
finds the claim declined.
|
|
"""
|
|
|
|
from celery import shared_task
|
|
from structlog import get_logger
|
|
|
|
from products.insights_ai.backend import runner
|
|
from products.insights_ai.backend.models import Question
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
@shared_task(ignore_result=True)
|
|
def ask_due_questions() -> None:
|
|
"""Hand every question whose cadence has come round to its own task."""
|
|
for question_id in runner.due().values_list("id", flat=True):
|
|
ask_question.delay(str(question_id))
|
|
|
|
|
|
@shared_task(ignore_result=True)
|
|
def ask_question(question_id: str) -> None:
|
|
"""Ask one standing question. Silent on a question that has since gone."""
|
|
question = Question.objects.filter(pk=question_id).select_related("team").first()
|
|
if question is None:
|
|
return
|
|
runner.ask(question)
|