taskuary 0.3.2.7__tar.gz → 0.3.2.8__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.
- {taskuary-0.3.2.7/taskuary.egg-info → taskuary-0.3.2.8}/PKG-INFO +2 -2
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/README.md +1 -1
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/pyproject.toml +1 -1
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/agents.py +30 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/assistant.py +57 -2
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/general.py +32 -4
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/guard.py +1 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/ingest.py +7 -4
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/llm.py +50 -15
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/server.py +148 -18
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/store.py +63 -4
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/templates/coder.md +6 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/templates/triage.md +2 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/terminal.py +57 -12
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/triage.py +18 -6
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/verdicts.py +44 -4
- taskuary-0.3.2.8/taskuary/web/assets/GeneralWorkspace-B-DtA5wQ.css +1 -0
- taskuary-0.3.2.7/taskuary/web/assets/GeneralWorkspace-Cmu6KGGe.js → taskuary-0.3.2.8/taskuary/web/assets/GeneralWorkspace-frZm-77j.js +1 -1
- taskuary-0.3.2.7/taskuary/web/assets/SocialView-kma4W7JV.js → taskuary-0.3.2.8/taskuary/web/assets/SocialView-BwRXJl-n.js +1 -1
- taskuary-0.3.2.7/taskuary/web/assets/index-rF2UKXaH.js → taskuary-0.3.2.8/taskuary/web/assets/index-XYtUh4Ma.js +114 -110
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/web/index.html +1 -1
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/whoami.py +33 -21
- {taskuary-0.3.2.7 → taskuary-0.3.2.8/taskuary.egg-info}/PKG-INFO +2 -2
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary.egg-info/SOURCES.txt +4 -4
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_agent_environment.py +19 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_answers.py +34 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_api.py +88 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_assistant.py +45 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_async_triage.py +3 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_core.py +20 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_general.py +38 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_guard.py +2 -1
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_kind_dispatch.py +1 -1
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_learnedgraph.py +2 -2
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_stay_open_doors.py +25 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_terminal.py +2 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_verdict_sticks.py +5 -1
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_whoami.py +15 -0
- taskuary-0.3.2.7/taskuary/web/assets/GeneralWorkspace-BpkuvgFU.css +0 -1
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/LICENSE +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/setup.cfg +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/__init__.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/aisetup.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/artifacts.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/aws.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/azure.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/blackboard.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/browserview.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/calendar.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/categories.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/channels.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/ci.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/cli.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/climodels.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/clis.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/coder.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/compose.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/config.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/context.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/counsel.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/db.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/demo.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/desktop.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/devtools.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/digest.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/docsync.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/evalset.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/github.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/handbook.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/handraise.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/histgen.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/hooks.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/imapmail.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/imessage.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/intacct.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/interview.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/knowledge.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/learn.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/learnedgraph.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/logs.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/mcp.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/messengers.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/msauth.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/mssql.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/outbound.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/outbox.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/ownwork.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/phone.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/playbooks.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/pm.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/policy.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/problems.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/promptmap.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/proof.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/proposals.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/quickbooks.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/rank.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/reports.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/research.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/reshape.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/responder.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/routing.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/scopes.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/selfclose.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/semantic.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/senders.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/setup.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/sharepoint.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/sheets.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/spawn.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/teller.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/templates/counsel.md +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/templates/digest.md +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/templates/learned.md +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/templates/playbook.md +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/templates/soul.md +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/templates/style.md +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/testing.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/toil.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/update.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/voice.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/wabridge.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/waitroom.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/web/assets/index-6GBZ9nXN.css +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/web/favicon.ico +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/web/favicon.png +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/webguard.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/whatsapp/bridge.mjs +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/whatsapp/package.json +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary/witness.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary.egg-info/dependency_links.txt +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary.egg-info/entry_points.txt +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary.egg-info/requires.txt +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/taskuary.egg-info/top_level.txt +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_agent_source.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_agent_wall.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_aisetup.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_answered_yourself.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_assistant_verdicts.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_audit_chain.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_aws_region.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_blackboard.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_browser_session.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_browserview.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_calendar.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_calendar_today.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_categories.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_cc_triage.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_chat_doors.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_chat_queue.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_climodels.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_clis.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_cloud.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_cloud_sources.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_compose.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_compose_sources.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_config.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_context.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_counsel.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_cwlogs_paging.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_demo.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_desktop.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_devtools.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_diff_review.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_digest_brief.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_docs_and_llm.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_docs_flow.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_duplicate_connectors.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_factory.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_feed_working.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_finished_chat_stays_finished.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_github_auto.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_handbook.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_handbook_topics.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_handoff_channels.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_handraise.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_harvest.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_histgen.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_home_is_never_yours.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_imap_history.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_imapmail.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_imessage.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_intacct.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_knowledge.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_local_file.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_mail_folders.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_mark_read.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_mcp.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_meeting_prep.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_message_thread.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_messengers.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_msauth.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_mssql.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_noop_ending.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_not_coding.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_note_retrieval.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_outbound_reports.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_own_rows_no_reply.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_playbooks.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_pm.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_poll_overlap.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_poll_sources.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_pr_comments.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_problems.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_promptmap_and_catalog.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_proof_ci_proposals.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_pypi_downloads.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_quickbooks.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_rank.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_reply_channels.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_reply_voice.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_report_alerts.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_report_triage.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_research.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_reviews.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_scopes.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_seed_fits_a_tty.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_seeded_reports.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_semantic.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_send_targets.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_setup.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_sheets_sharepoint.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_someone_else_replied.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_soul_interview.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_stay_open.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_store_app_cli.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_store_hotpaths.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_sync_clock.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_task_identity.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_teller.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_toil.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_triage_dataset.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_update.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_urgent_and_handoff.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_verdict_paths.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_vision.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_voice.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_wabridge.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_waitroom.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_watermark.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_web_entrypoint.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_webguard.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_whatsapp_documents.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_whatsapp_images.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_withdrawn.py +0 -0
- {taskuary-0.3.2.7 → taskuary-0.3.2.8}/tests/test_witness.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: taskuary
|
|
3
|
-
Version: 0.3.2.
|
|
3
|
+
Version: 0.3.2.8
|
|
4
4
|
Summary: Automate your job - the local-first operating system for your work: AI triage in, coding agents out.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/ldbumble/taskuary
|
|
@@ -64,7 +64,7 @@ and replies wait for your approval. Nothing sends or ships without you.
|
|
|
64
64
|
|
|
65
65
|

