docagent-cli 0.0.35__py3-none-any.whl
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.
- docagent_cli/__init__.py +36 -0
- docagent_cli/__main__.py +6 -0
- docagent_cli/_ask_user_types.py +90 -0
- docagent_cli/_cli_context.py +27 -0
- docagent_cli/_debug.py +52 -0
- docagent_cli/_env_vars.py +56 -0
- docagent_cli/_server_config.py +352 -0
- docagent_cli/_session_stats.py +114 -0
- docagent_cli/_testing_models.py +144 -0
- docagent_cli/_version.py +17 -0
- docagent_cli/agent.py +1193 -0
- docagent_cli/app.py +4979 -0
- docagent_cli/app.tcss +283 -0
- docagent_cli/ask_user.py +301 -0
- docagent_cli/built_in_skills/__init__.py +5 -0
- docagent_cli/built_in_skills/doc-coauthoring/SKILL.md +375 -0
- docagent_cli/built_in_skills/docx/LICENSE.txt +30 -0
- docagent_cli/built_in_skills/docx/SKILL.md +590 -0
- docagent_cli/built_in_skills/docx/scripts/__init__.py +1 -0
- docagent_cli/built_in_skills/docx/scripts/accept_changes.py +135 -0
- docagent_cli/built_in_skills/docx/scripts/comment.py +318 -0
- docagent_cli/built_in_skills/docx/scripts/office/helpers/__init__.py +0 -0
- docagent_cli/built_in_skills/docx/scripts/office/helpers/merge_runs.py +199 -0
- docagent_cli/built_in_skills/docx/scripts/office/helpers/simplify_redlines.py +197 -0
- docagent_cli/built_in_skills/docx/scripts/office/pack.py +159 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/mce/mc.xsd +75 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- docagent_cli/built_in_skills/docx/scripts/office/soffice.py +183 -0
- docagent_cli/built_in_skills/docx/scripts/office/unpack.py +132 -0
- docagent_cli/built_in_skills/docx/scripts/office/validate.py +111 -0
- docagent_cli/built_in_skills/docx/scripts/office/validators/__init__.py +15 -0
- docagent_cli/built_in_skills/docx/scripts/office/validators/base.py +847 -0
- docagent_cli/built_in_skills/docx/scripts/office/validators/docx.py +446 -0
- docagent_cli/built_in_skills/docx/scripts/office/validators/pptx.py +275 -0
- docagent_cli/built_in_skills/docx/scripts/office/validators/redlining.py +247 -0
- docagent_cli/built_in_skills/docx/scripts/templates/comments.xml +3 -0
- docagent_cli/built_in_skills/docx/scripts/templates/commentsExtended.xml +3 -0
- docagent_cli/built_in_skills/docx/scripts/templates/commentsExtensible.xml +3 -0
- docagent_cli/built_in_skills/docx/scripts/templates/commentsIds.xml +3 -0
- docagent_cli/built_in_skills/docx/scripts/templates/people.xml +3 -0
- docagent_cli/built_in_skills/pdf/LICENSE.txt +30 -0
- docagent_cli/built_in_skills/pdf/SKILL.md +314 -0
- docagent_cli/built_in_skills/pdf/forms.md +294 -0
- docagent_cli/built_in_skills/pdf/reference.md +612 -0
- docagent_cli/built_in_skills/pdf/scripts/check_bounding_boxes.py +65 -0
- docagent_cli/built_in_skills/pdf/scripts/check_fillable_fields.py +11 -0
- docagent_cli/built_in_skills/pdf/scripts/convert_pdf_to_images.py +33 -0
- docagent_cli/built_in_skills/pdf/scripts/create_validation_image.py +37 -0
- docagent_cli/built_in_skills/pdf/scripts/extract_form_field_info.py +122 -0
- docagent_cli/built_in_skills/pdf/scripts/extract_form_structure.py +115 -0
- docagent_cli/built_in_skills/pdf/scripts/fill_fillable_fields.py +98 -0
- docagent_cli/built_in_skills/pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
- docagent_cli/built_in_skills/pptx/LICENSE.txt +30 -0
- docagent_cli/built_in_skills/pptx/SKILL.md +232 -0
- docagent_cli/built_in_skills/pptx/editing.md +205 -0
- docagent_cli/built_in_skills/pptx/pptxgenjs.md +420 -0
- docagent_cli/built_in_skills/pptx/scripts/__init__.py +0 -0
- docagent_cli/built_in_skills/pptx/scripts/add_slide.py +195 -0
- docagent_cli/built_in_skills/pptx/scripts/clean.py +286 -0
- docagent_cli/built_in_skills/pptx/scripts/office/helpers/__init__.py +0 -0
- docagent_cli/built_in_skills/pptx/scripts/office/helpers/merge_runs.py +199 -0
- docagent_cli/built_in_skills/pptx/scripts/office/helpers/simplify_redlines.py +197 -0
- docagent_cli/built_in_skills/pptx/scripts/office/pack.py +159 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/mce/mc.xsd +75 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- docagent_cli/built_in_skills/pptx/scripts/office/soffice.py +183 -0
- docagent_cli/built_in_skills/pptx/scripts/office/unpack.py +132 -0
- docagent_cli/built_in_skills/pptx/scripts/office/validate.py +111 -0
- docagent_cli/built_in_skills/pptx/scripts/office/validators/__init__.py +15 -0
- docagent_cli/built_in_skills/pptx/scripts/office/validators/base.py +847 -0
- docagent_cli/built_in_skills/pptx/scripts/office/validators/docx.py +446 -0
- docagent_cli/built_in_skills/pptx/scripts/office/validators/pptx.py +275 -0
- docagent_cli/built_in_skills/pptx/scripts/office/validators/redlining.py +247 -0
- docagent_cli/built_in_skills/pptx/scripts/thumbnail.py +289 -0
- docagent_cli/built_in_skills/remember/SKILL.md +118 -0
- docagent_cli/built_in_skills/skill-creator/LICENSE.txt +202 -0
- docagent_cli/built_in_skills/skill-creator/SKILL.md +485 -0
- docagent_cli/built_in_skills/skill-creator/agents/analyzer.md +274 -0
- docagent_cli/built_in_skills/skill-creator/agents/comparator.md +202 -0
- docagent_cli/built_in_skills/skill-creator/agents/grader.md +223 -0
- docagent_cli/built_in_skills/skill-creator/assets/eval_review.html +146 -0
- docagent_cli/built_in_skills/skill-creator/eval-viewer/generate_review.py +471 -0
- docagent_cli/built_in_skills/skill-creator/eval-viewer/viewer.html +1325 -0
- docagent_cli/built_in_skills/skill-creator/references/schemas.md +430 -0
- docagent_cli/built_in_skills/skill-creator/scripts/__init__.py +0 -0
- docagent_cli/built_in_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- docagent_cli/built_in_skills/skill-creator/scripts/generate_report.py +326 -0
- docagent_cli/built_in_skills/skill-creator/scripts/improve_description.py +247 -0
- docagent_cli/built_in_skills/skill-creator/scripts/package_skill.py +136 -0
- docagent_cli/built_in_skills/skill-creator/scripts/quick_validate.py +103 -0
- docagent_cli/built_in_skills/skill-creator/scripts/run_eval.py +310 -0
- docagent_cli/built_in_skills/skill-creator/scripts/run_loop.py +328 -0
- docagent_cli/built_in_skills/skill-creator/scripts/utils.py +47 -0
- docagent_cli/built_in_skills/theme-factory/LICENSE.txt +202 -0
- docagent_cli/built_in_skills/theme-factory/SKILL.md +59 -0
- docagent_cli/built_in_skills/theme-factory/theme-showcase.pdf +0 -0
- docagent_cli/built_in_skills/theme-factory/themes/arctic-frost.md +19 -0
- docagent_cli/built_in_skills/theme-factory/themes/botanical-garden.md +19 -0
- docagent_cli/built_in_skills/theme-factory/themes/desert-rose.md +19 -0
- docagent_cli/built_in_skills/theme-factory/themes/forest-canopy.md +19 -0
- docagent_cli/built_in_skills/theme-factory/themes/golden-hour.md +19 -0
- docagent_cli/built_in_skills/theme-factory/themes/midnight-galaxy.md +19 -0
- docagent_cli/built_in_skills/theme-factory/themes/modern-minimalist.md +19 -0
- docagent_cli/built_in_skills/theme-factory/themes/ocean-depths.md +19 -0
- docagent_cli/built_in_skills/theme-factory/themes/sunset-boulevard.md +19 -0
- docagent_cli/built_in_skills/theme-factory/themes/tech-innovation.md +19 -0
- docagent_cli/built_in_skills/xlsx/LICENSE.txt +30 -0
- docagent_cli/built_in_skills/xlsx/SKILL.md +292 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/helpers/__init__.py +0 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/helpers/merge_runs.py +199 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/pack.py +159 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/soffice.py +183 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/unpack.py +132 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/validate.py +111 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/validators/__init__.py +15 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/validators/base.py +847 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/validators/docx.py +446 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/validators/pptx.py +275 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/validators/redlining.py +247 -0
- docagent_cli/built_in_skills/xlsx/scripts/recalc.py +184 -0
- docagent_cli/clipboard.py +128 -0
- docagent_cli/command_registry.py +284 -0
- docagent_cli/config.py +2418 -0
- docagent_cli/configurable_model.py +162 -0
- docagent_cli/default_agent_prompt.md +12 -0
- docagent_cli/editor.py +142 -0
- docagent_cli/file_ops.py +473 -0
- docagent_cli/formatting.py +28 -0
- docagent_cli/hooks.py +206 -0
- docagent_cli/input.py +787 -0
- docagent_cli/integrations/__init__.py +1 -0
- docagent_cli/integrations/sandbox_factory.py +873 -0
- docagent_cli/integrations/sandbox_provider.py +71 -0
- docagent_cli/local_context.py +718 -0
- docagent_cli/main.py +1641 -0
- docagent_cli/mcp_tools.py +707 -0
- docagent_cli/mcp_trust.py +168 -0
- docagent_cli/media_utils.py +478 -0
- docagent_cli/model_config.py +1620 -0
- docagent_cli/non_interactive.py +948 -0
- docagent_cli/offload.py +371 -0
- docagent_cli/output.py +69 -0
- docagent_cli/project_utils.py +188 -0
- docagent_cli/py.typed +0 -0
- docagent_cli/remote_client.py +515 -0
- docagent_cli/server.py +520 -0
- docagent_cli/server_graph.py +196 -0
- docagent_cli/server_manager.py +365 -0
- docagent_cli/sessions.py +1262 -0
- docagent_cli/skills/__init__.py +18 -0
- docagent_cli/skills/commands.py +1090 -0
- docagent_cli/skills/load.py +192 -0
- docagent_cli/subagents.py +173 -0
- docagent_cli/system_prompt.md +247 -0
- docagent_cli/textual_adapter.py +1352 -0
- docagent_cli/theme.py +842 -0
- docagent_cli/token_state.py +31 -0
- docagent_cli/tool_display.py +298 -0
- docagent_cli/tools.py +236 -0
- docagent_cli/ui.py +420 -0
- docagent_cli/unicode_security.py +516 -0
- docagent_cli/update_check.py +454 -0
- docagent_cli/widgets/__init__.py +9 -0
- docagent_cli/widgets/_links.py +63 -0
- docagent_cli/widgets/approval.py +442 -0
- docagent_cli/widgets/ask_user.py +398 -0
- docagent_cli/widgets/autocomplete.py +691 -0
- docagent_cli/widgets/chat_input.py +1827 -0
- docagent_cli/widgets/diff.py +248 -0
- docagent_cli/widgets/history.py +188 -0
- docagent_cli/widgets/loading.py +177 -0
- docagent_cli/widgets/mcp_viewer.py +362 -0
- docagent_cli/widgets/message_store.py +675 -0
- docagent_cli/widgets/messages.py +1751 -0
- docagent_cli/widgets/model_selector.py +964 -0
- docagent_cli/widgets/status.py +372 -0
- docagent_cli/widgets/theme_selector.py +164 -0
- docagent_cli/widgets/thread_selector.py +1905 -0
- docagent_cli/widgets/tool_renderers.py +148 -0
- docagent_cli/widgets/tool_widgets.py +274 -0
- docagent_cli/widgets/welcome.py +339 -0
- docagent_cli-0.0.35.data/data/docagent_cli/default_agent_prompt.md +12 -0
- docagent_cli-0.0.35.dist-info/METADATA +200 -0
- docagent_cli-0.0.35.dist-info/RECORD +300 -0
- docagent_cli-0.0.35.dist-info/WHEEL +4 -0
- docagent_cli-0.0.35.dist-info/entry_points.txt +3 -0
docagent_cli/sessions.py
ADDED
|
@@ -0,0 +1,1262 @@
|
|
|
1
|
+
"""Thread management using LangGraph's built-in checkpoint persistence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import sqlite3
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, NamedTuple, NotRequired, TypedDict, cast
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import AsyncIterator
|
|
15
|
+
|
|
16
|
+
import aiosqlite
|
|
17
|
+
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
|
18
|
+
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
|
19
|
+
|
|
20
|
+
from docagent_cli.output import OutputFormat
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
_aiosqlite_patched = False
|
|
25
|
+
_jsonplus_serializer: JsonPlusSerializer | None = None
|
|
26
|
+
_message_count_cache: dict[str, tuple[str | None, int]] = {}
|
|
27
|
+
_MAX_MESSAGE_COUNT_CACHE = 4096
|
|
28
|
+
_initial_prompt_cache: dict[str, tuple[str | None, str | None]] = {}
|
|
29
|
+
_MAX_INITIAL_PROMPT_CACHE = 4096
|
|
30
|
+
_recent_threads_cache: dict[tuple[str | None, int], list[ThreadInfo]] = {}
|
|
31
|
+
_MAX_RECENT_THREADS_CACHE_KEYS = 16
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _patch_aiosqlite() -> None:
|
|
35
|
+
"""Patch aiosqlite.Connection with `is_alive()` if missing.
|
|
36
|
+
|
|
37
|
+
Required by langgraph-checkpoint>=2.1.0.
|
|
38
|
+
See: https://github.com/langchain-ai/langgraph/issues/6583
|
|
39
|
+
"""
|
|
40
|
+
global _aiosqlite_patched # noqa: PLW0603 # Module-level flag requires global statement
|
|
41
|
+
if _aiosqlite_patched:
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
import aiosqlite as _aiosqlite
|
|
45
|
+
|
|
46
|
+
if not hasattr(_aiosqlite.Connection, "is_alive"):
|
|
47
|
+
|
|
48
|
+
def _is_alive(self: _aiosqlite.Connection) -> bool:
|
|
49
|
+
"""Check if the connection is still alive.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
True if connection is alive, False otherwise.
|
|
53
|
+
"""
|
|
54
|
+
return bool(self._running and self._connection is not None)
|
|
55
|
+
|
|
56
|
+
# Dynamically adding a method to aiosqlite.Connection at runtime.
|
|
57
|
+
# Type checkers can't understand this monkey-patch, so we suppress the
|
|
58
|
+
# "attr-defined" error that would otherwise be raised.
|
|
59
|
+
_aiosqlite.Connection.is_alive = _is_alive # type: ignore[attr-defined]
|
|
60
|
+
|
|
61
|
+
_aiosqlite_patched = True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@asynccontextmanager
|
|
65
|
+
async def _connect() -> AsyncIterator[aiosqlite.Connection]:
|
|
66
|
+
"""Import aiosqlite, apply the compatibility patch, and connect.
|
|
67
|
+
|
|
68
|
+
Centralizes the deferred import + patch + connect sequence used by every
|
|
69
|
+
database function in this module.
|
|
70
|
+
|
|
71
|
+
Yields:
|
|
72
|
+
An open aiosqlite connection to the sessions database.
|
|
73
|
+
"""
|
|
74
|
+
import aiosqlite as _aiosqlite
|
|
75
|
+
|
|
76
|
+
_patch_aiosqlite()
|
|
77
|
+
|
|
78
|
+
async with _aiosqlite.connect(str(get_db_path()), timeout=30.0) as conn:
|
|
79
|
+
yield conn
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class ThreadInfo(TypedDict):
|
|
83
|
+
"""Thread metadata returned by `list_threads`."""
|
|
84
|
+
|
|
85
|
+
thread_id: str
|
|
86
|
+
"""Unique identifier for the thread."""
|
|
87
|
+
|
|
88
|
+
agent_name: str | None
|
|
89
|
+
"""Name of the agent that owns the thread."""
|
|
90
|
+
|
|
91
|
+
updated_at: str | None
|
|
92
|
+
"""ISO timestamp of the last update."""
|
|
93
|
+
|
|
94
|
+
created_at: NotRequired[str | None]
|
|
95
|
+
"""ISO timestamp of thread creation (earliest checkpoint)."""
|
|
96
|
+
|
|
97
|
+
git_branch: NotRequired[str | None]
|
|
98
|
+
"""Git branch active when the thread was created."""
|
|
99
|
+
|
|
100
|
+
initial_prompt: NotRequired[str | None]
|
|
101
|
+
"""First human message in the thread."""
|
|
102
|
+
|
|
103
|
+
message_count: NotRequired[int]
|
|
104
|
+
"""Number of messages in the thread."""
|
|
105
|
+
|
|
106
|
+
latest_checkpoint_id: NotRequired[str | None]
|
|
107
|
+
"""Most recent checkpoint ID for cache invalidation."""
|
|
108
|
+
|
|
109
|
+
cwd: NotRequired[str | None]
|
|
110
|
+
"""Working directory where the thread was last used."""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class _CheckpointSummary(NamedTuple):
|
|
114
|
+
"""Structured data extracted from a thread's latest checkpoint."""
|
|
115
|
+
|
|
116
|
+
message_count: int
|
|
117
|
+
"""Number of messages in the latest checkpoint."""
|
|
118
|
+
|
|
119
|
+
initial_prompt: str | None
|
|
120
|
+
"""First human prompt recovered from the latest checkpoint."""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def format_timestamp(iso_timestamp: str | None) -> str:
|
|
124
|
+
"""Format ISO timestamp for display (e.g., 'Dec 30, 6:10pm').
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
iso_timestamp: ISO 8601 timestamp string, or `None`.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Formatted timestamp string or empty string if invalid.
|
|
131
|
+
"""
|
|
132
|
+
if not iso_timestamp:
|
|
133
|
+
return ""
|
|
134
|
+
try:
|
|
135
|
+
dt = datetime.fromisoformat(iso_timestamp).astimezone()
|
|
136
|
+
return (
|
|
137
|
+
dt.strftime("%b %d, %-I:%M%p")
|
|
138
|
+
.lower()
|
|
139
|
+
.replace("am", "am")
|
|
140
|
+
.replace("pm", "pm")
|
|
141
|
+
)
|
|
142
|
+
except (ValueError, TypeError):
|
|
143
|
+
logger.debug(
|
|
144
|
+
"Failed to parse timestamp %r; displaying as blank",
|
|
145
|
+
iso_timestamp,
|
|
146
|
+
exc_info=True,
|
|
147
|
+
)
|
|
148
|
+
return ""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def format_relative_timestamp(iso_timestamp: str | None) -> str:
|
|
152
|
+
"""Format ISO timestamp as relative time (e.g., '5m ago', '2h ago').
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
iso_timestamp: ISO 8601 timestamp string, or `None`.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Relative time string or empty string if invalid.
|
|
159
|
+
"""
|
|
160
|
+
if not iso_timestamp:
|
|
161
|
+
return ""
|
|
162
|
+
try:
|
|
163
|
+
dt = datetime.fromisoformat(iso_timestamp).astimezone()
|
|
164
|
+
except (ValueError, TypeError):
|
|
165
|
+
logger.debug(
|
|
166
|
+
"Failed to parse timestamp %r; displaying as blank",
|
|
167
|
+
iso_timestamp,
|
|
168
|
+
exc_info=True,
|
|
169
|
+
)
|
|
170
|
+
return ""
|
|
171
|
+
|
|
172
|
+
delta = datetime.now(tz=dt.tzinfo) - dt
|
|
173
|
+
seconds = int(delta.total_seconds())
|
|
174
|
+
if seconds < 0:
|
|
175
|
+
return "just now"
|
|
176
|
+
if seconds < 60: # noqa: PLR2004
|
|
177
|
+
return f"{seconds}s ago"
|
|
178
|
+
minutes = seconds // 60
|
|
179
|
+
if minutes < 60: # noqa: PLR2004
|
|
180
|
+
return f"{minutes}m ago"
|
|
181
|
+
hours = minutes // 60
|
|
182
|
+
if hours < 24: # noqa: PLR2004
|
|
183
|
+
return f"{hours}h ago"
|
|
184
|
+
days = hours // 24
|
|
185
|
+
if days < 30: # noqa: PLR2004
|
|
186
|
+
return f"{days}d ago"
|
|
187
|
+
months = days // 30
|
|
188
|
+
if months < 12: # noqa: PLR2004
|
|
189
|
+
return f"{months}mo ago"
|
|
190
|
+
years = days // 365
|
|
191
|
+
return f"{years}y ago"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def format_path(path: str | None) -> str:
|
|
195
|
+
"""Format a filesystem path for display.
|
|
196
|
+
|
|
197
|
+
Paths under the user's home directory are shown relative to `~`.
|
|
198
|
+
All other paths are returned as-is.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
path: Absolute filesystem path, or `None`.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Formatted path string, or empty string if path is falsy.
|
|
205
|
+
"""
|
|
206
|
+
if not path:
|
|
207
|
+
return ""
|
|
208
|
+
try:
|
|
209
|
+
home = str(Path.home())
|
|
210
|
+
if path == home:
|
|
211
|
+
return "~"
|
|
212
|
+
prefix = home + "/"
|
|
213
|
+
if path.startswith(prefix):
|
|
214
|
+
return "~/" + path[len(prefix) :]
|
|
215
|
+
except (RuntimeError, KeyError, OSError):
|
|
216
|
+
logger.debug(
|
|
217
|
+
"Could not resolve home directory for path formatting", exc_info=True
|
|
218
|
+
)
|
|
219
|
+
return path
|
|
220
|
+
else:
|
|
221
|
+
return path
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
_db_path: Path | None = None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def get_db_path() -> Path:
|
|
228
|
+
"""Get path to global database.
|
|
229
|
+
|
|
230
|
+
The result is cached after the first successful call to avoid repeated
|
|
231
|
+
filesystem operations.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Path to the SQLite database file.
|
|
235
|
+
"""
|
|
236
|
+
global _db_path # noqa: PLW0603 # Module-level cache requires global statement
|
|
237
|
+
if _db_path is not None:
|
|
238
|
+
return _db_path
|
|
239
|
+
db_dir = Path.home() / ".docagent"
|
|
240
|
+
db_dir.mkdir(parents=True, exist_ok=True)
|
|
241
|
+
_db_path = db_dir / "sessions.db"
|
|
242
|
+
return _db_path
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def generate_thread_id() -> str:
|
|
246
|
+
"""Generate a new thread ID as a full UUID7 string.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
UUID7 string (time-ordered for natural sort by creation time).
|
|
250
|
+
"""
|
|
251
|
+
from uuid_utils import uuid7
|
|
252
|
+
|
|
253
|
+
return str(uuid7())
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
async def _table_exists(conn: aiosqlite.Connection, table: str) -> bool:
|
|
257
|
+
"""Check if a table exists in the database.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
True if table exists, False otherwise.
|
|
261
|
+
"""
|
|
262
|
+
query = "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?"
|
|
263
|
+
async with conn.execute(query, (table,)) as cursor:
|
|
264
|
+
return await cursor.fetchone() is not None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
async def list_threads(
|
|
268
|
+
agent_name: str | None = None,
|
|
269
|
+
limit: int = 20,
|
|
270
|
+
include_message_count: bool = False,
|
|
271
|
+
sort_by: str = "updated",
|
|
272
|
+
branch: str | None = None,
|
|
273
|
+
) -> list[ThreadInfo]:
|
|
274
|
+
"""List threads from checkpoints table.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
agent_name: Optional filter by agent name.
|
|
278
|
+
limit: Maximum number of threads to return.
|
|
279
|
+
include_message_count: Whether to include message counts.
|
|
280
|
+
sort_by: Sort field — `"updated"` or `"created"`.
|
|
281
|
+
branch: Optional filter by git branch name.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
List of `ThreadInfo` dicts with `thread_id`, `agent_name`,
|
|
285
|
+
`updated_at`, `created_at`, `latest_checkpoint_id`, `git_branch`,
|
|
286
|
+
`cwd`, and optionally `message_count`.
|
|
287
|
+
|
|
288
|
+
Raises:
|
|
289
|
+
ValueError: If `sort_by` is not `"updated"` or `"created"`.
|
|
290
|
+
"""
|
|
291
|
+
async with _connect() as conn:
|
|
292
|
+
if not await _table_exists(conn, "checkpoints"):
|
|
293
|
+
return []
|
|
294
|
+
|
|
295
|
+
if sort_by not in {"updated", "created"}:
|
|
296
|
+
msg = f"Invalid sort_by {sort_by!r}; expected 'updated' or 'created'"
|
|
297
|
+
raise ValueError(msg)
|
|
298
|
+
order_col = "created_at" if sort_by == "created" else "updated_at"
|
|
299
|
+
|
|
300
|
+
where_clauses: list[str] = []
|
|
301
|
+
params_list: list[str | int] = []
|
|
302
|
+
|
|
303
|
+
if agent_name:
|
|
304
|
+
where_clauses.append("json_extract(metadata, '$.agent_name') = ?")
|
|
305
|
+
params_list.append(agent_name)
|
|
306
|
+
if branch:
|
|
307
|
+
where_clauses.append("json_extract(metadata, '$.git_branch') = ?")
|
|
308
|
+
params_list.append(branch)
|
|
309
|
+
|
|
310
|
+
where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
|
|
311
|
+
|
|
312
|
+
query = f"""
|
|
313
|
+
SELECT thread_id,
|
|
314
|
+
json_extract(metadata, '$.agent_name') as agent_name,
|
|
315
|
+
MAX(json_extract(metadata, '$.updated_at')) as updated_at,
|
|
316
|
+
MAX(checkpoint_id) as latest_checkpoint_id,
|
|
317
|
+
MIN(json_extract(metadata, '$.updated_at')) as created_at,
|
|
318
|
+
MAX(json_extract(metadata, '$.git_branch')) as git_branch,
|
|
319
|
+
MAX(json_extract(metadata, '$.cwd')) as cwd
|
|
320
|
+
FROM checkpoints
|
|
321
|
+
{where_sql}
|
|
322
|
+
GROUP BY thread_id
|
|
323
|
+
ORDER BY {order_col} DESC
|
|
324
|
+
LIMIT ?
|
|
325
|
+
""" # noqa: S608 # where_sql/order_col derived from controlled internal values; user values use ? placeholders
|
|
326
|
+
params: tuple = (*params_list, limit)
|
|
327
|
+
|
|
328
|
+
async with conn.execute(query, params) as cursor:
|
|
329
|
+
rows = await cursor.fetchall()
|
|
330
|
+
threads: list[ThreadInfo] = [
|
|
331
|
+
ThreadInfo(
|
|
332
|
+
thread_id=r[0],
|
|
333
|
+
agent_name=r[1],
|
|
334
|
+
updated_at=r[2],
|
|
335
|
+
latest_checkpoint_id=r[3],
|
|
336
|
+
created_at=r[4],
|
|
337
|
+
git_branch=r[5],
|
|
338
|
+
cwd=r[6],
|
|
339
|
+
)
|
|
340
|
+
for r in rows
|
|
341
|
+
]
|
|
342
|
+
|
|
343
|
+
# Fetch message counts if requested
|
|
344
|
+
if include_message_count and threads:
|
|
345
|
+
await _populate_message_counts(conn, threads)
|
|
346
|
+
|
|
347
|
+
# Only cache unfiltered results so the thread selector modal
|
|
348
|
+
# doesn't receive branch-filtered or differently-sorted data.
|
|
349
|
+
if sort_by == "updated" and branch is None:
|
|
350
|
+
_cache_recent_threads(agent_name, limit, threads)
|
|
351
|
+
return threads
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
async def populate_thread_message_counts(threads: list[ThreadInfo]) -> list[ThreadInfo]:
|
|
355
|
+
"""Populate `message_count` for an existing thread list.
|
|
356
|
+
|
|
357
|
+
This is used by the `/threads` modal to render rows quickly, then backfill
|
|
358
|
+
counts in the background without issuing a second thread-list query.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
threads: Thread rows to enrich in place.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
The same list object with `message_count` values populated.
|
|
365
|
+
"""
|
|
366
|
+
if not threads:
|
|
367
|
+
return threads
|
|
368
|
+
|
|
369
|
+
async with _connect() as conn:
|
|
370
|
+
await _populate_message_counts(conn, threads)
|
|
371
|
+
return threads
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
async def populate_thread_checkpoint_details(
|
|
375
|
+
threads: list[ThreadInfo],
|
|
376
|
+
*,
|
|
377
|
+
include_message_count: bool = True,
|
|
378
|
+
include_initial_prompt: bool = True,
|
|
379
|
+
) -> list[ThreadInfo]:
|
|
380
|
+
"""Populate checkpoint-derived fields for an existing thread list.
|
|
381
|
+
|
|
382
|
+
This is used by the `/threads` modal to enrich rows in one background pass,
|
|
383
|
+
so the latest checkpoint is fetched and deserialized at most once per row.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
threads: Thread rows to enrich in place.
|
|
387
|
+
include_message_count: Whether to populate `message_count`.
|
|
388
|
+
include_initial_prompt: Whether to populate `initial_prompt`.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
The same list object with missing checkpoint-derived fields populated.
|
|
392
|
+
"""
|
|
393
|
+
if not threads or (not include_message_count and not include_initial_prompt):
|
|
394
|
+
return threads
|
|
395
|
+
|
|
396
|
+
async with _connect() as conn:
|
|
397
|
+
await _populate_checkpoint_fields(
|
|
398
|
+
conn,
|
|
399
|
+
threads,
|
|
400
|
+
include_message_count=include_message_count,
|
|
401
|
+
include_initial_prompt=include_initial_prompt,
|
|
402
|
+
)
|
|
403
|
+
return threads
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
async def prewarm_thread_message_counts(limit: int | None = None) -> None:
|
|
407
|
+
"""Prewarm thread selector cache for faster `/threads` open.
|
|
408
|
+
|
|
409
|
+
Fetches a bounded list of recent threads and populates checkpoint-derived
|
|
410
|
+
fields for currently visible columns into the in-memory cache. Intended to
|
|
411
|
+
run in a background worker during app startup.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
limit: Maximum threads to prewarm. Uses `get_thread_limit()` when `None`.
|
|
415
|
+
"""
|
|
416
|
+
thread_limit = limit if limit is not None else get_thread_limit()
|
|
417
|
+
if thread_limit < 1:
|
|
418
|
+
return
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
from docagent_cli.model_config import load_thread_config
|
|
422
|
+
|
|
423
|
+
cfg = load_thread_config()
|
|
424
|
+
threads = await list_threads(limit=thread_limit, include_message_count=False)
|
|
425
|
+
if threads:
|
|
426
|
+
await populate_thread_checkpoint_details(
|
|
427
|
+
threads,
|
|
428
|
+
include_message_count=cfg.columns.get("messages", False),
|
|
429
|
+
include_initial_prompt=cfg.columns.get("initial_prompt", False),
|
|
430
|
+
)
|
|
431
|
+
_cache_recent_threads(None, thread_limit, threads)
|
|
432
|
+
except (OSError, sqlite3.Error):
|
|
433
|
+
logger.debug("Could not prewarm thread selector cache", exc_info=True)
|
|
434
|
+
except Exception:
|
|
435
|
+
logger.warning(
|
|
436
|
+
"Unexpected error while prewarming thread selector cache",
|
|
437
|
+
exc_info=True,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def get_cached_threads(
|
|
442
|
+
agent_name: str | None = None,
|
|
443
|
+
limit: int | None = None,
|
|
444
|
+
) -> list[ThreadInfo] | None:
|
|
445
|
+
"""Get cached recent threads, if available.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
agent_name: Optional agent-name filter key.
|
|
449
|
+
limit: Maximum rows requested. Uses `get_thread_limit()` when `None`.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
Copy of cached rows when available, otherwise `None`.
|
|
453
|
+
"""
|
|
454
|
+
|
|
455
|
+
def _copy_with_cached_counts(rows: list[ThreadInfo]) -> list[ThreadInfo]:
|
|
456
|
+
copied_rows = _copy_threads(rows)
|
|
457
|
+
apply_cached_thread_message_counts(copied_rows)
|
|
458
|
+
apply_cached_thread_initial_prompts(copied_rows)
|
|
459
|
+
return copied_rows
|
|
460
|
+
|
|
461
|
+
thread_limit = limit if limit is not None else get_thread_limit()
|
|
462
|
+
if thread_limit < 1:
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
exact = _recent_threads_cache.get((agent_name, thread_limit))
|
|
466
|
+
if exact is not None:
|
|
467
|
+
return _copy_with_cached_counts(exact)
|
|
468
|
+
|
|
469
|
+
best_key: tuple[str | None, int] | None = None
|
|
470
|
+
for key in _recent_threads_cache:
|
|
471
|
+
cache_agent, cache_limit = key
|
|
472
|
+
if cache_agent != agent_name or cache_limit < thread_limit:
|
|
473
|
+
continue
|
|
474
|
+
if best_key is None or cache_limit < best_key[1]:
|
|
475
|
+
best_key = key
|
|
476
|
+
|
|
477
|
+
if best_key is None:
|
|
478
|
+
return None
|
|
479
|
+
|
|
480
|
+
return _copy_with_cached_counts(_recent_threads_cache[best_key][:thread_limit])
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def apply_cached_thread_message_counts(threads: list[ThreadInfo]) -> int:
|
|
484
|
+
"""Apply cached message counts onto thread rows when freshness matches.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
threads: Thread rows to mutate in place.
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
Number of rows that were populated from cache.
|
|
491
|
+
"""
|
|
492
|
+
populated = 0
|
|
493
|
+
for thread in threads:
|
|
494
|
+
if "message_count" in thread:
|
|
495
|
+
continue
|
|
496
|
+
thread_id = thread["thread_id"]
|
|
497
|
+
freshness = _thread_freshness(thread)
|
|
498
|
+
cached = _message_count_cache.get(thread_id)
|
|
499
|
+
if cached is None or cached[0] != freshness:
|
|
500
|
+
continue
|
|
501
|
+
thread["message_count"] = cached[1]
|
|
502
|
+
populated += 1
|
|
503
|
+
return populated
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def apply_cached_thread_initial_prompts(threads: list[ThreadInfo]) -> int:
|
|
507
|
+
"""Apply cached initial prompts onto thread rows when freshness matches.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
threads: Thread rows to mutate in place.
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
Number of rows that were populated from cache.
|
|
514
|
+
"""
|
|
515
|
+
populated = 0
|
|
516
|
+
for thread in threads:
|
|
517
|
+
if "initial_prompt" in thread:
|
|
518
|
+
continue
|
|
519
|
+
thread_id = thread["thread_id"]
|
|
520
|
+
freshness = _thread_freshness(thread)
|
|
521
|
+
cached = _initial_prompt_cache.get(thread_id)
|
|
522
|
+
if cached is None or cached[0] != freshness:
|
|
523
|
+
continue
|
|
524
|
+
thread["initial_prompt"] = cached[1]
|
|
525
|
+
populated += 1
|
|
526
|
+
return populated
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
async def _populate_message_counts(
|
|
530
|
+
conn: aiosqlite.Connection,
|
|
531
|
+
threads: list[ThreadInfo],
|
|
532
|
+
) -> None:
|
|
533
|
+
"""Fill `message_count` on thread rows with cache-aware lookup."""
|
|
534
|
+
await _populate_checkpoint_fields(
|
|
535
|
+
conn,
|
|
536
|
+
threads,
|
|
537
|
+
include_message_count=True,
|
|
538
|
+
include_initial_prompt=False,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
async def _get_jsonplus_serializer() -> JsonPlusSerializer:
|
|
543
|
+
"""Return a cached JsonPlus serializer, loading it off the UI loop."""
|
|
544
|
+
global _jsonplus_serializer # noqa: PLW0603 # Module-level cache requires global statement
|
|
545
|
+
if _jsonplus_serializer is not None:
|
|
546
|
+
return _jsonplus_serializer
|
|
547
|
+
|
|
548
|
+
loop = asyncio.get_running_loop()
|
|
549
|
+
_jsonplus_serializer = await loop.run_in_executor(None, _create_jsonplus_serializer)
|
|
550
|
+
return _jsonplus_serializer
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _create_jsonplus_serializer() -> JsonPlusSerializer:
|
|
554
|
+
"""Import and create a JsonPlus serializer.
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
A ready `JsonPlusSerializer` instance.
|
|
558
|
+
"""
|
|
559
|
+
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
|
560
|
+
|
|
561
|
+
return JsonPlusSerializer()
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _cache_message_count(thread_id: str, freshness: str | None, count: int) -> None:
|
|
565
|
+
"""Cache a thread's message count with a freshness token."""
|
|
566
|
+
if len(_message_count_cache) >= _MAX_MESSAGE_COUNT_CACHE and (
|
|
567
|
+
thread_id not in _message_count_cache
|
|
568
|
+
):
|
|
569
|
+
oldest = next(iter(_message_count_cache))
|
|
570
|
+
_message_count_cache.pop(oldest, None)
|
|
571
|
+
_message_count_cache[thread_id] = (freshness, count)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _cache_initial_prompt(
|
|
575
|
+
thread_id: str,
|
|
576
|
+
freshness: str | None,
|
|
577
|
+
initial_prompt: str | None,
|
|
578
|
+
) -> None:
|
|
579
|
+
"""Cache a thread's initial prompt with a freshness token."""
|
|
580
|
+
if len(_initial_prompt_cache) >= _MAX_INITIAL_PROMPT_CACHE and (
|
|
581
|
+
thread_id not in _initial_prompt_cache
|
|
582
|
+
):
|
|
583
|
+
oldest = next(iter(_initial_prompt_cache))
|
|
584
|
+
_initial_prompt_cache.pop(oldest, None)
|
|
585
|
+
_initial_prompt_cache[thread_id] = (freshness, initial_prompt)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _thread_freshness(thread: ThreadInfo) -> str | None:
|
|
589
|
+
"""Return a cache freshness token for a thread row."""
|
|
590
|
+
return thread.get("latest_checkpoint_id") or thread.get("updated_at")
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _cache_recent_threads(
|
|
594
|
+
agent_name: str | None,
|
|
595
|
+
limit: int,
|
|
596
|
+
threads: list[ThreadInfo],
|
|
597
|
+
) -> None:
|
|
598
|
+
"""Store a copy of recent thread rows for fast selector startup."""
|
|
599
|
+
key = (agent_name, max(1, limit))
|
|
600
|
+
if len(_recent_threads_cache) >= _MAX_RECENT_THREADS_CACHE_KEYS and (
|
|
601
|
+
key not in _recent_threads_cache
|
|
602
|
+
):
|
|
603
|
+
_recent_threads_cache.clear()
|
|
604
|
+
_recent_threads_cache[key] = _copy_threads(threads)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _copy_threads(threads: list[ThreadInfo]) -> list[ThreadInfo]:
|
|
608
|
+
"""Return shallow-copied thread rows."""
|
|
609
|
+
return [ThreadInfo(**thread) for thread in threads]
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
async def _count_messages_from_checkpoint(
|
|
613
|
+
conn: aiosqlite.Connection,
|
|
614
|
+
thread_id: str,
|
|
615
|
+
serde: JsonPlusSerializer,
|
|
616
|
+
) -> int:
|
|
617
|
+
"""Count messages from the most recent checkpoint blob.
|
|
618
|
+
|
|
619
|
+
With `durability='exit'`, messages are stored in the checkpoint blob, not in
|
|
620
|
+
the writes table. This function deserializes the checkpoint and counts the
|
|
621
|
+
messages in channel_values.
|
|
622
|
+
|
|
623
|
+
Args:
|
|
624
|
+
conn: Database connection.
|
|
625
|
+
thread_id: The thread ID to count messages for.
|
|
626
|
+
serde: Serializer for decoding checkpoint data.
|
|
627
|
+
|
|
628
|
+
Returns:
|
|
629
|
+
Number of messages in the checkpoint, or 0 if not found.
|
|
630
|
+
"""
|
|
631
|
+
return (await _load_latest_checkpoint_summary(conn, thread_id, serde)).message_count
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
async def _extract_initial_prompt(
|
|
635
|
+
conn: aiosqlite.Connection,
|
|
636
|
+
thread_id: str,
|
|
637
|
+
serde: JsonPlusSerializer,
|
|
638
|
+
) -> str | None:
|
|
639
|
+
"""Extract the first human message from the latest checkpoint.
|
|
640
|
+
|
|
641
|
+
Args:
|
|
642
|
+
conn: Database connection.
|
|
643
|
+
thread_id: The thread ID to extract from.
|
|
644
|
+
serde: Serializer for decoding checkpoint data.
|
|
645
|
+
|
|
646
|
+
Returns:
|
|
647
|
+
First human message content, or None if not found.
|
|
648
|
+
"""
|
|
649
|
+
summary = await _load_latest_checkpoint_summary(conn, thread_id, serde)
|
|
650
|
+
return summary.initial_prompt
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
async def populate_thread_initial_prompts(threads: list[ThreadInfo]) -> None:
|
|
654
|
+
"""Populate `initial_prompt` for thread rows in the background.
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
threads: Thread rows to enrich in place.
|
|
658
|
+
"""
|
|
659
|
+
if not threads:
|
|
660
|
+
return
|
|
661
|
+
|
|
662
|
+
async with _connect() as conn:
|
|
663
|
+
await _populate_checkpoint_fields(
|
|
664
|
+
conn,
|
|
665
|
+
threads,
|
|
666
|
+
include_message_count=False,
|
|
667
|
+
include_initial_prompt=True,
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
async def _populate_checkpoint_fields(
|
|
672
|
+
conn: aiosqlite.Connection,
|
|
673
|
+
threads: list[ThreadInfo],
|
|
674
|
+
*,
|
|
675
|
+
include_message_count: bool,
|
|
676
|
+
include_initial_prompt: bool,
|
|
677
|
+
) -> None:
|
|
678
|
+
"""Populate checkpoint-derived thread fields with a batched latest-row pass."""
|
|
679
|
+
serde = await _get_jsonplus_serializer()
|
|
680
|
+
|
|
681
|
+
# Phase 1: apply cache hits, collect threads that need DB fetch.
|
|
682
|
+
uncached: list[ThreadInfo] = []
|
|
683
|
+
for thread in threads:
|
|
684
|
+
thread_id = thread["thread_id"]
|
|
685
|
+
freshness = _thread_freshness(thread)
|
|
686
|
+
needs_count = False
|
|
687
|
+
needs_prompt = False
|
|
688
|
+
|
|
689
|
+
if include_message_count:
|
|
690
|
+
cached = _message_count_cache.get(thread_id)
|
|
691
|
+
if cached is not None and cached[0] == freshness:
|
|
692
|
+
thread["message_count"] = cached[1]
|
|
693
|
+
else:
|
|
694
|
+
needs_count = True
|
|
695
|
+
|
|
696
|
+
if include_initial_prompt and "initial_prompt" not in thread:
|
|
697
|
+
cached_prompt = _initial_prompt_cache.get(thread_id)
|
|
698
|
+
if cached_prompt is not None and cached_prompt[0] == freshness:
|
|
699
|
+
thread["initial_prompt"] = cached_prompt[1]
|
|
700
|
+
else:
|
|
701
|
+
needs_prompt = True
|
|
702
|
+
|
|
703
|
+
if needs_count or needs_prompt:
|
|
704
|
+
uncached.append(thread)
|
|
705
|
+
|
|
706
|
+
if not uncached:
|
|
707
|
+
return
|
|
708
|
+
|
|
709
|
+
# Phase 2: batch-fetch all uncached threads.
|
|
710
|
+
uncached_ids = [t["thread_id"] for t in uncached]
|
|
711
|
+
batch_results = await _load_latest_checkpoint_summaries_batch(
|
|
712
|
+
conn, uncached_ids, serde
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
# Phase 3: apply results and update caches.
|
|
716
|
+
for thread in uncached:
|
|
717
|
+
thread_id = thread["thread_id"]
|
|
718
|
+
freshness = _thread_freshness(thread)
|
|
719
|
+
summary = batch_results.get(thread_id, _CheckpointSummary(0, None))
|
|
720
|
+
|
|
721
|
+
if include_message_count and "message_count" not in thread:
|
|
722
|
+
thread["message_count"] = summary.message_count
|
|
723
|
+
_cache_message_count(thread_id, freshness, summary.message_count)
|
|
724
|
+
if include_initial_prompt and "initial_prompt" not in thread:
|
|
725
|
+
thread["initial_prompt"] = summary.initial_prompt
|
|
726
|
+
_cache_initial_prompt(thread_id, freshness, summary.initial_prompt)
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
_SQLITE_MAX_VARIABLE_NUMBER = 500
|
|
730
|
+
"""Max `?` placeholders per SQL query.
|
|
731
|
+
|
|
732
|
+
SQLite limits how many `?` parameters a single query can have (default 999,
|
|
733
|
+
lower on some builds). If a user accumulates hundreds of threads and the
|
|
734
|
+
`/threads` modal fetches them all at once, the `IN (?, ?, ...)` clause could
|
|
735
|
+
exceed that limit. We chunk to this size to stay safe.
|
|
736
|
+
"""
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
async def _load_latest_checkpoint_summaries_batch(
|
|
740
|
+
conn: aiosqlite.Connection,
|
|
741
|
+
thread_ids: list[str],
|
|
742
|
+
serde: JsonPlusSerializer,
|
|
743
|
+
) -> dict[str, _CheckpointSummary]:
|
|
744
|
+
"""Batch-load the latest checkpoint summary for multiple threads.
|
|
745
|
+
|
|
746
|
+
Uses a window function to fetch the latest checkpoint per thread, issuing
|
|
747
|
+
one query per chunk for SQLite variable-limit safety.
|
|
748
|
+
|
|
749
|
+
Args:
|
|
750
|
+
conn: Database connection.
|
|
751
|
+
thread_ids: Thread IDs to look up.
|
|
752
|
+
serde: Serializer for decoding checkpoint blobs.
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
Dict mapping thread IDs to their checkpoint summaries.
|
|
756
|
+
"""
|
|
757
|
+
if not thread_ids:
|
|
758
|
+
return {}
|
|
759
|
+
|
|
760
|
+
results: dict[str, _CheckpointSummary] = {}
|
|
761
|
+
|
|
762
|
+
for start in range(0, len(thread_ids), _SQLITE_MAX_VARIABLE_NUMBER):
|
|
763
|
+
chunk = thread_ids[start : start + _SQLITE_MAX_VARIABLE_NUMBER]
|
|
764
|
+
placeholders = ",".join("?" * len(chunk))
|
|
765
|
+
query = f"""
|
|
766
|
+
SELECT thread_id, type, checkpoint FROM (
|
|
767
|
+
SELECT thread_id, type, checkpoint,
|
|
768
|
+
ROW_NUMBER() OVER (
|
|
769
|
+
PARTITION BY thread_id ORDER BY checkpoint_id DESC
|
|
770
|
+
) AS rn
|
|
771
|
+
FROM checkpoints
|
|
772
|
+
WHERE thread_id IN ({placeholders})
|
|
773
|
+
) WHERE rn = 1
|
|
774
|
+
""" # noqa: S608 # placeholders built from len(chunk); user values use ? params
|
|
775
|
+
async with conn.execute(query, chunk) as cursor:
|
|
776
|
+
rows = await cursor.fetchall()
|
|
777
|
+
|
|
778
|
+
loop = asyncio.get_running_loop()
|
|
779
|
+
for row in rows:
|
|
780
|
+
tid, type_str, checkpoint_blob = row
|
|
781
|
+
if not type_str or not checkpoint_blob:
|
|
782
|
+
results[tid] = _CheckpointSummary(message_count=0, initial_prompt=None)
|
|
783
|
+
continue
|
|
784
|
+
try:
|
|
785
|
+
data = await loop.run_in_executor(
|
|
786
|
+
None, serde.loads_typed, (type_str, checkpoint_blob)
|
|
787
|
+
)
|
|
788
|
+
results[tid] = _summarize_checkpoint(data)
|
|
789
|
+
except Exception:
|
|
790
|
+
logger.warning(
|
|
791
|
+
"Failed to deserialize checkpoint for thread %s; "
|
|
792
|
+
"message count and initial prompt may be incomplete",
|
|
793
|
+
tid,
|
|
794
|
+
exc_info=True,
|
|
795
|
+
)
|
|
796
|
+
results[tid] = _CheckpointSummary(message_count=0, initial_prompt=None)
|
|
797
|
+
|
|
798
|
+
return results
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
async def _load_latest_checkpoint_summary(
|
|
802
|
+
conn: aiosqlite.Connection,
|
|
803
|
+
thread_id: str,
|
|
804
|
+
serde: JsonPlusSerializer,
|
|
805
|
+
) -> _CheckpointSummary:
|
|
806
|
+
"""Load checkpoint-derived summary data from the latest checkpoint row.
|
|
807
|
+
|
|
808
|
+
Returns:
|
|
809
|
+
Message-count and prompt data extracted from the latest checkpoint row.
|
|
810
|
+
"""
|
|
811
|
+
query = """
|
|
812
|
+
SELECT type, checkpoint
|
|
813
|
+
FROM checkpoints
|
|
814
|
+
WHERE thread_id = ?
|
|
815
|
+
ORDER BY checkpoint_id DESC
|
|
816
|
+
LIMIT 1
|
|
817
|
+
"""
|
|
818
|
+
async with conn.execute(query, (thread_id,)) as cursor:
|
|
819
|
+
row = await cursor.fetchone()
|
|
820
|
+
if not row or not row[0] or not row[1]:
|
|
821
|
+
return _CheckpointSummary(message_count=0, initial_prompt=None)
|
|
822
|
+
|
|
823
|
+
type_str, checkpoint_blob = row
|
|
824
|
+
try:
|
|
825
|
+
data = serde.loads_typed((type_str, checkpoint_blob))
|
|
826
|
+
except (ValueError, TypeError, KeyError, AttributeError):
|
|
827
|
+
logger.warning(
|
|
828
|
+
"Failed to deserialize checkpoint for thread %s; "
|
|
829
|
+
"message count and initial prompt may be incomplete",
|
|
830
|
+
thread_id,
|
|
831
|
+
exc_info=True,
|
|
832
|
+
)
|
|
833
|
+
return _CheckpointSummary(message_count=0, initial_prompt=None)
|
|
834
|
+
|
|
835
|
+
return _summarize_checkpoint(data)
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def _summarize_checkpoint(data: object) -> _CheckpointSummary:
|
|
839
|
+
"""Extract message count and initial human prompt from checkpoint data.
|
|
840
|
+
|
|
841
|
+
Returns:
|
|
842
|
+
Structured summary for the decoded checkpoint payload.
|
|
843
|
+
"""
|
|
844
|
+
messages = _checkpoint_messages(data)
|
|
845
|
+
return _CheckpointSummary(
|
|
846
|
+
message_count=len(messages),
|
|
847
|
+
initial_prompt=_initial_prompt_from_messages(messages),
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _checkpoint_messages(data: object) -> list[object]:
|
|
852
|
+
"""Return checkpoint messages when the decoded payload has the expected shape."""
|
|
853
|
+
if not isinstance(data, dict):
|
|
854
|
+
return []
|
|
855
|
+
|
|
856
|
+
payload = cast("dict[str, object]", data)
|
|
857
|
+
channel_values = payload.get("channel_values")
|
|
858
|
+
if not isinstance(channel_values, dict):
|
|
859
|
+
return []
|
|
860
|
+
|
|
861
|
+
channel_values_dict = cast("dict[str, object]", channel_values)
|
|
862
|
+
messages = channel_values_dict.get("messages")
|
|
863
|
+
if not isinstance(messages, list):
|
|
864
|
+
return []
|
|
865
|
+
|
|
866
|
+
return cast("list[object]", messages)
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def _initial_prompt_from_messages(messages: list[object]) -> str | None:
|
|
870
|
+
"""Return the first human message content from a checkpoint message list."""
|
|
871
|
+
for msg in messages:
|
|
872
|
+
if getattr(msg, "type", None) == "human":
|
|
873
|
+
return _coerce_prompt_text(getattr(msg, "content", None))
|
|
874
|
+
return None
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _coerce_prompt_text(content: object) -> str | None:
|
|
878
|
+
"""Normalize checkpoint message content into displayable text.
|
|
879
|
+
|
|
880
|
+
Returns:
|
|
881
|
+
Displayable prompt text, or `None` when the content is empty.
|
|
882
|
+
"""
|
|
883
|
+
if isinstance(content, str):
|
|
884
|
+
return content
|
|
885
|
+
if isinstance(content, list):
|
|
886
|
+
parts: list[str] = []
|
|
887
|
+
for part in content:
|
|
888
|
+
if isinstance(part, dict):
|
|
889
|
+
part_dict = cast("dict[str, object]", part)
|
|
890
|
+
text = part_dict.get("text")
|
|
891
|
+
parts.append(text if isinstance(text, str) else "")
|
|
892
|
+
else:
|
|
893
|
+
parts.append(str(part))
|
|
894
|
+
joined = " ".join(parts).strip()
|
|
895
|
+
return joined or None
|
|
896
|
+
if content is None:
|
|
897
|
+
return None
|
|
898
|
+
return str(content)
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
async def get_most_recent(agent_name: str | None = None) -> str | None:
|
|
902
|
+
"""Get most recent thread_id, optionally filtered by agent.
|
|
903
|
+
|
|
904
|
+
Returns:
|
|
905
|
+
Most recent thread_id or None if no threads exist.
|
|
906
|
+
"""
|
|
907
|
+
async with _connect() as conn:
|
|
908
|
+
if not await _table_exists(conn, "checkpoints"):
|
|
909
|
+
return None
|
|
910
|
+
|
|
911
|
+
if agent_name:
|
|
912
|
+
query = """
|
|
913
|
+
SELECT thread_id FROM checkpoints
|
|
914
|
+
WHERE json_extract(metadata, '$.agent_name') = ?
|
|
915
|
+
ORDER BY checkpoint_id DESC
|
|
916
|
+
LIMIT 1
|
|
917
|
+
"""
|
|
918
|
+
params: tuple = (agent_name,)
|
|
919
|
+
else:
|
|
920
|
+
query = (
|
|
921
|
+
"SELECT thread_id FROM checkpoints ORDER BY checkpoint_id DESC LIMIT 1"
|
|
922
|
+
)
|
|
923
|
+
params = ()
|
|
924
|
+
|
|
925
|
+
async with conn.execute(query, params) as cursor:
|
|
926
|
+
row = await cursor.fetchone()
|
|
927
|
+
return row[0] if row else None
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
async def get_thread_agent(thread_id: str) -> str | None:
|
|
931
|
+
"""Get agent_name for a thread.
|
|
932
|
+
|
|
933
|
+
Returns:
|
|
934
|
+
Agent name associated with the thread, or None if not found.
|
|
935
|
+
"""
|
|
936
|
+
async with _connect() as conn:
|
|
937
|
+
if not await _table_exists(conn, "checkpoints"):
|
|
938
|
+
return None
|
|
939
|
+
|
|
940
|
+
query = """
|
|
941
|
+
SELECT json_extract(metadata, '$.agent_name')
|
|
942
|
+
FROM checkpoints
|
|
943
|
+
WHERE thread_id = ?
|
|
944
|
+
LIMIT 1
|
|
945
|
+
"""
|
|
946
|
+
async with conn.execute(query, (thread_id,)) as cursor:
|
|
947
|
+
row = await cursor.fetchone()
|
|
948
|
+
return row[0] if row else None
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
async def thread_exists(thread_id: str) -> bool:
|
|
952
|
+
"""Check if a thread exists in checkpoints.
|
|
953
|
+
|
|
954
|
+
Returns:
|
|
955
|
+
True if thread exists, False otherwise.
|
|
956
|
+
"""
|
|
957
|
+
async with _connect() as conn:
|
|
958
|
+
if not await _table_exists(conn, "checkpoints"):
|
|
959
|
+
return False
|
|
960
|
+
|
|
961
|
+
query = "SELECT 1 FROM checkpoints WHERE thread_id = ? LIMIT 1"
|
|
962
|
+
async with conn.execute(query, (thread_id,)) as cursor:
|
|
963
|
+
row = await cursor.fetchone()
|
|
964
|
+
return row is not None
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
async def find_similar_threads(thread_id: str, limit: int = 3) -> list[str]:
|
|
968
|
+
"""Find threads whose IDs start with the given prefix.
|
|
969
|
+
|
|
970
|
+
Args:
|
|
971
|
+
thread_id: Prefix to match against thread IDs.
|
|
972
|
+
limit: Maximum number of matching threads to return.
|
|
973
|
+
|
|
974
|
+
Returns:
|
|
975
|
+
List of thread IDs that begin with the given prefix.
|
|
976
|
+
"""
|
|
977
|
+
async with _connect() as conn:
|
|
978
|
+
if not await _table_exists(conn, "checkpoints"):
|
|
979
|
+
return []
|
|
980
|
+
|
|
981
|
+
query = """
|
|
982
|
+
SELECT DISTINCT thread_id
|
|
983
|
+
FROM checkpoints
|
|
984
|
+
WHERE thread_id LIKE ?
|
|
985
|
+
ORDER BY thread_id
|
|
986
|
+
LIMIT ?
|
|
987
|
+
"""
|
|
988
|
+
prefix = thread_id + "%"
|
|
989
|
+
async with conn.execute(query, (prefix, limit)) as cursor:
|
|
990
|
+
rows = await cursor.fetchall()
|
|
991
|
+
return [r[0] for r in rows]
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
async def delete_thread(thread_id: str) -> bool:
|
|
995
|
+
"""Delete thread checkpoints.
|
|
996
|
+
|
|
997
|
+
Returns:
|
|
998
|
+
True if thread was deleted, False if not found.
|
|
999
|
+
"""
|
|
1000
|
+
async with _connect() as conn:
|
|
1001
|
+
if not await _table_exists(conn, "checkpoints"):
|
|
1002
|
+
return False
|
|
1003
|
+
|
|
1004
|
+
cursor = await conn.execute(
|
|
1005
|
+
"DELETE FROM checkpoints WHERE thread_id = ?", (thread_id,)
|
|
1006
|
+
)
|
|
1007
|
+
deleted = cursor.rowcount > 0
|
|
1008
|
+
if await _table_exists(conn, "writes"):
|
|
1009
|
+
await conn.execute("DELETE FROM writes WHERE thread_id = ?", (thread_id,))
|
|
1010
|
+
await conn.commit()
|
|
1011
|
+
if deleted:
|
|
1012
|
+
_message_count_cache.pop(thread_id, None)
|
|
1013
|
+
for key, rows in list(_recent_threads_cache.items()):
|
|
1014
|
+
filtered = [row for row in rows if row["thread_id"] != thread_id]
|
|
1015
|
+
_recent_threads_cache[key] = filtered
|
|
1016
|
+
return deleted
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
@asynccontextmanager
|
|
1020
|
+
async def get_checkpointer() -> AsyncIterator[AsyncSqliteSaver]:
|
|
1021
|
+
"""Get AsyncSqliteSaver for the global database.
|
|
1022
|
+
|
|
1023
|
+
Yields:
|
|
1024
|
+
AsyncSqliteSaver instance for checkpoint persistence.
|
|
1025
|
+
"""
|
|
1026
|
+
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
|
1027
|
+
|
|
1028
|
+
_patch_aiosqlite()
|
|
1029
|
+
|
|
1030
|
+
async with AsyncSqliteSaver.from_conn_string(str(get_db_path())) as checkpointer:
|
|
1031
|
+
yield checkpointer
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
_DEFAULT_THREAD_LIMIT = 20
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def get_thread_limit() -> int:
|
|
1038
|
+
"""Read the thread listing limit from `DA_CLI_RECENT_THREADS`.
|
|
1039
|
+
|
|
1040
|
+
Falls back to `_DEFAULT_THREAD_LIMIT` when the variable is unset or contains
|
|
1041
|
+
a non-integer value. The result is clamped to a minimum of 1.
|
|
1042
|
+
|
|
1043
|
+
Returns:
|
|
1044
|
+
Number of threads to display.
|
|
1045
|
+
"""
|
|
1046
|
+
import os
|
|
1047
|
+
|
|
1048
|
+
raw = os.environ.get("DA_CLI_RECENT_THREADS")
|
|
1049
|
+
if raw is None:
|
|
1050
|
+
return _DEFAULT_THREAD_LIMIT
|
|
1051
|
+
try:
|
|
1052
|
+
return max(1, int(raw))
|
|
1053
|
+
except ValueError:
|
|
1054
|
+
logger.warning(
|
|
1055
|
+
"Invalid DA_CLI_RECENT_THREADS value %r, using default %d",
|
|
1056
|
+
raw,
|
|
1057
|
+
_DEFAULT_THREAD_LIMIT,
|
|
1058
|
+
)
|
|
1059
|
+
return _DEFAULT_THREAD_LIMIT
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
async def list_threads_command(
|
|
1063
|
+
agent_name: str | None = None,
|
|
1064
|
+
limit: int | None = None,
|
|
1065
|
+
sort_by: str | None = None,
|
|
1066
|
+
branch: str | None = None,
|
|
1067
|
+
verbose: bool = False,
|
|
1068
|
+
relative: bool | None = None,
|
|
1069
|
+
*,
|
|
1070
|
+
output_format: OutputFormat = "text",
|
|
1071
|
+
) -> None:
|
|
1072
|
+
"""CLI handler for `docagent threads list`.
|
|
1073
|
+
|
|
1074
|
+
Fetches and displays a table of recent conversation threads, optionally
|
|
1075
|
+
filtered by agent name or git branch.
|
|
1076
|
+
|
|
1077
|
+
Args:
|
|
1078
|
+
agent_name: Only show threads belonging to this agent.
|
|
1079
|
+
|
|
1080
|
+
When `None`, threads for all agents are shown.
|
|
1081
|
+
limit: Maximum number of threads to display.
|
|
1082
|
+
|
|
1083
|
+
When `None`, reads from `DA_CLI_RECENT_THREADS` or falls back to
|
|
1084
|
+
the default.
|
|
1085
|
+
sort_by: Sort field — `"updated"` or `"created"`.
|
|
1086
|
+
|
|
1087
|
+
When `None`, reads from config (`~/.docagent/config.toml`).
|
|
1088
|
+
branch: Only show threads from this git branch.
|
|
1089
|
+
verbose: When `True`, show all columns (branch, created, prompt).
|
|
1090
|
+
relative: Show timestamps as relative time (e.g., '5m ago').
|
|
1091
|
+
|
|
1092
|
+
When `None`, reads from config (`~/.docagent/config.toml`).
|
|
1093
|
+
output_format: Output format — `'text'` (Rich) or `'json'`.
|
|
1094
|
+
"""
|
|
1095
|
+
from docagent_cli.model_config import (
|
|
1096
|
+
load_thread_relative_time,
|
|
1097
|
+
load_thread_sort_order,
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
if sort_by is None:
|
|
1101
|
+
raw = load_thread_sort_order()
|
|
1102
|
+
sort_by = "created" if raw == "created_at" else "updated"
|
|
1103
|
+
if relative is None:
|
|
1104
|
+
relative = load_thread_relative_time()
|
|
1105
|
+
|
|
1106
|
+
fmt_ts = format_relative_timestamp if relative else format_timestamp
|
|
1107
|
+
|
|
1108
|
+
limit = get_thread_limit() if limit is None else max(1, limit)
|
|
1109
|
+
|
|
1110
|
+
threads = await list_threads(
|
|
1111
|
+
agent_name,
|
|
1112
|
+
limit=limit,
|
|
1113
|
+
include_message_count=True,
|
|
1114
|
+
sort_by=sort_by,
|
|
1115
|
+
branch=branch,
|
|
1116
|
+
)
|
|
1117
|
+
|
|
1118
|
+
if verbose and threads:
|
|
1119
|
+
await populate_thread_checkpoint_details(
|
|
1120
|
+
threads, include_message_count=False, include_initial_prompt=True
|
|
1121
|
+
)
|
|
1122
|
+
|
|
1123
|
+
if output_format == "json":
|
|
1124
|
+
from docagent_cli.output import write_json
|
|
1125
|
+
|
|
1126
|
+
write_json("threads list", list(threads))
|
|
1127
|
+
return
|
|
1128
|
+
|
|
1129
|
+
from rich.markup import escape as escape_markup
|
|
1130
|
+
from rich.table import Table
|
|
1131
|
+
|
|
1132
|
+
from docagent_cli import theme
|
|
1133
|
+
from docagent_cli.config import console
|
|
1134
|
+
|
|
1135
|
+
if not threads:
|
|
1136
|
+
filters = []
|
|
1137
|
+
if agent_name:
|
|
1138
|
+
filters.append(f"agent '{escape_markup(agent_name)}'")
|
|
1139
|
+
if branch:
|
|
1140
|
+
filters.append(f"branch '{escape_markup(branch)}'")
|
|
1141
|
+
if filters:
|
|
1142
|
+
console.print(
|
|
1143
|
+
f"[yellow]No threads found for {' and '.join(filters)}.[/yellow]"
|
|
1144
|
+
)
|
|
1145
|
+
else:
|
|
1146
|
+
console.print("[yellow]No threads found.[/yellow]")
|
|
1147
|
+
console.print("[dim]Start a conversation with: docagent[/dim]")
|
|
1148
|
+
return
|
|
1149
|
+
|
|
1150
|
+
title_parts = []
|
|
1151
|
+
if agent_name:
|
|
1152
|
+
title_parts.append(f"agent '{escape_markup(agent_name)}'")
|
|
1153
|
+
if branch:
|
|
1154
|
+
title_parts.append(f"branch '{escape_markup(branch)}'")
|
|
1155
|
+
|
|
1156
|
+
title_filter = f" for {' and '.join(title_parts)}" if title_parts else ""
|
|
1157
|
+
sort_label = "created" if sort_by == "created" else "updated"
|
|
1158
|
+
title = f"Recent Threads{title_filter} (last {limit}, by {sort_label})"
|
|
1159
|
+
|
|
1160
|
+
table = Table(title=title, show_header=True, header_style=f"bold {theme.PRIMARY}")
|
|
1161
|
+
table.add_column("Thread ID", style="bold")
|
|
1162
|
+
table.add_column("Agent")
|
|
1163
|
+
table.add_column("Messages", justify="right")
|
|
1164
|
+
if verbose:
|
|
1165
|
+
table.add_column("Created")
|
|
1166
|
+
table.add_column("Updated" if sort_by == "updated" else "Last Used")
|
|
1167
|
+
if verbose:
|
|
1168
|
+
table.add_column("Branch")
|
|
1169
|
+
table.add_column("Location")
|
|
1170
|
+
table.add_column("Prompt", max_width=40, no_wrap=True)
|
|
1171
|
+
|
|
1172
|
+
prompt_max = 40
|
|
1173
|
+
|
|
1174
|
+
for t in threads:
|
|
1175
|
+
row: list[str] = [
|
|
1176
|
+
t["thread_id"],
|
|
1177
|
+
t["agent_name"] or "unknown",
|
|
1178
|
+
str(t.get("message_count", 0)),
|
|
1179
|
+
]
|
|
1180
|
+
if verbose:
|
|
1181
|
+
row.append(fmt_ts(t.get("created_at")))
|
|
1182
|
+
row.append(fmt_ts(t.get("updated_at")))
|
|
1183
|
+
if verbose:
|
|
1184
|
+
prompt = " ".join((t.get("initial_prompt") or "").split())
|
|
1185
|
+
if len(prompt) > prompt_max:
|
|
1186
|
+
prompt = prompt[: prompt_max - 3] + "..."
|
|
1187
|
+
row.extend(
|
|
1188
|
+
[
|
|
1189
|
+
t.get("git_branch") or "",
|
|
1190
|
+
format_path(t.get("cwd")),
|
|
1191
|
+
prompt,
|
|
1192
|
+
]
|
|
1193
|
+
)
|
|
1194
|
+
table.add_row(*row)
|
|
1195
|
+
|
|
1196
|
+
console.print()
|
|
1197
|
+
console.print(table)
|
|
1198
|
+
if len(threads) >= limit:
|
|
1199
|
+
console.print(
|
|
1200
|
+
f"[dim]Showing last {limit} threads. "
|
|
1201
|
+
"Override with -n/--limit or DA_CLI_RECENT_THREADS.[/dim]"
|
|
1202
|
+
)
|
|
1203
|
+
console.print()
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
async def delete_thread_command(
|
|
1207
|
+
thread_id: str,
|
|
1208
|
+
*,
|
|
1209
|
+
dry_run: bool = False,
|
|
1210
|
+
output_format: OutputFormat = "text",
|
|
1211
|
+
) -> None:
|
|
1212
|
+
"""CLI handler for: docagent threads delete.
|
|
1213
|
+
|
|
1214
|
+
Args:
|
|
1215
|
+
thread_id: ID of the thread to delete.
|
|
1216
|
+
dry_run: If `True`, print what would happen without making changes.
|
|
1217
|
+
output_format: Output format — `'text'` (Rich) or `'json'`.
|
|
1218
|
+
"""
|
|
1219
|
+
if dry_run:
|
|
1220
|
+
exists = await thread_exists(thread_id)
|
|
1221
|
+
if output_format == "json":
|
|
1222
|
+
from docagent_cli.output import write_json
|
|
1223
|
+
|
|
1224
|
+
write_json(
|
|
1225
|
+
"threads delete",
|
|
1226
|
+
{"thread_id": thread_id, "exists": exists, "dry_run": True},
|
|
1227
|
+
)
|
|
1228
|
+
return
|
|
1229
|
+
|
|
1230
|
+
from rich.markup import escape as escape_markup
|
|
1231
|
+
|
|
1232
|
+
from docagent_cli.config import console
|
|
1233
|
+
|
|
1234
|
+
escaped_id = escape_markup(thread_id)
|
|
1235
|
+
if exists:
|
|
1236
|
+
console.print(f"Would delete thread '{escaped_id}'.")
|
|
1237
|
+
else:
|
|
1238
|
+
console.print(f"Thread '{escaped_id}' not found. Nothing to delete.")
|
|
1239
|
+
console.print("No changes made.", style="dim")
|
|
1240
|
+
return
|
|
1241
|
+
|
|
1242
|
+
deleted = await delete_thread(thread_id)
|
|
1243
|
+
|
|
1244
|
+
if output_format == "json":
|
|
1245
|
+
from docagent_cli.output import write_json
|
|
1246
|
+
|
|
1247
|
+
write_json("threads delete", {"thread_id": thread_id, "deleted": deleted})
|
|
1248
|
+
return
|
|
1249
|
+
|
|
1250
|
+
from rich.markup import escape as escape_markup
|
|
1251
|
+
|
|
1252
|
+
from docagent_cli import theme
|
|
1253
|
+
from docagent_cli.config import console
|
|
1254
|
+
|
|
1255
|
+
escaped_id = escape_markup(thread_id)
|
|
1256
|
+
if deleted:
|
|
1257
|
+
console.print(f"[green]Thread '{escaped_id}' deleted.[/green]")
|
|
1258
|
+
else:
|
|
1259
|
+
console.print(
|
|
1260
|
+
f"Thread '{escaped_id}' not found or already deleted.",
|
|
1261
|
+
style=theme.MUTED,
|
|
1262
|
+
)
|