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
|
@@ -0,0 +1,1620 @@
|
|
|
1
|
+
"""Model configuration management.
|
|
2
|
+
|
|
3
|
+
Handles loading and saving model configuration from TOML files, providing a
|
|
4
|
+
structured way to define available models and providers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import contextlib
|
|
10
|
+
import importlib.util
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import tempfile
|
|
14
|
+
import threading
|
|
15
|
+
import tomllib
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from types import MappingProxyType
|
|
19
|
+
from typing import TYPE_CHECKING, Any, NamedTuple, TypedDict
|
|
20
|
+
|
|
21
|
+
import tomli_w
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Mapping
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
_ENV_PREFIX = "DEEPAGENTS_CLI_"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def resolve_env_var(name: str) -> str | None:
|
|
32
|
+
"""Look up an env var with `DEEPAGENTS_CLI_` prefix override.
|
|
33
|
+
|
|
34
|
+
Checks `DEEPAGENTS_CLI_{name}` first, then falls back to `{name}`.
|
|
35
|
+
|
|
36
|
+
If the prefixed variable is *present* in the environment (even as an empty
|
|
37
|
+
string), the canonical variable is never consulted. This lets users
|
|
38
|
+
set `DEEPAGENTS_CLI_X=""` to shadow a canonically-set key -- the function
|
|
39
|
+
will return `None` (since empty strings are normalized to `None`),
|
|
40
|
+
effectively suppressing the canonical value.
|
|
41
|
+
|
|
42
|
+
If `name` already carries the prefix, the double-prefixed lookup is skipped
|
|
43
|
+
to avoid nonsensical `DEEPAGENTS_CLI_DEEPAGENTS_CLI_*` reads
|
|
44
|
+
(e.g., when the name comes from a user's `config.toml`).
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
name: The canonical environment variable name (e.g.
|
|
48
|
+
`ANTHROPIC_API_KEY`).
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The resolved value, or `None` when absent or empty.
|
|
52
|
+
"""
|
|
53
|
+
if not name.startswith(_ENV_PREFIX):
|
|
54
|
+
prefixed = f"{_ENV_PREFIX}{name}"
|
|
55
|
+
if prefixed in os.environ:
|
|
56
|
+
val = os.environ[prefixed]
|
|
57
|
+
if not val and os.environ.get(name):
|
|
58
|
+
logger.debug(
|
|
59
|
+
"%s is set but empty, blocking non-empty %s. "
|
|
60
|
+
"Unset %s to use the canonical variable.",
|
|
61
|
+
prefixed,
|
|
62
|
+
name,
|
|
63
|
+
prefixed,
|
|
64
|
+
)
|
|
65
|
+
return val or None
|
|
66
|
+
return os.environ.get(name) or None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ModelConfigError(Exception):
|
|
70
|
+
"""Raised when model configuration or creation fails."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class ModelSpec:
|
|
75
|
+
"""A model specification in `provider:model` format.
|
|
76
|
+
|
|
77
|
+
Examples:
|
|
78
|
+
>>> spec = ModelSpec.parse("anthropic:claude-sonnet-4-5")
|
|
79
|
+
>>> spec.provider
|
|
80
|
+
'anthropic'
|
|
81
|
+
>>> spec.model
|
|
82
|
+
'claude-sonnet-4-5'
|
|
83
|
+
>>> str(spec)
|
|
84
|
+
'anthropic:claude-sonnet-4-5'
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
provider: str
|
|
88
|
+
"""The provider name (e.g., `'anthropic'`, `'openai'`)."""
|
|
89
|
+
|
|
90
|
+
model: str
|
|
91
|
+
"""The model identifier (e.g., `'claude-sonnet-4-5'`, `'gpt-4o'`)."""
|
|
92
|
+
|
|
93
|
+
def __post_init__(self) -> None:
|
|
94
|
+
"""Validate the model spec after initialization.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ValueError: If provider or model is empty.
|
|
98
|
+
"""
|
|
99
|
+
if not self.provider:
|
|
100
|
+
msg = "Provider cannot be empty"
|
|
101
|
+
raise ValueError(msg)
|
|
102
|
+
if not self.model:
|
|
103
|
+
msg = "Model cannot be empty"
|
|
104
|
+
raise ValueError(msg)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def parse(cls, spec: str) -> ModelSpec:
|
|
108
|
+
"""Parse a model specification string.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
spec: Model specification in `'provider:model'` format.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Parsed ModelSpec instance.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ValueError: If the spec is not in valid `'provider:model'` format.
|
|
118
|
+
"""
|
|
119
|
+
if ":" not in spec:
|
|
120
|
+
msg = (
|
|
121
|
+
f"Invalid model spec '{spec}': must be in provider:model format "
|
|
122
|
+
"(e.g., 'anthropic:claude-sonnet-4-5')"
|
|
123
|
+
)
|
|
124
|
+
raise ValueError(msg)
|
|
125
|
+
provider, model = spec.split(":", 1)
|
|
126
|
+
return cls(provider=provider, model=model)
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def try_parse(cls, spec: str) -> ModelSpec | None:
|
|
130
|
+
"""Non-raising variant of `parse`.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
spec: Model specification in `provider:model` format.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Parsed `ModelSpec`, or `None` when *spec* is not valid.
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
return cls.parse(spec)
|
|
140
|
+
except ValueError:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
def __str__(self) -> str:
|
|
144
|
+
"""Return the model spec as a string in `provider:model` format."""
|
|
145
|
+
return f"{self.provider}:{self.model}"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ModelProfileEntry(TypedDict):
|
|
149
|
+
"""Profile data for a model with override tracking."""
|
|
150
|
+
|
|
151
|
+
profile: dict[str, Any]
|
|
152
|
+
"""Merged profile dict (upstream defaults + config.toml overrides).
|
|
153
|
+
|
|
154
|
+
Keys vary by provider (e.g., `max_input_tokens`, `tool_calling`).
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
overridden_keys: frozenset[str]
|
|
158
|
+
"""Keys in `profile` whose values came from config.toml rather than the
|
|
159
|
+
upstream provider package."""
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ProviderConfig(TypedDict, total=False):
|
|
163
|
+
"""Configuration for a model provider.
|
|
164
|
+
|
|
165
|
+
The optional `class_path` field allows bypassing `init_chat_model` entirely
|
|
166
|
+
and instantiating an arbitrary `BaseChatModel` subclass via importlib.
|
|
167
|
+
|
|
168
|
+
!!! warning
|
|
169
|
+
|
|
170
|
+
Setting `class_path` executes arbitrary Python code from the user's
|
|
171
|
+
config file. This has the same trust model as `pyproject.toml` build
|
|
172
|
+
scripts — the user controls their own machine.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
enabled: bool
|
|
176
|
+
"""Whether this provider appears in the model switcher.
|
|
177
|
+
|
|
178
|
+
Defaults to `True`. Set to `False` to hide a package-discovered provider
|
|
179
|
+
and all its models from the `/model` selector. Useful when a LangChain
|
|
180
|
+
provider package is installed as a transitive dependency but should not
|
|
181
|
+
be user-visible.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
models: list[str]
|
|
185
|
+
"""List of model identifiers available from this provider."""
|
|
186
|
+
|
|
187
|
+
api_key_env: str
|
|
188
|
+
"""Environment variable name containing the API key."""
|
|
189
|
+
|
|
190
|
+
base_url: str
|
|
191
|
+
"""Custom base URL."""
|
|
192
|
+
|
|
193
|
+
# Level 2: arbitrary BaseChatModel classes
|
|
194
|
+
|
|
195
|
+
class_path: str
|
|
196
|
+
"""Fully-qualified Python class in `module.path:ClassName` format.
|
|
197
|
+
|
|
198
|
+
When set, `create_model` imports this class and instantiates it directly
|
|
199
|
+
instead of calling `init_chat_model`.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
params: dict[str, Any]
|
|
203
|
+
"""Extra keyword arguments forwarded to the model constructor.
|
|
204
|
+
|
|
205
|
+
Flat keys (e.g., `temperature = 0`) are provider-wide defaults applied to
|
|
206
|
+
every model from this provider. Model-keyed sub-tables (e.g.,
|
|
207
|
+
`[params."qwen3:4b"]`) override individual values for that model only;
|
|
208
|
+
the merge is shallow (model wins on conflict).
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
profile: dict[str, Any]
|
|
212
|
+
"""Overrides merged into the model's runtime profile dict.
|
|
213
|
+
|
|
214
|
+
Flat keys (e.g., `max_input_tokens = 4096`) are provider-wide defaults.
|
|
215
|
+
Model-keyed sub-tables (e.g., `[profile."claude-sonnet-4-5"]`) override
|
|
216
|
+
individual values for that model only; the merge is shallow.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
DEFAULT_CONFIG_DIR = Path.home() / ".docagent"
|
|
221
|
+
"""Directory for user-level Deep Agents configuration (`~/.docagent`)."""
|
|
222
|
+
|
|
223
|
+
DEFAULT_CONFIG_PATH = DEFAULT_CONFIG_DIR / "config.toml"
|
|
224
|
+
"""Path to the user's model configuration file (`~/.docagent/config.toml`)."""
|
|
225
|
+
|
|
226
|
+
PROVIDER_API_KEY_ENV: dict[str, str] = {
|
|
227
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
228
|
+
"azure_openai": "AZURE_OPENAI_API_KEY",
|
|
229
|
+
"baseten": "BASETEN_API_KEY",
|
|
230
|
+
"cohere": "COHERE_API_KEY",
|
|
231
|
+
"deepseek": "DEEPSEEK_API_KEY",
|
|
232
|
+
"fireworks": "FIREWORKS_API_KEY",
|
|
233
|
+
"google_genai": "GOOGLE_API_KEY",
|
|
234
|
+
"google_vertexai": "GOOGLE_CLOUD_PROJECT",
|
|
235
|
+
"groq": "GROQ_API_KEY",
|
|
236
|
+
"huggingface": "HUGGINGFACEHUB_API_TOKEN",
|
|
237
|
+
"ibm": "WATSONX_APIKEY",
|
|
238
|
+
"litellm": "LITELLM_API_KEY",
|
|
239
|
+
"mistralai": "MISTRAL_API_KEY",
|
|
240
|
+
"nvidia": "NVIDIA_API_KEY",
|
|
241
|
+
"openai": "OPENAI_API_KEY",
|
|
242
|
+
"openrouter": "OPENROUTER_API_KEY",
|
|
243
|
+
"perplexity": "PPLX_API_KEY",
|
|
244
|
+
"together": "TOGETHER_API_KEY",
|
|
245
|
+
"xai": "XAI_API_KEY",
|
|
246
|
+
}
|
|
247
|
+
"""Well-known providers mapped to the env var that holds their API key.
|
|
248
|
+
|
|
249
|
+
Used by `has_provider_credentials` to verify credentials *before* model
|
|
250
|
+
creation, so the UI can show a warning icon and a specific error message
|
|
251
|
+
(e.g., "ANTHROPIC_API_KEY not set") instead of letting the provider fail at call
|
|
252
|
+
time.
|
|
253
|
+
|
|
254
|
+
Providers not listed here fall through to the config-file check or the langchain
|
|
255
|
+
registry fallback.
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# Module-level caches — cleared by `clear_caches()`.
|
|
260
|
+
_available_models_cache: dict[str, list[str]] | None = None
|
|
261
|
+
_builtin_providers_cache: dict[str, Any] | None = None
|
|
262
|
+
_default_config_cache: ModelConfig | None = None
|
|
263
|
+
_provider_profiles_cache: dict[str, dict[str, Any]] = {}
|
|
264
|
+
_provider_profiles_lock = threading.Lock()
|
|
265
|
+
_profiles_cache: Mapping[str, ModelProfileEntry] | None = None
|
|
266
|
+
_profiles_override_cache: tuple[int, Mapping[str, ModelProfileEntry]] | None = None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def clear_caches() -> None:
|
|
270
|
+
"""Reset module-level caches so the next call recomputes from scratch.
|
|
271
|
+
|
|
272
|
+
Intended for tests and for the `/reload` command.
|
|
273
|
+
"""
|
|
274
|
+
global _available_models_cache, _builtin_providers_cache, _default_config_cache, _profiles_cache, _profiles_override_cache # noqa: PLW0603, E501 # Module-level caches require global statement
|
|
275
|
+
_available_models_cache = None
|
|
276
|
+
_builtin_providers_cache = None
|
|
277
|
+
_default_config_cache = None
|
|
278
|
+
_provider_profiles_cache.clear()
|
|
279
|
+
_profiles_cache = None
|
|
280
|
+
_profiles_override_cache = None
|
|
281
|
+
invalidate_thread_config_cache()
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _get_builtin_providers() -> dict[str, Any]:
|
|
285
|
+
"""Return langchain's built-in provider registry.
|
|
286
|
+
|
|
287
|
+
Tries the newer `_BUILTIN_PROVIDERS` name first, then falls back to
|
|
288
|
+
the legacy `_SUPPORTED_PROVIDERS` for older langchain versions.
|
|
289
|
+
|
|
290
|
+
Results are cached after the first call; use `clear_caches()` to reset.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
The provider registry dict from `langchain.chat_models.base`.
|
|
294
|
+
"""
|
|
295
|
+
global _builtin_providers_cache # noqa: PLW0603 # Module-level cache requires global statement
|
|
296
|
+
if _builtin_providers_cache is not None:
|
|
297
|
+
return _builtin_providers_cache
|
|
298
|
+
|
|
299
|
+
# Deferred: langchain.chat_models pulls in heavy provider registry,
|
|
300
|
+
# only needed when resolving provider names for model config.
|
|
301
|
+
from langchain.chat_models import base
|
|
302
|
+
|
|
303
|
+
registry: dict[str, Any] | None = getattr(base, "_BUILTIN_PROVIDERS", None)
|
|
304
|
+
if registry is None:
|
|
305
|
+
registry = getattr(base, "_SUPPORTED_PROVIDERS", None)
|
|
306
|
+
_builtin_providers_cache = registry if registry is not None else {}
|
|
307
|
+
return _builtin_providers_cache
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _get_provider_profile_modules() -> list[tuple[str, str]]:
|
|
311
|
+
"""Build a `(provider, profile_module)` list from langchain's provider registry.
|
|
312
|
+
|
|
313
|
+
Reads the built-in provider registry from `langchain.chat_models.base`
|
|
314
|
+
to discover every provider that `init_chat_model` knows about, then derives
|
|
315
|
+
the `<package>.data._profiles` module path for each.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
List of `(provider_name, profile_module_path)` tuples.
|
|
319
|
+
"""
|
|
320
|
+
providers = _get_builtin_providers()
|
|
321
|
+
|
|
322
|
+
result: list[tuple[str, str]] = []
|
|
323
|
+
seen: set[tuple[str, str]] = set()
|
|
324
|
+
|
|
325
|
+
for provider_name, (module_path, *_rest) in providers.items():
|
|
326
|
+
package_root = module_path.split(".", maxsplit=1)[0]
|
|
327
|
+
profile_module = f"{package_root}.data._profiles"
|
|
328
|
+
key = (provider_name, profile_module)
|
|
329
|
+
if key not in seen:
|
|
330
|
+
seen.add(key)
|
|
331
|
+
result.append((provider_name, profile_module))
|
|
332
|
+
|
|
333
|
+
return result
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _load_provider_profiles(module_path: str) -> dict[str, Any]:
|
|
337
|
+
"""Load `_PROFILES` from a provider's data module.
|
|
338
|
+
|
|
339
|
+
Results are cached by `module_path` so repeated calls (e.g., from both
|
|
340
|
+
`get_available_models` and `get_model_profiles`) reuse the same dict.
|
|
341
|
+
Use `clear_caches()` to reset.
|
|
342
|
+
|
|
343
|
+
Locates the package on disk with `importlib.util.find_spec` and loads *only*
|
|
344
|
+
the `_profiles.py` file via `spec_from_file_location`.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
module_path: Dotted module path (e.g., `"langchain_openai.data._profiles"`).
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
The `_PROFILES` dictionary from the module, or an empty dict if
|
|
351
|
+
the module has no such attribute.
|
|
352
|
+
|
|
353
|
+
Raises:
|
|
354
|
+
ImportError: If the package is not installed or the profile module
|
|
355
|
+
cannot be found on disk.
|
|
356
|
+
"""
|
|
357
|
+
with _provider_profiles_lock:
|
|
358
|
+
cached = _provider_profiles_cache.get(module_path)
|
|
359
|
+
if cached is not None: # `is not None` so empty profile dicts are cached
|
|
360
|
+
return cached
|
|
361
|
+
|
|
362
|
+
parts = module_path.split(".")
|
|
363
|
+
package_root = parts[0]
|
|
364
|
+
|
|
365
|
+
spec = importlib.util.find_spec(package_root)
|
|
366
|
+
if spec is None:
|
|
367
|
+
msg = f"Package {package_root} is not installed"
|
|
368
|
+
raise ImportError(msg)
|
|
369
|
+
|
|
370
|
+
# Determine the package directory from the spec.
|
|
371
|
+
if spec.origin:
|
|
372
|
+
package_dir = Path(spec.origin).parent
|
|
373
|
+
elif spec.submodule_search_locations:
|
|
374
|
+
package_dir = Path(next(iter(spec.submodule_search_locations)))
|
|
375
|
+
else:
|
|
376
|
+
msg = f"Cannot determine location for {package_root}"
|
|
377
|
+
raise ImportError(msg)
|
|
378
|
+
|
|
379
|
+
# Build the path to the target file (e.g., data/_profiles.py).
|
|
380
|
+
relative_parts = parts[1:] # ["data", "_profiles"]
|
|
381
|
+
profiles_path = package_dir.joinpath(
|
|
382
|
+
*relative_parts[:-1], f"{relative_parts[-1]}.py"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
if not profiles_path.exists():
|
|
386
|
+
msg = f"Profile module not found: {profiles_path}"
|
|
387
|
+
raise ImportError(msg)
|
|
388
|
+
|
|
389
|
+
file_spec = importlib.util.spec_from_file_location(module_path, profiles_path)
|
|
390
|
+
if file_spec is None or file_spec.loader is None:
|
|
391
|
+
msg = f"Could not create module spec for {profiles_path}"
|
|
392
|
+
raise ImportError(msg)
|
|
393
|
+
|
|
394
|
+
module = importlib.util.module_from_spec(file_spec)
|
|
395
|
+
file_spec.loader.exec_module(module)
|
|
396
|
+
profiles = getattr(module, "_PROFILES", {})
|
|
397
|
+
_provider_profiles_cache[module_path] = profiles
|
|
398
|
+
return profiles
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _profile_module_from_class_path(class_path: str) -> str | None:
|
|
402
|
+
"""Derive the profile module path from a `class_path` config value.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
class_path: Fully-qualified class in `module.path:ClassName` format.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
Dotted module path like `langchain_baseten.data._profiles`, or None
|
|
409
|
+
if `class_path` is malformed.
|
|
410
|
+
"""
|
|
411
|
+
if ":" not in class_path:
|
|
412
|
+
return None
|
|
413
|
+
module_part, _ = class_path.split(":", 1)
|
|
414
|
+
package_root = module_part.split(".", maxsplit=1)[0]
|
|
415
|
+
if not package_root:
|
|
416
|
+
return None
|
|
417
|
+
return f"{package_root}.data._profiles"
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def get_available_models() -> dict[str, list[str]]:
|
|
421
|
+
"""Get available models dynamically from installed LangChain provider packages.
|
|
422
|
+
|
|
423
|
+
Imports model profiles from each provider package and extracts model names.
|
|
424
|
+
|
|
425
|
+
Results are cached after the first call; use `clear_caches()` to reset.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Dictionary mapping provider names to lists of model identifiers.
|
|
429
|
+
Includes providers from the langchain registry, config-file
|
|
430
|
+
providers with explicit model lists, and `class_path` providers
|
|
431
|
+
whose packages expose a `_profiles` module.
|
|
432
|
+
"""
|
|
433
|
+
global _available_models_cache # noqa: PLW0603 # Module-level cache requires global statement
|
|
434
|
+
if _available_models_cache is not None:
|
|
435
|
+
return _available_models_cache
|
|
436
|
+
|
|
437
|
+
available: dict[str, list[str]] = {}
|
|
438
|
+
config = ModelConfig.load()
|
|
439
|
+
|
|
440
|
+
# Try to load from langchain provider profile data.
|
|
441
|
+
# Build the list dynamically from langchain's supported-provider registry
|
|
442
|
+
# so new providers are picked up automatically when langchain adds them.
|
|
443
|
+
provider_modules = _get_provider_profile_modules()
|
|
444
|
+
registry_providers: set[str] = set()
|
|
445
|
+
|
|
446
|
+
for provider, module_path in provider_modules:
|
|
447
|
+
registry_providers.add(provider)
|
|
448
|
+
# Skip providers explicitly disabled in config.
|
|
449
|
+
if not config.is_provider_enabled(provider):
|
|
450
|
+
logger.debug(
|
|
451
|
+
"Provider '%s' is disabled in config; skipping registry discovery",
|
|
452
|
+
provider,
|
|
453
|
+
)
|
|
454
|
+
continue
|
|
455
|
+
try:
|
|
456
|
+
profiles = _load_provider_profiles(module_path)
|
|
457
|
+
except ImportError:
|
|
458
|
+
logger.debug(
|
|
459
|
+
"Could not import profiles from %s (package may not be installed)",
|
|
460
|
+
module_path,
|
|
461
|
+
)
|
|
462
|
+
continue
|
|
463
|
+
except Exception:
|
|
464
|
+
logger.warning(
|
|
465
|
+
"Failed to load profiles from %s, skipping provider '%s'",
|
|
466
|
+
module_path,
|
|
467
|
+
provider,
|
|
468
|
+
exc_info=True,
|
|
469
|
+
)
|
|
470
|
+
continue
|
|
471
|
+
|
|
472
|
+
# Filter to models that support tool calling and text I/O.
|
|
473
|
+
models = [
|
|
474
|
+
name
|
|
475
|
+
for name, profile in profiles.items()
|
|
476
|
+
if profile.get("tool_calling", False)
|
|
477
|
+
and profile.get("text_inputs", True) is not False
|
|
478
|
+
and profile.get("text_outputs", True) is not False
|
|
479
|
+
]
|
|
480
|
+
|
|
481
|
+
models.sort()
|
|
482
|
+
if models:
|
|
483
|
+
available[provider] = models
|
|
484
|
+
|
|
485
|
+
# Merge in models from config file (custom providers like ollama, fireworks)
|
|
486
|
+
for provider_name, provider_config in config.providers.items():
|
|
487
|
+
# Respect enabled = false (hide provider entirely).
|
|
488
|
+
if not config.is_provider_enabled(provider_name):
|
|
489
|
+
logger.debug(
|
|
490
|
+
"Provider '%s' is disabled in config; skipping",
|
|
491
|
+
provider_name,
|
|
492
|
+
)
|
|
493
|
+
continue
|
|
494
|
+
|
|
495
|
+
config_models = list(provider_config.get("models", []))
|
|
496
|
+
|
|
497
|
+
# For class_path providers not in the built-in registry, auto-discover
|
|
498
|
+
# models from the package's _profiles.py when no explicit models list.
|
|
499
|
+
if (
|
|
500
|
+
not config_models
|
|
501
|
+
and provider_name not in registry_providers
|
|
502
|
+
and provider_name not in available
|
|
503
|
+
):
|
|
504
|
+
class_path = provider_config.get("class_path", "")
|
|
505
|
+
profile_module = _profile_module_from_class_path(class_path)
|
|
506
|
+
if profile_module:
|
|
507
|
+
try:
|
|
508
|
+
profiles = _load_provider_profiles(profile_module)
|
|
509
|
+
except ImportError:
|
|
510
|
+
logger.debug(
|
|
511
|
+
"Could not import profiles from %s for class_path "
|
|
512
|
+
"provider '%s' (package may not be installed)",
|
|
513
|
+
profile_module,
|
|
514
|
+
provider_name,
|
|
515
|
+
)
|
|
516
|
+
except Exception:
|
|
517
|
+
logger.warning(
|
|
518
|
+
"Failed to load profiles from %s for class_path provider '%s'",
|
|
519
|
+
profile_module,
|
|
520
|
+
provider_name,
|
|
521
|
+
exc_info=True,
|
|
522
|
+
)
|
|
523
|
+
else:
|
|
524
|
+
config_models = sorted(
|
|
525
|
+
name
|
|
526
|
+
for name, profile in profiles.items()
|
|
527
|
+
if profile.get("tool_calling", False)
|
|
528
|
+
and profile.get("text_inputs", True) is not False
|
|
529
|
+
and profile.get("text_outputs", True) is not False
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
if provider_name not in available:
|
|
533
|
+
if config_models:
|
|
534
|
+
available[provider_name] = config_models
|
|
535
|
+
else:
|
|
536
|
+
# Append any config models not already discovered
|
|
537
|
+
existing = set(available[provider_name])
|
|
538
|
+
for model in config_models:
|
|
539
|
+
if model not in existing:
|
|
540
|
+
available[provider_name].append(model)
|
|
541
|
+
|
|
542
|
+
_available_models_cache = available
|
|
543
|
+
return available
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _build_entry(
|
|
547
|
+
base: dict[str, Any],
|
|
548
|
+
overrides: dict[str, Any],
|
|
549
|
+
cli_override: dict[str, Any] | None,
|
|
550
|
+
) -> ModelProfileEntry:
|
|
551
|
+
"""Build a profile entry by merging base, overrides, and CLI override.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
base: Upstream profile dict (empty for config-only models).
|
|
555
|
+
overrides: `config.toml` profile overrides.
|
|
556
|
+
cli_override: Extra fields from `--profile-override`.
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
Profile entry with merged data and override tracking.
|
|
560
|
+
"""
|
|
561
|
+
merged = {**base, **overrides}
|
|
562
|
+
overridden_keys = set(overrides)
|
|
563
|
+
if cli_override:
|
|
564
|
+
merged = {**merged, **cli_override}
|
|
565
|
+
overridden_keys |= set(cli_override)
|
|
566
|
+
return ModelProfileEntry(
|
|
567
|
+
profile=merged,
|
|
568
|
+
overridden_keys=frozenset(overridden_keys),
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def get_model_profiles(
|
|
573
|
+
*,
|
|
574
|
+
cli_override: dict[str, Any] | None = None,
|
|
575
|
+
) -> Mapping[str, ModelProfileEntry]:
|
|
576
|
+
"""Load upstream profiles merged with config.toml overrides.
|
|
577
|
+
|
|
578
|
+
Keyed by `provider:model` spec string. Each entry contains the
|
|
579
|
+
merged profile dict and the set of keys overridden by config.toml.
|
|
580
|
+
|
|
581
|
+
Unlike `get_available_models()`, this includes all models from upstream
|
|
582
|
+
profiles regardless of capability filters (tool calling, text I/O).
|
|
583
|
+
|
|
584
|
+
Results are cached; use `clear_caches()` to reset. When `cli_override` is
|
|
585
|
+
provided the result is stored in a single-slot cache keyed by
|
|
586
|
+
`id(cli_override)`. This relies on the caller retaining the same dict
|
|
587
|
+
object for the session (the CLI stores it once on the app instance);
|
|
588
|
+
passing a different dict with the same contents will bypass the cache
|
|
589
|
+
and overwrite the previous entry.
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
cli_override: Extra profile fields from `--profile-override`.
|
|
593
|
+
|
|
594
|
+
When provided, these are merged on top of every profile entry
|
|
595
|
+
(after upstream + config.toml) and their keys are added to
|
|
596
|
+
`overridden_keys`.
|
|
597
|
+
|
|
598
|
+
Returns:
|
|
599
|
+
Read-only mapping of spec strings to profile entries.
|
|
600
|
+
"""
|
|
601
|
+
global _profiles_cache, _profiles_override_cache # noqa: PLW0603 # Module-level caches require global statement
|
|
602
|
+
if cli_override is None and _profiles_cache is not None:
|
|
603
|
+
return _profiles_cache
|
|
604
|
+
if cli_override is not None and _profiles_override_cache is not None:
|
|
605
|
+
cached_id, cached_result = _profiles_override_cache
|
|
606
|
+
if cached_id == id(cli_override):
|
|
607
|
+
return cached_result
|
|
608
|
+
|
|
609
|
+
result: dict[str, ModelProfileEntry] = {}
|
|
610
|
+
config = ModelConfig.load()
|
|
611
|
+
|
|
612
|
+
# Collect upstream profiles from provider packages.
|
|
613
|
+
seen_specs: set[str] = set()
|
|
614
|
+
provider_modules = _get_provider_profile_modules()
|
|
615
|
+
registry_providers: set[str] = set()
|
|
616
|
+
for provider, module_path in provider_modules:
|
|
617
|
+
registry_providers.add(provider)
|
|
618
|
+
# Skip providers explicitly disabled in config.
|
|
619
|
+
if not config.is_provider_enabled(provider):
|
|
620
|
+
logger.debug(
|
|
621
|
+
"Provider '%s' is disabled in config; skipping profiles",
|
|
622
|
+
provider,
|
|
623
|
+
)
|
|
624
|
+
continue
|
|
625
|
+
try:
|
|
626
|
+
profiles = _load_provider_profiles(module_path)
|
|
627
|
+
except ImportError:
|
|
628
|
+
logger.debug(
|
|
629
|
+
"Could not import profiles from %s for provider '%s'",
|
|
630
|
+
module_path,
|
|
631
|
+
provider,
|
|
632
|
+
)
|
|
633
|
+
continue
|
|
634
|
+
except Exception:
|
|
635
|
+
logger.warning(
|
|
636
|
+
"Failed to load profiles from %s for provider '%s'",
|
|
637
|
+
module_path,
|
|
638
|
+
provider,
|
|
639
|
+
exc_info=True,
|
|
640
|
+
)
|
|
641
|
+
continue
|
|
642
|
+
|
|
643
|
+
for model_name, upstream_profile in profiles.items():
|
|
644
|
+
spec = f"{provider}:{model_name}"
|
|
645
|
+
seen_specs.add(spec)
|
|
646
|
+
overrides = config.get_profile_overrides(provider, model_name=model_name)
|
|
647
|
+
result[spec] = _build_entry(upstream_profile, overrides, cli_override)
|
|
648
|
+
|
|
649
|
+
# Add config-only models and class_path provider profiles.
|
|
650
|
+
for provider_name, provider_config in config.providers.items():
|
|
651
|
+
if not config.is_provider_enabled(provider_name):
|
|
652
|
+
logger.debug(
|
|
653
|
+
"Provider '%s' is disabled in config; skipping profiles",
|
|
654
|
+
provider_name,
|
|
655
|
+
)
|
|
656
|
+
continue
|
|
657
|
+
# For class_path providers not in the built-in registry, load
|
|
658
|
+
# upstream profiles from the package's _profiles.py.
|
|
659
|
+
if provider_name not in registry_providers:
|
|
660
|
+
class_path = provider_config.get("class_path", "")
|
|
661
|
+
profile_module = _profile_module_from_class_path(class_path)
|
|
662
|
+
if profile_module:
|
|
663
|
+
try:
|
|
664
|
+
pkg_profiles = _load_provider_profiles(profile_module)
|
|
665
|
+
except ImportError:
|
|
666
|
+
logger.debug(
|
|
667
|
+
"Could not import profiles from %s for class_path "
|
|
668
|
+
"provider '%s' (package may not be installed)",
|
|
669
|
+
profile_module,
|
|
670
|
+
provider_name,
|
|
671
|
+
)
|
|
672
|
+
except Exception:
|
|
673
|
+
logger.warning(
|
|
674
|
+
"Failed to load profiles from %s for class_path provider '%s'",
|
|
675
|
+
profile_module,
|
|
676
|
+
provider_name,
|
|
677
|
+
exc_info=True,
|
|
678
|
+
)
|
|
679
|
+
else:
|
|
680
|
+
for model_name, upstream_profile in pkg_profiles.items():
|
|
681
|
+
spec = f"{provider_name}:{model_name}"
|
|
682
|
+
seen_specs.add(spec)
|
|
683
|
+
overrides = config.get_profile_overrides(
|
|
684
|
+
provider_name, model_name=model_name
|
|
685
|
+
)
|
|
686
|
+
result[spec] = _build_entry(
|
|
687
|
+
upstream_profile, overrides, cli_override
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
config_models = provider_config.get("models", [])
|
|
691
|
+
for model_name in config_models:
|
|
692
|
+
spec = f"{provider_name}:{model_name}"
|
|
693
|
+
if spec not in seen_specs:
|
|
694
|
+
overrides = config.get_profile_overrides(
|
|
695
|
+
provider_name, model_name=model_name
|
|
696
|
+
)
|
|
697
|
+
result[spec] = _build_entry({}, overrides, cli_override)
|
|
698
|
+
|
|
699
|
+
frozen = MappingProxyType(result)
|
|
700
|
+
if cli_override is None:
|
|
701
|
+
_profiles_cache = frozen
|
|
702
|
+
else:
|
|
703
|
+
_profiles_override_cache = (id(cli_override), frozen)
|
|
704
|
+
return frozen
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def has_provider_credentials(provider: str) -> bool | None:
|
|
708
|
+
"""Check if credentials are available for a provider.
|
|
709
|
+
|
|
710
|
+
Resolution order:
|
|
711
|
+
|
|
712
|
+
1. Config-file providers (`config.toml`) with `api_key_env` — takes
|
|
713
|
+
priority so user overrides are respected.
|
|
714
|
+
2. Config-file providers with `class_path` but no `api_key_env` —
|
|
715
|
+
assumed to manage their own auth (e.g., custom headers, JWT, mTLS).
|
|
716
|
+
3. Hardcoded `PROVIDER_API_KEY_ENV` mapping (anthropic, openai, etc.).
|
|
717
|
+
4. For any other provider (e.g., third-party langchain provider
|
|
718
|
+
packages), credential status is unknown — the provider itself will
|
|
719
|
+
report auth failures at model-creation time.
|
|
720
|
+
|
|
721
|
+
Args:
|
|
722
|
+
provider: Provider name.
|
|
723
|
+
|
|
724
|
+
Returns:
|
|
725
|
+
True if credentials are confirmed available or the provider is
|
|
726
|
+
expected to manage its own auth (e.g., `class_path` providers),
|
|
727
|
+
False if confirmed missing, or None if credential status cannot
|
|
728
|
+
be determined.
|
|
729
|
+
"""
|
|
730
|
+
# Config-file providers take priority when api_key_env is specified.
|
|
731
|
+
config = ModelConfig.load()
|
|
732
|
+
provider_config = config.providers.get(provider)
|
|
733
|
+
if provider_config:
|
|
734
|
+
result = config.has_credentials(provider)
|
|
735
|
+
if result is not None:
|
|
736
|
+
return result
|
|
737
|
+
# class_path providers that omit api_key_env manage their own auth
|
|
738
|
+
# (e.g., custom headers, JWT, mTLS) — treat as available.
|
|
739
|
+
if provider_config.get("class_path"):
|
|
740
|
+
return True
|
|
741
|
+
# No api_key_env in config — fall through to hardcoded map.
|
|
742
|
+
|
|
743
|
+
# Fall back to hardcoded well-known providers.
|
|
744
|
+
env_var = PROVIDER_API_KEY_ENV.get(provider)
|
|
745
|
+
if env_var:
|
|
746
|
+
return bool(resolve_env_var(env_var))
|
|
747
|
+
|
|
748
|
+
# Provider not found in config or hardcoded map — credential status is
|
|
749
|
+
# unknown. The provider itself will report auth failures at
|
|
750
|
+
# model-creation time.
|
|
751
|
+
logger.debug(
|
|
752
|
+
"No credential information for provider '%s'; deferring auth to provider",
|
|
753
|
+
provider,
|
|
754
|
+
)
|
|
755
|
+
return None
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def get_credential_env_var(provider: str) -> str | None:
|
|
759
|
+
"""Return the env var name that holds credentials for a provider.
|
|
760
|
+
|
|
761
|
+
Checks the config file first (user override), then falls back to the
|
|
762
|
+
hardcoded `PROVIDER_API_KEY_ENV` map.
|
|
763
|
+
|
|
764
|
+
Args:
|
|
765
|
+
provider: Provider name.
|
|
766
|
+
|
|
767
|
+
Returns:
|
|
768
|
+
Environment variable name, or None if unknown.
|
|
769
|
+
"""
|
|
770
|
+
config = ModelConfig.load()
|
|
771
|
+
config_env = config.get_api_key_env(provider)
|
|
772
|
+
if config_env:
|
|
773
|
+
return config_env
|
|
774
|
+
return PROVIDER_API_KEY_ENV.get(provider)
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
@dataclass(frozen=True)
|
|
778
|
+
class ModelConfig:
|
|
779
|
+
"""Parsed model configuration from `config.toml`.
|
|
780
|
+
|
|
781
|
+
Instances are immutable once constructed. The `providers` mapping is
|
|
782
|
+
wrapped in `MappingProxyType` to prevent accidental mutation of the
|
|
783
|
+
globally cached singleton returned by `load()`.
|
|
784
|
+
"""
|
|
785
|
+
|
|
786
|
+
default_model: str | None = None
|
|
787
|
+
"""The user's intentional default model (from config file `[models].default`)."""
|
|
788
|
+
|
|
789
|
+
recent_model: str | None = None
|
|
790
|
+
"""The most recently switched-to model (from config file `[models].recent`)."""
|
|
791
|
+
|
|
792
|
+
providers: Mapping[str, ProviderConfig] = field(default_factory=dict)
|
|
793
|
+
"""Read-only mapping of provider names to their configurations."""
|
|
794
|
+
|
|
795
|
+
def __post_init__(self) -> None:
|
|
796
|
+
"""Freeze the providers dict into a read-only proxy."""
|
|
797
|
+
if not isinstance(self.providers, MappingProxyType):
|
|
798
|
+
object.__setattr__(self, "providers", MappingProxyType(self.providers))
|
|
799
|
+
|
|
800
|
+
@classmethod
|
|
801
|
+
def load(cls, config_path: Path | None = None) -> ModelConfig:
|
|
802
|
+
"""Load config from file.
|
|
803
|
+
|
|
804
|
+
When called with the default path, results are cached for the
|
|
805
|
+
lifetime of the process. Use `clear_caches()` to reset.
|
|
806
|
+
|
|
807
|
+
Args:
|
|
808
|
+
config_path: Path to config file. Defaults to ~/.docagent/config.toml.
|
|
809
|
+
|
|
810
|
+
Returns:
|
|
811
|
+
Parsed `ModelConfig` instance.
|
|
812
|
+
Returns empty config if file is missing, unreadable, or contains
|
|
813
|
+
invalid TOML syntax.
|
|
814
|
+
"""
|
|
815
|
+
global _default_config_cache # noqa: PLW0603 # Module-level cache requires global statement
|
|
816
|
+
is_default = config_path is None
|
|
817
|
+
if is_default and _default_config_cache is not None:
|
|
818
|
+
return _default_config_cache
|
|
819
|
+
|
|
820
|
+
if config_path is None:
|
|
821
|
+
config_path = DEFAULT_CONFIG_PATH
|
|
822
|
+
|
|
823
|
+
if not config_path.exists():
|
|
824
|
+
fallback = cls()
|
|
825
|
+
if is_default:
|
|
826
|
+
_default_config_cache = fallback
|
|
827
|
+
return fallback
|
|
828
|
+
|
|
829
|
+
try:
|
|
830
|
+
with config_path.open("rb") as f:
|
|
831
|
+
data = tomllib.load(f)
|
|
832
|
+
except tomllib.TOMLDecodeError as e:
|
|
833
|
+
logger.warning(
|
|
834
|
+
"Config file %s has invalid TOML syntax: %s. "
|
|
835
|
+
"Ignoring config file. Fix the file or delete it to reset.",
|
|
836
|
+
config_path,
|
|
837
|
+
e,
|
|
838
|
+
)
|
|
839
|
+
fallback = cls()
|
|
840
|
+
if is_default:
|
|
841
|
+
_default_config_cache = fallback
|
|
842
|
+
return fallback
|
|
843
|
+
except (PermissionError, OSError) as e:
|
|
844
|
+
logger.warning("Could not read config file %s: %s", config_path, e)
|
|
845
|
+
fallback = cls()
|
|
846
|
+
if is_default:
|
|
847
|
+
_default_config_cache = fallback
|
|
848
|
+
return fallback
|
|
849
|
+
|
|
850
|
+
models_section = data.get("models", {})
|
|
851
|
+
config = cls(
|
|
852
|
+
default_model=models_section.get("default"),
|
|
853
|
+
recent_model=models_section.get("recent"),
|
|
854
|
+
providers=models_section.get("providers", {}),
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
# Validate config consistency
|
|
858
|
+
config._validate()
|
|
859
|
+
|
|
860
|
+
if is_default:
|
|
861
|
+
_default_config_cache = config
|
|
862
|
+
|
|
863
|
+
return config
|
|
864
|
+
|
|
865
|
+
def _validate(self) -> None:
|
|
866
|
+
"""Validate internal consistency of the config.
|
|
867
|
+
|
|
868
|
+
Issues warnings for invalid configurations but does not raise exceptions,
|
|
869
|
+
allowing the app to continue with potentially degraded functionality.
|
|
870
|
+
"""
|
|
871
|
+
# Warn if default_model is set but doesn't use provider:model format
|
|
872
|
+
if self.default_model and ":" not in self.default_model:
|
|
873
|
+
logger.warning(
|
|
874
|
+
"default_model '%s' should use provider:model format "
|
|
875
|
+
"(e.g., 'anthropic:claude-sonnet-4-5')",
|
|
876
|
+
self.default_model,
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
# Warn if recent_model is set but doesn't use provider:model format
|
|
880
|
+
if self.recent_model and ":" not in self.recent_model:
|
|
881
|
+
logger.warning(
|
|
882
|
+
"recent_model '%s' should use provider:model format "
|
|
883
|
+
"(e.g., 'anthropic:claude-sonnet-4-5')",
|
|
884
|
+
self.recent_model,
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
# Validate enabled field type and class_path format / params references
|
|
888
|
+
for name, provider in self.providers.items():
|
|
889
|
+
enabled = provider.get("enabled")
|
|
890
|
+
if enabled is not None and not isinstance(enabled, bool):
|
|
891
|
+
logger.warning(
|
|
892
|
+
"Provider '%s' has non-boolean 'enabled' value %r "
|
|
893
|
+
"(expected true/false). Provider will remain visible.",
|
|
894
|
+
name,
|
|
895
|
+
enabled,
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
class_path = provider.get("class_path")
|
|
899
|
+
if class_path and ":" not in class_path:
|
|
900
|
+
logger.warning(
|
|
901
|
+
"Provider '%s' has invalid class_path '%s': "
|
|
902
|
+
"must be in module.path:ClassName format "
|
|
903
|
+
"(e.g., 'my_package.models:MyChatModel')",
|
|
904
|
+
name,
|
|
905
|
+
class_path,
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
models = set(provider.get("models", []))
|
|
909
|
+
|
|
910
|
+
params = provider.get("params", {})
|
|
911
|
+
for key, value in params.items():
|
|
912
|
+
if isinstance(value, dict) and key not in models:
|
|
913
|
+
logger.warning(
|
|
914
|
+
"Provider '%s' has params for '%s' "
|
|
915
|
+
"which is not in its models list",
|
|
916
|
+
name,
|
|
917
|
+
key,
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
def is_provider_enabled(self, provider_name: str) -> bool:
|
|
921
|
+
"""Check whether a provider should appear in the model switcher.
|
|
922
|
+
|
|
923
|
+
A provider is disabled when its config explicitly sets
|
|
924
|
+
`enabled = false`. Providers not present in the config file are
|
|
925
|
+
always considered enabled.
|
|
926
|
+
|
|
927
|
+
Args:
|
|
928
|
+
provider_name: The provider to check.
|
|
929
|
+
|
|
930
|
+
Returns:
|
|
931
|
+
`False` if the provider is explicitly disabled, `True` otherwise.
|
|
932
|
+
"""
|
|
933
|
+
provider = self.providers.get(provider_name)
|
|
934
|
+
if not provider:
|
|
935
|
+
return True
|
|
936
|
+
return provider.get("enabled") is not False
|
|
937
|
+
|
|
938
|
+
def get_all_models(self) -> list[tuple[str, str]]:
|
|
939
|
+
"""Get all models as `(model_name, provider_name)` tuples.
|
|
940
|
+
|
|
941
|
+
Returns raw config data — does not filter by `is_provider_enabled`.
|
|
942
|
+
For the filtered set shown in the model switcher, use
|
|
943
|
+
`get_available_models()`.
|
|
944
|
+
|
|
945
|
+
Returns:
|
|
946
|
+
List of tuples containing `(model_name, provider_name)`.
|
|
947
|
+
"""
|
|
948
|
+
return [
|
|
949
|
+
(model, provider_name)
|
|
950
|
+
for provider_name, provider_config in self.providers.items()
|
|
951
|
+
for model in provider_config.get("models", [])
|
|
952
|
+
]
|
|
953
|
+
|
|
954
|
+
def get_provider_for_model(self, model_name: str) -> str | None:
|
|
955
|
+
"""Find the provider that contains this model.
|
|
956
|
+
|
|
957
|
+
Returns raw config data — does not filter by `is_provider_enabled`.
|
|
958
|
+
|
|
959
|
+
Args:
|
|
960
|
+
model_name: The model identifier to look up.
|
|
961
|
+
|
|
962
|
+
Returns:
|
|
963
|
+
Provider name if found, None otherwise.
|
|
964
|
+
"""
|
|
965
|
+
for provider_name, provider_config in self.providers.items():
|
|
966
|
+
if model_name in provider_config.get("models", []):
|
|
967
|
+
return provider_name
|
|
968
|
+
return None
|
|
969
|
+
|
|
970
|
+
def has_credentials(self, provider_name: str) -> bool | None:
|
|
971
|
+
"""Check if credentials are available for a provider.
|
|
972
|
+
|
|
973
|
+
This is the config-file-driven credential check, supporting custom
|
|
974
|
+
providers (e.g., local Ollama with no key required). For the hardcoded
|
|
975
|
+
`PROVIDER_API_KEY_ENV`-based check used in the hot-swap path, see the
|
|
976
|
+
module-level `has_provider_credentials()`.
|
|
977
|
+
|
|
978
|
+
Args:
|
|
979
|
+
provider_name: The provider to check.
|
|
980
|
+
|
|
981
|
+
Returns:
|
|
982
|
+
True if credentials are confirmed available, False if confirmed
|
|
983
|
+
missing, or None if no `api_key_env` is configured and
|
|
984
|
+
credential status cannot be determined.
|
|
985
|
+
"""
|
|
986
|
+
provider = self.providers.get(provider_name)
|
|
987
|
+
if not provider:
|
|
988
|
+
return False
|
|
989
|
+
env_var = provider.get("api_key_env")
|
|
990
|
+
if not env_var:
|
|
991
|
+
return None # No key configured — can't verify
|
|
992
|
+
return bool(resolve_env_var(env_var))
|
|
993
|
+
|
|
994
|
+
def get_base_url(self, provider_name: str) -> str | None:
|
|
995
|
+
"""Get custom base URL.
|
|
996
|
+
|
|
997
|
+
Args:
|
|
998
|
+
provider_name: The provider to get base URL for.
|
|
999
|
+
|
|
1000
|
+
Returns:
|
|
1001
|
+
Base URL if configured, None otherwise.
|
|
1002
|
+
"""
|
|
1003
|
+
provider = self.providers.get(provider_name)
|
|
1004
|
+
return provider.get("base_url") if provider else None
|
|
1005
|
+
|
|
1006
|
+
def get_api_key_env(self, provider_name: str) -> str | None:
|
|
1007
|
+
"""Get the environment variable name for a provider's API key.
|
|
1008
|
+
|
|
1009
|
+
Args:
|
|
1010
|
+
provider_name: The provider to get API key env var for.
|
|
1011
|
+
|
|
1012
|
+
Returns:
|
|
1013
|
+
Environment variable name if configured, None otherwise.
|
|
1014
|
+
"""
|
|
1015
|
+
provider = self.providers.get(provider_name)
|
|
1016
|
+
return provider.get("api_key_env") if provider else None
|
|
1017
|
+
|
|
1018
|
+
def get_class_path(self, provider_name: str) -> str | None:
|
|
1019
|
+
"""Get the custom class path for a provider.
|
|
1020
|
+
|
|
1021
|
+
Args:
|
|
1022
|
+
provider_name: The provider to look up.
|
|
1023
|
+
|
|
1024
|
+
Returns:
|
|
1025
|
+
Class path in `module.path:ClassName` format, or None.
|
|
1026
|
+
"""
|
|
1027
|
+
provider = self.providers.get(provider_name)
|
|
1028
|
+
return provider.get("class_path") if provider else None
|
|
1029
|
+
|
|
1030
|
+
def get_kwargs(
|
|
1031
|
+
self, provider_name: str, *, model_name: str | None = None
|
|
1032
|
+
) -> dict[str, Any]:
|
|
1033
|
+
"""Get extra constructor kwargs for a provider.
|
|
1034
|
+
|
|
1035
|
+
Reads the `params` table from the provider config. Flat keys are
|
|
1036
|
+
provider-wide defaults; model-keyed sub-tables are per-model
|
|
1037
|
+
overrides that shallow-merge on top (model wins on conflict).
|
|
1038
|
+
|
|
1039
|
+
Args:
|
|
1040
|
+
provider_name: The provider to look up.
|
|
1041
|
+
model_name: Optional model name for per-model overrides.
|
|
1042
|
+
|
|
1043
|
+
Returns:
|
|
1044
|
+
Dictionary of extra kwargs (empty if none configured).
|
|
1045
|
+
"""
|
|
1046
|
+
provider = self.providers.get(provider_name)
|
|
1047
|
+
if not provider:
|
|
1048
|
+
return {}
|
|
1049
|
+
params = provider.get("params", {})
|
|
1050
|
+
result = {k: v for k, v in params.items() if not isinstance(v, dict)}
|
|
1051
|
+
if model_name:
|
|
1052
|
+
overrides = params.get(model_name)
|
|
1053
|
+
if isinstance(overrides, dict):
|
|
1054
|
+
result.update(overrides)
|
|
1055
|
+
return result
|
|
1056
|
+
|
|
1057
|
+
def get_profile_overrides(
|
|
1058
|
+
self, provider_name: str, *, model_name: str | None = None
|
|
1059
|
+
) -> dict[str, Any]:
|
|
1060
|
+
"""Get profile overrides for a provider.
|
|
1061
|
+
|
|
1062
|
+
Reads the `profile` table from the provider config. Flat keys are
|
|
1063
|
+
provider-wide defaults; model-keyed sub-tables are per-model overrides
|
|
1064
|
+
that shallow-merge on top (model wins on conflict).
|
|
1065
|
+
|
|
1066
|
+
Args:
|
|
1067
|
+
provider_name: The provider to look up.
|
|
1068
|
+
model_name: Optional model name for per-model overrides.
|
|
1069
|
+
|
|
1070
|
+
Returns:
|
|
1071
|
+
Dictionary of profile overrides (empty if none configured).
|
|
1072
|
+
"""
|
|
1073
|
+
provider = self.providers.get(provider_name)
|
|
1074
|
+
if not provider:
|
|
1075
|
+
return {}
|
|
1076
|
+
profile = provider.get("profile", {})
|
|
1077
|
+
result = {k: v for k, v in profile.items() if not isinstance(v, dict)}
|
|
1078
|
+
if model_name:
|
|
1079
|
+
overrides = profile.get(model_name)
|
|
1080
|
+
if isinstance(overrides, dict):
|
|
1081
|
+
result.update(overrides)
|
|
1082
|
+
return result
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def _save_model_field(
|
|
1086
|
+
field: str, model_spec: str, config_path: Path | None = None
|
|
1087
|
+
) -> bool:
|
|
1088
|
+
"""Read-modify-write a `[models].<field>` key in the config file.
|
|
1089
|
+
|
|
1090
|
+
Args:
|
|
1091
|
+
field: Key name under the `[models]` table (e.g., `'default'` or `'recent'`).
|
|
1092
|
+
model_spec: The model to save in `provider:model` format.
|
|
1093
|
+
config_path: Path to config file.
|
|
1094
|
+
|
|
1095
|
+
Defaults to `~/.docagent/config.toml`.
|
|
1096
|
+
|
|
1097
|
+
Returns:
|
|
1098
|
+
True if save succeeded, False if it failed due to I/O errors.
|
|
1099
|
+
"""
|
|
1100
|
+
if config_path is None:
|
|
1101
|
+
config_path = DEFAULT_CONFIG_PATH
|
|
1102
|
+
|
|
1103
|
+
try:
|
|
1104
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1105
|
+
|
|
1106
|
+
# Read existing config or start fresh
|
|
1107
|
+
if config_path.exists():
|
|
1108
|
+
with config_path.open("rb") as f:
|
|
1109
|
+
data = tomllib.load(f)
|
|
1110
|
+
else:
|
|
1111
|
+
data = {}
|
|
1112
|
+
|
|
1113
|
+
if "models" not in data:
|
|
1114
|
+
data["models"] = {}
|
|
1115
|
+
data["models"][field] = model_spec
|
|
1116
|
+
|
|
1117
|
+
# Write to temp file then rename to prevent corruption if write is interrupted
|
|
1118
|
+
fd, tmp_path = tempfile.mkstemp(dir=config_path.parent, suffix=".tmp")
|
|
1119
|
+
try:
|
|
1120
|
+
with os.fdopen(fd, "wb") as f:
|
|
1121
|
+
tomli_w.dump(data, f)
|
|
1122
|
+
Path(tmp_path).replace(config_path)
|
|
1123
|
+
except BaseException:
|
|
1124
|
+
# Clean up temp file on any failure
|
|
1125
|
+
with contextlib.suppress(OSError):
|
|
1126
|
+
Path(tmp_path).unlink()
|
|
1127
|
+
raise
|
|
1128
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
1129
|
+
logger.exception("Could not save %s model preference", field)
|
|
1130
|
+
return False
|
|
1131
|
+
else:
|
|
1132
|
+
# Invalidate config cache so the next load() picks up the change.
|
|
1133
|
+
global _default_config_cache # noqa: PLW0603 # Module-level cache requires global statement
|
|
1134
|
+
_default_config_cache = None
|
|
1135
|
+
return True
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
def save_default_model(model_spec: str, config_path: Path | None = None) -> bool:
|
|
1139
|
+
"""Update the default model in config file.
|
|
1140
|
+
|
|
1141
|
+
Reads existing config (if any), updates `[models].default`, and writes
|
|
1142
|
+
back using proper TOML serialization.
|
|
1143
|
+
|
|
1144
|
+
Args:
|
|
1145
|
+
model_spec: The model to set as default in `provider:model` format.
|
|
1146
|
+
config_path: Path to config file.
|
|
1147
|
+
|
|
1148
|
+
Defaults to `~/.docagent/config.toml`.
|
|
1149
|
+
|
|
1150
|
+
Returns:
|
|
1151
|
+
True if save succeeded, False if it failed due to I/O errors.
|
|
1152
|
+
|
|
1153
|
+
Note:
|
|
1154
|
+
This function does not preserve comments in the config file.
|
|
1155
|
+
"""
|
|
1156
|
+
return _save_model_field("default", model_spec, config_path)
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
def clear_default_model(config_path: Path | None = None) -> bool:
|
|
1160
|
+
"""Remove the default model from the config file.
|
|
1161
|
+
|
|
1162
|
+
Deletes the `[models].default` key so that future launches fall back to
|
|
1163
|
+
`[models].recent` or environment auto-detection.
|
|
1164
|
+
|
|
1165
|
+
Args:
|
|
1166
|
+
config_path: Path to config file.
|
|
1167
|
+
|
|
1168
|
+
Defaults to `~/.docagent/config.toml`.
|
|
1169
|
+
|
|
1170
|
+
Returns:
|
|
1171
|
+
True if the key was removed (or was already absent), False on I/O error.
|
|
1172
|
+
"""
|
|
1173
|
+
if config_path is None:
|
|
1174
|
+
config_path = DEFAULT_CONFIG_PATH
|
|
1175
|
+
|
|
1176
|
+
if not config_path.exists():
|
|
1177
|
+
return True # Nothing to clear
|
|
1178
|
+
|
|
1179
|
+
try:
|
|
1180
|
+
with config_path.open("rb") as f:
|
|
1181
|
+
data = tomllib.load(f)
|
|
1182
|
+
|
|
1183
|
+
models_section = data.get("models")
|
|
1184
|
+
if not isinstance(models_section, dict) or "default" not in models_section:
|
|
1185
|
+
return True # Already absent
|
|
1186
|
+
|
|
1187
|
+
del models_section["default"]
|
|
1188
|
+
|
|
1189
|
+
fd, tmp_path = tempfile.mkstemp(dir=config_path.parent, suffix=".tmp")
|
|
1190
|
+
try:
|
|
1191
|
+
with os.fdopen(fd, "wb") as f:
|
|
1192
|
+
tomli_w.dump(data, f)
|
|
1193
|
+
Path(tmp_path).replace(config_path)
|
|
1194
|
+
except BaseException:
|
|
1195
|
+
with contextlib.suppress(OSError):
|
|
1196
|
+
Path(tmp_path).unlink()
|
|
1197
|
+
raise
|
|
1198
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
1199
|
+
logger.exception("Could not clear default model preference")
|
|
1200
|
+
return False
|
|
1201
|
+
else:
|
|
1202
|
+
global _default_config_cache # noqa: PLW0603 # Module-level cache requires global statement
|
|
1203
|
+
_default_config_cache = None
|
|
1204
|
+
return True
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def is_warning_suppressed(key: str, config_path: Path | None = None) -> bool:
|
|
1208
|
+
"""Check if a warning key is suppressed in the config file.
|
|
1209
|
+
|
|
1210
|
+
Reads the `[warnings].suppress` list from `config.toml` and checks
|
|
1211
|
+
whether `key` is present.
|
|
1212
|
+
|
|
1213
|
+
Args:
|
|
1214
|
+
key: Warning identifier to check (e.g., `'ripgrep'`).
|
|
1215
|
+
config_path: Path to config file.
|
|
1216
|
+
|
|
1217
|
+
Defaults to `~/.docagent/config.toml`.
|
|
1218
|
+
|
|
1219
|
+
Returns:
|
|
1220
|
+
`True` if the warning is suppressed, `False` otherwise (including
|
|
1221
|
+
when the file is missing, unreadable, or has no
|
|
1222
|
+
`[warnings]` section).
|
|
1223
|
+
"""
|
|
1224
|
+
if config_path is None:
|
|
1225
|
+
config_path = DEFAULT_CONFIG_PATH
|
|
1226
|
+
|
|
1227
|
+
try:
|
|
1228
|
+
if not config_path.exists():
|
|
1229
|
+
return False
|
|
1230
|
+
with config_path.open("rb") as f:
|
|
1231
|
+
data = tomllib.load(f)
|
|
1232
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
1233
|
+
logger.debug(
|
|
1234
|
+
"Could not read config file %s for warning suppression check",
|
|
1235
|
+
config_path,
|
|
1236
|
+
exc_info=True,
|
|
1237
|
+
)
|
|
1238
|
+
return False
|
|
1239
|
+
|
|
1240
|
+
suppress_list = data.get("warnings", {}).get("suppress", [])
|
|
1241
|
+
if not isinstance(suppress_list, list):
|
|
1242
|
+
logger.debug(
|
|
1243
|
+
"[warnings].suppress in %s should be a list, got %s",
|
|
1244
|
+
config_path,
|
|
1245
|
+
type(suppress_list).__name__,
|
|
1246
|
+
)
|
|
1247
|
+
return False
|
|
1248
|
+
return key in suppress_list
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
def suppress_warning(key: str, config_path: Path | None = None) -> bool:
|
|
1252
|
+
"""Add a warning key to the suppression list in the config file.
|
|
1253
|
+
|
|
1254
|
+
Reads existing config (if any), adds `key` to `[warnings].suppress`,
|
|
1255
|
+
and writes back using atomic temp-file rename. Deduplicates entries.
|
|
1256
|
+
|
|
1257
|
+
Args:
|
|
1258
|
+
key: Warning identifier to suppress (e.g., `'ripgrep'`).
|
|
1259
|
+
config_path: Path to config file.
|
|
1260
|
+
|
|
1261
|
+
Defaults to `~/.docagent/config.toml`.
|
|
1262
|
+
|
|
1263
|
+
Returns:
|
|
1264
|
+
`True` if save succeeded, `False` if it failed due to I/O errors.
|
|
1265
|
+
"""
|
|
1266
|
+
if config_path is None:
|
|
1267
|
+
config_path = DEFAULT_CONFIG_PATH
|
|
1268
|
+
|
|
1269
|
+
try:
|
|
1270
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1271
|
+
|
|
1272
|
+
if config_path.exists():
|
|
1273
|
+
with config_path.open("rb") as f:
|
|
1274
|
+
data = tomllib.load(f)
|
|
1275
|
+
else:
|
|
1276
|
+
data = {}
|
|
1277
|
+
|
|
1278
|
+
if "warnings" not in data:
|
|
1279
|
+
data["warnings"] = {}
|
|
1280
|
+
suppress_list: list[str] = data["warnings"].get("suppress", [])
|
|
1281
|
+
if key not in suppress_list:
|
|
1282
|
+
suppress_list.append(key)
|
|
1283
|
+
data["warnings"]["suppress"] = suppress_list
|
|
1284
|
+
|
|
1285
|
+
fd, tmp_path = tempfile.mkstemp(dir=config_path.parent, suffix=".tmp")
|
|
1286
|
+
try:
|
|
1287
|
+
with os.fdopen(fd, "wb") as f:
|
|
1288
|
+
tomli_w.dump(data, f)
|
|
1289
|
+
Path(tmp_path).replace(config_path)
|
|
1290
|
+
except BaseException:
|
|
1291
|
+
with contextlib.suppress(OSError):
|
|
1292
|
+
Path(tmp_path).unlink()
|
|
1293
|
+
raise
|
|
1294
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
1295
|
+
logger.exception("Could not save warning suppression for '%s'", key)
|
|
1296
|
+
return False
|
|
1297
|
+
return True
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
THREAD_COLUMN_DEFAULTS: dict[str, bool] = {
|
|
1301
|
+
"thread_id": False,
|
|
1302
|
+
"messages": True,
|
|
1303
|
+
"created_at": True,
|
|
1304
|
+
"updated_at": True,
|
|
1305
|
+
"git_branch": False,
|
|
1306
|
+
"cwd": False,
|
|
1307
|
+
"initial_prompt": True,
|
|
1308
|
+
"agent_name": False,
|
|
1309
|
+
}
|
|
1310
|
+
"""Default visibility for thread selector columns."""
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
class ThreadConfig(NamedTuple):
|
|
1314
|
+
"""Coalesced thread-selector configuration read from a single TOML parse."""
|
|
1315
|
+
|
|
1316
|
+
columns: dict[str, bool]
|
|
1317
|
+
"""Column visibility settings."""
|
|
1318
|
+
|
|
1319
|
+
relative_time: bool
|
|
1320
|
+
"""Whether to display timestamps as relative time."""
|
|
1321
|
+
|
|
1322
|
+
sort_order: str
|
|
1323
|
+
"""`'updated_at'` or `'created_at'`."""
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
_thread_config_cache: ThreadConfig | None = None
|
|
1327
|
+
|
|
1328
|
+
|
|
1329
|
+
def load_thread_config(config_path: Path | None = None) -> ThreadConfig:
|
|
1330
|
+
"""Load all thread-selector settings from one config file read.
|
|
1331
|
+
|
|
1332
|
+
Returns a cached result when reading the default config path. The
|
|
1333
|
+
prewarm worker calls this at startup so subsequent opens of the
|
|
1334
|
+
`/threads` modal avoid disk I/O entirely.
|
|
1335
|
+
|
|
1336
|
+
Args:
|
|
1337
|
+
config_path: Path to config file.
|
|
1338
|
+
|
|
1339
|
+
Returns:
|
|
1340
|
+
Coalesced thread configuration.
|
|
1341
|
+
"""
|
|
1342
|
+
global _thread_config_cache # noqa: PLW0603 # Module-level cache requires global statement
|
|
1343
|
+
|
|
1344
|
+
if config_path is None:
|
|
1345
|
+
if _thread_config_cache is not None:
|
|
1346
|
+
return _thread_config_cache
|
|
1347
|
+
config_path = DEFAULT_CONFIG_PATH
|
|
1348
|
+
use_default = config_path == DEFAULT_CONFIG_PATH
|
|
1349
|
+
|
|
1350
|
+
columns = dict(THREAD_COLUMN_DEFAULTS)
|
|
1351
|
+
relative_time = True
|
|
1352
|
+
sort_order = "updated_at"
|
|
1353
|
+
|
|
1354
|
+
try:
|
|
1355
|
+
if not config_path.exists():
|
|
1356
|
+
result = ThreadConfig(columns, relative_time, sort_order)
|
|
1357
|
+
if use_default:
|
|
1358
|
+
_thread_config_cache = result
|
|
1359
|
+
return result
|
|
1360
|
+
with config_path.open("rb") as f:
|
|
1361
|
+
data = tomllib.load(f)
|
|
1362
|
+
threads_section = data.get("threads", {})
|
|
1363
|
+
|
|
1364
|
+
# columns
|
|
1365
|
+
raw_columns = threads_section.get("columns", {})
|
|
1366
|
+
if isinstance(raw_columns, dict):
|
|
1367
|
+
for key in columns:
|
|
1368
|
+
if key in raw_columns and isinstance(raw_columns[key], bool):
|
|
1369
|
+
columns[key] = raw_columns[key]
|
|
1370
|
+
|
|
1371
|
+
# relative_time
|
|
1372
|
+
rt_value = threads_section.get("relative_time")
|
|
1373
|
+
if isinstance(rt_value, bool):
|
|
1374
|
+
relative_time = rt_value
|
|
1375
|
+
|
|
1376
|
+
# sort_order
|
|
1377
|
+
so_value = threads_section.get("sort_order")
|
|
1378
|
+
if so_value in {"updated_at", "created_at"}:
|
|
1379
|
+
sort_order = so_value
|
|
1380
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
1381
|
+
logger.warning("Could not read thread config; using defaults", exc_info=True)
|
|
1382
|
+
# Do not cache on error — allow retry on next call in case the
|
|
1383
|
+
# file is fixed or permissions are restored.
|
|
1384
|
+
return ThreadConfig(columns, relative_time, sort_order)
|
|
1385
|
+
|
|
1386
|
+
result = ThreadConfig(columns, relative_time, sort_order)
|
|
1387
|
+
if use_default:
|
|
1388
|
+
_thread_config_cache = result
|
|
1389
|
+
return result
|
|
1390
|
+
|
|
1391
|
+
|
|
1392
|
+
def invalidate_thread_config_cache() -> None:
|
|
1393
|
+
"""Clear the cached `ThreadConfig` so the next load re-reads disk."""
|
|
1394
|
+
global _thread_config_cache # noqa: PLW0603 # Module-level cache requires global statement
|
|
1395
|
+
_thread_config_cache = None
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
def load_thread_columns(config_path: Path | None = None) -> dict[str, bool]:
|
|
1399
|
+
"""Load thread column visibility from config file.
|
|
1400
|
+
|
|
1401
|
+
Args:
|
|
1402
|
+
config_path: Path to config file.
|
|
1403
|
+
|
|
1404
|
+
Returns:
|
|
1405
|
+
Dict mapping column names to visibility booleans.
|
|
1406
|
+
"""
|
|
1407
|
+
if config_path is None:
|
|
1408
|
+
config_path = DEFAULT_CONFIG_PATH
|
|
1409
|
+
|
|
1410
|
+
result = dict(THREAD_COLUMN_DEFAULTS)
|
|
1411
|
+
try:
|
|
1412
|
+
if not config_path.exists():
|
|
1413
|
+
return result
|
|
1414
|
+
with config_path.open("rb") as f:
|
|
1415
|
+
data = tomllib.load(f)
|
|
1416
|
+
columns = data.get("threads", {}).get("columns", {})
|
|
1417
|
+
if isinstance(columns, dict):
|
|
1418
|
+
for key in result:
|
|
1419
|
+
if key in columns and isinstance(columns[key], bool):
|
|
1420
|
+
result[key] = columns[key]
|
|
1421
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
1422
|
+
logger.debug("Could not read thread column config", exc_info=True)
|
|
1423
|
+
return result
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
def save_thread_columns(
|
|
1427
|
+
columns: dict[str, bool], config_path: Path | None = None
|
|
1428
|
+
) -> bool:
|
|
1429
|
+
"""Save thread column visibility to config file.
|
|
1430
|
+
|
|
1431
|
+
Args:
|
|
1432
|
+
columns: Dict mapping column names to visibility booleans.
|
|
1433
|
+
config_path: Path to config file.
|
|
1434
|
+
|
|
1435
|
+
Returns:
|
|
1436
|
+
True if save succeeded, False on I/O error.
|
|
1437
|
+
"""
|
|
1438
|
+
if config_path is None:
|
|
1439
|
+
config_path = DEFAULT_CONFIG_PATH
|
|
1440
|
+
|
|
1441
|
+
try:
|
|
1442
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1443
|
+
|
|
1444
|
+
if config_path.exists():
|
|
1445
|
+
with config_path.open("rb") as f:
|
|
1446
|
+
data = tomllib.load(f)
|
|
1447
|
+
else:
|
|
1448
|
+
data = {}
|
|
1449
|
+
|
|
1450
|
+
if "threads" not in data:
|
|
1451
|
+
data["threads"] = {}
|
|
1452
|
+
data["threads"]["columns"] = columns
|
|
1453
|
+
|
|
1454
|
+
fd, tmp_path = tempfile.mkstemp(dir=config_path.parent, suffix=".tmp")
|
|
1455
|
+
try:
|
|
1456
|
+
with os.fdopen(fd, "wb") as f:
|
|
1457
|
+
tomli_w.dump(data, f)
|
|
1458
|
+
Path(tmp_path).replace(config_path)
|
|
1459
|
+
except BaseException:
|
|
1460
|
+
with contextlib.suppress(OSError):
|
|
1461
|
+
Path(tmp_path).unlink()
|
|
1462
|
+
raise
|
|
1463
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
1464
|
+
logger.exception("Could not save thread column preferences")
|
|
1465
|
+
return False
|
|
1466
|
+
invalidate_thread_config_cache()
|
|
1467
|
+
return True
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
def load_thread_relative_time(config_path: Path | None = None) -> bool:
|
|
1471
|
+
"""Load the relative-time display preference for thread timestamps.
|
|
1472
|
+
|
|
1473
|
+
Args:
|
|
1474
|
+
config_path: Path to config file.
|
|
1475
|
+
|
|
1476
|
+
Returns:
|
|
1477
|
+
True if timestamps should display as relative time.
|
|
1478
|
+
"""
|
|
1479
|
+
if config_path is None:
|
|
1480
|
+
config_path = DEFAULT_CONFIG_PATH
|
|
1481
|
+
try:
|
|
1482
|
+
if not config_path.exists():
|
|
1483
|
+
return True
|
|
1484
|
+
with config_path.open("rb") as f:
|
|
1485
|
+
data = tomllib.load(f)
|
|
1486
|
+
value = data.get("threads", {}).get("relative_time")
|
|
1487
|
+
if isinstance(value, bool):
|
|
1488
|
+
return value
|
|
1489
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
1490
|
+
logger.debug("Could not read thread relative_time config", exc_info=True)
|
|
1491
|
+
return True
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
def save_thread_relative_time(enabled: bool, config_path: Path | None = None) -> bool:
|
|
1495
|
+
"""Save the relative-time display preference for thread timestamps.
|
|
1496
|
+
|
|
1497
|
+
Args:
|
|
1498
|
+
enabled: Whether to display relative timestamps.
|
|
1499
|
+
config_path: Path to config file.
|
|
1500
|
+
|
|
1501
|
+
Returns:
|
|
1502
|
+
True if save succeeded, False on I/O error.
|
|
1503
|
+
"""
|
|
1504
|
+
if config_path is None:
|
|
1505
|
+
config_path = DEFAULT_CONFIG_PATH
|
|
1506
|
+
try:
|
|
1507
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1508
|
+
if config_path.exists():
|
|
1509
|
+
with config_path.open("rb") as f:
|
|
1510
|
+
data = tomllib.load(f)
|
|
1511
|
+
else:
|
|
1512
|
+
data = {}
|
|
1513
|
+
if "threads" not in data:
|
|
1514
|
+
data["threads"] = {}
|
|
1515
|
+
data["threads"]["relative_time"] = enabled
|
|
1516
|
+
fd, tmp_path = tempfile.mkstemp(dir=config_path.parent, suffix=".tmp")
|
|
1517
|
+
try:
|
|
1518
|
+
with os.fdopen(fd, "wb") as f:
|
|
1519
|
+
tomli_w.dump(data, f)
|
|
1520
|
+
Path(tmp_path).replace(config_path)
|
|
1521
|
+
except BaseException:
|
|
1522
|
+
with contextlib.suppress(OSError):
|
|
1523
|
+
Path(tmp_path).unlink()
|
|
1524
|
+
raise
|
|
1525
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
1526
|
+
logger.exception("Could not save thread relative_time preference")
|
|
1527
|
+
return False
|
|
1528
|
+
invalidate_thread_config_cache()
|
|
1529
|
+
return True
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
def load_thread_sort_order(config_path: Path | None = None) -> str:
|
|
1533
|
+
"""Load the sort order preference for the thread selector.
|
|
1534
|
+
|
|
1535
|
+
Args:
|
|
1536
|
+
config_path: Path to config file.
|
|
1537
|
+
|
|
1538
|
+
Returns:
|
|
1539
|
+
`"updated_at"` or `"created_at"`.
|
|
1540
|
+
"""
|
|
1541
|
+
if config_path is None:
|
|
1542
|
+
config_path = DEFAULT_CONFIG_PATH
|
|
1543
|
+
try:
|
|
1544
|
+
if not config_path.exists():
|
|
1545
|
+
return "updated_at"
|
|
1546
|
+
with config_path.open("rb") as f:
|
|
1547
|
+
data = tomllib.load(f)
|
|
1548
|
+
value = data.get("threads", {}).get("sort_order")
|
|
1549
|
+
if value in {"updated_at", "created_at"}:
|
|
1550
|
+
return value
|
|
1551
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
1552
|
+
logger.debug("Could not read thread sort_order config", exc_info=True)
|
|
1553
|
+
return "updated_at"
|
|
1554
|
+
|
|
1555
|
+
|
|
1556
|
+
def save_thread_sort_order(sort_order: str, config_path: Path | None = None) -> bool:
|
|
1557
|
+
"""Save the sort order preference for the thread selector.
|
|
1558
|
+
|
|
1559
|
+
Args:
|
|
1560
|
+
sort_order: `"updated_at"` or `"created_at"`.
|
|
1561
|
+
config_path: Path to config file.
|
|
1562
|
+
|
|
1563
|
+
Returns:
|
|
1564
|
+
True if save succeeded, False on I/O error.
|
|
1565
|
+
|
|
1566
|
+
Raises:
|
|
1567
|
+
ValueError: If `sort_order` is not a recognised value.
|
|
1568
|
+
"""
|
|
1569
|
+
if sort_order not in {"updated_at", "created_at"}:
|
|
1570
|
+
msg = (
|
|
1571
|
+
f"Invalid sort_order {sort_order!r}; expected 'updated_at' or 'created_at'"
|
|
1572
|
+
)
|
|
1573
|
+
raise ValueError(msg)
|
|
1574
|
+
if config_path is None:
|
|
1575
|
+
config_path = DEFAULT_CONFIG_PATH
|
|
1576
|
+
try:
|
|
1577
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1578
|
+
if config_path.exists():
|
|
1579
|
+
with config_path.open("rb") as f:
|
|
1580
|
+
data = tomllib.load(f)
|
|
1581
|
+
else:
|
|
1582
|
+
data = {}
|
|
1583
|
+
if "threads" not in data:
|
|
1584
|
+
data["threads"] = {}
|
|
1585
|
+
data["threads"]["sort_order"] = sort_order
|
|
1586
|
+
fd, tmp_path = tempfile.mkstemp(dir=config_path.parent, suffix=".tmp")
|
|
1587
|
+
try:
|
|
1588
|
+
with os.fdopen(fd, "wb") as f:
|
|
1589
|
+
tomli_w.dump(data, f)
|
|
1590
|
+
Path(tmp_path).replace(config_path)
|
|
1591
|
+
except Exception:
|
|
1592
|
+
with contextlib.suppress(OSError):
|
|
1593
|
+
Path(tmp_path).unlink()
|
|
1594
|
+
raise
|
|
1595
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
1596
|
+
logger.exception("Could not save thread sort_order preference")
|
|
1597
|
+
return False
|
|
1598
|
+
invalidate_thread_config_cache()
|
|
1599
|
+
return True
|
|
1600
|
+
|
|
1601
|
+
|
|
1602
|
+
def save_recent_model(model_spec: str, config_path: Path | None = None) -> bool:
|
|
1603
|
+
"""Update the recently used model in config file.
|
|
1604
|
+
|
|
1605
|
+
Writes to `[models].recent` instead of `[models].default`, so that `/model`
|
|
1606
|
+
switches do not overwrite the user's intentional default.
|
|
1607
|
+
|
|
1608
|
+
Args:
|
|
1609
|
+
model_spec: The model to save in `provider:model` format.
|
|
1610
|
+
config_path: Path to config file.
|
|
1611
|
+
|
|
1612
|
+
Defaults to `~/.docagent/config.toml`.
|
|
1613
|
+
|
|
1614
|
+
Returns:
|
|
1615
|
+
True if save succeeded, False if it failed due to I/O errors.
|
|
1616
|
+
|
|
1617
|
+
Note:
|
|
1618
|
+
This function does not preserve comments in the config file.
|
|
1619
|
+
"""
|
|
1620
|
+
return _save_model_field("recent", model_spec, config_path)
|