|
|
66
66
|
|
|
67
|
-
Taskuary is early—currently **v0.3.2.
|
|
67
|
+
Taskuary is early—currently **v0.3.2.8**—and moving fast. The funnel, review queue,
|
|
68
68
|
agent sessions, and reports pipeline are in daily use; breaking changes are still possible
|
|
69
69
|
before 1.0.
|
|
70
70
|
|
|
@@ -13,7 +13,7 @@ and replies wait for your approval. Nothing sends or ships without you.
|
|
|
13
13
|
|
|
14
14
|

|
|
15
15
|
|
|
16
|
-
Taskuary is early—currently **v0.3.2.
|
|
16
|
+
Taskuary is early—currently **v0.3.2.8**—and moving fast. The funnel, review queue,
|
|
17
17
|
agent sessions, and reports pipeline are in daily use; breaking changes are still possible
|
|
18
18
|
before 1.0.
|
|
19
19
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "taskuary"
|
|
7
|
-
version = "0.3.2.
|
|
7
|
+
version = "0.3.2.8"
|
|
8
8
|
description = "Automate your job - the local-first operating system for your work: AI triage in, coding agents out."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -200,6 +200,12 @@ def _live_line(j):
|
|
|
200
200
|
# the CLI's own login has lapsed - nothing in Taskuary can renew it, only the user at a terminal can
|
|
201
201
|
_SIGNED_OUT = re.compile(r'OAuth session expired|Failed to authenticate|not logged in|Not logged in|please (?:run )?[`\']?(?:claude )?/?login|codex login|401 Unauthorized', re.I)
|
|
202
202
|
_LOGIN_HOW = {'claude': "run `claude`, type `/login` and finish the sign-in", 'codex': "run `codex login` and finish the sign-in"}
|
|
203
|
+
# Provider/plan exhaustion is different from an agent failing the work. Only this availability
|
|
204
|
+
# class is safe to hand to another configured agent automatically: a compile error should remain
|
|
205
|
+
# with the agent that owns it, while "session limit; resets at 11:50" should not strand the task.
|
|
206
|
+
_UNAVAILABLE = re.compile(
|
|
207
|
+
r'session limit|usage limit|rate limit|quota|capacity|temporarily unavailable|service unavailable|'
|
|
208
|
+
r'too many requests|resource exhausted|try again (?:at|after|later)|resets? (?:at|in)', re.I)
|
|
203
209
|
|
|
204
210
|
def signed_out_msg(name: str, why: str) -> str:
|
|
205
211
|
how = _LOGIN_HOW.get(name, f"run `{name}` and sign in again")
|
|
@@ -256,6 +262,13 @@ def runs_here(profile: dict) -> bool:
|
|
|
256
262
|
except (FileNotFoundError, OSError): return False
|
|
257
263
|
|
|
258
264
|
|
|
265
|
+
def availability_failure(error) -> bool:
|
|
266
|
+
"""Can the same untouched work safely be retried on a backup provider?"""
|
|
267
|
+
text = str(error or '')
|
|
268
|
+
return isinstance(error, (FileNotFoundError, PermissionError)) or bool(
|
|
269
|
+
_UNAVAILABLE.search(text) or _SIGNED_OUT.search(text) or _DENIED.search(text) or _NO_HOME.search(text))
|
|
270
|
+
|
|
271
|
+
|
|
259
272
|
def profiles(store) -> dict:
|
|
260
273
|
out = {}
|
|
261
274
|
for a in store.list_agents():
|
|
@@ -277,6 +290,23 @@ def default_agent(store) -> str:
|
|
|
277
290
|
return next((n for n, prof in profs.items() if runs_here(prof)), want)
|
|
278
291
|
|
|
279
292
|
|
|
293
|
+
def agent_chain(store, primary: str = None) -> list[str]:
|
|
294
|
+
"""Primary plus ordered configured fallbacks, once each.
|
|
295
|
+
|
|
296
|
+
`backup_agents=*` is the out-of-box resilient choice: every other roster entry, in the
|
|
297
|
+
stable order the owner sees. Naming a CSV narrows and orders the chain explicitly.
|
|
298
|
+
"""
|
|
299
|
+
names = [str(a.get('Name') or '').strip() for a in store.list_agents() if a.get('Name')]
|
|
300
|
+
head = str(primary or default_agent(store) or '').strip()
|
|
301
|
+
if head and head not in names: return [head]
|
|
302
|
+
setting = str(store.get_settings().get('backup_agents') or '').strip()
|
|
303
|
+
backups = names if setting == '*' else [x.strip() for x in setting.split(',') if x.strip()]
|
|
304
|
+
out = []
|
|
305
|
+
for name in [head, *backups]:
|
|
306
|
+
if name and name not in out and name in names: out.append(name)
|
|
307
|
+
return out
|
|
308
|
+
|
|
309
|
+
|
|
280
310
|
def run_cli(profile: dict, prompt: str, trace, resume: str = None, cancel=None):
|
|
281
311
|
"""One headless invocation of the configured CLI, output STREAMED line by line into
|
|
282
312
|
the run trace so the Board shows the agent working live. claude's stream-json events
|
|
@@ -131,6 +131,11 @@ def _gist(body, n=180) -> str:
|
|
|
131
131
|
from .triage import strip_boilerplate
|
|
132
132
|
return _short(strip_boilerplate(_BANNER.sub('', str(body or ''))), n)
|
|
133
133
|
|
|
134
|
+
|
|
135
|
+
def _reply_text(review: dict | None) -> str:
|
|
136
|
+
"""The words that left through Review, never an unsent draft."""
|
|
137
|
+
return str((review or {}).get('FinalText') or (review or {}).get('DraftText') or '').strip()
|
|
138
|
+
|
|
134
139
|
_OOO = re.compile(r'^(automatic reply|auto(matic)?[ -]?reply|out of (the )?office)', re.I)
|
|
135
140
|
_UNTIL = re.compile(r'\b(until|through|returning( on)?|back (on|in the office on))\s+([A-Z][a-z]+day,?\s+)?([A-Z][a-z]+ \d{1,2}(st|nd|rd|th)?|\d{1,2}/\d{1,2}(/\d{2,4})?)', re.I)
|
|
136
141
|
def ooo(store, days: int = 14) -> dict:
|
|
@@ -394,7 +399,26 @@ def _people_context(store, days: int = 2) -> tuple[str, list[int]]:
|
|
|
394
399
|
who = next((c.get('FromName') or c.get('FromEmail') for c in reversed(chain) if not mine(c)), rs[0].get('FromName') or '?')
|
|
395
400
|
tid = next((c.get('TaskId') for c in reversed(chain) if c.get('TaskId')), None)
|
|
396
401
|
t = store.get_task(tid) if tid else None
|
|
397
|
-
|
|
402
|
+
# A reply sent from Review has a durable send receipt there, but many channels do not
|
|
403
|
+
# ingest that outbound line back into `message` immediately. Without this virtual final
|
|
404
|
+
# message, the Assistant saw the finished task and the inbound ask, then confidently
|
|
405
|
+
# claimed "nobody told them" minutes after the owner had approved and sent the reply.
|
|
406
|
+
sent = store.sent_reply(task_id=tid) if tid else None
|
|
407
|
+
if not sent:
|
|
408
|
+
for c in reversed(chain):
|
|
409
|
+
sent = store.sent_reply(message_id=c['MessageId'])
|
|
410
|
+
if sent: break
|
|
411
|
+
sent_text = _reply_text(sent)
|
|
412
|
+
sent_at = (sent or {}).get('DecidedAt') or (sent or {}).get('CreatedAt')
|
|
413
|
+
# Do not print it twice once the channel has ingested the same outbound line as context.
|
|
414
|
+
sent_norm = ' '.join(sent_text.split()).casefold()
|
|
415
|
+
own_after = any(mine(c) and (
|
|
416
|
+
' '.join(str(c.get('BodyText') or '').split()).casefold() == sent_norm
|
|
417
|
+
or (sent_at and _ts(c.get('SentAt')) >= _ts(sent_at))) for c in chain)
|
|
418
|
+
virtual_reply = bool(sent_text and not own_after)
|
|
419
|
+
last_mine = virtual_reply or mine(last)
|
|
420
|
+
last_at = sent_at if virtual_reply else last.get('SentAt')
|
|
421
|
+
head = (f"- {who} [{rs[0].get('Channel')}] re \"{_short(rs[0].get('Subject'), 60)}\" - {len(rs)} new, last word {'YOURS' if last_mine else 'THEIRS'} {_when(last_at)}"
|
|
398
422
|
+ (f", {task_ref(tid)} {t.get('Kind')} {t.get('Status')}" if t else '') + f" (latest mid {rs[0]['MessageId']})")
|
|
399
423
|
first = lambda c: ((c.get('FromName') or c.get('FromEmail') or '?').split(',')[0].split() or ['?'])[0]
|
|
400
424
|
def quote(c):
|
|
@@ -404,6 +428,8 @@ def _people_context(store, days: int = 2) -> tuple[str, list[int]]:
|
|
|
404
428
|
return (f" {'you' if mine(c) else first(c)} {_when(c['SentAt'])[:6]}: \"{_gist(c.get('BodyText'), 150)}\""
|
|
405
429
|
+ (f" [attachments: {files}]" if files else ''))
|
|
406
430
|
quotes = [quote(c) for c in chain]
|
|
431
|
+
if virtual_reply:
|
|
432
|
+
quotes.append(f" you {_when(sent_at)[:6]}: \"{_gist(sent_text, 150)}\" [reply sent from Review]")
|
|
407
433
|
block = '\n'.join([head] + [q for q in quotes if not q.endswith(': ""')])
|
|
408
434
|
if used + len(block) > PEOPLE_CHARS: break
|
|
409
435
|
out.append(block); used += len(block); mids += [c['MessageId'] for c in chain]
|
|
@@ -510,6 +536,34 @@ def parse(text: str, cands: list, max_lines: int = MAX_LINES) -> list:
|
|
|
510
536
|
return out
|
|
511
537
|
|
|
512
538
|
|
|
539
|
+
_UNANSWERED_CLAIM = re.compile(
|
|
540
|
+
r"\b(no (?:reply|response|answer)|nobody (?:told|replied|answered)|"
|
|
541
|
+
r"has(?:n['’]?t| not) (?:been )?(?:told|answered)|"
|
|
542
|
+
r"(?:i['’]?d|would) (?:reply|answer)|ask (?:him|her|them) to retry)\b", re.I)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def sent_reply_for(store, line: dict) -> dict | None:
|
|
546
|
+
"""Find the successful send behind an idea, including another message in its task thread."""
|
|
547
|
+
action = line.get('action') or {}
|
|
548
|
+
mid, tid = action.get('mid'), action.get('tid')
|
|
549
|
+
reply = store.sent_reply(message_id=mid) if mid else None
|
|
550
|
+
if not tid and mid:
|
|
551
|
+
message = store.get_message(mid)
|
|
552
|
+
tid = (message or {}).get('TaskId')
|
|
553
|
+
return reply or (store.sent_reply(task_id=tid) if tid else None)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def contradicts_sent_reply(store, line: dict) -> bool:
|
|
557
|
+
"""Reject a model idea whose premise is disproved by an actual send receipt.
|
|
558
|
+
|
|
559
|
+
The full reply is also put into the model's people context. This is the final factual gate:
|
|
560
|
+
a model may overlook context, but Taskuary must not publish "nobody replied" when its own
|
|
561
|
+
Review table records the exact response and the successful decision.
|
|
562
|
+
"""
|
|
563
|
+
if not _UNANSWERED_CLAIM.search(str(line.get('text') or '')): return False
|
|
564
|
+
return bool(sent_reply_for(store, line))
|
|
565
|
+
|
|
566
|
+
|
|
513
567
|
def _ids(raw) -> list[int]:
|
|
514
568
|
if not isinstance(raw, list): raw = [] if raw in (None, '') else [raw]
|
|
515
569
|
out = []
|
|
@@ -896,7 +950,8 @@ def _run(store, llm, instruction) -> dict:
|
|
|
896
950
|
# the state is read AGAIN here: another process may have posted while the model was thinking, and a
|
|
897
951
|
# model echoing a dismissed key changes nothing
|
|
898
952
|
state = {i['Key']: i for i in store.list_ideas()}
|
|
899
|
-
say = [s | {'why': s.get('why') or s.get('facts') or ''} for s in say
|
|
953
|
+
say = [s | {'why': s.get('why') or s.get('facts') or ''} for s in say
|
|
954
|
+
if fresh(state, s, now) and not contradicts_sent_reply(store, s)]
|
|
900
955
|
rv = reviewed(cands, say, _recent(store), _open(store), _said(store), used, _week(store), _people(store)) | {'notes': note}
|
|
901
956
|
if not say: return {'ran': True, 'said': 0, 'reviewed': rv, 'inputs': read}
|
|
902
957
|
stamp = now.strftime('%Y-%m-%d %H:%M:%S')
|
|
@@ -140,6 +140,19 @@ def _cut(text, n):
|
|
|
140
140
|
return text if len(text) <= n else text[:n] + f'\n[trimmed {len(text) - n:,} characters]'
|
|
141
141
|
|
|
142
142
|
|
|
143
|
+
def _task_files(store, tid: int) -> list[dict]:
|
|
144
|
+
"""Files that arrived with the task's source messages, once each, in message order."""
|
|
145
|
+
detail = store.task_detail(tid) or {}
|
|
146
|
+
out, seen = [], set()
|
|
147
|
+
for message in detail.get('messages') or []:
|
|
148
|
+
for attachment in store.list_attachments(message['MessageId']):
|
|
149
|
+
path = str(attachment.get('Path') or '').strip()
|
|
150
|
+
key = path or f"attachment:{attachment.get('AttachmentId')}"
|
|
151
|
+
if key in seen: continue
|
|
152
|
+
seen.add(key); out.append(attachment)
|
|
153
|
+
return out
|
|
154
|
+
|
|
155
|
+
|
|
143
156
|
# The chat is on the wall too (blackboard.py). It has no checkout, so it reads and writes the
|
|
144
157
|
# HOUSE lane - the notes with no repository behind them - and it can only write when a CLI is
|
|
145
158
|
# doing the thinking, because an API provider has no shell to run the command in.
|
|
@@ -185,7 +198,11 @@ def _prompt(store, tid: int) -> tuple[str, str]:
|
|
|
185
198
|
sources = []
|
|
186
199
|
for m in (detail.get('messages') or [])[-12:]:
|
|
187
200
|
who = m.get('FromName') or m.get('FromEmail') or m.get('SourceName') or m.get('Channel') or 'source'
|
|
188
|
-
|
|
201
|
+
files = store.list_attachments(m['MessageId'])
|
|
202
|
+
file_line = ('\nATTACHMENTS: ' + '; '.join(
|
|
203
|
+
str(a.get('Name') or 'file') + (f" ({a.get('Path')})" if a.get('Path') else '')
|
|
204
|
+
for a in files[:8])) if files else ''
|
|
205
|
+
sources.append(f"FROM {who} ({m.get('SentAt') or ''})\n{_cut(m.get('BodyText'), 3_000)}{file_line}")
|
|
189
206
|
turns = []
|
|
190
207
|
for c in chat_rows(store, tid)[-30:]:
|
|
191
208
|
role = 'ASSISTANT' if c.get('ActorType') == ASSISTANT_TYPE else 'USER'
|
|
@@ -460,9 +477,9 @@ class GeneralSession:
|
|
|
460
477
|
system = f'{system}\n\n{POST_LINE}'
|
|
461
478
|
from . import handbook
|
|
462
479
|
if handbook.enabled(self.store): system = f'{system}\n\n{SOCIAL_LINE}'
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
480
|
+
source_paths = [a.get('Path') for a in _task_files(self.store, self.task_id) if a.get('Path')]
|
|
481
|
+
paths = list(dict.fromkeys(source_paths + list(attachments or [])
|
|
482
|
+
+ [m.group('path') for m in _IMAGE_PATH.finditer(text)]))
|
|
466
483
|
def visible(kind, name, detail):
|
|
467
484
|
self._remember_trace(kind, name, detail)
|
|
468
485
|
if trace: trace(kind, name, detail)
|
|
@@ -480,6 +497,11 @@ class GeneralSession:
|
|
|
480
497
|
trace=visible, cancel=cancel, resume=self.cli_sid or None)
|
|
481
498
|
if not brain: raise RuntimeError('the selected AI connector is unavailable')
|
|
482
499
|
if self.cli_sid: user = _turn_only(self.store, self.task_id, text)
|
|
500
|
+
# Paths are harmless context to an API brain and essential if its CLI backup takes
|
|
501
|
+
# over after a quota failure. The source message already names its files too; this
|
|
502
|
+
# line also carries images attached directly in the composer.
|
|
503
|
+
if paths:
|
|
504
|
+
user += '\n\nATTACHED FILES (read these when relevant)\n' + '\n'.join(str(Path(p).resolve()) for p in paths)
|
|
483
505
|
try:
|
|
484
506
|
reply = str(brain(system, user, max_tokens=MAX_REPLY_TOKENS, images=_images(paths)) or '').strip()
|
|
485
507
|
except Exception:
|
|
@@ -493,6 +515,12 @@ class GeneralSession:
|
|
|
493
515
|
trace=visible, cancel=cancel)
|
|
494
516
|
reply = str(brain(system, user, max_tokens=MAX_REPLY_TOKENS, images=_images(paths)) or '').strip()
|
|
495
517
|
self.cli_sid = getattr(brain, 'session_id', '') or self.cli_sid
|
|
518
|
+
actual = getattr(brain, 'last_pick', '')
|
|
519
|
+
if actual and actual != self.pick:
|
|
520
|
+
choice = next((o for o in provider_options(self.store) if o['pick'] == actual), None)
|
|
521
|
+
self.pick = actual
|
|
522
|
+
if choice:
|
|
523
|
+
self.provider, self.model = choice['label'], choice.get('model') or ''
|
|
496
524
|
if not reply: raise RuntimeError('the model returned an empty response')
|
|
497
525
|
# the assistant's own "and that's the job done" (selfclose.CHAT_LINE). The marker is a
|
|
498
526
|
# signal to Taskuary, not prose for the owner, so it comes out of what gets filed and
|
|
@@ -51,6 +51,7 @@ DENIED = (
|
|
|
51
51
|
(r'POST', r'^/api/tasks/\d+/handoff$', 'handing work to a person sends them a message'),
|
|
52
52
|
(r'POST', r'^/api/outbox$', 'starting an outbound message is the owner\'s door, not an agent\'s'),
|
|
53
53
|
(r'POST', r'^/api/messages/\d+/reply$', 'opening a reply is the first half of sending one'),
|
|
54
|
+
(r'POST', r'^/api/tasks/\d+/clarify$', 'opening a clarification is the first half of sending one'),
|
|
54
55
|
(r'POST|PUT|PATCH|DELETE', r'^/api/(connectors|sources)', 'credentials and where mail goes'),
|
|
55
56
|
(r'GET', r'^/api/send-targets$', 'the address book of everywhere this install can send'),
|
|
56
57
|
# ...and anything that would let it widen its own reach
|
|
@@ -289,7 +289,8 @@ def ingest_message(store, msg: dict, actor: str = 'router', llm=None, file_only:
|
|
|
289
289
|
# and the only sign was rows that stayed on "triaging…"
|
|
290
290
|
mid = _land(store, msg, None, 'filed')
|
|
291
291
|
store.add_route(mid, None, 'file', None,
|
|
292
|
-
f"AI triage failed ({fail['err']}) - filed; fix the AI connector and it will classify new mail",
|
|
292
|
+
f"AI triage failed ({fail['err']}) - filed; fix the AI connector and it will classify new mail",
|
|
293
|
+
[], 'triage', parse_error=fail['err'])
|
|
293
294
|
store.set_setting('triage_last_error', fail['err'][:200], 'system')
|
|
294
295
|
logger.warning(f"ingest: AI triage failed, filed - {fail['err']}")
|
|
295
296
|
return {'status': 'filed', 'task_id': None, 'message_id': mid}
|
|
@@ -301,7 +302,8 @@ def ingest_message(store, msg: dict, actor: str = 'router', llm=None, file_only:
|
|
|
301
302
|
mid = _land(store, msg, None, 'filed')
|
|
302
303
|
store.add_route(mid, None, 'file', None,
|
|
303
304
|
'AI triage returned an answer it could not read as a verdict - filed rather than '
|
|
304
|
-
'assumed to be work' + _notes_note(), [], 'triage'
|
|
305
|
+
'assumed to be work' + _notes_note(), [], 'triage',
|
|
306
|
+
raw_output=intent.get('raw_output'), parse_error=intent.get('parse_error'))
|
|
305
307
|
logger.warning(f"ingest: unusable AI verdict, filed - {msg.get('subject') or ''}")
|
|
306
308
|
return {'status': 'filed', 'task_id': None, 'message_id': mid}
|
|
307
309
|
else:
|
|
@@ -326,8 +328,9 @@ def ingest_message(store, msg: dict, actor: str = 'router', llm=None, file_only:
|
|
|
326
328
|
# 'escalate' was declared in the policy precedence and then read by nobody. It IS
|
|
327
329
|
# the urgency rule: the owner names the senders whose mail jumps the queue, and that
|
|
328
330
|
# is the only thing that marks a task urgent.
|
|
329
|
-
# `kind` ROUTES the work: coding = an agent on a checkout, general =
|
|
330
|
-
# reply = the responder and Review. It is
|
|
331
|
+
# `kind` ROUTES the work: coding = an agent on a checkout, general = a non-coding agent
|
|
332
|
+
# conversation, task = the owner's own list, reply = the responder and Review. It is
|
|
333
|
+
# triage's judgement, made against TRIAGE.md, and
|
|
331
334
|
# the keyword scan in draft_task_fields is only the fallback for a brain that did not say
|
|
332
335
|
# (or triage switched off). Nothing downstream second-guesses it - see auto_code_ok.
|
|
333
336
|
judged = cfg.get('intent_classify_enabled', '1') == '1' # a brain (or a by-construction rule) said 'task'
|
|
@@ -104,21 +104,56 @@ def build_llm(store, pick=None, model=None, trace=None, cancel=None, resume=None
|
|
|
104
104
|
def _build_llm(store, pick=None, model=None, trace=None, cancel=None, resume=None):
|
|
105
105
|
"""The brain named by `pick` ('' = first active AI connector, 'connector:<id>',
|
|
106
106
|
'cli:<agent>'), defaulting to the triage_ai setting - callers like reports may name
|
|
107
|
-
their OWN brain and model per job instead of riding the triage tier.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
for
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return
|
|
121
|
-
|
|
107
|
+
their OWN brain and model per job instead of riding the triage tier. The owner's ordered
|
|
108
|
+
backup-brain setting applies to both cases, but a fallback uses its own model and starts a
|
|
109
|
+
fresh conversation - provider-specific model/session identifiers never cross that line."""
|
|
110
|
+
settings = store.get_settings()
|
|
111
|
+
primary = str(pick if pick is not None else settings.get('triage_ai') or '').strip()
|
|
112
|
+
backups = [x.strip() for x in str(settings.get('triage_backup_ai') or '').split(',') if x.strip()]
|
|
113
|
+
picks = []
|
|
114
|
+
for candidate in [primary, *backups]:
|
|
115
|
+
if candidate not in picks: picks.append(candidate)
|
|
116
|
+
|
|
117
|
+
def one(candidate, first=False):
|
|
118
|
+
chosen_model, chosen_resume = (model, resume) if first else (None, None)
|
|
119
|
+
if candidate.startswith('cli:'):
|
|
120
|
+
return make_cli_llm(store, candidate[4:], chosen_model, trace=trace, cancel=cancel,
|
|
121
|
+
resume=chosen_resume)
|
|
122
|
+
want = candidate[10:] if candidate.startswith('connector:') else None
|
|
123
|
+
want_id = int(want) if want and want.isdigit() else None
|
|
124
|
+
for c in store.list_connectors():
|
|
125
|
+
# a local model server (ollama) is the one brain that needs no key to be real
|
|
126
|
+
ready = c['Active'] and (c['HasSecret'] or c['Type'] == 'ollama')
|
|
127
|
+
selected = not want or (c['ConnectorId'] == want_id if want_id is not None else c['Type'] == want)
|
|
128
|
+
if c['Type'] in AI_TYPES and ready and selected:
|
|
129
|
+
full = store.get_connector(c['ConnectorId'], with_secret=True)
|
|
130
|
+
config = json.loads(full.get('ConfigJson') or '{}')
|
|
131
|
+
if chosen_model: config = {**config, 'model': chosen_model}
|
|
132
|
+
return make_llm(full['Type'], config, full.get('Secret'))
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
brains = [(candidate, one(candidate, i == 0)) for i, candidate in enumerate(picks)]
|
|
136
|
+
brains = [(candidate, brain) for candidate, brain in brains if brain]
|
|
137
|
+
if not brains: return None
|
|
138
|
+
if len(brains) == 1: return brains[0][1]
|
|
139
|
+
|
|
140
|
+
def failover(system, user, **kwargs):
|
|
141
|
+
from .agents import availability_failure
|
|
142
|
+
last = None
|
|
143
|
+
for i, (candidate, brain) in enumerate(brains):
|
|
144
|
+
try:
|
|
145
|
+
out = brain(system, user, **kwargs)
|
|
146
|
+
failover.last_pick = candidate
|
|
147
|
+
failover.session_id = getattr(brain, 'session_id', None)
|
|
148
|
+
return out
|
|
149
|
+
except Exception as e:
|
|
150
|
+
last = e
|
|
151
|
+
if i == len(brains) - 1 or not availability_failure(e): raise
|
|
152
|
+
if trace: trace('progress', 'fallback',
|
|
153
|
+
f'{candidate or "automatic AI"} is unavailable; trying {brains[i + 1][0]}')
|
|
154
|
+
raise last
|
|
155
|
+
failover.last_pick, failover.session_id = primary, resume
|
|
156
|
+
return failover
|
|
122
157
|
|
|
123
158
|
|
|
124
159
|
def make_llm(t, cfg: dict, key: str):
|