├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── config.yml
│ ├── documentation.md
│ ├── feature_request.md
│ ├── question.md
│ ├── theme_request.md
│ └── tool_request.md
├── pull_request_template.md
└── workflows
│ ├── ci.yml
│ ├── docs.yml
│ └── publish.yml
├── .gitignore
├── .python-version
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── assets
├── demo.png
├── feature1.gif
└── logo.png
├── dasshh
├── __init__.py
├── __main__.py
├── apps
│ ├── __init__.py
│ ├── general.py
│ └── os
│ │ ├── __init__.py
│ │ ├── file_operations.py
│ │ ├── network_tools.py
│ │ ├── process_management.py
│ │ └── system_info.py
├── core
│ ├── __init__.py
│ ├── exceptions.py
│ ├── logging.py
│ ├── registry.py
│ ├── runtime.py
│ └── tools
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── decorator.py
│ │ └── function_tool.py
├── data
│ ├── __init__.py
│ ├── client.py
│ ├── models.py
│ └── session.py
└── ui
│ ├── __init__.py
│ ├── app.py
│ ├── components
│ ├── __init__.py
│ ├── chat
│ │ ├── __init__.py
│ │ ├── action.py
│ │ ├── actions_panel.py
│ │ ├── chat_input.py
│ │ ├── chat_panel.py
│ │ ├── history_item.py
│ │ ├── history_panel.py
│ │ └── message.py
│ ├── navbar.py
│ └── settings
│ │ ├── __init__.py
│ │ ├── checkbox.py
│ │ ├── dasshh_config.py
│ │ ├── model_config.py
│ │ └── settings_section.py
│ ├── events.py
│ ├── screens
│ ├── __init__.py
│ ├── help.py
│ └── main.py
│ ├── themes
│ ├── __init__.py
│ └── lime.py
│ ├── types.py
│ ├── utils.py
│ └── views
│ ├── __init__.py
│ ├── about.py
│ ├── chat.py
│ └── settings.py
├── docs
├── README.md
├── api
│ ├── data.md
│ ├── events.md
│ ├── runtime.md
│ ├── tools.md
│ └── ui.md
├── assets
│ ├── actions.png
│ ├── chat.png
│ ├── demo.png
│ ├── demo2.gif
│ ├── favicon.png
│ ├── feature1.gif
│ ├── feature2.gif
│ ├── feature3.gif
│ ├── feature4.gif
│ ├── logo.png
│ ├── sessions.png
│ ├── settings.png
│ └── tools.gif
├── contributing
│ └── contributing.md
├── getting-started
│ ├── installation.md
│ ├── introduction.md
│ └── quick-start.md
├── guide
│ ├── abilities.md
│ ├── basics.md
│ ├── configuration.md
│ ├── keybindings.md
│ ├── own-tools.md
│ └── themes.md
├── index.md
└── stylesheets
│ └── extra.css
├── mkdocs.yml
├── pyproject.toml
├── tests
├── README.md
├── __init__.py
├── conftest.py
└── unit
│ ├── core
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_logging.py
│ ├── test_registry.py
│ ├── test_runtime.py
│ └── tools
│ │ ├── __init__.py
│ │ ├── test_base.py
│ │ ├── test_decorator.py
│ │ └── test_function_tool.py
│ ├── data
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_client.py
│ ├── test_models.py
│ └── test_session.py
│ ├── test_main.py
│ └── ui
│ ├── __init__.py
│ ├── components
│ ├── __init__.py
│ ├── chat
│ │ ├── __init__.py
│ │ ├── test_action.py
│ │ └── test_message.py
│ └── test_navbar.py
│ ├── test_events.py
│ └── test_types.py
└── uv.lock
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐛 Bug Report
3 | about: Create a report to help us improve dasshh
4 | title: "[BUG] "
5 | labels: ["bug", "triage"]
6 | assignees: []
7 | ---
8 |
9 | ## 🐛 Bug Description
10 |
11 |
12 |
13 | ## 🔄 Steps to Reproduce
14 |
15 |
16 |
17 | ## ✅ Expected Behavior
18 |
19 |
20 |
21 | ## ❌ Actual Behavior
22 |
23 |
24 |
25 | ## 📱 Screenshots/Logs
26 |
27 |
28 |
29 | ## 🖥️ Environment
30 |
31 | **Please complete the following information:**
32 |
33 | - **OS**: [e.g. macOS 14.0, Ubuntu 22.04, Windows 11]
34 | - **Python Version**: [e.g. 3.13.0]
35 | - **dasshh Version**: [e.g. 0.1.3] (run `dasshh --version`)
36 | - **Installation Method**: [e.g. pip, uv, from source]
37 | - **Terminal**: [e.g. iTerm2, Terminal.app, Command Prompt, PowerShell]
38 | - **Shell**: [e.g. bash, zsh, fish, cmd]
39 |
40 | ## 🔧 Additional Context
41 |
42 |
43 |
44 | ## 🔍 Possible Solution
45 |
46 |
47 |
48 | ## 📋 Checklist
49 |
50 | Before submitting this issue, please confirm:
51 |
52 | - [ ] I have searched existing issues to make sure this isn't a duplicate
53 | - [ ] I have provided all the requested information above
54 | - [ ] I have tested this with the latest version of dasshh
55 | - [ ] I have included relevant logs/screenshots
56 | - [ ] I have checked the [documentation](https://blog.vgnshiyer.dev/dasshh) for solutions
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: 📖 Documentation
4 | url: https://blog.vgnshiyer.dev/dasshh
5 | about: Check our documentation for answers to common questions
6 | - name: 💬 Discussions
7 | url: https://github.com/vgnshiyer/dasshh/discussions
8 | about: Ask questions and discuss ideas with the community
9 | - name: 🐦 Twitter
10 | url: https://twitter.com/vgnshiyer
11 | about: Follow for updates and announcements
12 | - name: 💼 LinkedIn
13 | url: https://www.linkedin.com/in/vgnshiyer/
14 | about: Connect with the maintainer
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/documentation.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 📚 Documentation
3 | about: Report an issue with documentation or suggest improvements
4 | title: "[DOCS] "
5 | labels: ["documentation", "triage"]
6 | assignees: []
7 | ---
8 |
9 | ## 📚 Documentation Issue
10 |
11 | **What type of documentation issue is this?**
12 |
13 | - [ ] 📝 Missing documentation
14 | - [ ] ❌ Incorrect information
15 | - [ ] 🔄 Outdated content
16 | - [ ] 🧩 Unclear explanation
17 | - [ ] 🔗 Broken links
18 | - [ ] 📖 Suggest new documentation
19 | - [ ] 🌐 Translation issue
20 | - [ ] Other: [Please describe]
21 |
22 | ## 📍 Location
23 |
24 | **Where is the documentation issue located? (Provide only one)**
25 |
26 | - **Page/Section**: [e.g., README.md, Installation Guide, API Reference]
27 | - **URL**: [If applicable, provide the direct link]
28 | - **File Path**: [e.g., docs/getting-started.md, line 42]
29 |
30 | ## 🐛 Current Problem
31 |
32 |
33 |
34 | ## ✅ Suggested Improvement
35 |
36 |
37 |
38 | ## 📝 Content Suggestion
39 |
40 |
41 |
42 | ## 📝 Additional Context
43 |
44 |
45 |
46 | ## 📋 Checklist
47 |
48 | Before submitting this documentation issue, please confirm:
49 |
50 | - [ ] I have searched existing issues to make sure this isn't a duplicate
51 | - [ ] I have provided specific location information
52 | - [ ] I have clearly described the problem and suggested improvement
53 | - [ ] I have checked the latest version of the documentation
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: ✨ Feature Request
3 | about: Suggest an idea for dasshh
4 | title: "[FEATURE] "
5 | labels: ["enhancement", "triage"]
6 | assignees: []
7 | ---
8 |
9 | ## 🚀 Feature Description
10 |
11 |
12 |
13 | ## 🎯 Problem Statement
14 |
15 |
16 |
17 | ## 💡 Proposed Solution
18 |
19 |
20 |
21 | ## 🔄 Use Cases
22 |
23 |
24 |
25 |
29 |
30 | ## 🖥️ Mockups/Examples
31 |
32 |
33 |
34 |
36 |
37 | ## 🔀 Alternatives Considered
38 |
39 |
40 |
41 | ## 📊 Expected Impact
42 |
43 | **How would this feature benefit users?**
44 |
45 | - [ ] Improves user experience
46 | - [ ] Saves time
47 | - [ ] Reduces complexity
48 | - [ ] Adds new functionality
49 | - [ ] Improves performance
50 | - [ ] Other: [Please describe]
51 |
52 | ## 📝 Additional Context
53 |
54 |
55 |
56 | ## 📋 Checklist
57 |
58 | Before submitting this feature request, please confirm:
59 |
60 | - [ ] I have searched existing issues to make sure this isn't a duplicate
61 | - [ ] I have clearly described the problem this feature would solve
62 | - [ ] I have provided specific use cases
63 | - [ ] I have considered how this fits with dasshh's existing functionality
64 | - [ ] I have checked if this feature already exists in the current version
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: ❓ Question
3 | about: Ask a question or get support
4 | title: "[QUESTION] "
5 | labels: ["question", "support"]
6 | assignees: []
7 | ---
8 |
9 | ## ❓ Question
10 |
11 | **What would you like to know?**
12 |
13 |
14 |
15 | ## 🎯 Context
16 |
17 | **What are you trying to accomplish?**
18 |
19 |
20 |
21 | ## 🔍 What You've Tried
22 |
23 | **What have you already attempted?**
24 |
25 | - [ ] Searched the documentation
26 | - [ ] Searched existing issues
27 | - [ ] Tried different approaches
28 | - [ ] Asked in community forums
29 |
30 | ## 🖥️ Environment (if relevant)
31 |
32 | - **OS**: [e.g. macOS 14.0, Ubuntu 22.04, Windows 11]
33 | - **Python Version**: [e.g. 3.13.0]
34 | - **dasshh Version**: [e.g. 0.1.3]
35 | - **Installation Method**: [e.g. pip, uv, from source]
36 |
37 | ## 📝 Additional Context
38 |
39 |
40 |
41 |
42 |
43 | ## 🎯 Expected Answer
44 |
45 | **What type of answer are you looking for?**
46 |
47 | - [ ] Step-by-step instructions
48 | - [ ] Code example
49 | - [ ] Conceptual explanation
50 | - [ ] Link to documentation
51 | - [ ] Confirmation that something is/isn't possible
52 | - [ ] Best practices recommendation
53 |
54 | ## 📋 Checklist
55 |
56 | Before submitting this question, please confirm:
57 |
58 | - [ ] I have searched existing issues and discussions
59 | - [ ] I have checked the [documentation](https://blog.vgnshiyer.dev/dasshh)
60 | - [ ] I have provided sufficient context about what I'm trying to achieve
61 | - [ ] This is not a bug report (use the bug template instead)
62 | - [ ] This is not a feature request (use the feature request template instead)
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/theme_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🎨 Theme Request
3 | about: Suggest a new theme for dasshh
4 | title: "[THEME] "
5 | labels: ["theme-request", "enhancement", "triage"]
6 | assignees: []
7 | ---
8 |
9 | ## 🎨 Theme Description
10 |
11 |
12 |
13 | ## 🌈 Theme Inspiration
14 |
15 |
16 |
17 | - [ ] 🌙 Popular dark themes (e.g., Dracula, Nord, One Dark)
18 | - [ ] ☀️ Popular light themes (e.g., Solarized Light, GitHub Light)
19 | - [ ] 🎮 Gaming/retro themes (e.g., cyberpunk, neon, terminal green)
20 | - [ ] 🌿 Nature-inspired (e.g., forest, ocean, sunset)
21 | - [ ] 🎯 Brand/company colors
22 | - [ ] 🎭 Accessibility-focused (high contrast, colorblind-friendly)
23 | - [ ] 🌍 Cultural/regional themes
24 | - [ ] Other: [Please describe]
25 |
26 | ## 🎨 Color Palette
27 |
28 |
29 |
30 | ## 🖼️ Visual References
31 |
32 |
33 |
34 | ## 📋 Checklist
35 |
36 | Before submitting this theme request, please confirm:
37 |
38 | - [ ] I have searched existing issues to make sure this theme isn't already requested
39 | - [ ] I have provided a clear description of the theme concept
40 | - [ ] I have considered accessibility implications
41 | - [ ] I have included color specifications or references where possible
42 | - [ ] I have checked existing themes to avoid duplicates
43 | - [ ] This is specifically a theme request (not a general UI change)
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/tool_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🔧 Tool Request
3 | about: Suggest a new tool for dasshh
4 | title: "[TOOL] "
5 | labels: ["tool-request", "enhancement", "triage"]
6 | assignees: []
7 | ---
8 |
9 | ## 🔧 Tool Description
10 |
11 |
12 |
13 | ## 🎯 Problem Statement
14 |
15 |
16 |
17 | ## 💡 Proposed Functionality
18 |
19 |
20 |
21 | ## 🔄 Use Cases
22 |
23 |
24 |
25 |
29 |
30 | ## 🖥️ Example Usage
31 |
32 |
46 |
47 | ## 📊 Tool Category
48 |
49 | **What category does this tool belong to?**
50 |
51 | - [ ] 🖥️ System monitoring (CPU, memory, disk usage)
52 | - [ ] 📁 File operations (create, move, search files)
53 | - [ ] 🌐 Network utilities (ping, speed test, connectivity)
54 | - [ ] 🔧 Development tools (git operations, code analysis)
55 | - [ ] 📊 Data processing (CSV, JSON manipulation)
56 | - [ ] 🎨 Media tools (image processing, file conversion)
57 | - [ ] 🔒 Security tools (password generation, encryption)
58 | - [ ] 📱 Communication (email, notifications)
59 | - [ ] 🌍 Web scraping/API integration
60 | - [ ] Other: [Please describe]
61 |
62 | ## 🛠️ Implementation Ideas
63 |
64 |
65 |
66 | ## 🔀 Alternatives Considered
67 |
68 |
69 |
70 | ## 📝 Additional Context
71 |
72 |
73 |
74 | ## 📋 Checklist
75 |
76 | Before submitting this tool request, please confirm:
77 |
78 | - [ ] I have searched existing issues to make sure this tool isn't already requested
79 | - [ ] I have clearly described what the tool should do
80 | - [ ] I have provided specific use cases
81 | - [ ] I have considered implementation challenges
82 | - [ ] I have checked if similar functionality exists in other tools
83 | - [ ] This is specifically a tool request (not a general feature request)
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## 📋 Pull Request Description
2 |
3 | ### What does this PR do?
4 |
5 |
6 | ### 🔗 Related Issue(s)
7 |
8 | - Fixes #
9 | - Related to #
10 |
11 | ## 🧪 Type of Change
12 |
13 | Please select the relevant option(s):
14 |
15 | - [ ] 🐛 Bug fix (non-breaking change that fixes an issue)
16 | - [ ] ✨ New feature (non-breaking change that adds functionality)
17 | - [ ] 📚 Documentation update
18 | - [ ] 🧹 Code cleanup/refactoring
19 | - [ ] 🧪 Test additions or improvements
20 |
21 | ## 📱 Screenshots/Demo (if applicable)
22 |
23 |
24 |
25 | ## 🤝 Review Checklist for Reviewers
26 |
27 | ### Functionality
28 | - [ ] The code accomplishes what it's supposed to do
29 | - [ ] Edge cases are handled appropriately
30 | - [ ] Error handling is implemented where needed
31 |
32 | ### Code Quality
33 | - [ ] Code is readable and maintainable
34 | - [ ] No code duplication
35 | - [ ] Functions are appropriately sized
36 | - [ ] Variable names are clear and descriptive
37 |
38 | ### Testing
39 | - [ ] Tests are comprehensive and cover edge cases
40 | - [ ] Tests are well-written and maintainable
41 | - [ ] Test coverage is adequate
42 |
43 | ## 📝 Additional Notes
44 |
45 |
46 |
47 | ## 🙏 Acknowledgments
48 |
49 |
50 |
51 | ---
52 |
53 | Thank you for contributing to dasshh! 🚀
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | branches: [ main ]
6 | push:
7 | branches: [ main ]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | python-version: ["3.13"]
15 |
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v4
19 |
20 | - name: Set up Python
21 | uses: actions/setup-python@v5
22 | with:
23 | python-version: ${{ matrix.python-version }}
24 |
25 | - name: Install uv
26 | run: |
27 | curl -Ls https://astral.sh/uv/install.sh | sh
28 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH
29 |
30 | - name: Create virtual environment
31 | run: uv venv
32 |
33 | - name: Install dependencies
34 | run: |
35 | uv pip install -e ".[dev]"
36 | uv pip install build
37 |
38 | - name: Lint with ruff (if available)
39 | run: |
40 | uv pip install ruff || echo "Ruff not available, skipping linting"
41 | uv run ruff check . || echo "No ruff configuration found, skipping linting"
42 | uv run ruff format --check . || echo "No ruff configuration found, skipping linting"
43 | continue-on-error: true
44 |
45 | - name: Run tests with pytest
46 | run: |
47 | uv run pytest tests/ -v --cov=dasshh --cov-report=xml --cov-report=term-missing --cov-report=html
48 |
49 | - name: Upload coverage reports to Codecov
50 | uses: codecov/codecov-action@v5
51 | with:
52 | file: ./coverage.xml
53 | fail_ci_if_error: false
54 | token: ${{ secrets.CODECOV_TOKEN }}
55 |
56 | - name: Upload HTML coverage report
57 | uses: actions/upload-artifact@v4
58 | with:
59 | name: coverage-html-report
60 | path: htmlcov/
61 | if-no-files-found: warn
62 |
63 | - name: Test build package
64 | run: |
65 | uv run python -m build
66 |
67 | - name: Test package installation
68 | run: |
69 | uv pip install dist/*.whl
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Deploy docs
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - 'docs/**'
9 | - 'mkdocs.yml'
10 | - '.github/workflows/docs.yml'
11 |
12 | permissions:
13 | contents: write
14 |
15 | jobs:
16 | deploy:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v4
20 | - name: Set up Python
21 | uses: actions/setup-python@v5
22 | with:
23 | python-version: '3.12'
24 | - name: Install dependencies
25 | run: |
26 | python -m pip install --upgrade pip
27 | pip install mkdocs-material
28 | - name: Deploy docs
29 | run: mkdocs gh-deploy --force
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish Python package 📦 to PyPI
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | build-and-publish:
9 | runs-on: ubuntu-latest
10 | environment:
11 | name: pypi
12 | url: https://pypi.org/project/dasshh/
13 | permissions:
14 | contents: read
15 | id-token: write
16 |
17 | steps:
18 | - name: Checkout code
19 | uses: actions/checkout@v4
20 |
21 | - name: Set up Python
22 | uses: actions/setup-python@v5
23 | with:
24 | python-version: '3.x'
25 |
26 | - name: Install uv
27 | run: |
28 | curl -Ls https://astral.sh/uv/install.sh | sh
29 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH
30 |
31 | - name: Setup virtual environment and install dependencies
32 | run: |
33 | uv venv
34 | uv pip install build twine
35 |
36 | - name: Build the package
37 | run: |
38 | uv run python -m build
39 |
40 | - name: Publish package
41 | uses: pypa/gh-action-pypi-publish@release/v1
42 | with:
43 | skip-existing: true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # UV
98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | #uv.lock
102 |
103 | # poetry
104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105 | # This is especially recommended for binary packages to ensure reproducibility, and is more
106 | # commonly ignored for libraries.
107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108 | #poetry.lock
109 |
110 | # pdm
111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112 | #pdm.lock
113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114 | # in version control.
115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116 | .pdm.toml
117 | .pdm-python
118 | .pdm-build/
119 |
120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121 | __pypackages__/
122 |
123 | # Celery stuff
124 | celerybeat-schedule
125 | celerybeat.pid
126 |
127 | # SageMath parsed files
128 | *.sage.py
129 |
130 | # Environments
131 | .env
132 | .venv
133 | env/
134 | venv/
135 | ENV/
136 | env.bak/
137 | venv.bak/
138 |
139 | # Spyder project settings
140 | .spyderproject
141 | .spyproject
142 |
143 | # Rope project settings
144 | .ropeproject
145 |
146 | # mkdocs documentation
147 | /site
148 |
149 | # mypy
150 | .mypy_cache/
151 | .dmypy.json
152 | dmypy.json
153 |
154 | # Pyre type checker
155 | .pyre/
156 |
157 | # pytype static type analyzer
158 | .pytype/
159 |
160 | # Cython debug symbols
161 | cython_debug/
162 |
163 | # PyCharm
164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166 | # and can be added to the global gitignore or merged into this file. For a more nuclear
167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168 | #.idea/
169 |
170 | # Ruff stuff:
171 | .ruff_cache/
172 |
173 | # PyPI configuration file
174 | .pypirc
175 |
176 | .DS_Store
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
1 | 3.13
2 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to dasshh
2 |
3 | Thank you for your interest in contributing to dasshh! 🎉
4 |
5 | This guide will help you get started with contributing to the project.
6 |
7 | ## Table of Contents
8 |
9 | - [Code of Conduct](#code-of-conduct)
10 | - [Getting Started](#getting-started)
11 | - [Development Setup](#development-setup)
12 | - [Development Workflow](#development-workflow)
13 | - [Testing](#testing)
14 | - [Code Style](#code-style)
15 | - [Pull Request Process](#pull-request-process)
16 | - [Issue Reporting](#issue-reporting)
17 | - [Documentation](#documentation)
18 |
19 | ## Code of Conduct
20 |
21 | By participating in this project, you agree to abide by our Code of Conduct. Please be respectful and constructive in all interactions.
22 |
23 | ## Getting Started
24 |
25 | ### Prerequisites
26 |
27 | - Python 3.13 or higher
28 | - [uv](https://docs.astral.sh/uv/) package manager
29 | - Git
30 |
31 | ### Fork and Clone
32 |
33 | 1. Fork the repository on GitHub
34 | 2. Clone your fork locally:
35 | ```bash
36 | git clone https://github.com/YOUR-USERNAME/dasshh.git
37 | cd dasshh
38 | ```
39 |
40 | ## Development Setup
41 |
42 | 1. **Install uv** (if not already installed):
43 | ```bash
44 | curl -LsSf https://astral.sh/uv/install.sh | sh
45 | ```
46 |
47 | 2. **Create and activate virtual environment**:
48 | ```bash
49 | uv venv
50 | source .venv/bin/activate # On Windows: .venv\Scripts\activate
51 | ```
52 |
53 | 3. **Install development dependencies**:
54 | ```bash
55 | uv sync
56 | ```
57 |
58 | 4. **Running the application**:
59 | ```bash
60 | python -m dasshh
61 | ```
62 |
63 | ## Development Workflow
64 |
65 | ### Branch Strategy
66 |
67 | - `main` - Production-ready code
68 | - `feature/feature-name` - New features
69 | - `bugfix/issue-description` - Bug fixes
70 | - `docs/update-description` - Documentation updates
71 |
72 | ### Making Changes
73 |
74 | 1. **Create a new branch**:
75 | ```bash
76 | git checkout -b feature/your-feature-name
77 | ```
78 |
79 | 2. **Make your changes** following the project conventions
80 |
81 | 3. **Run tests locally**:
82 | ```bash
83 | pytest -v --cov=dasshh --cov-report=html
84 | ```
85 |
86 | 4. **Run linting** (if configured):
87 | ```bash
88 | uv pip install ruff
89 | ruff check .
90 | ruff format .
91 | ```
92 |
93 | 5. **Commit your changes**:
94 | ```bash
95 | git add .
96 | git commit -m "feat: add your feature description"
97 | ```
98 |
99 | ### Commit Message Convention
100 |
101 | We follow conventional commit format:
102 |
103 | - `feat:` - New features
104 | - `fix:` - Bug fixes
105 | - `docs:` - Documentation changes
106 | - `test:` - Test additions or modifications
107 | - `refactor:` - Code refactoring
108 | - `chore:` - Maintenance tasks
109 |
110 | Examples:
111 | ```
112 | feat: add new command for file analysis
113 | fix: resolve issue with terminal output formatting
114 | docs: update installation instructions
115 | test: add unit tests for config parser
116 | ```
117 |
118 | ## Testing
119 |
120 | ### Writing Tests
121 |
122 | - Place tests in the `tests/` directory
123 | - Use descriptive test names: `test_should_parse_config_when_valid_file_provided`
124 | - Follow AAA pattern: Arrange, Act, Assert
125 | - Mock external dependencies
126 |
127 | Example test structure:
128 | ```python
129 | def test_should_process_command_when_valid_input_provided():
130 | # Arrange
131 | command = "test command"
132 | expected_result = "processed"
133 |
134 | # Act
135 | result = process_command(command)
136 |
137 | # Assert
138 | assert result == expected_result
139 | ```
140 |
141 | ## Code Style
142 |
143 | ### Python Style Guidelines
144 |
145 | - Follow [PEP 8](https://pep8.org/)
146 | - Use type hints where appropriate
147 | - Write docstrings for public functions and classes
148 | - Keep functions small and focused
149 | - Use descriptive variable names
150 |
151 | ## Pull Request Process
152 |
153 | 1. **Ensure your branch is up to date**:
154 | ```bash
155 | git checkout main
156 | git pull origin main
157 | git checkout your-feature-branch
158 | git rebase main
159 | ```
160 |
161 | 2. **Push your branch**:
162 | ```bash
163 | git push origin your-feature-branch
164 | ```
165 |
166 | 3. **Create a Pull Request** on GitHub with:
167 | - Clear title and description
168 | - Reference any related issues
169 | - Include screenshots/demos if relevant
170 | - Ensure all CI checks pass
171 |
172 | 4. **Address review feedback** promptly and respectfully
173 |
174 | 5. **Squash commits** if requested before merging
175 |
176 | ## Issue Reporting
177 |
178 | When reporting issues, please include:
179 |
180 | - **Environment details**: OS, Python version, dasshh version
181 | - **Steps to reproduce** the issue
182 | - **Expected vs actual behavior**
183 | - **Error messages** or logs
184 | - **Screenshots** if relevant
185 |
186 | ## Documentation
187 |
188 | ### Building Documentation
189 |
190 | ```bash
191 | # Install documentation dependencies
192 | uv pip install mkdocs-material
193 |
194 | # Serve documentation locally
195 | mkdocs serve
196 |
197 | # Build documentation
198 | mkdocs build
199 | ```
200 |
201 | ## Getting Help
202 |
203 | - Check the [documentation](https://blog.vgnshiyer.dev/dasshh)
204 | - [Open an issue](https://github.com/vgnshiyer/dasshh/issues) for bugs
205 | - [Start a discussion](https://github.com/vgnshiyer/dasshh/discussions) for questions
206 | - Contact maintainers at vgnshiyer@gmail.com or via [LinkedIn](https://www.linkedin.com/in/vgnshiyer/)
207 |
208 | ## Recognition
209 |
210 | Contributors will be acknowledged in:
211 | - Release notes
212 | - GitHub contributors page
213 |
214 | Thank you for contributing to dasshh!
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Vignesh Iyer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # 🗲 *Dasshh* 🗲
4 |
5 | ***An AI Agent on your terminal, to preserve your brain juice.***
6 |
7 | Dasshh is a tui built with [textual](https://textual.textualize.io/) that allows you to interact with your computer using natural language.
8 |
9 |
10 |
11 |
12 |
13 | [](https://pypi.org/project/dasshh/)
14 | [](https://opensource.org/licenses/MIT)
15 | [](https://github.com/vgnshiyer/dasshh/actions/workflows/ci.yml)
16 | [](https://www.linkedin.com/comm/mynetwork/discovery-see-all?usecase=PEOPLE_FOLLOWS&followMember=vgnshiyer)
17 | [](https://www.buymeacoffee.com/vgnshiyer)
18 |
19 |
20 |
21 | **Note:** This project is still in early development. Suggestions and contributions are welcome!
22 |
23 | ## ✨ Features
24 |
25 | - Interactive & minimal chat UI
26 | - Chat with your personal assistant on your terminal
27 | - Perform actions on your computer with plain English
28 | - Extensible with your own tools
29 |
30 | ## 📦 Installation
31 |
32 | ### Using `uv` (Recommended)
33 |
34 | If you haven't tried [uv](https://github.com/astral-sh/uv) yet, it's highly recommended for fast Python package management.
35 |
36 | ```bash
37 | # Install uv on macOS
38 | brew install uv
39 |
40 | # Or using curl
41 | curl -LsSf https://astral.sh/uv/install.sh | sh
42 |
43 | # Install dasshh
44 | uv tool install dasshh
45 | ```
46 |
47 | ### Using `pipx`
48 |
49 | ```bash
50 | # Install pipx if you haven't already
51 | pip install --user pipx
52 | pipx ensurepath
53 |
54 | # Install dasshh
55 | pipx install dasshh
56 | ```
57 |
58 | ### Verify Installation
59 |
60 | ```bash
61 | dasshh --version
62 | ```
63 |
64 | ## 🚀 Quick Start
65 |
66 | ### 1. Initialize Configuration
67 |
68 | ```bash
69 | dasshh init-config
70 | ```
71 |
72 | This creates a config file at `~/.dasshh/config.yaml`.
73 |
74 | ### 2. Configure Your Model
75 |
76 | Edit the config file to set your model and API key:
77 |
78 | ```yaml
79 | model:
80 | name: gemini/gemini-2.0-flash
81 | api_key:
82 | ```
83 |
84 | > See [litellm docs](https://docs.litellm.ai/docs/providers) for all supported models and providers.
85 |
86 | ### 3. Launch Dasshh
87 |
88 | ```bash
89 | dasshh
90 | ```
91 |
92 | ### 4. Start Chatting
93 |
94 | Ask Dasshh to help with system tasks:
95 |
96 | ```
97 | • What's the current CPU usage?
98 | • Show me the top memory-intensive processes
99 | • List files in my downloads folder
100 | • Create a new directory called "projects"
101 | ```
102 |
103 | **Exit:** Press `Ctrl+C` to terminate.
104 |
105 | ## 📖 Documentation
106 |
107 | For comprehensive documentation, visit [https://vgnshiyer.github.io/dasshh/](https://vgnshiyer.github.io/dasshh/).
108 |
109 | ## 🤝 Contributing
110 |
111 | We welcome contributions! 🎉
112 |
113 | - **Read our [Contributing Guide](CONTRIBUTING.md)** for development setup and guidelines
114 | - **Report bugs** using our [issue templates](.github/ISSUE_TEMPLATE/)
115 | - **Request features** or ask questions in our [discussions](https://github.com/vgnshiyer/dasshh/discussions)
116 | - **Improve documentation** - we appreciate all help!
117 |
118 | ## 📝 License
119 |
120 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
121 |
122 |
--------------------------------------------------------------------------------
/assets/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/assets/demo.png
--------------------------------------------------------------------------------
/assets/feature1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/assets/feature1.gif
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/assets/logo.png
--------------------------------------------------------------------------------
/dasshh/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/dasshh/__init__.py
--------------------------------------------------------------------------------
/dasshh/__main__.py:
--------------------------------------------------------------------------------
1 | import click
2 | import time
3 | from rich.console import Console
4 |
5 | from dasshh.core.logging import setup_logging
6 | from dasshh.ui.utils import load_config, DEFAULT_CONFIG_PATH
7 |
8 | __version__ = "0.1.3"
9 |
10 |
11 | @click.group(
12 | context_settings={
13 | "help_option_names": ["-h", "--help"],
14 | },
15 | invoke_without_command=True,
16 | )
17 | @click.version_option(version=__version__)
18 | @click.option(
19 | "--log-file",
20 | help="Path to log file. Default is ~/.dasshh/logs/dasshh.log",
21 | type=click.Path(),
22 | )
23 | @click.option(
24 | "--debug",
25 | is_flag=True,
26 | help="Enable debug logging",
27 | )
28 | @click.pass_context
29 | def main(ctx, version: bool = False, log_file=None, debug=False) -> None:
30 | import logging
31 |
32 | log_level = logging.DEBUG if debug else logging.INFO
33 | setup_logging(log_file=log_file, log_level=log_level)
34 | logger = logging.getLogger("dasshh.main")
35 |
36 | if version:
37 | click.echo(__version__)
38 | logger.debug(f"Version {__version__} requested")
39 | ctx.exit()
40 |
41 | if ctx.invoked_subcommand is None:
42 | console = Console()
43 | with console.status("Starting Dasshh 🗲 ", spinner="dots"):
44 | time.sleep(1.2)
45 | console.clear()
46 | from dasshh.ui.app import Dasshh
47 |
48 | app = Dasshh()
49 | app.run()
50 |
51 |
52 | @main.command()
53 | def init_config():
54 | """Initialize the configuration file."""
55 | load_config()
56 | click.echo(f"Config file created at: {DEFAULT_CONFIG_PATH}")
57 | click.echo(
58 | "Please edit this file to set your model API key before starting the application."
59 | )
60 |
61 |
62 | if __name__ == "__main__":
63 | main()
64 |
--------------------------------------------------------------------------------
/dasshh/apps/__init__.py:
--------------------------------------------------------------------------------
1 | from .general import get_available_tools
2 | from . import os
3 |
4 | __all__ = ["get_available_tools", "os"]
5 |
--------------------------------------------------------------------------------
/dasshh/apps/general.py:
--------------------------------------------------------------------------------
1 | from dasshh.core.registry import Registry
2 | from dasshh.core.tools.decorator import tool
3 |
4 |
5 | @tool
6 | def get_available_tools() -> dict:
7 | """
8 | Get all available tools from the registry with their details.
9 |
10 | Returns:
11 | A list of dictionaries containing tool information (name, description, parameters).
12 | """
13 | registry = Registry()
14 | tools = registry.get_tools()
15 |
16 | return {"available_tools": [tool.get_declaration() for tool in tools]}
17 |
--------------------------------------------------------------------------------
/dasshh/apps/os/__init__.py:
--------------------------------------------------------------------------------
1 | from .file_operations import (
2 | current_directory,
3 | list_files,
4 | file_info,
5 | create_directory,
6 | delete_file,
7 | copy_file,
8 | move_file,
9 | )
10 |
11 | from .system_info import (
12 | system_info,
13 | cpu_info,
14 | memory_info,
15 | disk_info,
16 | network_info,
17 | )
18 |
19 | from .process_management import (
20 | process_list,
21 | find_process,
22 | get_process_info,
23 | kill_process,
24 | run_command,
25 | )
26 |
27 | from .network_tools import ping, get_ip_address, check_port, public_ip, traceroute
28 |
29 | # Import all tools to ensure they are registered
30 | __all__ = [
31 | # File operations
32 | "current_directory",
33 | "list_files",
34 | "file_info",
35 | "create_directory",
36 | "delete_file",
37 | "copy_file",
38 | "move_file",
39 | # System info
40 | "system_info",
41 | "cpu_info",
42 | "memory_info",
43 | "disk_info",
44 | "network_info",
45 | # Process management
46 | "process_list",
47 | "find_process",
48 | "get_process_info",
49 | "kill_process",
50 | "run_command",
51 | # Network tools
52 | "ping",
53 | "get_ip_address",
54 | "check_port",
55 | "public_ip",
56 | "traceroute",
57 | ]
58 |
--------------------------------------------------------------------------------
/dasshh/apps/os/file_operations.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | from datetime import datetime
4 | from typing import List, Dict
5 |
6 | from dasshh.core.tools.decorator import tool
7 |
8 |
9 | @tool
10 | def current_directory() -> str:
11 | """Get the current working directory.
12 |
13 | Returns:
14 | str: The current working directory.
15 | """
16 | return os.getcwd()
17 |
18 |
19 | @tool
20 | def list_files(directory: str) -> List[Dict]:
21 | """List all files and directories in the specified directory.
22 |
23 | Args:
24 | directory (str): The path to the directory to list.
25 | """
26 | if not os.path.exists(directory):
27 | return [{"error": f"Directory '{directory}' does not exist"}]
28 |
29 | files = []
30 | for item in os.listdir(directory):
31 | path = os.path.join(directory, item)
32 | stat = os.stat(path)
33 | files.append(
34 | {
35 | "name": item,
36 | "path": path,
37 | "size": stat.st_size,
38 | "is_dir": os.path.isdir(path),
39 | "modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
40 | }
41 | )
42 | return files
43 |
44 |
45 | @tool
46 | def file_info(path: str) -> Dict:
47 | """
48 | Get detailed information about a file or directory.
49 |
50 | Args:
51 | path (str): The path to the file or directory to get information about.
52 | """
53 | if not os.path.exists(path):
54 | return {"error": f"Path '{path}' does not exist"}
55 |
56 | stat = os.stat(path)
57 | return {
58 | "name": os.path.basename(path),
59 | "path": os.path.abspath(path),
60 | "size": stat.st_size,
61 | "is_dir": os.path.isdir(path),
62 | "is_file": os.path.isfile(path),
63 | "created": datetime.fromtimestamp(stat.st_ctime).isoformat(),
64 | "modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
65 | "accessed": datetime.fromtimestamp(stat.st_atime).isoformat(),
66 | "permissions": oct(stat.st_mode)[-3:],
67 | }
68 |
69 |
70 | @tool
71 | def read_file(path: str) -> Dict:
72 | """Read the contents of a file.
73 |
74 | Args:
75 | path (str): The path to the file to read.
76 | """
77 | if not os.path.exists(path):
78 | return {"error": f"Path '{path}' does not exist"}
79 |
80 | with open(path, "r") as file:
81 | return file.read()
82 |
83 |
84 | @tool
85 | def create_directory(path: str) -> Dict:
86 | """
87 | Create a new directory at the specified path.
88 |
89 | Args:
90 | path (str): The path to the new directory.
91 | """
92 | if os.path.exists(path):
93 | return {"error": f"Path '{path}' already exists"}
94 |
95 | try:
96 | os.makedirs(path, exist_ok=True)
97 | return {"success": True, "path": path}
98 | except Exception as e:
99 | return {"success": False, "error": str(e)}
100 |
101 |
102 | @tool
103 | def delete_file(path: str, recursive: bool = False) -> Dict:
104 | """
105 | Delete a file or directory.
106 |
107 | Args:
108 | path (str): The path to the file or directory to delete.
109 | recursive (bool): Whether to delete non-empty directories recursively.
110 | """
111 | if not os.path.exists(path):
112 | return {"error": f"Path '{path}' does not exist"}
113 |
114 | try:
115 | if os.path.isdir(path):
116 | if recursive:
117 | shutil.rmtree(path)
118 | else:
119 | os.rmdir(path)
120 | else:
121 | os.remove(path)
122 | return {"success": True, "path": path}
123 | except Exception as e:
124 | return {"success": False, "error": str(e)}
125 |
126 |
127 | @tool
128 | def copy_file(source: str, destination: str) -> Dict:
129 | """
130 | Copy a file or directory from source to destination.
131 |
132 | Args:
133 | source (str): The path to the source file or directory.
134 | destination (str): The path to the destination file or directory.
135 | """
136 | if not os.path.exists(source):
137 | return {"error": f"Source '{source}' does not exist"}
138 |
139 | try:
140 | if os.path.isdir(source):
141 | shutil.copytree(source, destination)
142 | else:
143 | shutil.copy2(source, destination)
144 | return {"success": True, "source": source, "destination": destination}
145 | except Exception as e:
146 | return {"success": False, "error": str(e)}
147 |
148 |
149 | @tool
150 | def move_file(source: str, destination: str) -> Dict:
151 | """
152 | Move a file or directory from source to destination.
153 |
154 | Args:
155 | source (str): The path to the source file or directory.
156 | destination (str): The path to the destination file or directory.
157 | """
158 | if not os.path.exists(source):
159 | return {"error": f"Source '{source}' does not exist"}
160 |
161 | try:
162 | shutil.move(source, destination)
163 | return {"success": True, "source": source, "destination": destination}
164 | except Exception as e:
165 | return {"success": False, "error": str(e)}
166 |
--------------------------------------------------------------------------------
/dasshh/apps/os/network_tools.py:
--------------------------------------------------------------------------------
1 | import socket
2 | import subprocess
3 | import platform
4 | import requests
5 | from typing import Dict, Optional
6 |
7 | from dasshh.core.tools.decorator import tool
8 |
9 |
10 | @tool
11 | def ping(host: str, count: int = 4) -> Dict:
12 | """Ping a host and return the results"""
13 | param = "-n" if platform.system().lower() == "windows" else "-c"
14 | command = f"ping {param} {count} {host}"
15 |
16 | try:
17 | result = subprocess.run(
18 | command, shell=True, capture_output=True, text=True, timeout=10
19 | )
20 |
21 | return {
22 | "host": host,
23 | "command": command,
24 | "success": result.returncode == 0,
25 | "output": result.stdout,
26 | "error": result.stderr,
27 | }
28 | except Exception as e:
29 | return {"host": host, "success": False, "error": str(e)}
30 |
31 |
32 | @tool
33 | def get_ip_address(hostname: Optional[str] = None) -> Dict:
34 | """Get IP address information for a hostname or the local machine"""
35 | if hostname:
36 | try:
37 | ip = socket.gethostbyname(hostname)
38 | return {"hostname": hostname, "ip": ip}
39 | except socket.gaierror as e:
40 | return {
41 | "hostname": hostname,
42 | "error": f"Could not resolve hostname: {str(e)}",
43 | }
44 | else:
45 | # Get local hostname and IP
46 | hostname = socket.gethostname()
47 | try:
48 | ip = socket.gethostbyname(hostname)
49 | return {"hostname": hostname, "ip": ip}
50 | except socket.gaierror as e:
51 | return {
52 | "hostname": hostname,
53 | "error": f"Could not resolve local hostname: {str(e)}",
54 | }
55 |
56 |
57 | @tool
58 | def check_port(port: int) -> Dict:
59 | """Check if a specific port is in use on a localhost"""
60 | host = "localhost"
61 | timeout = 3
62 | try:
63 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
64 | sock.settimeout(timeout)
65 | result = sock.connect_ex((host, port))
66 | sock.close()
67 |
68 | return {"host": host, "port": port, "in_use": result == 0}
69 | except Exception as e:
70 | return {"host": host, "port": port, "error": str(e)}
71 |
72 |
73 | @tool
74 | def public_ip() -> Dict:
75 | """Get the public IP address of the current machine"""
76 | try:
77 | response = requests.get("https://api.ipify.org?format=json", timeout=5)
78 | return response.json()
79 | except Exception as e:
80 | return {"error": f"Could not get public IP: {str(e)}"}
81 |
82 |
83 | @tool
84 | def traceroute(host: str) -> Dict:
85 | """Perform a traceroute to a host"""
86 | command = (
87 | f"traceroute {host}"
88 | if platform.system().lower() != "windows"
89 | else f"tracert {host}"
90 | )
91 |
92 | try:
93 | result = subprocess.run(
94 | command, shell=True, capture_output=True, text=True, timeout=30
95 | )
96 |
97 | return {
98 | "host": host,
99 | "command": command,
100 | "success": result.returncode == 0,
101 | "output": result.stdout,
102 | "error": result.stderr,
103 | }
104 | except Exception as e:
105 | return {"host": host, "success": False, "error": str(e)}
106 |
--------------------------------------------------------------------------------
/dasshh/apps/os/process_management.py:
--------------------------------------------------------------------------------
1 | import psutil
2 | import subprocess
3 | from typing import Dict, List, Optional
4 |
5 | from dasshh.core.tools.decorator import tool
6 |
7 |
8 | @tool
9 | def process_list() -> List[Dict]:
10 | """
11 | List all running processes with basic information.
12 | """
13 | processes = []
14 | for proc in psutil.process_iter(
15 | ["pid", "name", "username", "memory_percent", "cpu_percent"]
16 | ):
17 | processes.append(proc.info)
18 |
19 | return processes
20 |
21 |
22 | @tool
23 | def find_process(name: str) -> List[Dict]:
24 | """
25 | Find all processes matching a name pattern.
26 |
27 | Args:
28 | name (str): The name pattern to search for.
29 | """
30 | matching_processes = []
31 | for proc in psutil.process_iter(["pid", "name", "username", "cmdline"]):
32 | if name.lower() in proc.info["name"].lower():
33 | matching_processes.append(proc.info)
34 | return matching_processes
35 |
36 |
37 | @tool
38 | def get_process_info(pid: int) -> Dict:
39 | """
40 | Get detailed information about a specific process by its PID.
41 |
42 | Args:
43 | pid (int): The PID of the process to get information about.
44 | """
45 | try:
46 | proc = psutil.Process(pid)
47 | return {
48 | "pid": proc.pid,
49 | "name": proc.name(),
50 | "status": proc.status(),
51 | "created": proc.create_time(),
52 | "username": proc.username(),
53 | "cmdline": proc.cmdline(),
54 | "cwd": proc.cwd(),
55 | "memory_info": proc.memory_info()._asdict(),
56 | "cpu_percent": proc.cpu_percent(interval=0.1),
57 | "num_threads": proc.num_threads(),
58 | "open_files": [f._asdict() for f in proc.open_files()],
59 | "connections": [c._asdict() for c in proc.connections()],
60 | }
61 | except psutil.NoSuchProcess:
62 | return {"error": f"No process with PID {pid} found"}
63 | except psutil.AccessDenied:
64 | return {"error": f"Access denied to process with PID {pid}"}
65 |
66 |
67 | @tool
68 | def kill_process(pid: int) -> Dict:
69 | """
70 | Terminate a process by its PID.
71 |
72 | Args:
73 | pid (int): The PID of the process to terminate.
74 | """
75 | try:
76 | proc = psutil.Process(pid)
77 | proc_name = proc.name()
78 | proc.terminate()
79 |
80 | # Wait for process to terminate (max 3 seconds)
81 | gone, still_alive = psutil.wait_procs([proc], timeout=3)
82 |
83 | if still_alive:
84 | # If still alive, kill it more forcefully
85 | proc.kill()
86 | return {
87 | "success": True,
88 | "pid": pid,
89 | "name": proc_name,
90 | "force_killed": True,
91 | }
92 | else:
93 | return {
94 | "success": True,
95 | "pid": pid,
96 | "name": proc_name,
97 | "force_killed": False,
98 | }
99 | except psutil.NoSuchProcess:
100 | return {"error": f"No process with PID {pid} found"}
101 | except psutil.AccessDenied:
102 | return {"error": f"Access denied when trying to kill process with PID {pid}"}
103 |
104 |
105 | @tool
106 | def run_command(command: str, timeout: Optional[int] = None) -> Dict:
107 | """
108 | Run a shell command and return its output.
109 |
110 | Args:
111 | command (str): The command to run.
112 | timeout (int, optional): The timeout for the command.
113 | """
114 | try:
115 | result = subprocess.run(
116 | command, shell=True, capture_output=True, text=True, timeout=timeout
117 | )
118 |
119 | return {
120 | "command": command,
121 | "success": result.returncode == 0,
122 | "return_code": result.returncode,
123 | "stdout": result.stdout,
124 | "stderr": result.stderr,
125 | }
126 | except subprocess.TimeoutExpired:
127 | return {
128 | "command": command,
129 | "success": False,
130 | "error": f"Command timed out after {timeout} seconds",
131 | }
132 | except Exception as e:
133 | return {"command": command, "success": False, "error": str(e)}
134 |
--------------------------------------------------------------------------------
/dasshh/apps/os/system_info.py:
--------------------------------------------------------------------------------
1 | import platform
2 | import psutil
3 | from typing import Dict
4 |
5 | from dasshh.core.tools.decorator import tool
6 |
7 |
8 | @tool
9 | def system_info() -> Dict:
10 | """
11 | Get detailed information about the operating system.
12 | """
13 | info = {
14 | "system": platform.system(),
15 | "node": platform.node(),
16 | "release": platform.release(),
17 | "version": platform.version(),
18 | "machine": platform.machine(),
19 | "processor": platform.processor(),
20 | "python_version": platform.python_version(),
21 | }
22 | return info
23 |
24 |
25 | @tool
26 | def cpu_info() -> Dict:
27 | """
28 | Get CPU information and current usage.
29 | """
30 | cpu_count = psutil.cpu_count()
31 | cpu_percent = psutil.cpu_percent(interval=1, percpu=True)
32 |
33 | return {
34 | "physical_cores": psutil.cpu_count(logical=False),
35 | "total_cores": cpu_count,
36 | "usage_per_core": cpu_percent,
37 | "total_usage": sum(cpu_percent) / len(cpu_percent),
38 | "frequency": psutil.cpu_freq()._asdict() if psutil.cpu_freq() else None,
39 | }
40 |
41 |
42 | @tool
43 | def memory_info() -> Dict:
44 | """
45 | Get memory (RAM) information and usage.
46 | """
47 | mem = psutil.virtual_memory()
48 | swap = psutil.swap_memory()
49 |
50 | return {
51 | "total": mem.total,
52 | "available": mem.available,
53 | "used": mem.used,
54 | "percent": mem.percent,
55 | "swap_total": swap.total,
56 | "swap_used": swap.used,
57 | "swap_percent": swap.percent,
58 | }
59 |
60 |
61 | @tool
62 | def disk_info(path: str = "/") -> Dict:
63 | """
64 | Get disk space information for the specified path.
65 | """
66 | disk = psutil.disk_usage(path)
67 |
68 | return {
69 | "path": path,
70 | "total": disk.total,
71 | "used": disk.used,
72 | "free": disk.free,
73 | "percent": disk.percent,
74 | }
75 |
76 |
77 | @tool
78 | def network_info() -> Dict:
79 | """
80 | Get network interface information.
81 | """
82 | interfaces = psutil.net_if_addrs()
83 | io_counters = psutil.net_io_counters(pernic=True)
84 |
85 | result = {}
86 | for interface, addresses in interfaces.items():
87 | result[interface] = {
88 | "addresses": [addr._asdict() for addr in addresses],
89 | "stats": io_counters.get(interface)._asdict()
90 | if interface in io_counters
91 | else None,
92 | }
93 |
94 | return result
95 |
--------------------------------------------------------------------------------
/dasshh/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/dasshh/core/__init__.py
--------------------------------------------------------------------------------
/dasshh/core/exceptions.py:
--------------------------------------------------------------------------------
1 | # TODO
2 |
--------------------------------------------------------------------------------
/dasshh/core/logging.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from logging.handlers import RotatingFileHandler
3 | from pathlib import Path
4 | import litellm
5 |
6 | DEFAULT_LOG_DIR = Path.home() / ".dasshh" / "logs"
7 | DEFAULT_LOG_FILE = DEFAULT_LOG_DIR / "dasshh.log"
8 |
9 | DEFAULT_LOG_DIR.mkdir(parents=True, exist_ok=True)
10 |
11 |
12 | def setup_logging(log_file=None, log_level=logging.INFO):
13 | log_file = log_file or DEFAULT_LOG_FILE
14 | formatter = logging.Formatter(
15 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
16 | )
17 |
18 | litellm_logger = litellm.verbose_logger
19 | root_logger = logging.getLogger()
20 |
21 | for handler in root_logger.handlers[:] + litellm_logger.handlers[:]:
22 | litellm_logger.removeHandler(handler)
23 | root_logger.removeHandler(handler)
24 |
25 | file_handler = RotatingFileHandler(
26 | log_file,
27 | maxBytes=10 * 1024 * 1024, # 10 MB
28 | backupCount=5,
29 | encoding="utf-8",
30 | )
31 | file_handler.setFormatter(formatter)
32 | root_logger.addHandler(file_handler)
33 | root_logger.setLevel(log_level)
34 | litellm_logger.addHandler(file_handler)
35 | litellm_logger.setLevel(log_level)
36 | logging.info(f"-- Dasshh logging initialized. Log file: {log_file} --")
37 |
38 |
39 | def get_logger(name):
40 | return logging.getLogger(name)
41 |
--------------------------------------------------------------------------------
/dasshh/core/registry.py:
--------------------------------------------------------------------------------
1 | from dasshh.core.tools.base import BaseTool
2 |
3 |
4 | class Registry:
5 | """
6 | A registry for tools.
7 | """
8 |
9 | _instance: "Registry" = None
10 | """The singleton instance of the registry."""
11 | tools: dict[str, BaseTool] = {}
12 | """The tools in the registry."""
13 |
14 | def __new__(cls, *args, **kwargs):
15 | if cls._instance is None:
16 | cls._instance = super().__new__(cls, *args, **kwargs)
17 | return cls._instance
18 |
19 | def add_tool(self, tool: BaseTool):
20 | """
21 | Add a tool to the registry.
22 | """
23 | if tool.name in self.tools:
24 | raise ValueError(
25 | f"Tool name must be unique, there is already a tool named {tool.name}"
26 | )
27 | self.tools[tool.name] = tool
28 |
29 | def get_tools(self) -> list[BaseTool]:
30 | """
31 | Get all registered tools.
32 | """
33 | return list(self.tools.values())
34 |
35 | def get_tool(self, tool_name: str) -> BaseTool | None:
36 | """
37 | Get a tool by name.
38 | """
39 | return self.tools.get(tool_name, None)
40 |
41 | def get_tool_declarations(self) -> list[dict]:
42 | """
43 | Get all registered tool declarations.
44 | """
45 | return [tool.get_declaration() for tool in self.get_tools()]
46 |
--------------------------------------------------------------------------------
/dasshh/core/tools/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/dasshh/core/tools/__init__.py
--------------------------------------------------------------------------------
/dasshh/core/tools/base.py:
--------------------------------------------------------------------------------
1 | from abc import ABC
2 |
3 |
4 | class BaseTool(ABC):
5 | """
6 | Base class for all tools.
7 | """
8 |
9 | name: str
10 | """The name of the tool."""
11 | description: str
12 | """The description of the tool."""
13 | parameters: dict
14 | """The parameters of the tool."""
15 |
16 | def __init__(self, name: str, description: str, parameters: dict):
17 | self.name = name
18 | self.description = description
19 | self.parameters = parameters
20 |
21 | def __call__(self, *args, **kwargs):
22 | raise NotImplementedError("This tool has no implementation")
23 |
24 | def get_declaration(self) -> dict:
25 | """
26 | Get the declaration of the tool.
27 | """
28 | raise NotImplementedError("This tool has no implementation")
29 |
--------------------------------------------------------------------------------
/dasshh/core/tools/decorator.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | from dasshh.core.registry import Registry
4 | from dasshh.core.tools.function_tool import FunctionTool
5 |
6 |
7 | def tool(func: Callable):
8 | """
9 | A decorator to convert a function into a tool.
10 | """
11 |
12 | tool_instance = FunctionTool(
13 | name=func.__name__,
14 | description=func.__doc__,
15 | parameters=func.__annotations__,
16 | func=func,
17 | )
18 |
19 | registry = Registry()
20 | registry.add_tool(tool_instance)
21 |
22 | return tool_instance
23 |
--------------------------------------------------------------------------------
/dasshh/core/tools/function_tool.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 | from litellm.utils import function_to_dict
3 |
4 | from dasshh.core.tools.base import BaseTool
5 |
6 |
7 | class FunctionTool(BaseTool):
8 | """
9 | A tool is a function that can be used to help the user.
10 | """
11 |
12 | func: Callable = None
13 | """The function of the tool."""
14 |
15 | def __init__(
16 | self, name: str, description: str, parameters: dict, func: Callable = None
17 | ):
18 | super().__init__(name, description, parameters)
19 | self.func = func
20 |
21 | def __call__(self, *args, **kwargs):
22 | if self.func:
23 | return self.func(*args, **kwargs)
24 | raise NotImplementedError("This tool has no implementation")
25 |
26 | def get_declaration(self) -> dict:
27 | """
28 | Get the declaration of the tool.
29 | """
30 | return function_to_dict(self.func)
31 |
--------------------------------------------------------------------------------
/dasshh/data/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/dasshh/data/__init__.py
--------------------------------------------------------------------------------
/dasshh/data/client.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from typing import Generator
3 |
4 | from sqlalchemy.orm import Session, sessionmaker
5 | from sqlalchemy import create_engine, Engine
6 | from sqlalchemy.orm import DeclarativeBase
7 |
8 |
9 | class Base(DeclarativeBase):
10 | """Base class for all database models."""
11 |
12 | pass
13 |
14 |
15 | class DBClient:
16 | """Dasshh database client."""
17 |
18 | db_path = Path.home() / ".dasshh" / "db" / "dasshh.db"
19 |
20 | def __init__(self):
21 | self.db_path.parent.mkdir(parents=True, exist_ok=True)
22 | self.engine: Engine = create_engine(f"sqlite:///{self.db_path}")
23 | self.DatabaseSessionFactory: sessionmaker = sessionmaker(bind=self.engine)
24 |
25 | Base.metadata.create_all(bind=self.engine)
26 |
27 | def get_db(self) -> Generator[Session, None, None]:
28 | """Get a database session."""
29 | db: Session = self.DatabaseSessionFactory()
30 | return db
31 |
--------------------------------------------------------------------------------
/dasshh/data/models.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from datetime import datetime, timezone
3 | from sqlalchemy import Column, String, DateTime, ForeignKey, JSON
4 | from sqlalchemy.orm import relationship
5 |
6 | from dasshh.data.client import Base
7 |
8 |
9 | class StorageSession(Base):
10 | __tablename__ = "sessions"
11 |
12 | id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
13 | detail = Column(String)
14 | created_at = Column(DateTime, default=datetime.now(timezone.utc))
15 | updated_at = Column(DateTime, default=datetime.now(timezone.utc))
16 |
17 | events = relationship("StorageEvent", back_populates="session")
18 |
19 |
20 | class StorageEvent(Base):
21 | __tablename__ = "events"
22 |
23 | id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
24 | invocation_id = Column(String)
25 | session_id = Column(String, ForeignKey("sessions.id"))
26 | created_at = Column(DateTime, default=datetime.now(timezone.utc))
27 | content = Column(JSON)
28 |
29 | session = relationship("StorageSession", back_populates="events")
30 |
--------------------------------------------------------------------------------
/dasshh/data/session.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timezone
2 |
3 | from sqlalchemy.orm import noload
4 |
5 | from dasshh.data.client import DBClient
6 | from dasshh.data.models import StorageSession, StorageEvent
7 |
8 |
9 | class SessionService:
10 | """Dasshh database session service."""
11 |
12 | def __init__(self, db_client: DBClient):
13 | self.db_client = db_client
14 |
15 | def new_session(
16 | self,
17 | detail: str = "New Session",
18 | ) -> StorageSession:
19 | """Create a new session."""
20 | session = StorageSession(detail=detail)
21 | with self.db_client.get_db() as db:
22 | db.add(session)
23 | db.commit()
24 | db.refresh(session)
25 | return session
26 |
27 | def get_session(self, *, session_id: str) -> StorageSession | None:
28 | """Get a session by its ID."""
29 | with self.db_client.get_db() as db:
30 | session: StorageSession | None = db.get(StorageSession, session_id)
31 | if session is None:
32 | return None
33 | return session
34 |
35 | def get_events(self, *, session_id: str) -> list[StorageEvent]:
36 | """Get all events for a session."""
37 | with self.db_client.get_db() as db:
38 | events = (
39 | db.query(StorageEvent)
40 | .filter(StorageEvent.session_id == session_id)
41 | .all()
42 | )
43 | return events
44 |
45 | def get_recent_session(self) -> StorageSession | None:
46 | """Get the most recent session."""
47 | with self.db_client.get_db() as db:
48 | session: StorageSession | None = (
49 | db.query(StorageSession)
50 | .order_by(StorageSession.updated_at.desc())
51 | .first()
52 | )
53 | if not session:
54 | return None
55 | return session
56 |
57 | def update_session(
58 | self,
59 | *,
60 | session_id: str,
61 | detail: str,
62 | ) -> None:
63 | """Update a session."""
64 | with self.db_client.get_db() as db:
65 | session = db.get(StorageSession, session_id)
66 | session.detail = detail
67 | session.updated_at = datetime.now(timezone.utc)
68 | db.commit()
69 |
70 | def list_sessions(self, include_events: bool = False) -> list[StorageSession]:
71 | """List all sessions."""
72 | with self.db_client.get_db() as db:
73 | if include_events:
74 | sessions = db.query(StorageSession).all()
75 | else:
76 | sessions = (
77 | db.query(StorageSession)
78 | .options(noload(StorageSession.events))
79 | .all()
80 | )
81 | return sessions
82 |
83 | def delete_session(self, *, session_id: str) -> None:
84 | """Delete a session by its ID."""
85 | with self.db_client.get_db() as db:
86 | session = db.get(StorageSession, session_id)
87 | if session is None:
88 | return
89 | db.delete(session)
90 | db.commit()
91 |
92 | def add_event(
93 | self,
94 | *,
95 | invocation_id: str,
96 | session_id: str,
97 | content: dict,
98 | ) -> None:
99 | """Append an event to a session."""
100 | event = StorageEvent(
101 | invocation_id=invocation_id,
102 | session_id=session_id,
103 | content=content,
104 | )
105 | with self.db_client.get_db() as db:
106 | db.add(event)
107 | db.commit()
108 | db.refresh(event)
109 |
--------------------------------------------------------------------------------
/dasshh/ui/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/dasshh/ui/__init__.py
--------------------------------------------------------------------------------
/dasshh/ui/app.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import yaml
3 | from textual.app import App
4 | from textual.command import CommandPalette
5 | from textual.theme import ThemeProvider
6 |
7 | from dasshh.ui.screens.main import MainScreen
8 | from dasshh.data.client import DBClient
9 | from dasshh.data.session import SessionService
10 | from dasshh.core.runtime import DasshhRuntime
11 | from dasshh.ui.utils import load_tools, load_config, DEFAULT_CONFIG_PATH
12 | from dasshh.ui.themes.lime import lime
13 |
14 |
15 | class Dasshh(App):
16 | """Dasshh 🗲"""
17 |
18 | SCREENS = {
19 | "main": MainScreen,
20 | }
21 |
22 | BINDINGS = [
23 | ("ctrl+c", "quit", "Quit"),
24 | ("ctrl+t", "toggle_theme", "Toggle Theme"),
25 | ]
26 |
27 | ENABLE_COMMAND_PALETTE = False
28 |
29 | logger: logging.Logger
30 | """Dasshh logger."""
31 |
32 | runtime: DasshhRuntime
33 | """Dasshh runtime."""
34 |
35 | session_service: SessionService
36 | """The database service."""
37 |
38 | config: dict
39 | """Dasshh config."""
40 |
41 | def __init__(self, *args, **kwargs):
42 | super().__init__(*args, **kwargs)
43 | self.config = load_config()
44 | load_tools()
45 |
46 | self.session_service = SessionService(DBClient())
47 | self.runtime = DasshhRuntime(self.session_service)
48 |
49 | self.logger = logging.getLogger("dasshh.app")
50 | self.logger.debug("-- Dasshh 🗲 initialized --")
51 |
52 | async def on_mount(self):
53 | self.startup()
54 | self.logger.debug("Pushing main screen")
55 | self.push_screen("main")
56 | await self.runtime.start()
57 |
58 | async def on_unmount(self):
59 | self.logger.debug("Application shutting down")
60 | await self.runtime.stop()
61 |
62 | def startup(self):
63 | self.register_theme(lime)
64 | self.theme = self.config.get("dasshh", {}).get("theme", "lime")
65 | self.logger.debug(f"Theme set to {self.theme}")
66 |
67 | def action_toggle_theme(self) -> None:
68 | self.push_screen(
69 | CommandPalette(
70 | providers=[ThemeProvider],
71 | placeholder="Search for themes…",
72 | ),
73 | )
74 |
75 | def watch_theme(self, theme: str) -> None:
76 | self.config["dasshh"]["theme"] = theme
77 | self.update_config()
78 |
79 | def update_config(self) -> None:
80 | self.logger.debug(f"Updating config: {self.config}")
81 | DEFAULT_CONFIG_PATH.write_text(yaml.dump(self.config))
82 |
83 |
84 | if __name__ == "__main__":
85 | app = Dasshh()
86 | app.run()
87 |
--------------------------------------------------------------------------------
/dasshh/ui/components/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/dasshh/ui/components/__init__.py
--------------------------------------------------------------------------------
/dasshh/ui/components/chat/__init__.py:
--------------------------------------------------------------------------------
1 | from dasshh.ui.components.chat.chat_panel import ChatPanel, ChatMessage, ChatInput
2 | from dasshh.ui.components.chat.history_panel import HistoryPanel
3 | from dasshh.ui.components.chat.actions_panel import ActionsPanel
4 |
5 | __all__ = ["ChatPanel", "ChatMessage", "ChatInput", "HistoryPanel", "ActionsPanel"]
6 |
--------------------------------------------------------------------------------
/dasshh/ui/components/chat/action.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from textual.widgets import Static
4 | from textual.reactive import reactive
5 | from rich.syntax import Syntax
6 | from rich.console import Group
7 | from rich.text import Text
8 |
9 |
10 | class Action(Static):
11 | """A action display component."""
12 |
13 | DEFAULT_CSS = """
14 | Action {
15 | width: 100%;
16 | margin: 1 0;
17 | padding: 1;
18 | border-left: thick $success;
19 | background: $panel 15%;
20 | }
21 | """
22 |
23 | name: reactive[str] = reactive("", layout=True)
24 | args: reactive[str] = reactive("", layout=True)
25 | result: reactive[str] = reactive("", layout=True)
26 |
27 | def __init__(
28 | self,
29 | invocation_id: str,
30 | tool_call_id: str,
31 | name: str,
32 | args: str,
33 | result: str,
34 | *a: Any,
35 | **kw: Any,
36 | ) -> None:
37 | super().__init__(*a, **kw)
38 | self.invocation_id = invocation_id
39 | self.tool_call_id = tool_call_id
40 | self.name = name
41 | self.args = args
42 | self.result = result
43 |
44 | def render(self):
45 | tool_call_title = Text(f" Using tool: {self.name}", style="bold green")
46 | panel_color = self.app.get_css_variables().get("panel", "")
47 | args_syntax = Syntax(
48 | self.args, "json", background_color=panel_color, word_wrap=True
49 | )
50 |
51 | if self.result:
52 | result_title = Text(f" Result: {self.name}", style="bold blue")
53 | result_syntax = Syntax(
54 | self.result, "json", background_color=panel_color, word_wrap=True
55 | )
56 | return Group(
57 | tool_call_title, args_syntax, Text(""), result_title, result_syntax
58 | )
59 | else:
60 | return Group(tool_call_title, args_syntax)
61 |
--------------------------------------------------------------------------------
/dasshh/ui/components/chat/actions_panel.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | from textual.widget import Widget
3 | from textual.widgets import Static
4 | from textual.app import ComposeResult
5 | from textual.containers import ScrollableContainer
6 |
7 | from dasshh.ui.components.chat.action import Action
8 | from dasshh.ui.types import UIAction
9 |
10 |
11 | class ActionsPanel(Widget):
12 | DEFAULT_CSS = """
13 | ActionsPanel {
14 | border: round $secondary;
15 | layout: vertical;
16 |
17 | &:focus, &:hover, &:focus-within {
18 | border: round $primary;
19 | }
20 |
21 | #actions-header {
22 | height: auto;
23 | text-align: center;
24 | text-style: bold;
25 | }
26 |
27 | #actions-container {
28 | height: 1fr;
29 | }
30 |
31 | ScrollableContainer {
32 | scrollbar-color: $secondary $background;
33 | scrollbar-background: $background;
34 | scrollbar-corner-color: $background;
35 | scrollbar-size: 1 1;
36 | scrollbar-gutter: stable;
37 | margin: 0 1;
38 | }
39 | }
40 | """
41 |
42 | def compose(self) -> ComposeResult:
43 | yield Static("Actions", id="actions-header")
44 | yield ScrollableContainer(id="actions-container")
45 |
46 | def on_show(self) -> None:
47 | """Actions panel shown."""
48 | self.query_one("#actions-container").scroll_end(animate=False)
49 |
50 | def reset(self) -> None:
51 | """Reset the actions panel."""
52 | container = self.query_one("#actions-container")
53 | container.remove_children()
54 |
55 | def load_actions(self, actions: List[UIAction]):
56 | container = self.query_one("#actions-container", ScrollableContainer)
57 | container.remove_children()
58 | for action in actions:
59 | self.add_action(action)
60 | container.scroll_end()
61 |
62 | def add_action(self, action: UIAction) -> None:
63 | """Add a action to the actions panel."""
64 | container = self.query_one("#actions-container", ScrollableContainer)
65 | action_widget = Action(
66 | invocation_id=action.invocation_id,
67 | tool_call_id=action.tool_call_id,
68 | name=action.name,
69 | args=action.args,
70 | result=action.result,
71 | )
72 | container.mount(action_widget)
73 | container.scroll_end()
74 |
75 | def update_action(self, invocation_id: str, tool_call_id: str, result: str) -> None:
76 | """Update an action in the actions panel."""
77 | action_widget = self.get_action_widget(invocation_id, tool_call_id)
78 | if action_widget:
79 | action_widget.result = result
80 | container = self.query_one("#actions-container", ScrollableContainer)
81 | container.scroll_end()
82 |
83 | def get_action_widget(self, invocation_id: str, tool_call_id: str) -> Action | None:
84 | """Get an action widget by invocation id and tool call id."""
85 | container = self.query_one("#actions-container", ScrollableContainer)
86 | for action in container.query(Action):
87 | if (
88 | action.invocation_id == invocation_id
89 | and action.tool_call_id == tool_call_id
90 | ):
91 | return action
92 | return None
93 |
94 | def handle_error(self, error: str) -> None:
95 | """Handle an error during an action by showing a toast notification."""
96 | self.notify(f"Tool Error: {error}", severity="error", timeout=5)
97 |
--------------------------------------------------------------------------------
/dasshh/ui/components/chat/chat_input.py:
--------------------------------------------------------------------------------
1 | from textual.widgets import Input, Button
2 | from textual.widget import Widget
3 | from textual.app import ComposeResult
4 | from textual import on
5 | from textual.binding import Binding
6 | from textual.containers import Horizontal
7 |
8 | from dasshh.ui.events import NewMessage
9 |
10 |
11 | class ChatInput(Widget):
12 | """Input area for chat messages."""
13 |
14 | BINDINGS = [Binding("escape", "blur_input", "Blur Input")]
15 |
16 | DEFAULT_CSS = """
17 | ChatInput {
18 | dock: bottom;
19 | width: 100%;
20 | height: 3;
21 | text-overflow: ellipsis;
22 |
23 | ScrollView {
24 | overflow-y: hidden;
25 | overflow-x: hidden;
26 | }
27 | }
28 |
29 | #message-input {
30 | width: 1fr;
31 | border: round $secondary;
32 | background: $background;
33 |
34 | &:focus, &:hover {
35 | border: round $primary;
36 | background: $background;
37 | background-tint: $background;
38 | }
39 |
40 | &:disabled {
41 | border: round $panel-darken-1;
42 | color: $text-muted;
43 | }
44 | }
45 |
46 | #send-button {
47 | width: auto;
48 | min-width: 10;
49 | border: round $secondary;
50 | color: $secondary;
51 | background: $background;
52 | text-style: bold;
53 |
54 | &:disabled {
55 | border: round $panel-darken-1;
56 | color: $text-muted;
57 | }
58 | }
59 |
60 | #send-button:focus, #send-button:hover {
61 | border: round $primary;
62 | color: $primary;
63 | background: $background;
64 | background-tint: $background;
65 | text-style: bold;
66 |
67 | &.-active {
68 | tint: $background;
69 | }
70 | }
71 | """
72 |
73 | def compose(self) -> ComposeResult:
74 | with Horizontal():
75 | yield Input(placeholder="Type your message here...", id="message-input")
76 | yield Button("Send", id="send-button", variant="primary")
77 |
78 | @on(Input.Submitted)
79 | def on_input_submitted(self) -> None:
80 | """Handle when the user presses Enter in the input field."""
81 | self.send_message()
82 |
83 | @on(Button.Pressed, "#send-button")
84 | def on_button_pressed(self) -> None:
85 | """Handle when the send button is clicked."""
86 | self.send_message()
87 |
88 | def action_blur_input(self) -> None:
89 | """Remove focus from the input field when Escape is pressed."""
90 | self.screen.set_focus(None)
91 |
92 | def send_message(self) -> None:
93 | """Send the message from the input field."""
94 | input_field = self.query_one("#message-input", Input)
95 | message = input_field.value
96 |
97 | if message.strip():
98 | input_field.value = ""
99 | self.post_message(NewMessage(message=message))
100 |
101 | def disable(self) -> None:
102 | """Disable the input field and send button."""
103 | input_field = self.query_one("#message-input", Input)
104 | send_button = self.query_one("#send-button", Button)
105 |
106 | input_field.disabled = True
107 | send_button.disabled = True
108 |
109 | def enable(self) -> None:
110 | """Enable the input field and send button."""
111 | input_field = self.query_one("#message-input", Input)
112 | send_button = self.query_one("#send-button", Button)
113 |
114 | input_field.disabled = False
115 | send_button.disabled = False
116 |
--------------------------------------------------------------------------------
/dasshh/ui/components/chat/chat_panel.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from textual.widget import Widget
4 | from textual.widgets import Static
5 | from textual.app import ComposeResult
6 | from textual.containers import ScrollableContainer
7 | from rich.text import Text
8 | from dasshh.ui.types import UIMessage
9 |
10 | from dasshh.ui.components.chat.message import ChatMessage
11 | from dasshh.ui.components.chat.chat_input import ChatInput
12 |
13 |
14 | class ChatPanel(Widget):
15 | """Main chat panel containing the chat history and input area."""
16 |
17 | DEFAULT_CSS = """
18 | ChatPanel {
19 | layout: vertical;
20 | border: round $secondary;
21 |
22 | &:focus, &:hover, &:focus-within {
23 | border: round $primary;
24 | }
25 |
26 | #messages-container {
27 | height: 1fr;
28 | overflow-y: auto;
29 | }
30 |
31 | #chat-header {
32 | height: auto;
33 | text-align: center;
34 | text-style: bold;
35 | }
36 |
37 | ScrollableContainer {
38 | scrollbar-color: $secondary $background;
39 | scrollbar-background: $background;
40 | scrollbar-corner-color: $background;
41 | scrollbar-size: 1 1;
42 | scrollbar-gutter: stable;
43 | }
44 | }
45 | """
46 |
47 | def compose(self) -> ComposeResult:
48 | yield Static("Chat", id="chat-header")
49 | yield ScrollableContainer(id="messages-container")
50 | yield ChatInput(id="chat-input")
51 |
52 | def on_show(self) -> None:
53 | """Chat panel shown."""
54 | self.query_one("#messages-container").scroll_end(animate=False)
55 |
56 | def reset(self) -> None:
57 | """Reset the chat panel."""
58 | container = self.query_one("#messages-container")
59 | container.remove_children()
60 |
61 | text = Static(
62 | Text("Start a new session or load a previous one.", style="dim"),
63 | classes="chat-message",
64 | )
65 | text.styles.text_align = "center"
66 | text.styles.margin = (1, 1, 1, 1)
67 | container.mount(text)
68 |
69 | chat_input = self.query_one(ChatInput)
70 | chat_input.disable()
71 |
72 | def load_messages(self, messages: List[UIMessage]) -> None:
73 | """Load messages from a previous chat session."""
74 | container = self.query_one("#messages-container")
75 | container.remove_children()
76 | for message in messages:
77 | self.add_new_message(message)
78 | chat_input = self.query_one(ChatInput)
79 | chat_input.enable()
80 |
81 | def add_new_message(self, message: UIMessage) -> None:
82 | """Add a new message to the chat history."""
83 | container = self.query_one("#messages-container")
84 | message_widget = ChatMessage(
85 | invocation_id=message.invocation_id,
86 | role=message.role,
87 | content=message.content,
88 | classes="chat-message",
89 | )
90 | container.mount(message_widget)
91 | container.scroll_end()
92 |
93 | def update_assistant_message(
94 | self, *, invocation_id: str, content: str, final: bool = False
95 | ) -> None:
96 | """Update the content of the most recent assistant message (used for streaming)."""
97 | message_widget = self.get_message_widget(invocation_id)
98 | if not message_widget:
99 | return
100 |
101 | if final:
102 | message_widget.content = content
103 | else:
104 | message_widget.content += content
105 |
106 | container = self.query_one("#messages-container")
107 | container.scroll_end()
108 |
109 | def get_message_widget(self, invocation_id: str) -> ChatMessage | None:
110 | """Get the message widget for a given invocation id."""
111 | container = self.query_one("#messages-container")
112 | for message in container.query(ChatMessage):
113 | if message.invocation_id == invocation_id:
114 | return message
115 | return None
116 |
--------------------------------------------------------------------------------
/dasshh/ui/components/chat/history_item.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timezone
2 | from typing import Any
3 |
4 | from textual.widgets import Static
5 | from textual.reactive import reactive
6 | from rich.console import Group
7 | from rich.text import Text
8 |
9 | from dasshh.ui.events import LoadSession, DeleteSession
10 |
11 |
12 | class DeleteIcon(Static):
13 | """A static widget that displays a delete icon."""
14 |
15 | DEFAULT_CSS = """
16 | DeleteIcon {
17 | padding: 0 1;
18 | color: $text-muted;
19 | align: center middle;
20 | text-align: center;
21 |
22 | &:hover {
23 | color: $error;
24 | }
25 | }
26 | """
27 |
28 | selected: reactive[bool] = reactive(False, layout=True)
29 |
30 | def __init__(self, session_id: str, *args: Any, **kwargs: Any) -> None:
31 | super().__init__(*args, **kwargs)
32 | self.session_id = session_id
33 |
34 | def watch_selected(self, selected: bool) -> None:
35 | """Watch for changes to the selected state."""
36 | if selected:
37 | self.add_class("selected")
38 | else:
39 | self.remove_class("selected")
40 |
41 | def on_click(self) -> None:
42 | """Handle click events on this widget."""
43 | self.post_message(DeleteSession(self.session_id))
44 |
45 | def render(self):
46 | return Text(" Delete")
47 |
48 |
49 | class HistoryItem(Static):
50 | """A history item display component for chat sessions."""
51 |
52 | DEFAULT_CSS = """
53 | HistoryItem {
54 | margin-top: 1;
55 | padding: 1;
56 |
57 | }
58 | """
59 |
60 | detail: reactive[str] = reactive("", layout=True)
61 | selected: reactive[bool] = reactive(False, layout=True)
62 |
63 | def __init__(
64 | self,
65 | session_id: str,
66 | detail: str,
67 | created_at: datetime,
68 | *args: Any,
69 | **kwargs: Any,
70 | ) -> None:
71 | super().__init__(*args, **kwargs)
72 | self.session_id = session_id
73 | self.detail = detail
74 | self.created_at = created_at
75 |
76 | def watch_selected(self, selected: bool) -> None:
77 | """Watch for changes to the selected state."""
78 | if selected:
79 | self.add_class("selected")
80 | else:
81 | self.remove_class("selected")
82 |
83 | def on_click(self) -> None:
84 | """Handle click events on this widget."""
85 | self.post_message(LoadSession(self.session_id))
86 |
87 | def render(self):
88 | truncated_detail = (
89 | (self.detail[:40] + "...") if len(self.detail) > 40 else self.detail
90 | )
91 |
92 | local_timestamp = self.created_at.replace(tzinfo=timezone.utc).astimezone(
93 | tz=None
94 | )
95 |
96 | now = datetime.now()
97 | time_str = local_timestamp.strftime("%I:%M %p")
98 |
99 | if local_timestamp.date() == now.date():
100 | date_str = f"Today at {time_str}"
101 | elif (now.date() - local_timestamp.date()).days == 1:
102 | date_str = f"Yesterday at {time_str}"
103 | else:
104 | date_str = local_timestamp.strftime("%b %d at %I:%M %p")
105 |
106 | title = Text(truncated_detail, style="bold")
107 | date = Text(date_str, style="dim")
108 |
109 | return Group(title, date)
110 |
--------------------------------------------------------------------------------
/dasshh/ui/components/chat/history_panel.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from textual.widget import Widget
4 | from textual.widgets import Static, Button
5 | from textual.app import ComposeResult
6 | from textual.containers import ScrollableContainer
7 | from textual import on
8 |
9 | from dasshh.ui.events import NewSession, DeleteSession
10 | from dasshh.ui.components.chat.history_item import HistoryItem, DeleteIcon
11 | from dasshh.ui.types import UISession
12 |
13 |
14 | class HistoryPanel(Widget):
15 | DEFAULT_CSS = """
16 | HistoryPanel {
17 | border: round $secondary;
18 | layout: vertical;
19 |
20 | &:focus, &:hover, &:focus-within {
21 | border: round $primary;
22 | }
23 |
24 | #history-header {
25 | height: auto;
26 | text-align: center;
27 | text-style: bold;
28 | }
29 |
30 | #history-container {
31 | height: 1fr;
32 | margin: 1;
33 | }
34 |
35 | #new-session {
36 | width: 100%;
37 | min-width: 10;
38 | border: round $secondary;
39 | color: $secondary;
40 | background: $background;
41 | text-style: bold;
42 | }
43 |
44 | #new-session:focus, #new-session:hover {
45 | border: round $primary;
46 | color: $primary;
47 | background: $background;
48 | background-tint: $background;
49 | text-style: bold;
50 |
51 | &.-active {
52 | tint: $background;
53 | }
54 | }
55 |
56 | ScrollableContainer {
57 | scrollbar-color: $secondary $background;
58 | scrollbar-background: $background;
59 | scrollbar-corner-color: $background;
60 | scrollbar-size: 1 1;
61 | scrollbar-gutter: stable;
62 | }
63 |
64 | HistoryItem, DeleteIcon {
65 | width: 100%;
66 | height: auto;
67 | border-left: thick $accent-darken-2;
68 | background: $panel-darken-1;
69 |
70 | &:hover {
71 | border-left: thick $accent;
72 | background: $panel 20%;
73 | }
74 |
75 | &.selected {
76 | border-left: thick $success;
77 | background: $panel-lighten-1;
78 | }
79 | }
80 | }
81 | """
82 |
83 | def compose(self) -> ComposeResult:
84 | yield Static("Sessions", id="history-header")
85 | yield ScrollableContainer(id="history-container")
86 | yield Button("New Session", id="new-session")
87 |
88 | def on_show(self) -> None:
89 | """History panel shown."""
90 | self.query_one("#history-container").scroll_end(animate=False)
91 |
92 | @on(Button.Pressed, "#new-session")
93 | def on_button_pressed(self) -> None:
94 | """Handle button presses."""
95 | self.post_message(NewSession())
96 |
97 | @on(DeleteSession)
98 | def on_delete_session(self, event: DeleteSession) -> None:
99 | """Handle session deletion request."""
100 | item = self.get_history_item_widget(event.session_id)
101 | if item:
102 | item.remove()
103 |
104 | container = self.query_one("#history-container", ScrollableContainer)
105 | for item in container.query(DeleteIcon):
106 | if item.session_id == event.session_id:
107 | item.remove()
108 | break
109 |
110 | def load_sessions(self, sessions: List[UISession], current: str) -> None:
111 | """Load session history from a list of sessions."""
112 | container = self.query_one("#history-container", ScrollableContainer)
113 | container.remove_children()
114 |
115 | for session in sessions:
116 | history_item = HistoryItem(
117 | session_id=session.id,
118 | detail=session.detail,
119 | created_at=session.updated_at,
120 | )
121 | delete_icon = DeleteIcon(session_id=session.id)
122 | # Mark current session
123 | if session.id == current:
124 | history_item.selected = True
125 | delete_icon.selected = True
126 | container.mount(history_item, delete_icon)
127 | container.scroll_end()
128 |
129 | def add_session(self, session: UISession) -> None:
130 | """Add a session to the history panel."""
131 | container = self.query_one("#history-container", ScrollableContainer)
132 | history_item = HistoryItem(
133 | session_id=session.id,
134 | detail=session.detail,
135 | created_at=session.updated_at,
136 | )
137 | delete_icon = DeleteIcon(session_id=session.id)
138 | container.mount(history_item, delete_icon)
139 | container.scroll_end()
140 |
141 | def set_current_session(self, session_id: str) -> None:
142 | """Set the current selected session."""
143 | # Update selected state for all items
144 | container = self.query_one("#history-container", ScrollableContainer)
145 | for item in container.query(HistoryItem):
146 | item.selected = item.session_id == session_id
147 | for item in container.query(DeleteIcon):
148 | item.selected = item.session_id == session_id
149 |
150 | def get_history_item_widget(self, session_id: str) -> HistoryItem | None:
151 | """Get a history item widget by session id."""
152 | container = self.query_one("#history-container", ScrollableContainer)
153 | for item in container.query(HistoryItem):
154 | if item.session_id == session_id:
155 | return item
156 | return None
157 |
--------------------------------------------------------------------------------
/dasshh/ui/components/chat/message.py:
--------------------------------------------------------------------------------
1 | from textual.reactive import reactive
2 | from textual.widgets import Static
3 | from rich.markdown import Markdown
4 | from rich.console import Group
5 | from rich.text import Text
6 | from typing import Any
7 |
8 |
9 | class ChatMessage(Static):
10 | """A chat message display component."""
11 |
12 | DEFAULT_CSS = """
13 | ChatMessage {
14 | width: 100%;
15 | margin: 1 1;
16 | padding: 0 1;
17 | }
18 |
19 | .user {
20 | border-left: thick $primary;
21 | background: $background-lighten-1;
22 | padding: 1;
23 | margin: 1 5 1 1;
24 | }
25 |
26 | .assistant {
27 | border-left: thick $accent;
28 | background: $background-lighten-2;
29 | padding: 1;
30 | margin: 1 1 1 5;
31 | }
32 | """
33 | user_icon: str = ""
34 | # assistant_icon: str = ""
35 | assistant_icon: str = "🗲"
36 |
37 | content: reactive[str] = reactive("", layout=True)
38 |
39 | def __init__(
40 | self, invocation_id: str, role: str, content: str, *args: Any, **kwargs: Any
41 | ) -> None:
42 | super().__init__(*args, **kwargs)
43 | self.invocation_id = invocation_id
44 | if role == "user":
45 | self.role = "you"
46 | elif role == "assistant":
47 | self.role = "dasshh"
48 | self.content = content
49 |
50 | # Add CSS class based on role
51 | self.add_class(role)
52 |
53 | def render(self):
54 | # Show typing indicator if the message is from assistant but empty
55 | if self.role == "dasshh" and not self.content:
56 | return Text("typing...", style="italic dim")
57 |
58 | role_icon = self.user_icon if self.role == "you" else self.assistant_icon
59 | role_style = "bold #ffff8d" if self.role == "you" else "bold #76ff03"
60 |
61 | title = Text(f"{role_icon} {self.role.capitalize()}", style=role_style)
62 | text = Markdown(self.content) if self.content else Text("")
63 |
64 | return Group(title, text)
65 |
--------------------------------------------------------------------------------
/dasshh/ui/components/navbar.py:
--------------------------------------------------------------------------------
1 | from textual import on
2 | from textual.app import ComposeResult
3 | from textual.containers import Container, Horizontal
4 | from textual.widgets import Static
5 |
6 | from dasshh.ui.events import ChangeView
7 |
8 |
9 | class NavItem(Static):
10 | """A navigation item."""
11 |
12 | DEFAULT_CSS = """
13 | NavItem {
14 | content-align: center middle;
15 | width: auto;
16 | height: auto;
17 | padding: 0 2;
18 | color: $text-muted;
19 | }
20 |
21 | NavItem:hover {
22 | color: $primary-lighten-1;
23 | }
24 |
25 | NavItem.active {
26 | color: $primary-lighten-1;
27 | text-style: bold;
28 | }
29 | """
30 |
31 | def __init__(self, route: str, label: str, icon: str = "", **kwargs):
32 | super().__init__(f"{icon} {label}", **kwargs)
33 | self.route = route
34 |
35 | def on_click(self) -> None:
36 | self.add_class("active")
37 | self.post_message(ChangeView(self.route))
38 |
39 |
40 | class Logo(Static):
41 | """The logo."""
42 |
43 | DEFAULT_CSS = """
44 | Logo {
45 | color: $primary;
46 | content-align: center middle;
47 | text-style: italic bold;
48 | width: auto;
49 | height: 100%;
50 | margin-top: -1;
51 | }
52 |
53 | Logo:hover {
54 | color: $primary-darken-1;
55 | }
56 | """
57 |
58 | txt = """
59 | ╭──────────╮
60 | │ Dasshh 🗲 │
61 | ╰──────────╯
62 | """
63 |
64 | def render(self) -> str:
65 | return self.txt
66 |
67 |
68 | class Navbar(Container):
69 | """The Navbar"""
70 |
71 | DEFAULT_CSS = """
72 | Navbar {
73 | content-align: center middle;
74 | layout: horizontal;
75 | background: $background-lighten-1;
76 | width: 100%;
77 | height: 3;
78 | }
79 |
80 | Navbar > #logo {
81 | width: 1fr;
82 | align: right middle;
83 | }
84 |
85 | Navbar > #nav-items {
86 | width: 1fr;
87 | align: left middle;
88 | }
89 | """
90 |
91 | def compose(self) -> ComposeResult:
92 | """Create the navbar items."""
93 | yield Logo(id="logo")
94 | with Horizontal(id="nav-items"):
95 | yield NavItem(route="chat", label="Chat", icon="", id="chat")
96 | yield NavItem(route="settings", label="Settings", icon="", id="settings")
97 | yield NavItem(route="about", label="About", icon="", id="about")
98 |
99 | def on_mount(self) -> None:
100 | self.query_one("#chat").add_class("active")
101 |
102 | @on(ChangeView)
103 | def change_view(self, event: ChangeView):
104 | for item in self.query("NavItem"):
105 | item.remove_class("active")
106 | self.query_one(f"#{event.view}").add_class("active")
107 |
--------------------------------------------------------------------------------
/dasshh/ui/components/settings/__init__.py:
--------------------------------------------------------------------------------
1 | from .checkbox import Checkbox
2 | from .settings_section import SettingsSection
3 | from .dasshh_config import DasshhConfig
4 | from .model_config import ModelConfig
5 |
6 | __all__ = [
7 | "Checkbox",
8 | "SettingsSection",
9 | "DasshhConfig",
10 | "ModelConfig",
11 | ]
12 |
--------------------------------------------------------------------------------
/dasshh/ui/components/settings/checkbox.py:
--------------------------------------------------------------------------------
1 | from textual.widgets._checkbox import Checkbox as TextualCheckbox
2 | from textual.content import Content
3 |
4 |
5 | class Checkbox(TextualCheckbox):
6 | """A checkbox widget that displays a checkmark or cross."""
7 |
8 | DEFAULT_CSS = """
9 | Checkbox {
10 | & > .toggle--button {
11 | color: $secondary;
12 | background: $background;
13 | }
14 |
15 | &.-on > .toggle--button {
16 | color: $primary;
17 | background: $background;
18 | }
19 | }
20 | """
21 |
22 | checked_label = "✓"
23 | unchecked_label = "✗"
24 |
25 | def render(self) -> Content:
26 | """Render the content of the widget."""
27 | button_style = self.get_visual_style("toggle--button")
28 | label_style = self.get_visual_style("toggle--label")
29 | label = self._label.stylize_before(label_style)
30 | spacer = " " if label else ""
31 |
32 | if self.value:
33 | button = (self.checked_label, button_style)
34 | else:
35 | button = (self.unchecked_label, button_style)
36 |
37 | if self._button_first:
38 | content = Content.assemble(button, spacer, label)
39 | else:
40 | content = Content.assemble(label, spacer, button)
41 | return content
42 |
--------------------------------------------------------------------------------
/dasshh/ui/components/settings/dasshh_config.py:
--------------------------------------------------------------------------------
1 | from textual.app import ComposeResult
2 | from textual.widgets import Static, Input, Select
3 | from textual.validation import Regex
4 | from textual import on
5 |
6 | from .settings_section import SettingsSection
7 | from .checkbox import Checkbox
8 |
9 |
10 | class DasshhConfig(SettingsSection):
11 | """Dasshh Configuration section component."""
12 |
13 | def __init__(self, **kwargs):
14 | super().__init__("Dasshh Configuration", **kwargs)
15 |
16 | def compose(self) -> ComposeResult:
17 | yield from super().compose()
18 |
19 | yield Checkbox(id="skip-summarization", label="Skip Summarization")
20 |
21 | yield Static("System Prompt:")
22 | yield Input(placeholder="Custom system prompt...", id="system-prompt", valid_empty=True)
23 |
24 | yield Static("Theme:")
25 | yield Select([("lime", "lime")], prompt="Select theme", id="theme", allow_blank=False)
26 |
27 | yield Static("Tool Directories:")
28 | yield Input(
29 | placeholder="Comma-separated paths (e.g., /path/to/tool1,/path/to/tool2)",
30 | id="tool-directories",
31 | type="text",
32 | validators=[
33 | Regex(
34 | r"^[a-zA-Z0-9/_-]+(,[a-zA-Z0-9/_-]+)*$",
35 | failure_description="Tool directories must be valid comma-separated paths",
36 | )
37 | ],
38 | valid_empty=True
39 | )
40 |
41 | @on(Checkbox.Changed, "#skip-summarization")
42 | def on_skip_summarization_changed(self, event: Checkbox.Changed) -> None:
43 | settings_widget = self.parent.parent
44 | if hasattr(settings_widget, 'skip_summarization'):
45 | settings_widget.skip_summarization = event.value
46 |
47 | @on(Input.Changed, "#system-prompt")
48 | def on_system_prompt_changed(self, event: Input.Changed) -> None:
49 | settings_widget = self.parent.parent
50 | if hasattr(settings_widget, 'system_prompt'):
51 | settings_widget.system_prompt = event.value if event.value else None
52 |
53 | @on(Select.Changed, "#theme")
54 | def on_theme_changed(self, event: Select.Changed) -> None:
55 | settings_widget = self.parent.parent
56 | if hasattr(settings_widget, 'app'):
57 | settings_widget.app.theme = event.value
58 |
59 | @on(Input.Changed, "#tool-directories")
60 | def on_tool_directories_changed(self, event: Input.Changed) -> None:
61 | settings_widget = self.parent.parent
62 | if event.validation_result.is_valid:
63 | if hasattr(settings_widget, 'tool_directories'):
64 | settings_widget.tool_directories = event.value if event.value else None
65 | else:
66 | if hasattr(settings_widget, 'notify'):
67 | failure_descriptions = "\n".join(event.validation_result.failure_descriptions)
68 | settings_widget.notify(failure_descriptions, severity="error", timeout=5)
69 |
--------------------------------------------------------------------------------
/dasshh/ui/components/settings/model_config.py:
--------------------------------------------------------------------------------
1 | from textual.app import ComposeResult
2 | from textual.widgets import Static, Input
3 | from textual.validation import Number, Function
4 | from textual import on
5 |
6 | from .settings_section import SettingsSection
7 |
8 |
9 | class ModelConfig(SettingsSection):
10 | """Model Configuration section component."""
11 |
12 | def __init__(self, **kwargs):
13 | super().__init__("Model Configuration", **kwargs)
14 |
15 | def compose(self) -> ComposeResult:
16 | yield from super().compose()
17 |
18 | yield Static("Model Name:")
19 | yield Input(
20 | placeholder="e.g., gemini/gemini-2.0-flash",
21 | id="model-name",
22 | validators=[
23 | Function(lambda value: value is not None and value.strip()),
24 | ],
25 | valid_empty=False
26 | )
27 |
28 | yield Static("API Base:")
29 | yield Input(placeholder="API base URL (optional)", id="api-base", valid_empty=True)
30 |
31 | yield Static("API Key:")
32 | yield Input(
33 | placeholder="Your API key", password=True, id="api-key",
34 | validators=[
35 | Function(lambda value: value is not None and value.strip()),
36 | ],
37 | valid_empty=False
38 | )
39 |
40 | yield Static("API Version:")
41 | yield Input(placeholder="API version (optional)", id="api-version", valid_empty=True)
42 |
43 | yield Static("Temperature:")
44 | yield Input(
45 | placeholder="0.0 - 1.0 (default: 1.0)",
46 | id="temperature",
47 | type="number",
48 | validators=[
49 | Number(
50 | minimum=0.0,
51 | maximum=1.0,
52 | failure_description="Temperature must be a value in the range 0.0 to 1.0",
53 | )
54 | ],
55 | valid_empty=False
56 | )
57 |
58 | yield Static("Top P:")
59 | yield Input(
60 | placeholder="0.0 - 1.0 (default: 1.0)",
61 | id="top-p",
62 | type="number",
63 | validators=[
64 | Number(
65 | minimum=0.0,
66 | maximum=1.0,
67 | failure_description="Top P must be a value in the range 0.0 to 1.0",
68 | )
69 | ],
70 | valid_empty=False
71 | )
72 |
73 | yield Static("Max Tokens:")
74 | yield Input(
75 | placeholder="Maximum tokens (optional)",
76 | id="max-tokens",
77 | type="integer",
78 | valid_empty=True
79 | )
80 |
81 | yield Static("Max Completion Tokens:")
82 | yield Input(
83 | placeholder="Maximum completion tokens (optional)",
84 | id="max-completion-tokens",
85 | type="integer",
86 | valid_empty=True
87 | )
88 |
89 | @on(Input.Changed, "#model-name")
90 | def on_model_name_changed(self, event: Input.Changed) -> None:
91 | settings_widget = self.parent.parent
92 | if event.validation_result.is_valid and event.value:
93 | if hasattr(settings_widget, 'model_name'):
94 | settings_widget.model_name = event.value
95 |
96 | if not event.validation_result.is_valid:
97 | if hasattr(settings_widget, 'notify'):
98 | settings_widget.notify("Model name is required", severity="error", timeout=5)
99 |
100 | @on(Input.Changed, "#api-base")
101 | def on_api_base_changed(self, event: Input.Changed) -> None:
102 | settings_widget = self.parent.parent
103 | if hasattr(settings_widget, 'api_base'):
104 | settings_widget.api_base = event.value if event.value else None
105 |
106 | @on(Input.Changed, "#api-key")
107 | def on_api_key_changed(self, event: Input.Changed) -> None:
108 | settings_widget = self.parent.parent
109 | if event.validation_result.is_valid and event.value:
110 | if hasattr(settings_widget, 'api_key'):
111 | settings_widget.api_key = event.value
112 |
113 | if not event.validation_result.is_valid:
114 | if hasattr(settings_widget, 'notify'):
115 | settings_widget.notify("API key is required", severity="error", timeout=5)
116 |
117 | @on(Input.Changed, "#api-version")
118 | def on_api_version_changed(self, event: Input.Changed) -> None:
119 | settings_widget = self.parent.parent
120 | if hasattr(settings_widget, 'api_version'):
121 | settings_widget.api_version = event.value if event.value else None
122 |
123 | @on(Input.Changed, "#temperature")
124 | def on_temperature_changed(self, event: Input.Changed) -> None:
125 | settings_widget = self.parent.parent
126 | if event.validation_result.is_valid and event.value:
127 | if hasattr(settings_widget, 'temperature'):
128 | settings_widget.temperature = float(event.value)
129 |
130 | if not event.validation_result.is_valid:
131 | if hasattr(settings_widget, 'notify'):
132 | settings_widget.notify(
133 | "Temperature must be a value in the range 0.0 to 1.0",
134 | severity="error",
135 | timeout=5
136 | )
137 |
138 | @on(Input.Changed, "#top-p")
139 | def on_top_p_changed(self, event: Input.Changed) -> None:
140 | settings_widget = self.parent.parent
141 | if event.validation_result.is_valid and event.value:
142 | if hasattr(settings_widget, 'top_p'):
143 | settings_widget.top_p = float(event.value)
144 |
145 | if not event.validation_result.is_valid:
146 | if hasattr(settings_widget, 'notify'):
147 | settings_widget.notify(
148 | "Top P must be a value in the range 0.0 to 1.0",
149 | severity="error",
150 | timeout=5
151 | )
152 |
153 | @on(Input.Changed, "#max-tokens")
154 | def on_max_tokens_changed(self, event: Input.Changed) -> None:
155 | settings_widget = self.parent.parent
156 | if hasattr(settings_widget, 'max_tokens'):
157 | settings_widget.max_tokens = int(event.value) if event.value else None
158 |
159 | @on(Input.Changed, "#max-completion-tokens")
160 | def on_max_completion_tokens_changed(self, event: Input.Changed) -> None:
161 | settings_widget = self.parent.parent
162 | if hasattr(settings_widget, 'max_completion_tokens'):
163 | settings_widget.max_completion_tokens = int(event.value) if event.value else None
164 |
--------------------------------------------------------------------------------
/dasshh/ui/components/settings/settings_section.py:
--------------------------------------------------------------------------------
1 | from textual.app import ComposeResult
2 | from textual.containers import Container
3 | from textual.widgets import Label
4 |
5 |
6 | class SettingsSection(Container):
7 | """A section container for grouping related settings."""
8 |
9 | DEFAULT_CSS = """
10 | SettingsSection {
11 | layout: vertical;
12 | width: 100%;
13 | height: auto;
14 | padding: 2;
15 | border: round $secondary;
16 | }
17 |
18 | SettingsSection > .section-title {
19 | text-style: bold;
20 | color: $primary;
21 | margin-top: 0;
22 | margin-bottom: 1;
23 | dock: top;
24 | }
25 |
26 | SettingsSection > Static {
27 | margin-top: 1;
28 | margin-left: 1;
29 | }
30 |
31 | SettingsSection > Input,
32 | SettingsSection > Checkbox,
33 | SettingsSection > Select {
34 | border: round $secondary;
35 | background: $background;
36 |
37 | &:focus, &:hover {
38 | border: round $primary;
39 | background: $background;
40 | background-tint: $background;
41 | }
42 |
43 | &:disabled {
44 | border: round $panel-darken-1;
45 | color: $text-muted;
46 | }
47 |
48 | &.-invalid, &.-invalid:focus {
49 | border: round $error;
50 | background: $background;
51 | background-tint: $background;
52 | }
53 | }
54 |
55 | SettingsSection > Input {
56 | width: 75%;
57 | }
58 |
59 | SettingsSection > Checkbox {
60 | width: 25%;
61 | margin-left: 0;
62 | margin-top: 1;
63 | }
64 |
65 | SettingsSection > Select {
66 | width: 75%;
67 |
68 | & > SelectCurrent, &:focus > SelectCurrent {
69 | border: none;
70 | background: $background;
71 | background-tint: $background;
72 | }
73 |
74 | & > SelectOverlay {
75 | border: round $primary;
76 | background: $background;
77 | background-tint: $background;
78 | scrollbar-color: $secondary $background;
79 | scrollbar-background: $background;
80 | scrollbar-corner-color: $background;
81 | scrollbar-size: 1 1;
82 | scrollbar-gutter: stable;
83 | }
84 | }
85 | """
86 |
87 | def __init__(self, title: str, **kwargs):
88 | super().__init__(**kwargs)
89 | self.title = title
90 |
91 | def compose(self) -> ComposeResult:
92 | yield Label(self.title, classes="section-title")
93 |
--------------------------------------------------------------------------------
/dasshh/ui/events.py:
--------------------------------------------------------------------------------
1 | from textual.message import Message
2 |
3 |
4 | # -- Main screen events -- #
5 |
6 |
7 | class ChangeView(Message):
8 | """Change the view on the main screen."""
9 |
10 | def __init__(self, view: str):
11 | super().__init__()
12 | self.view = view
13 |
14 |
15 | # -- Chat events -- #
16 |
17 |
18 | class NewMessage(Message):
19 | """Send a message to the chat."""
20 |
21 | def __init__(self, message: str):
22 | super().__init__()
23 | self.message = message
24 |
25 |
26 | class NewSession(Message):
27 | """Create a new session."""
28 |
29 | def __init__(self):
30 | super().__init__()
31 |
32 |
33 | class LoadSession(Message):
34 | """Load a previous session."""
35 |
36 | def __init__(self, session_id: str):
37 | super().__init__()
38 | self.session_id = session_id
39 |
40 |
41 | class DeleteSession(Message):
42 | """Delete an existing session."""
43 |
44 | def __init__(self, session_id: str):
45 | super().__init__()
46 | self.session_id = session_id
47 |
48 |
49 | # -- Agent runtime events -- #
50 |
51 |
52 | class AssistantResponseStart(Message):
53 | """Event triggered before agent starts processing a query."""
54 |
55 | def __init__(self, invocation_id: str):
56 | super().__init__()
57 | self.invocation_id = invocation_id
58 |
59 |
60 | class AssistantResponseUpdate(Message):
61 | """Event triggered when agent returns a partial response."""
62 |
63 | def __init__(self, invocation_id: str, content: str):
64 | super().__init__()
65 | self.invocation_id = invocation_id
66 | self.content = content
67 |
68 |
69 | class AssistantResponseComplete(Message):
70 | """Event triggered when agent completes processing a query."""
71 |
72 | def __init__(self, invocation_id: str, content: str):
73 | super().__init__()
74 | self.invocation_id = invocation_id
75 | self.content = content
76 |
77 |
78 | class AssistantResponseError(Message):
79 | """Event triggered when agent encounters an error."""
80 |
81 | def __init__(self, invocation_id: str, error: str):
82 | super().__init__()
83 | self.invocation_id = invocation_id
84 | self.error = error
85 |
86 |
87 | class AssistantToolCallStart(Message):
88 | """Event triggered when agent starts a tool call."""
89 |
90 | def __init__(
91 | self, invocation_id: str, tool_call_id: str, tool_name: str, args: str
92 | ):
93 | super().__init__()
94 | self.invocation_id = invocation_id
95 | self.tool_call_id = tool_call_id
96 | self.tool_name = tool_name
97 | self.args = args
98 |
99 |
100 | class AssistantToolCallComplete(Message):
101 | """Event triggered when agent completes a tool call."""
102 |
103 | def __init__(
104 | self, invocation_id: str, tool_call_id: str, tool_name: str, result: str
105 | ):
106 | super().__init__()
107 | self.invocation_id = invocation_id
108 | self.tool_call_id = tool_call_id
109 | self.tool_name = tool_name
110 | self.result = result
111 |
112 |
113 | class AssistantToolCallError(Message):
114 | """Event triggered when agent encounters an error during a tool call."""
115 |
116 | def __init__(
117 | self, invocation_id: str, tool_call_id: str, tool_name: str, error: str
118 | ):
119 | super().__init__()
120 | self.invocation_id = invocation_id
121 | self.tool_call_id = tool_call_id
122 | self.tool_name = tool_name
123 | self.error = error
124 |
--------------------------------------------------------------------------------
/dasshh/ui/screens/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/dasshh/ui/screens/__init__.py
--------------------------------------------------------------------------------
/dasshh/ui/screens/help.py:
--------------------------------------------------------------------------------
1 | from textual.widgets import Static
2 | from textual.app import ComposeResult
3 | from textual.widget import Widget
4 |
5 |
6 | class Help(Widget):
7 | def compose(self) -> ComposeResult:
8 | yield Static("Help")
9 |
--------------------------------------------------------------------------------
/dasshh/ui/screens/main.py:
--------------------------------------------------------------------------------
1 | from textual import on
2 | from textual.screen import Screen
3 | from textual.widgets import ContentSwitcher
4 |
5 | from dasshh.ui.events import ChangeView
6 | from dasshh.ui.views import Chat, Settings, About
7 | from dasshh.ui.components.navbar import Navbar
8 |
9 |
10 | class MainScreen(Screen):
11 | """Main screen"""
12 |
13 | DEFAULT_CSS = """
14 | MainScreen {
15 | layout: vertical;
16 | height: 1fr;
17 | width: 1fr;
18 | overflow: hidden;
19 | }
20 |
21 | ContentSwitcher {
22 | align: center middle;
23 | padding: 0 0 4 0;
24 | }
25 | """
26 |
27 | def compose(self):
28 | yield Navbar()
29 | with ContentSwitcher(id="content", initial="chat"):
30 | yield Chat(id="chat")
31 | yield Settings(id="settings")
32 | yield About(id="about")
33 |
34 | @on(ChangeView)
35 | def change_view(self, event: ChangeView):
36 | self.query_one(ContentSwitcher).current = event.view
37 |
--------------------------------------------------------------------------------
/dasshh/ui/themes/__init__.py:
--------------------------------------------------------------------------------
1 | from .lime import lime
2 |
3 | __all__ = ["lime"]
4 |
--------------------------------------------------------------------------------
/dasshh/ui/themes/lime.py:
--------------------------------------------------------------------------------
1 | from textual.theme import Theme
2 |
3 | lime = Theme(
4 | name="lime",
5 | primary="#76ff03",
6 | secondary="#64dd17",
7 | accent="#4caf50",
8 | foreground="#f0f0f0",
9 | background="#101a10",
10 | success="#b9f6ca",
11 | warning="#ffff8d",
12 | error="#ff8a80",
13 | surface="#162316",
14 | panel="#1e2a1e",
15 | dark=True,
16 | variables={
17 | "footer-key-foreground": "#76ff03",
18 | "input-selection-background": "#64dd1740",
19 | "block-cursor-background": "#76ff03",
20 | "block-cursor-foreground": "#101a10",
21 | "border": "#76ff03",
22 | "scrollbar": "#4caf50",
23 | "scrollbar-hover": "#64dd17",
24 | "scrollbar-active": "#76ff03",
25 | "link-color": "#9cff57",
26 | "link-color-hover": "#76ff03",
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/dasshh/ui/types.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Literal, List
3 | from pydantic import BaseModel, Field
4 |
5 |
6 | class UIMessage(BaseModel):
7 | """A message to be displayed in the UI."""
8 |
9 | invocation_id: str = Field("", description="The invocation id of the message.")
10 | role: Literal["user", "assistant"] = Field(
11 | ..., description="The role of the message."
12 | )
13 | content: str = Field(..., description="The content of the message.")
14 |
15 |
16 | class UIAction(BaseModel):
17 | """An action to be displayed in the UI."""
18 |
19 | tool_call_id: str = Field(..., description="The tool call id of the action.")
20 | invocation_id: str = Field(..., description="The invocation id of the action.")
21 | name: str = Field(..., description="The name of the action.")
22 | args: str = Field(
23 | ...,
24 | description="The arguments of the action. This has to be a JSON string with indent=2.",
25 | )
26 | result: str = Field(
27 | ...,
28 | description="The result of the action. This has to be a JSON string with indent=2.",
29 | )
30 |
31 |
32 | class UISession(BaseModel):
33 | """A session to be displayed in the UI."""
34 |
35 | id: str = Field(..., description="The id of the session.")
36 | detail: str = Field(..., description="The detail of the session.")
37 | created_at: datetime = Field(..., description="The creation time of the session.")
38 | updated_at: datetime = Field(
39 | ..., description="The last update time of the session."
40 | )
41 | messages: List[UIMessage] = Field(..., description="The messages of the session.")
42 | actions: List[UIAction] = Field(..., description="The actions of the session.")
43 |
--------------------------------------------------------------------------------
/dasshh/ui/utils.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import sys
4 | import yaml
5 | from pathlib import Path
6 | from typing import List
7 | from importlib import import_module
8 | import importlib.resources as pkg_resources
9 |
10 | from dasshh.data.models import StorageSession, StorageEvent
11 | from dasshh.ui.types import (
12 | UISession,
13 | UIMessage,
14 | UIAction,
15 | )
16 | from dasshh.core.logging import get_logger
17 |
18 | logger = get_logger(__name__)
19 |
20 |
21 | DEFAULT_CONFIG_PATH = Path.home() / ".dasshh" / "config.yaml"
22 | try:
23 | DASSHH_EXEC_PATH = str(Path(pkg_resources.files("dasshh")))
24 | except (ImportError, TypeError):
25 | DASSHH_EXEC_PATH = str(Path(__file__).parent.parent)
26 |
27 | DEFAULT_TOOLS_PATH = str(Path(DASSHH_EXEC_PATH) / "apps")
28 |
29 | DEFAULT_CONFIG = f"""
30 | dasshh:
31 | skip_summarization: false
32 | system_prompt:
33 | tool_directories:
34 | - {DEFAULT_TOOLS_PATH}
35 | theme: lime
36 |
37 | model:
38 | name: gemini/gemini-2.0-flash
39 | api_base:
40 | api_key:
41 | api_version:
42 | temperature: 1.0
43 | top_p: 1.0
44 | max_tokens:
45 | max_completion_tokens:
46 | """
47 |
48 |
49 | def convert_session_obj(
50 | session_obj: StorageSession, events: List[StorageEvent] | None = None
51 | ) -> UISession:
52 | messages, actions = [], {}
53 | if events:
54 | for event in events:
55 | invocation_id = event.invocation_id
56 | content = event.content
57 | if content["role"] == "assistant" and "tool_calls" in content:
58 | for tool_call in content["tool_calls"]:
59 | tool_call_id = tool_call["id"]
60 | args = json.dumps(
61 | json.loads(tool_call["function"]["arguments"]), indent=2
62 | )
63 | actions[tool_call_id] = UIAction(
64 | invocation_id=invocation_id,
65 | tool_call_id=tool_call_id,
66 | name=tool_call["function"]["name"],
67 | args=args,
68 | result="",
69 | )
70 | elif content["role"] == "tool":
71 | tool_call_id = content["tool_call_id"]
72 | actions[tool_call_id].result = content["content"]
73 | elif content["role"] in ["user", "assistant"]:
74 | messages.append(
75 | UIMessage(
76 | invocation_id=invocation_id,
77 | role=content["role"],
78 | content=content["content"],
79 | )
80 | )
81 | return UISession(
82 | id=session_obj.id,
83 | detail=session_obj.detail,
84 | created_at=session_obj.created_at,
85 | updated_at=session_obj.updated_at,
86 | messages=messages,
87 | actions=list(actions.values()),
88 | )
89 |
90 |
91 | def load_tools() -> None:
92 | """
93 | Load all tools from the given directories recursively.
94 |
95 | Args:
96 | dirs: A list of directory paths to load tools from (absolute file paths).
97 | If None, will use paths from config or fall back to default.
98 | """
99 | tool_dirs_config = get_from_config("dasshh.tool_directories")
100 | if tool_dirs_config:
101 | dirs = tool_dirs_config
102 | else:
103 | dirs = [DEFAULT_TOOLS_PATH]
104 |
105 | for dir_path in dirs:
106 | if os.path.exists(dir_path):
107 | for root, dirs, files in os.walk(dir_path):
108 | if "__init__.py" in files:
109 | # Determine full module path
110 | rel_path = os.path.relpath(root, dir_path)
111 | if rel_path == ".":
112 | module_path = os.path.basename(dir_path)
113 | module_parent = os.path.dirname(dir_path)
114 | else:
115 | module_path = rel_path.replace(os.sep, ".")
116 | module_parent = dir_path
117 |
118 | if module_parent not in sys.path:
119 | sys.path.append(module_parent)
120 |
121 | try:
122 | import_module(module_path)
123 | logger.info(f"Imported module: {module_path}")
124 | except ImportError as e:
125 | logger.error(f"Failed to import {module_path}: {e}")
126 |
127 |
128 | def load_config() -> dict:
129 | """Load the configuration file."""
130 | if DEFAULT_CONFIG_PATH.exists():
131 | return yaml.safe_load(DEFAULT_CONFIG_PATH.read_text())
132 |
133 | DEFAULT_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
134 | DEFAULT_CONFIG_PATH.write_text(DEFAULT_CONFIG)
135 | return yaml.safe_load(DEFAULT_CONFIG)
136 |
137 |
138 | def get_from_config(key: str) -> dict | str | List | None:
139 | """Get a value from the configuration file."""
140 | if not DEFAULT_CONFIG_PATH.exists():
141 | return None
142 |
143 | with open(DEFAULT_CONFIG_PATH, "r") as f:
144 | config = yaml.safe_load(f)
145 |
146 | if "." in key:
147 | parts = key.split(".")
148 | curr = config
149 | for part in parts:
150 | if curr is None or part not in curr:
151 | return None
152 | curr = curr.get(part)
153 | return curr
154 |
155 | return config.get(key, None)
156 |
--------------------------------------------------------------------------------
/dasshh/ui/views/__init__.py:
--------------------------------------------------------------------------------
1 | from .chat import Chat
2 | from .settings import Settings
3 | from .about import About
4 |
5 | __all__ = ["Chat", "Settings", "About"]
6 |
--------------------------------------------------------------------------------
/dasshh/ui/views/about.py:
--------------------------------------------------------------------------------
1 | from textual.app import ComposeResult
2 | from textual.widgets import Static
3 | from textual.widget import Widget
4 | from textual.containers import ScrollableContainer
5 |
6 |
7 | class About(Widget):
8 | """About"""
9 |
10 | DEFAULT_CSS = """
11 | About {
12 | layout: vertical;
13 | height: 1fr;
14 | width: 1fr;
15 | align: center middle;
16 | padding: 2 4;
17 | }
18 |
19 | .about-content {
20 | text-align: center;
21 | content-align: center middle;
22 | padding: 1;
23 | width: 90%;
24 | max-width: 100;
25 | }
26 | """
27 |
28 | def compose(self) -> ComposeResult:
29 | with ScrollableContainer(classes="about-content"):
30 | yield Static(
31 | "Dasshh 🗲 is your friendly assistant built right into the terminal - "
32 | "where you spend most of your time.\n\n"
33 | "Designed to reduce cognitive load, Dasshh handles repetitive tasks "
34 | "on your behalf so that you can focus on what truly matters.\n\n"
35 | "The Goal: Prompt to Action!\n\n"
36 | "Note: This project is under active development. Contributions are welcome!\n\n"
37 | "Star it to show your support: https://github.com/vgnshiyer/dasshh",
38 | )
39 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Dasshh Documentation
2 |
3 | This directory contains the documentation for the Dasshh project, built with [MkDocs](https://www.mkdocs.org/) and the [Material theme](https://squidfunk.github.io/mkdocs-material/).
4 |
5 | ## Running the documentation locally
6 |
7 | ```bash
8 | # Install dependencies
9 | uv pip install mkdocs-material
10 |
11 | # Serve the documentation
12 | mkdocs serve
13 | ```
14 |
15 | Then visit [http://localhost:8000](http://localhost:8000) in your browser.
16 |
17 | ## Building the documentation
18 |
19 | ```bash
20 | mkdocs build
21 | ```
22 |
23 | This will create a `site` directory with the static HTML files.
24 |
25 | ## Deploying the documentation
26 |
27 | The documentation is automatically deployed to GitHub Pages when changes are pushed to the main branch.
28 |
29 | You can also manually deploy the documentation by running:
30 |
31 | ```bash
32 | mkdocs gh-deploy
33 | ```
--------------------------------------------------------------------------------
/docs/api/tools.md:
--------------------------------------------------------------------------------
1 | # Tools
2 |
3 | This page lays out all the components available in the tools module.
4 |
5 | !!! tip
6 | Tools are the core mechanism for extending Dasshh's capabilities. Use the `@tool` decorator to convert functions into tools that the AI assistant can use.
7 |
8 |
9 |
10 | ## BaseTool
11 |
12 | Abstract base class for all tools in Dasshh.
13 |
14 | ### `attr` name
15 |
16 | The name of the tool
17 |
18 | ```python
19 | name: str
20 | ```
21 |
22 | ### `attr` description
23 |
24 | The description of the tool
25 |
26 | ```python
27 | description: str
28 | ```
29 |
30 | ### `attr` parameters
31 |
32 | The parameters of the tool
33 |
34 | ```python
35 | parameters: dict
36 | ```
37 |
38 | ### `method` __init__
39 |
40 | ```python
41 | __init__(name: str, description: str, parameters: dict)
42 | ```
43 |
44 | Initialize a new BaseTool instance
45 |
46 | **Parameters:**
47 |
48 | | Param | Default
| Description |
49 | | ------------- | :----------------: | :----------------------------------------------------------------------------------------|
50 | | name | | The name of the tool |
51 | | description | | The description of what the tool does |
52 | | parameters | | Dictionary containing the tool's parameters |
53 |
54 | ### `method` __call__
55 |
56 | ```python
57 | __call__(*args, **kwargs)
58 | ```
59 |
60 | Execute the tool with the given arguments
61 |
62 | **Parameters:**
63 |
64 | | Param | Default
| Description |
65 | | ------------- | :----------------: | :----------------------------------------------------------------------------------------|
66 | | *args | | Positional arguments to pass to the tool |
67 | | **kwargs | | Keyword arguments to pass to the tool |
68 |
69 | **Raises:**
70 |
71 | | Type | Default
| Description |
72 | | ------------- | :----------------: | :----------------------------------------------------------------------------------------|
73 | | NotImplementedError | | If the tool has no implementation |
74 |
75 | ### `method` get_declaration
76 |
77 | ```python
78 | get_declaration() -> dict
79 | ```
80 |
81 | Get the declaration of the tool for the AI model
82 |
83 | **Returns:**
84 |
85 | | Type | Default
| Description |
86 | | ------------- | :----------------: | :----------------------------------------------------------------------------------------|
87 | | dict | | Tool declaration formatted for the AI model |
88 |
89 | **Raises:**
90 |
91 | | Type | Default
| Description |
92 | | ------------- | :----------------: | :----------------------------------------------------------------------------------------|
93 | | NotImplementedError | | If the tool has no implementation |
94 |
95 |
96 |
97 | ## FunctionTool
98 |
99 | A concrete implementation of BaseTool that wraps Python functions.
100 |
101 | ### `attr` func
102 |
103 | The function wrapped by this tool
104 |
105 | ```python
106 | func: Callable = None
107 | ```
108 |
109 | ### `method` __init__
110 |
111 | ```python
112 | __init__(name: str, description: str, parameters: dict, func: Callable = None)
113 | ```
114 |
115 | Initialize a new FunctionTool instance
116 |
117 | **Parameters:**
118 |
119 | | Param | Default
| Description |
120 | | ------------- | :----------------: | :----------------------------------------------------------------------------------------|
121 | | name | | The name of the tool |
122 | | description | | The description of what the tool does |
123 | | parameters | | Dictionary containing the tool's parameters |
124 | | func | None | The callable function to wrap |
125 |
126 | ### `method` __call__
127 |
128 | ```python
129 | __call__(*args, **kwargs)
130 | ```
131 |
132 | Execute the wrapped function with the given arguments
133 |
134 | **Parameters:**
135 |
136 | | Param | Default
| Description |
137 | | ------------- | :----------------: | :----------------------------------------------------------------------------------------|
138 | | *args | | Positional arguments to pass to the function |
139 | | **kwargs | | Keyword arguments to pass to the function |
140 |
141 | **Returns:**
142 |
143 | | Type | Default
| Description |
144 | | ------------- | :----------------: | :----------------------------------------------------------------------------------------|
145 | | Any | | The return value of the wrapped function |
146 |
147 | **Raises:**
148 |
149 | | Type | Default
| Description |
150 | | ------------- | :----------------: | :----------------------------------------------------------------------------------------|
151 | | NotImplementedError | | If no function is set |
152 |
153 | ### `method` get_declaration
154 |
155 | ```python
156 | get_declaration() -> dict
157 | ```
158 |
159 | Get the declaration of the tool formatted for the AI model
160 |
161 | **Returns:**
162 |
163 | | Type | Default
| Description |
164 | | ------------- | :----------------: | :----------------------------------------------------------------------------------------|
165 | | dict | | Tool declaration using litellm's function_to_dict format |
166 |
167 |
168 |
169 | ## @tool Decorator
170 |
171 | The main decorator for creating tools in Dasshh.
172 |
173 | ### `decorator` tool
174 |
175 | ```python
176 | @tool
177 | def your_function():
178 | pass
179 | ```
180 |
181 | Convert a function into a tool and register it with Dasshh
182 |
183 | **Usage:**
184 |
185 | ```python
186 | from dasshh.core.tools.decorator import tool
187 | from typing import Dict
188 |
189 | @tool
190 | def hello_world(name: str = "World") -> Dict:
191 | """
192 | A simple greeting tool.
193 |
194 | Args:
195 | name (str, optional): The name to greet. Defaults to "World".
196 |
197 | Returns:
198 | Dict: A dictionary containing the greeting message.
199 | """
200 | return {"message": f"Hello, {name}!"}
201 | ```
202 |
203 | **Parameters:**
204 |
205 | | Param | Default
| Description |
206 | | ------------- | :----------------: | :----------------------------------------------------------------------------------------|
207 | | func | | The function to convert into a tool |
208 |
209 | **Returns:**
210 |
211 | | Type | Default
| Description |
212 | | ------------- | :----------------: | :----------------------------------------------------------------------------------------|
213 | | FunctionTool | | A FunctionTool instance wrapping the original function |
214 |
215 | **Notes:**
216 |
217 | - The decorator automatically extracts the function name as the tool name
218 | - The function's docstring becomes the tool description
219 | - Function annotations are used to define tool parameters
220 | - The tool is automatically registered with the global Registry
221 |
222 | !!! tip
223 | Checkout this [guide](../guide/own-tools.md) to build your own tools.
224 |
--------------------------------------------------------------------------------
/docs/assets/actions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/docs/assets/actions.png
--------------------------------------------------------------------------------
/docs/assets/chat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/docs/assets/chat.png
--------------------------------------------------------------------------------
/docs/assets/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/docs/assets/demo.png
--------------------------------------------------------------------------------
/docs/assets/demo2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/docs/assets/demo2.gif
--------------------------------------------------------------------------------
/docs/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/docs/assets/favicon.png
--------------------------------------------------------------------------------
/docs/assets/feature1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/docs/assets/feature1.gif
--------------------------------------------------------------------------------
/docs/assets/feature2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/docs/assets/feature2.gif
--------------------------------------------------------------------------------
/docs/assets/feature3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/docs/assets/feature3.gif
--------------------------------------------------------------------------------
/docs/assets/feature4.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/docs/assets/feature4.gif
--------------------------------------------------------------------------------
/docs/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/docs/assets/logo.png
--------------------------------------------------------------------------------
/docs/assets/sessions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/docs/assets/sessions.png
--------------------------------------------------------------------------------
/docs/assets/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/docs/assets/settings.png
--------------------------------------------------------------------------------
/docs/assets/tools.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vgnshiyer/dasshh/a025e0672393df30a87fa7d515c3287be6cf0361/docs/assets/tools.gif
--------------------------------------------------------------------------------
/docs/contributing/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing to dasshh
2 |
3 | Thank you for your interest in contributing to dasshh! 🎉
4 |
5 | This guide will help you get started with contributing to the project.
6 |
7 | ## Table of Contents
8 |
9 | - [Code of Conduct](#code-of-conduct)
10 | - [Getting Started](#getting-started)
11 | - [Development Setup](#development-setup)
12 | - [Development Workflow](#development-workflow)
13 | - [Testing](#testing)
14 | - [Code Style](#code-style)
15 | - [Pull Request Process](#pull-request-process)
16 | - [Issue Reporting](#issue-reporting)
17 | - [Documentation](#documentation)
18 |
19 | ## Code of Conduct
20 |
21 | By participating in this project, you agree to abide by our Code of Conduct. Please be respectful and constructive in all interactions.
22 |
23 | ## Getting Started
24 |
25 | ### Prerequisites
26 |
27 | - Python 3.13 or higher
28 | - [uv](https://docs.astral.sh/uv/) package manager
29 | - Git
30 |
31 | ### Fork and Clone
32 |
33 | 1. Fork the repository on GitHub
34 | 2. Clone your fork locally:
35 | ```bash
36 | git clone https://github.com/YOUR-USERNAME/dasshh.git
37 | cd dasshh
38 | ```
39 |
40 | ## Development Setup
41 |
42 | 1. **Install uv** (if not already installed):
43 | ```bash
44 | curl -LsSf https://astral.sh/uv/install.sh | sh
45 | ```
46 |
47 | 2. **Create and activate virtual environment**:
48 | ```bash
49 | uv venv
50 | source .venv/bin/activate # On Windows: .venv\Scripts\activate
51 | ```
52 |
53 | 3. **Install development dependencies**:
54 | ```bash
55 | uv sync
56 | ```
57 |
58 | 4. **Running the application**:
59 | ```bash
60 | python -m dasshh
61 | ```
62 |
63 | ## Development Workflow
64 |
65 | ### Branch Strategy
66 |
67 | - `main` - Production-ready code
68 | - `feature/feature-name` - New features
69 | - `bugfix/issue-description` - Bug fixes
70 | - `docs/update-description` - Documentation updates
71 |
72 | ### Making Changes
73 |
74 | 1. **Create a new branch**:
75 | ```bash
76 | git checkout -b feature/your-feature-name
77 | ```
78 |
79 | 2. **Make your changes** following the project conventions
80 |
81 | 3. **Run tests locally**:
82 | ```bash
83 | uv run pytest tests/ -v --cov=dasshh --cov-report=html
84 | ```
85 |
86 | 4. **Run linting** (if configured):
87 | ```bash
88 | uv pip install ruff
89 | ruff check .
90 | ruff format .
91 | ```
92 |
93 | 5. **Commit your changes**:
94 | ```bash
95 | git add .
96 | git commit -m "feat: add your feature description"
97 | ```
98 |
99 | ### Commit Message Convention
100 |
101 | We follow conventional commit format:
102 |
103 | - `feat:` - New features
104 | - `fix:` - Bug fixes
105 | - `docs:` - Documentation changes
106 | - `test:` - Test additions or modifications
107 | - `refactor:` - Code refactoring
108 | - `chore:` - Maintenance tasks
109 |
110 | Examples:
111 | ```
112 | feat: add new command for file analysis
113 | fix: resolve issue with terminal output formatting
114 | docs: update installation instructions
115 | test: add unit tests for config parser
116 | ```
117 |
118 | ## Testing
119 |
120 | ### Running Tests
121 |
122 | ```bash
123 | # Run all tests
124 | uv run pytest
125 |
126 | # Run tests with coverage
127 | uv run pytest --cov=dasshh --cov-report=html
128 |
129 | # Run specific test file
130 | uv run pytest tests/unit/test_specific.py
131 |
132 | # Run tests in verbose mode
133 | uv run pytest -v
134 | ```
135 |
136 | ### Writing Tests
137 |
138 | - Place tests in the `tests/` directory
139 | - Use descriptive test names: `test_should_parse_config_when_valid_file_provided`
140 | - Follow AAA pattern: Arrange, Act, Assert
141 | - Mock external dependencies
142 |
143 | Example test structure:
144 | ```python
145 | def test_should_process_command_when_valid_input_provided():
146 | # Arrange
147 | command = "test command"
148 | expected_result = "processed"
149 |
150 | # Act
151 | result = process_command(command)
152 |
153 | # Assert
154 | assert result == expected_result
155 | ```
156 |
157 | ## Code Style
158 |
159 | ### Python Style Guidelines
160 |
161 | - Follow [PEP 8](https://pep8.org/)
162 | - Use type hints where appropriate
163 | - Write docstrings for public functions and classes
164 | - Keep functions small and focused
165 | - Use descriptive variable names
166 |
167 | ### Code Formatting
168 |
169 | We recommend using `ruff` for formatting:
170 |
171 | ```bash
172 | # Install ruff
173 | uv pip install ruff
174 |
175 | # Format code
176 | ruff format .
177 |
178 | # Check for issues
179 | ruff check .
180 | ```
181 |
182 | ## Pull Request Process
183 |
184 | 1. **Ensure your branch is up to date**:
185 | ```bash
186 | git checkout main
187 | git pull origin main
188 | git checkout your-feature-branch
189 | git rebase main
190 | ```
191 |
192 | 2. **Push your branch**:
193 | ```bash
194 | git push origin your-feature-branch
195 | ```
196 |
197 | 3. **Create a Pull Request** on GitHub with:
198 | - Clear title and description
199 | - Reference any related issues
200 | - Include screenshots/demos if relevant
201 | - Ensure all CI checks pass
202 |
203 | 4. **Address review feedback** promptly and respectfully
204 |
205 | 5. **Squash commits** if requested before merging
206 |
207 | ## Issue Reporting
208 |
209 | When reporting issues, please include:
210 |
211 | - **Environment details**: OS, Python version, dasshh version
212 | - **Steps to reproduce** the issue
213 | - **Expected vs actual behavior**
214 | - **Error messages** or logs
215 | - **Screenshots** if relevant
216 |
217 | ## Documentation
218 |
219 | ### Building Documentation
220 |
221 | ```bash
222 | # Install documentation dependencies
223 | uv pip install mkdocs-material
224 |
225 | # Serve documentation locally
226 | mkdocs serve
227 |
228 | # Build documentation
229 | mkdocs build
230 | ```
231 |
232 | ## Getting Help
233 |
234 | - Check the [documentation](https://blog.vgnshiyer.dev/dasshh)
235 | - [Open an issue](https://github.com/vgnshiyer/dasshh/issues) for bugs
236 | - [Start a discussion](https://github.com/vgnshiyer/dasshh/discussions) for questions
237 | - Contact maintainers at vgnshiyer@gmail.com or via [LinkedIn](https://www.linkedin.com/in/vgnshiyer/)
238 |
239 | ## Recognition
240 |
241 | Contributors will be acknowledged in:
242 | - Release notes
243 | - GitHub contributors page
244 |
245 | Thank you for contributing to dasshh!
--------------------------------------------------------------------------------
/docs/getting-started/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | Dasshh can be installed on MacOS, Linux and Windows using various package managers. Choose the one that works best for you.
4 |
5 | ## Using `uv` (Recommended)
6 |
7 | If you haven't tried [uv](https://github.com/astral-sh/uv) yet, it's one of those *"I will rewrite this in rust"* projects that is actually awesome. Highly recommend trying it out.
8 |
9 | You can install `uv` using `brew` if you are on macOS:
10 |
11 | ```bash
12 | # Install uv on macOS
13 | brew install uv
14 | ```
15 |
16 | Or using `curl`.
17 |
18 | ```bash
19 | curl -LsSf https://astral.sh/uv/install.sh | sh
20 | ```
21 |
22 | Install `dasshh` using `uv`:
23 |
24 | ```bash
25 | uv tool install dasshh
26 | ```
27 |
28 | ## Using `pipx`
29 |
30 | You can also use [pipx](https://pypa.github.io/pipx/) to install Dasshh.
31 |
32 | ```bash
33 | # Install pipx if you haven't already
34 | pip install --user pipx
35 | pipx ensurepath
36 |
37 | # Install dasshh with pipx
38 | pipx install dasshh
39 | ```
40 |
41 | ## Verifying Installation
42 |
43 | After installation, you should be able to run Dasshh from your terminal:
44 |
45 | ```bash
46 | dasshh --version
47 | ```
48 |
49 | If the installation was successful, you should see the version of Dasshh printed to the terminal.
50 |
--------------------------------------------------------------------------------
/docs/getting-started/introduction.md:
--------------------------------------------------------------------------------
1 | # Welcome to Dasshh!
2 |
3 | ## What is Dasshh?
4 |
5 | Dasshh is an open-source terminal-based AI assistant that allows you to interact with your computer using natural language. It's designed to save your cognitive energy by enabling you to accomplish repetitive tasks on your computer with simple, conversational commands.
6 |
7 | Instead of remembering arcane command-line syntax or navigating complex directory structures, Dasshh lets you describe what you want to do in plain English, and it handles the technical details for you.
8 |
9 | Moreover, you can add your own tools to Dasshh to make it even more powerful, allowing it to interact with your applications and services.
10 |
11 | ## Technical Foundation
12 |
13 | Dasshh is built using several powerful Python libraries:
14 |
15 | - [**Textual**](https://textual.textualize.io/) - The modern TUI (Text User Interface) framework that powers Dasshh's beautiful terminal interface
16 | - [**LiteLLM**](https://docs.litellm.ai/) - Provides the AI model connection layer, making it easy to work with various LLM providers
17 | - [**SQLAlchemy**](https://www.sqlalchemy.org/) - Manages persistent data storage for chat history and application state
18 | - [**Click**](https://click.palletsprojects.com/) - Powers the command-line interface for Dasshh
19 |
20 | ## Development Status
21 |
22 | Dasshh is currently in early development. While it already offers valuable functionality, we're actively working on expanding its capabilities, improving stability, and enhancing the user experience.
23 |
24 | We welcome contributions, suggestions, and feedback from the community to help shape the future of Dasshh.
25 |
26 | ## Getting Started
27 |
28 | Ready to try Dasshh? Head over to the [Installation](/dasshh/getting-started/installation) guide to get started, or check out the [Usage Guide](/dasshh/getting-started/quick-start) to learn how to make the most of your new terminal AI assistant.
29 |
--------------------------------------------------------------------------------
/docs/getting-started/quick-start.md:
--------------------------------------------------------------------------------
1 | # Quick Start
2 |
3 | This little guide will help you get started with Dasshh.
4 |
5 | ## Configure a Model
6 |
7 | Dasshh uses LLM models via the [litellm](https://docs.litellm.ai/docs/providers) library, which provides a unified interface to various AI providers.
8 |
9 | Setup your config file by running:
10 |
11 | ```bash
12 | dasshh init-config
13 | ```
14 |
15 | This will create a config file at `~/.dasshh/config.yaml`.
16 |
17 | To get started, you need to set the model you want to use.
18 |
19 | 1. `model.name`: The model provider and model name.
20 | 2. `model.api_key`: Your API key for the chosen provider.
21 |
22 | **Example:**
23 |
24 | ```yaml
25 | model:
26 | name: gemini/gemini-2.0-flash
27 | api_key:
28 | ```
29 |
30 | You can also set the model name and API key in the settings menu.
31 |
32 |
33 |
34 | !!! tip
35 | Checkout the list of supported models and providers [here](https://docs.litellm.ai/docs/providers).
36 |
37 | ## Launch Dasshh
38 |
39 | Once you have configured the model and your API key, you can launch Dasshh by running:
40 |
41 | ```bash
42 | dasshh
43 | ```
44 |
45 | This will open the Dasshh interface in your terminal.
46 |
47 | ## Basic Interaction
48 |
49 | Dasshh provides a conversational interface to interact with your computer.
50 |
51 |
52 |
53 | You can:
54 |
55 | 1. Ask questions
56 | 2. Request information about your system
57 | 3. Execute commands on your behalf
58 |
59 | to name a few.
60 |
61 | More capabilities will be added in the future, across different applications. If you have any suggestions for new tools, please [open an issue](https://github.com/vgnshiyer/dasshh/issues).
62 |
63 | ### Example Questions
64 |
65 | Here are some examples of what you can ask Dasshh to do:
66 |
67 | ```
68 | # Ask for information
69 | 1. What's the current CPU usage?
70 | 2. Show me the top memory-intensive processes
71 |
72 | # File operations
73 | 1. List files in my downloads folder
74 | 2. Create a new directory called "projects" in the current directory
75 | ```
76 |
77 | ## Terminating Dasshh
78 |
79 | To exit Dasshh, you can press `Ctrl+C` to terminate the application.
80 |
--------------------------------------------------------------------------------
/docs/guide/abilities.md:
--------------------------------------------------------------------------------
1 | # Abilities
2 |
3 | Dasshh supports various tools to interact with your system using natural language commands.
4 |
5 | More tools will be added in future versions. Please feel free to drop your suggestions [here](https://github.com/vgnshiyer/dasshh/issues).
6 |
7 | !!! note
8 | Permission based tool execution is currently under development. As of now, Dasshh will perform a tool call if it seems appropriate.
9 |
10 | ## Available Tools
11 |
12 | In the current version of Dasshh, the following tools are available:
13 |
14 | ### System Information
15 |
16 | These tools provide information about your system's hardware and software:
17 |
18 | | Tool Name | Description | Example Commands |
19 | |-----------|-------------|-----------------|
20 | | `system_info` | Get detailed information about your operating system | "What OS am I running?" |
21 | | `cpu_info` | Get information about CPU and its current usage | "Show my CPU usage" |
22 | | `memory_info` | Get memory (RAM) information and usage | "How much RAM do I have available?" |
23 | | `disk_info` | Get disk space information for a specified path | "How much disk space is left on my main drive?" |
24 | | `network_info` | Get network interface information | "Show my network interface details" |
25 |
26 | ### Process Management
27 |
28 | These tools help you manage running processes on your system:
29 |
30 | | Tool Name | Description | Example Commands |
31 | |-----------|-------------|-----------------|
32 | | `process_list` | List all running processes with basic information | "Show all running processes" |
33 | | `find_process` | Find processes matching a specific name pattern | "Find all Chrome processes" |
34 | | `get_process_info` | Get detailed information about a specific process by PID | "Give me details about process 1234" |
35 | | `kill_process` | Terminate a process by its PID | "Kill process 1234" |
36 | | `run_command` | Run a shell command and return its output | "Run ls -la" |
37 |
38 | ### File Operations
39 |
40 | These tools help you manage files and directories:
41 |
42 | | Tool Name | Description | Example Commands |
43 | |-----------|-------------|-----------------|
44 | | `current_directory` | Get the current working directory | "What's my current directory?" |
45 | | `list_files` | List all files and directories in a specified directory | "List files in ~/Downloads" |
46 | | `file_info` | Get detailed information about a file or directory | "Tell me about file.txt" |
47 | | `read_file` | Read the contents of a file | "Read config.json" |
48 | | `create_directory` | Create a new directory at the specified path | "Create a directory called 'projects'" |
49 | | `delete_file` | Delete a file or directory | "Delete file.txt" |
50 | | `copy_file` | Copy a file or directory from source to destination | "Copy file.txt to ~/backup/" |
51 | | `move_file` | Move a file or directory from source to destination | "Move file.txt to ~/archive/" |
52 |
53 | ### Network Tools
54 |
55 | These tools help you with network-related tasks:
56 |
57 | | Tool Name | Description | Example Commands |
58 | |-----------|-------------|-----------------|
59 | | `ping` | Ping a host and return the results | "Ping google.com" |
60 | | `get_ip_address` | Get IP address for a hostname or the local machine | "What's my IP address?" |
61 | | `check_port` | Check if a specific port is in use on localhost | "Is port 8080 in use?" |
62 | | `public_ip` | Get the public IP address of the current machine | "What's my public IP?" |
63 | | `traceroute` | Perform a traceroute to a host | "Run traceroute to github.com" |
64 |
65 | !!! note
66 | Based on your query, Dasshh can use multiple tools together to perform complex tasks.
--------------------------------------------------------------------------------
/docs/guide/basics.md:
--------------------------------------------------------------------------------
1 | # Basics
2 |
3 | Dasshh is a terminal-based AI assistant that helps you interact with your computer using natural language. Here's what you need to know to get started.
4 |
5 | ## Core Components
6 |
7 | ### Chat Interface
8 |
9 | The main component you'll interact with is the chat interface, which allows you to:
10 |
11 | With a focus on simplicity, Dasshh's chat interface is divided into three main panels:
12 |
13 |
14 |
15 | ### Sessions Panel
16 |
17 | The Sessions Panel helps manage your conversation history.
18 |
19 |
20 |
21 | - Displays all your previous chat sessions
22 | - Allows you to switch between different conversations
23 | - Shows your last message on the session card
24 | - Lets you create new sessions or delete existing ones
25 |
26 | ### Chat Panel
27 |
28 | The Chat Panel is the main area where your conversation happens.
29 |
30 |
31 |
32 | - Shows the conversation history between you and the assistant
33 | - Displays your messages and the assistant's responses
34 | - Includes an input box at the bottom where you type your messages
35 |
36 | ### Actions Panel
37 |
38 | The Actions Panel shows detailed information about tools used by the assistant.
39 |
40 |
41 |
42 | - Lists all the tools called during the current session
43 | - Shows the parameters passed to each tool
44 | - Displays the results returned by each tool
45 |
46 | ## AI Assistant
47 |
48 | The AI assistant is the brain of Dasshh that:
49 |
50 | - Understands your requests
51 | - Provides helpful responses
52 | - Determines when to use tools to accomplish tasks
53 |
54 | !!! tip
55 | You can update the default `system_prompt` in the config file to change the behavior of the AI assistant.
56 |
57 | ## Tools
58 |
59 | Tools are nothing but the functions that the AI assistant can use to perform actions on your computer. You can find the list of tools [here](abilities.md).
60 |
61 |
62 |
63 | Additionally, Dasshh allows you to build your own tools. You can find the guide [here](own-tools.md).
64 |
65 | ### Requesting a new tool
66 |
67 | If you have a tool you'd like to see added to Dasshh, please [open an issue](https://github.com/vgnshiyer/dasshh/issues/new?template=tool_request.md) with your request.
68 |
69 | **Future Plans**
70 |
71 | - MCP support
72 | - API call tools
73 |
74 | ## How It Works
75 |
76 | 1. You type a request in natural language
77 | 2. The AI assistant interprets what you want to do
78 | 3. If needed, the assistant uses tools to perform actions on your computer
79 | 4. Results are displayed in the chat interface
80 |
81 | ## Example Interactions
82 |
83 | ```bash
84 | # system information
85 | What's my current CPU and memory usage?
86 |
87 | # file operations
88 | Create a new folder called "projects".
89 |
90 | # process management
91 | Which process is using the most memory?
92 | ```
93 |
94 | ## Getting Help
95 |
96 | If you need help while using Dasshh, simply ask questions like `What can you do?`.
97 |
98 |
99 |
100 | The assistant will guide you through the available features and capabilities.
101 |
102 | ## Important Paths
103 |
104 | | Path | Description |
105 | |------|-------------|
106 | | `~/.dasshh/config.yaml` | Dasshh configuration file |
107 | | `~/.dasshh/db/dasshh.db` | Dasshh database |
108 | | `~/.dasshh/logs/dasshh.log` | Dasshh logs file |
--------------------------------------------------------------------------------
/docs/guide/configuration.md:
--------------------------------------------------------------------------------
1 | # Configuration
2 |
3 | Below is a list of available configuration options.
4 |
5 | ## Initial Setup
6 |
7 | The default configuration file is located at `~/.dasshh/config.yaml`.
8 |
9 | Below command helps you initialize the configuration file.
10 |
11 | ```bash
12 | dasshh init-config
13 | ```
14 |
15 | ## Configuration Options
16 |
17 | ### Dasshh Configuration
18 |
19 | | Option | Description |
20 | |--------|-------------|
21 | | `skip_summarization` | Skip summarization of tool call results |
22 | | `system_prompt` | The system prompt for the assistant |
23 | | `theme` | The theme to use for the UI |
24 | | `tool_directories` | The directories to search for tools |
25 |
26 | ### Model Configuration
27 |
28 | | Option | Description |
29 | |--------|-------------|
30 | | `model.name` | The model provider and model name |
31 | | `model.api_key` | Your API key for the chosen provider |
32 | | `model.api_base` | The base URL for the chosen provider |
33 | | `model.api_version` | The version of the API to use |
34 | | `model.temperature` | The temperature for the model |
35 | | `model.top_p` | The top-p value for the model |
36 | | `model.max_tokens` | The maximum number of tokens to generate |
37 | | `model.max_completion_tokens` | The maximum number of tokens to generate for the completion |
38 |
39 | ## Example Configuration File
40 |
41 | The configuration file is a YAML file with the following structure.
42 |
43 | ```yaml
44 | # Dasshh configuration
45 | dasshh:
46 | skip_summarization: false
47 | system_prompt: |
48 | You are a helpful assistant that can help with tasks on the system.
49 | Your goal is to save user's time by performing tasks on their behalf.
50 | theme: lime
51 | tool_directories:
52 | - /Users/viiyer/repos/dasshh/dasshh/apps
53 |
54 | # Model configuration
55 | model:
56 | name: gemini/gemini-2.0-flash
57 | api_base:
58 | api_key:
59 | api_version:
60 | temperature: 1.0
61 | top_p: 1.0
62 | max_tokens: 1000
63 | max_completion_tokens: 1000
64 | ```
65 |
66 | ## Supported Models
67 |
68 | Dasshh supports a variety of models through [LiteLLM](https://docs.litellm.ai/docs/providers).
69 |
70 | ### Format for specifying a model
71 |
72 | ```yaml
73 | model:
74 | name: /
75 | api_key:
76 | ```
--------------------------------------------------------------------------------
/docs/guide/keybindings.md:
--------------------------------------------------------------------------------
1 | # Keybindings
2 |
3 | ## Current Keybindings
4 |
5 | Dasshh currently supports a minimal set of keyboard shortcuts:
6 |
7 | | Key Combination | Action |
8 | | --------------- | ------ |
9 | | Ctrl+c | Quit the application |
10 | | Ctrl+t | Toggle theme |
11 |
12 | ## Future Plans
13 |
14 | More customizable keybindings are currently in development and will be added in future releases. These will include:
15 |
16 | - Navigation shortcuts
17 | - Message editing shortcuts
18 | - Session management shortcuts
19 | - Vim based text interactions
20 | - Customizable user-defined keybindings
21 |
22 | Stay tuned for updates as we continue to enhance the keyboard navigation experience in Dasshh.
23 |
24 | If you have suggestions for specific keybindings you'd like to see implemented, please [open an issue](https://github.com/vgnshiyer/dasshh/issues) on our GitHub repository.
25 |
--------------------------------------------------------------------------------
/docs/guide/own-tools.md:
--------------------------------------------------------------------------------
1 | # Build your own tools
2 |
3 | Dasshh allows you to extend its capabilities by creating your own custom tools. This guide will walk you through the process of building, testing, and integrating your own tools into Dasshh.
4 |
5 | ## Tool basics
6 |
7 | In Dasshh, a tool is a Python function decorated with the `@tool` decorator. When you create a tool, it's automatically registered with Dasshh's tool registry, making it available for the AI assistant to use in conversations.
8 |
9 | ## Creating a simple tool
10 |
11 | Let's create a simple "hello world" tool to understand the basics:
12 |
13 | 1. Create a new Python file in your desired location (we'll use a custom directory for this example)
14 | 2. Define a function with proper type annotations and docstring
15 | 3. Decorate it with the `@tool` decorator
16 |
17 | Here's an example:
18 |
19 | ```python
20 | from dasshh.core.tools.decorator import tool
21 | from typing import Dict
22 |
23 | @tool
24 | def hello_world(name: str = "World") -> Dict:
25 | """
26 | A simple greeting tool that says hello to the provided name.
27 |
28 | Args:
29 | name (str, optional): The name to greet. Defaults to "World".
30 |
31 | Returns:
32 | Dict: A dictionary containing the greeting message.
33 | """
34 | return {
35 | "message": f"Hello, {name}!"
36 | }
37 | ```
38 |
39 | !!! note
40 | Functions must return a python dictionary.
41 |
42 | ## Tool components
43 |
44 | Every tool can have the following components:
45 |
46 | 1. **Function Name**: This becomes the tool's name in the registry (e.g., `hello_world`)
47 | 2. **Docstring**: This becomes the tool's description, explaining what it does
48 | 3. **Type Annotations**: Define the input parameters and return type
49 | 4. **Implementation**: The actual code that executes when the tool is called
50 |
51 | ## Setting up your custom tools directory
52 |
53 | To organize your custom tools, it's best to create a dedicated directory structure like this:
54 |
55 | ```
56 | my_dasshh_tools/
57 | ├── __init__.py
58 | └── weather/
59 | ├── __init__.py
60 | └── weather_tools.py
61 | ```
62 |
63 | ### Step 1: Create your directory structure
64 |
65 | ```bash
66 | mkdir -p my_dasshh_tools/weather
67 | touch my_dasshh_tools/__init__.py
68 | touch my_dasshh_tools/weather/__init__.py
69 | touch my_dasshh_tools/weather/weather_tools.py
70 | ```
71 |
72 | ### Step 2: Add your tools
73 |
74 | For example, in `my_dasshh_tools/weather/weather_tools.py`:
75 |
76 | ```python
77 | from typing import Dict
78 | from dasshh.core.tools.decorator import tool
79 |
80 | @tool
81 | def get_weather(city: str) -> Dict:
82 | """
83 | Get the current weather for a specified city.
84 |
85 | Args:
86 | city (str): The name of the city
87 |
88 | Returns:
89 | Dict: Weather information for the specified city
90 | """
91 | if city == "San Francisco":
92 | return {
93 | "temperature": 15,
94 | "description": "sunny"
95 | }
96 | else:
97 | return {
98 | "temperature": None,
99 | "description": "Weather not available for this city"
100 | }
101 | ```
102 |
103 | !!! tip
104 | Don't worry about your interpreter complaining about `dasshh.core.tools.decorator`. When you run dasshh from the global binary, it will be available during runtime.
105 |
106 | ### Step 3: Import your tools in the `__init__.py` files
107 |
108 | In `my_dasshh_tools/__init__.py`:
109 |
110 | ```python
111 | from .weather.weather_tools import myweather
112 |
113 | __all__ = ['myweather']
114 | ```
115 |
116 | The `__init__.py` inside the `weather/` directory can be empty. It is only used to indicate that the `weather/` directory is a package.
117 |
118 | ## Registering your tools with Dasshh
119 |
120 | To make your tools available to Dasshh, you need to add your custom directory to Dasshh's configuration.
121 |
122 | 1. Open your Dasshh configuration file at `~/.dasshh/config.yaml`
123 | 2. Add your custom directory to the `tool_directories` list.
124 |
125 | ```yaml
126 | dasshh:
127 | skip_summarization: false
128 | system_prompt: |
129 | You are a helpful assistant that can help with tasks on the system.
130 | tool_directories:
131 | - /path/to/dasshh/apps # Default tools directory
132 | - /path/to/my_dasshh_tools # Your custom tools directory
133 | ```
134 |
135 | ## Tool design best practices
136 |
137 | When creating your tools, follow these best practices:
138 |
139 | 1. **Clear Names**: Use descriptive function names that indicate what the tool does
140 | 2. **Detailed Docstrings**: Write clear descriptions of what the tool does, including parameter explanations
141 | 3. **Proper Type Annotations**: Use Python type hints for all parameters and return values
142 | 4. **Error Handling**: Handle exceptions gracefully and return informative error messages
143 | 5. **Return Structured Data**: Always return dictionaries or similar structured data that's easy to parse
144 | 6. **Keep It Focused**: Each tool should do one thing well rather than many things
145 |
146 | ## Testing tool discovery
147 |
148 | To test if your tools are being discovered, ask Dasshh to list all tools.
149 |
150 |
151 |
152 | If you see your tools listed, you're good to go!
153 |
154 | ## Debugging your tools
155 |
156 | If your tool isn't working as expected within Dasshh:
157 |
158 | 1. Check the Dasshh logs at `~/.dasshh/logs/dasshh.log`
159 | 2. Ensure your tool directory is correctly listed in the configuration
160 | 3. Verify that your tool is being imported correctly
161 | 4. Check that your function signatures and type annotations are correct
162 |
163 | ## Understanding how tools are registered
164 |
165 | When you decorate a function with `@tool`, the following happens:
166 |
167 | 1. A `FunctionTool` instance is created with the function's name, docstring, and type annotations
168 | 2. This tool is added to the global `Registry`
169 | 3. When Dasshh starts, it scans all directories listed in `tool_directories` and imports them
170 | 4. During import, the decorators run and register all tools
171 | 5. The assistant can then access this registry to use the tools
172 |
173 | Now you're ready to create your own custom tools for Dasshh!
174 |
175 | ## Additional Resources
176 |
177 | - [Function Calling best practices (OpenAI)](https://platform.openai.com/docs/guides/function-calling?api-mode=responses)
178 |
--------------------------------------------------------------------------------
/docs/guide/themes.md:
--------------------------------------------------------------------------------
1 | # Themes
2 |
3 | Dasshh supports multiple themes.
4 |
5 | ## Creating a custom theme
6 |
7 | A theme is a simple Python object that inherits from `textual.theme.Theme`.
8 |
9 | ```python
10 | from textual.theme import Theme
11 |
12 | my_theme = Theme(
13 | name="my_theme",
14 | primary="#76ff03",
15 | secondary="#64dd17",
16 | accent="#4caf50",
17 | foreground="#f0f0f0",
18 | background="#101a10",
19 | success="#b9f6ca",
20 | warning="#ffff8d",
21 | error="#ff8a80",
22 | surface="#162316",
23 | panel="#1e2a1e",
24 | dark=True,
25 | variables={
26 | "footer-key-foreground": "#76ff03",
27 | "input-selection-background": "#64dd1740",
28 | "block-cursor-background": "#76ff03",
29 | "block-cursor-foreground": "#101a10",
30 | "border": "#76ff03",
31 | "scrollbar": "#4caf50",
32 | "scrollbar-hover": "#64dd17",
33 | "scrollbar-active": "#76ff03",
34 | "link-color": "#9cff57",
35 | "link-color-hover": "#76ff03",
36 | },
37 | )
38 | ```
39 |
40 | ## Registering a theme
41 |
42 | You can register a theme to Dasshh by overriding the `startup` method in your `App` subclass.
43 |
44 | ```python
45 | from dasshh.ui.app import Dasshh
46 |
47 | class MyApp(Dasshh):
48 | def startup(self):
49 | super().startup()
50 | self.register_theme(my_theme)
51 | self.theme = "my_theme"
52 | ```
53 |
54 | Once you have registered a theme, you can toggle between themes using the `Ctrl+t` keybinding.
55 |
56 | !!! tip
57 | For more information on theme variables, see the [Textual documentation](https://textual.textualize.io/guide/design/#theme-variables).
58 |
59 | ## Requesting a new theme
60 |
61 | If you have a theme you'd like to see added to Dasshh, please [open an issue](https://github.com/vgnshiyer/dasshh/issues/new?template=theme_request.md) with your request.
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Dasshh
2 |
3 | An AI Agent on your terminal, to preserve your brain juice.
4 |
5 | Dasshh is an open source tui-application built with [textual](https://textual.textual.sh/) that allows you to interact with your computer using natural language.
6 |
7 |
8 |
9 |
10 | !!! note
11 | This project is still in early development. Suggestions and contributions are welcome!
12 |
13 | Features
14 |
15 |
16 |
17 |
18 |
Natural Language Computer Control
19 |
20 |
21 |

22 |
23 |
24 | - Execute system commands with plain English
25 | - Manage files and directories conversationally
26 | - Get real-time system information
27 | - Automate repetitive tasks
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
Interactive & Minimal Chat UI
36 |
37 |
38 |

39 |
40 |
41 | - Clean, distraction-free interface
42 |
43 | - Syntax highlighting for code snippets
44 | - Multiple themes to choose from
45 | - Persistent chat memory across sessions
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
Extensible Tool System
54 |
55 |
56 |

57 |
58 |
59 | - Add custom tools easily
60 | - Integrate with your existing workflows
61 | - MCP Support (Coming Soon)
62 | - Share tools with the community
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
Terminal-Native Experience
71 |
72 |
73 |

74 |
75 |
76 | - Works where you already work - your terminal
77 | - Lightweight and fast
78 | - Cross-platform compatibility
79 | - No GUI dependencies
80 |
81 |
82 |
83 |
84 | Get Involved
85 |
86 | Dasshh is evolving rapidly and your feedback is essential to its development. The future direction of this project will be heavily influenced by community input.
87 |
88 | How to Contribute
89 |
90 | - [Try it out](https://github.com/vgnshiyer/dasshh/releases) and share your experience
91 | - [Spread the word](https://twitter.com/intent/tweet?text=Check%20out%20Dasshh%20-%20an%20AI%20Agent%20for%20your%20terminal!%20%F0%9F%97%B2%20&url=https%3A%2F%2Fgithub.com%2Fvgnshiyer%2Fdasshh) about Dasshh
92 | - [Suggest features](https://github.com/vgnshiyer/dasshh/issues/new?template=feature_request.md) or [report bugs](https://github.com/vgnshiyer/dasshh/issues/new?template=bug_report.md)
93 | - [Request a new tool](https://github.com/vgnshiyer/dasshh/issues/new?template=tool_request.md)
94 | - [Share ideas](https://github.com/vgnshiyer/dasshh/discussions) for new tools and integrations
95 | - [Start a discussion](https://github.com/vgnshiyer/dasshh/discussions) for questions
96 |
97 | Join us in building an AI assistant that truly understands how developers work in the terminal!
98 |
99 |
124 |
125 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Dasshh
2 | site_description: An AI Agent on your terminal, to preserve your brain juice
3 | site_url: https://vgnshiyer.github.io/dasshh/
4 | repo_url: https://github.com/vgnshiyer/dasshh
5 | repo_name: vgnshiyer/dasshh
6 |
7 | theme:
8 | name: material
9 | favicon: assets/favicon.png
10 | logo: assets/logo.png
11 | font:
12 | text: Roboto Mono
13 | code: Roboto Mono
14 | palette:
15 | # Dark mode (default)
16 | - scheme: slate
17 | primary: light green
18 | accent: lime
19 |
20 | features:
21 | - navigation.tabs
22 | - navigation.sections
23 | - navigation.path
24 | - navigation.top
25 | - navigation.footer
26 | - search.suggest
27 | - search.highlight
28 | - content.tabs.link
29 | - content.code.copy
30 | - toc.integrate
31 | - home.page
32 |
33 | # Custom CSS
34 | extra_css:
35 | - stylesheets/extra.css
36 |
37 | markdown_extensions:
38 | - admonition
39 | - pymdownx.details
40 | - pymdownx.superfences
41 | - pymdownx.highlight:
42 | anchor_linenums: true
43 | line_spans: __span
44 | pygments_lang_class: true
45 | - pymdownx.inlinehilite
46 | - pymdownx.snippets
47 | - pymdownx.tabbed:
48 | alternate_style: true
49 | - tables
50 |
51 | plugins:
52 | - search
53 |
54 | # Navigation structure
55 | nav:
56 | - "": index.md
57 | - Getting Started:
58 | - Introduction: getting-started/introduction.md
59 | - Installation: getting-started/installation.md
60 | - Quick Start: getting-started/quick-start.md
61 | - Guide:
62 | - Basics: guide/basics.md
63 | - Configuration: guide/configuration.md
64 | - Themes: guide/themes.md
65 | - Keybindings: guide/keybindings.md
66 | - Abilities: guide/abilities.md
67 | - Build your own tools: guide/own-tools.md
68 | - Contributing: contributing/contributing.md
69 | - API Reference:
70 | - Runtime: api/runtime.md
71 | - Tools: api/tools.md
72 | - Data: api/data.md
73 | - Events: api/events.md
74 | - UI: api/ui.md
75 |
76 | extra:
77 | generator: false
78 | social:
79 | - icon: fontawesome/brands/github
80 | link: https://github.com/vgnshiyer/dasshh
81 | - icon: fontawesome/brands/linkedin
82 | link: https://www.linkedin.com/in/vgnshiyer/
83 | - icon: fontawesome/solid/blog
84 | link: https://blog.vgnshiyer.dev
85 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "dasshh"
3 | version = "0.1.4"
4 | description = "An AI Agent on your terminal, to preserve your brain juice."
5 | readme = "README.md"
6 | license = { text = "MIT" }
7 | authors = [
8 | { name="Vignesh Iyer", email="vgnshiyer@gmail.com" }
9 | ]
10 | requires-python = ">=3.13"
11 | dependencies = [
12 | "click>=8.2.0",
13 | "litellm>=1.69.3",
14 | "numpydoc>=1.8.0",
15 | "psutil>=7.0.0",
16 | "sqlalchemy>=2.0.41",
17 | "textual>=3.2.0",
18 | ]
19 |
20 | [project.urls]
21 | Documentation = "https://blog.vgnshiyer.dev/dasshh"
22 | Homepage = "https://github.com/vgnshiyer/dasshh"
23 | Repository = "https://github.com/vgnshiyer/dasshh"
24 | "Bug Tracker" = "https://github.com/vgnshiyer/dasshh/issues"
25 |
26 | [build-system]
27 | requires = ["setuptools>=61.0"]
28 | build-backend = "setuptools.build_meta"
29 |
30 | [tool.pytest.ini_options]
31 | testpaths = ["tests"]
32 | python_files = "test_*.py"
33 | python_functions = "test_*"
34 | python_classes = "Test*"
35 | markers = ["asyncio: mark a test as an asyncio test"]
36 |
37 | [project.optional-dependencies]
38 | dev = [
39 | "pytest>=7.4.0",
40 | "pytest-cov>=4.1.0",
41 | "pytest-asyncio>=0.21.0",
42 | "mkdocs-material>=9.6.0",
43 | ]
44 |
45 | [tool.setuptools.packages.find]
46 | include = ["dasshh*"]
47 |
48 | [project.scripts]
49 | dasshh = "dasshh.__main__:main"
50 |
51 | [tool.coverage.run]
52 | source = ["dasshh"]
53 | omit = ["tests/*"]
54 |
55 | [tool.coverage.report]
56 | exclude_lines = [
57 | "pragma: no cover",
58 | "def __repr__",
59 | "raise NotImplementedError",
60 | "if __name__ == .__main__.:",
61 | "pass",
62 | "raise ImportError",
63 | ]
64 |
--------------------------------------------------------------------------------
/tests/README.md:
--------------------------------------------------------------------------------
1 | # Testing Dasshh
2 |
3 | This directory contains tests for Dasshh.
4 |
5 | ## Structure
6 |
7 | - `unit/`: Unit tests for individual components
8 | - `core/`: Tests for core functionality
9 | - `ui/`: Tests for UI components
10 | - `apps/`: Tests for application modules
11 | - `data/`: Tests for data models and storage
12 |
13 | ## Running Tests
14 |
15 | To run all tests:
16 |
17 | ```bash
18 | python -m pytest
19 | ```
20 |
21 | To run tests with coverage:
22 |
23 | ```bash
24 | python -m pytest --cov=dasshh
25 | ```
26 |
27 | To generate a coverage report:
28 |
29 | ```bash
30 | python -m pytest --cov=dasshh --cov-report=html
31 | ```
32 |
33 | This will create a directory called `htmlcov` with an HTML report of the coverage.
34 |
35 | ## Adding Tests
36 |
37 | When adding new functionality, please add corresponding tests. Test files should follow the naming convention `test_*.py` and test functions should be named `test_*`.
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | """Test package for dasshh."""
2 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | """
2 | Shared pytest fixtures and configuration.
3 | """
4 |
5 | import os
6 | import sys
7 | import pytest
8 |
9 | # Add the project root directory to the path so we can import the package
10 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
11 |
12 |
13 | @pytest.fixture
14 | def cli_runner():
15 | """Provide a click CLI test runner."""
16 | from click.testing import CliRunner
17 |
18 | return CliRunner()
19 |
--------------------------------------------------------------------------------
/tests/unit/core/__init__.py:
--------------------------------------------------------------------------------
1 | """Tests for the core module."""
2 |
--------------------------------------------------------------------------------
/tests/unit/core/conftest.py:
--------------------------------------------------------------------------------
1 | """
2 | Test fixtures for the core module.
3 | """
4 |
5 | import os
6 | import tempfile
7 | from unittest.mock import MagicMock, patch
8 |
9 | import pytest
10 |
11 | from dasshh.core.registry import Registry
12 | from dasshh.core.tools.base import BaseTool
13 | from dasshh.data.session import SessionService
14 |
15 |
16 | @pytest.fixture
17 | def mock_logger():
18 | """Mock logger for testing."""
19 | with patch("logging.getLogger") as mock_get_logger:
20 | with patch("logging.FileHandler"), patch("logging.StreamHandler"):
21 | mock_logger = MagicMock()
22 | mock_get_logger.return_value = mock_logger
23 | yield mock_logger
24 |
25 |
26 | @pytest.fixture
27 | def test_log_file():
28 | """Create a temporary log file."""
29 | fd, path = tempfile.mkstemp()
30 | try:
31 | yield path
32 | finally:
33 | os.close(fd)
34 | os.unlink(path)
35 |
36 |
37 | @pytest.fixture
38 | def reset_registry():
39 | """Reset the Registry singleton between tests."""
40 | Registry._instance = None
41 | Registry.tools = {}
42 | yield
43 | Registry._instance = None
44 | Registry.tools = {}
45 |
46 |
47 | @pytest.fixture
48 | def mock_tool():
49 | """Create a mock tool for testing."""
50 | tool_parameters = {
51 | "type": "object",
52 | "properties": {
53 | "test_param": {"type": "string", "description": "Test parameter"}
54 | },
55 | }
56 |
57 | class TestTool(BaseTool):
58 | def __init__(self):
59 | super().__init__(
60 | name="test_tool", description="A test tool", parameters=tool_parameters
61 | )
62 |
63 | def __call__(self, test_param=None):
64 | return {"result": f"Test result with {test_param}"}
65 |
66 | def get_declaration(self):
67 | return {
68 | "type": "function",
69 | "function": {
70 | "name": self.name,
71 | "description": self.description,
72 | "parameters": self.parameters,
73 | },
74 | }
75 |
76 | return TestTool()
77 |
78 |
79 | @pytest.fixture
80 | def mock_session_service():
81 | """Create a mock session service for testing."""
82 | return MagicMock(spec=SessionService)
83 |
--------------------------------------------------------------------------------
/tests/unit/core/test_logging.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for the logging module.
3 | """
4 |
5 | from pathlib import Path
6 | from unittest.mock import patch, MagicMock
7 |
8 | from dasshh.core.logging import get_logger, DEFAULT_LOG_DIR, DEFAULT_LOG_FILE
9 |
10 |
11 | def test_get_logger():
12 | """Test get_logger function."""
13 | with patch("logging.getLogger") as mock_get_logger:
14 | mock_logger = MagicMock()
15 | mock_get_logger.return_value = mock_logger
16 | logger = get_logger("test_logger")
17 | mock_get_logger.assert_called_once_with("test_logger")
18 | assert logger == mock_logger
19 |
20 |
21 | def test_default_log_dir_exists():
22 | """Test that the default log directory exists."""
23 | assert DEFAULT_LOG_DIR.exists()
24 | assert DEFAULT_LOG_DIR == Path.home() / ".dasshh" / "logs"
25 |
26 |
27 | def test_default_log_file_path():
28 | """Test the default log file path."""
29 | assert DEFAULT_LOG_FILE == DEFAULT_LOG_DIR / "dasshh.log"
30 |
--------------------------------------------------------------------------------
/tests/unit/core/test_registry.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for the registry module.
3 | """
4 |
5 | import pytest
6 | from unittest.mock import Mock
7 |
8 | from dasshh.core.registry import Registry
9 | from dasshh.core.tools.base import BaseTool
10 |
11 |
12 | def test_registry_singleton(reset_registry):
13 | """Test that Registry is a singleton."""
14 | registry1 = Registry()
15 | registry2 = Registry()
16 | assert registry1 is registry2
17 |
18 | tool = Mock(spec=BaseTool)
19 | tool.name = "test_tool"
20 | registry1.add_tool(tool)
21 |
22 | assert "test_tool" in registry2.tools
23 |
24 |
25 | def test_add_tool(reset_registry, mock_tool):
26 | """Test adding a tool to the registry."""
27 | registry = Registry()
28 |
29 | registry.add_tool(mock_tool)
30 |
31 | assert mock_tool.name in registry.tools
32 | assert registry.tools[mock_tool.name] is mock_tool
33 |
34 |
35 | def test_add_duplicate_tool(reset_registry, mock_tool):
36 | """Test adding a tool with a duplicate name."""
37 | registry = Registry()
38 | registry.add_tool(mock_tool)
39 |
40 | duplicate_tool = Mock(spec=BaseTool)
41 | duplicate_tool.name = mock_tool.name
42 |
43 | with pytest.raises(
44 | ValueError,
45 | match=f"Tool name must be unique, there is already a tool named {mock_tool.name}",
46 | ):
47 | registry.add_tool(duplicate_tool)
48 |
49 |
50 | def test_get_tools(reset_registry, mock_tool):
51 | """Test getting all tools from the registry."""
52 | registry = Registry()
53 | registry.add_tool(mock_tool)
54 | tools = registry.get_tools()
55 | assert len(tools) == 1
56 | assert tools[0] is mock_tool
57 |
58 |
59 | def test_get_tool(reset_registry, mock_tool):
60 | """Test getting a specific tool by name."""
61 | registry = Registry()
62 | registry.add_tool(mock_tool)
63 | tool = registry.get_tool(mock_tool.name)
64 | assert tool is mock_tool
65 |
66 |
67 | def test_get_nonexistent_tool(reset_registry):
68 | """Test getting a tool that doesn't exist."""
69 | registry = Registry()
70 | tool = registry.get_tool("nonexistent_tool")
71 | assert tool is None
72 |
73 |
74 | def test_get_tool_declarations(reset_registry, mock_tool):
75 | """Test getting tool declarations."""
76 | registry = Registry()
77 | registry.add_tool(mock_tool)
78 | declarations = registry.get_tool_declarations()
79 | assert len(declarations) == 1
80 | assert declarations[0] == mock_tool.get_declaration()
81 |
--------------------------------------------------------------------------------
/tests/unit/core/tools/__init__.py:
--------------------------------------------------------------------------------
1 | """Tests for the core.tools module."""
2 |
--------------------------------------------------------------------------------
/tests/unit/core/tools/test_base.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for the base tool class.
3 | """
4 |
5 | import pytest
6 |
7 | from dasshh.core.tools.base import BaseTool
8 |
9 |
10 | class TestTool(BaseTool):
11 | """Test implementation of BaseTool."""
12 |
13 | def __init__(self, name="test_tool", description="Test tool", parameters=None):
14 | parameters = parameters or {"type": "object", "properties": {}}
15 | super().__init__(name, description, parameters)
16 |
17 | def __call__(self, *args, **kwargs):
18 | return {"result": "test_result"}
19 |
20 | def get_declaration(self):
21 | return {
22 | "type": "function",
23 | "function": {
24 | "name": self.name,
25 | "description": self.description,
26 | "parameters": self.parameters,
27 | },
28 | }
29 |
30 |
31 | def test_base_tool_initialization():
32 | """Test initializing a BaseTool subclass."""
33 | name = "test_tool"
34 | description = "A test tool"
35 | parameters = {"type": "object", "properties": {"test_param": {"type": "string"}}}
36 |
37 | tool = TestTool(name, description, parameters)
38 |
39 | assert tool.name == name
40 | assert tool.description == description
41 | assert tool.parameters == parameters
42 |
43 |
44 | def test_base_tool_call_not_implemented():
45 | """Test that calling BaseTool directly raises NotImplementedError."""
46 | tool = BaseTool(
47 | name="base_tool",
48 | description="Base tool for testing",
49 | parameters={"type": "object", "properties": {}},
50 | )
51 |
52 | with pytest.raises(NotImplementedError, match="This tool has no implementation"):
53 | tool()
54 |
55 |
56 | def test_base_tool_get_declaration_not_implemented():
57 | """Test that get_declaration on BaseTool raises NotImplementedError."""
58 | tool = BaseTool(
59 | name="base_tool",
60 | description="Base tool for testing",
61 | parameters={"type": "object", "properties": {}},
62 | )
63 |
64 | with pytest.raises(NotImplementedError, match="This tool has no implementation"):
65 | tool.get_declaration()
66 |
67 |
68 | def test_tool_implementation():
69 | """Test a concrete implementation of BaseTool."""
70 | tool = TestTool()
71 |
72 | result = tool()
73 | assert result == {"result": "test_result"}
74 |
75 | declaration = tool.get_declaration()
76 | assert declaration["type"] == "function"
77 | assert declaration["function"]["name"] == "test_tool"
78 | assert declaration["function"]["description"] == "Test tool"
79 | assert declaration["function"]["parameters"] == {"type": "object", "properties": {}}
80 |
--------------------------------------------------------------------------------
/tests/unit/core/tools/test_decorator.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for the tool decorator.
3 | """
4 |
5 | from unittest.mock import patch, MagicMock
6 |
7 | from dasshh.core.tools.decorator import tool
8 | from dasshh.core.tools.function_tool import FunctionTool
9 |
10 |
11 | def test_tool_decorator():
12 | """Test the @tool decorator."""
13 | mock_registry = MagicMock()
14 |
15 | def test_function(param1: str = "default") -> dict:
16 | """Test function docstring."""
17 | return {"result": param1}
18 |
19 | with patch("dasshh.core.tools.decorator.Registry", return_value=mock_registry):
20 | decorated = tool(test_function)
21 |
22 | assert isinstance(decorated, FunctionTool)
23 |
24 | mock_registry.add_tool.assert_called_once_with(decorated)
25 |
26 | assert decorated.name == "test_function"
27 | assert decorated.description == "Test function docstring."
28 | assert decorated.func == test_function
29 |
30 | assert decorated.parameters == test_function.__annotations__
31 |
32 | result = decorated(param1="test")
33 | assert result == {"result": "test"}
34 |
35 |
36 | def test_tool_decorator_integration():
37 | """Test the @tool decorator in a more realistic scenario."""
38 | with patch("dasshh.core.tools.decorator.Registry") as MockRegistry:
39 | mock_registry_instance = MagicMock()
40 | MockRegistry.return_value = mock_registry_instance
41 |
42 | @tool
43 | def example_tool(param1: str = "default") -> dict:
44 | """Example tool docstring."""
45 | return {"result": param1}
46 |
47 | assert isinstance(example_tool, FunctionTool)
48 |
49 | mock_registry_instance.add_tool.assert_called_once()
50 |
51 | result = example_tool(param1="test_value")
52 | assert result == {"result": "test_value"}
53 |
--------------------------------------------------------------------------------
/tests/unit/core/tools/test_function_tool.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for the function tool class.
3 | """
4 |
5 | import pytest
6 | from unittest.mock import patch
7 |
8 | from dasshh.core.tools.function_tool import FunctionTool
9 |
10 |
11 | def test_function_tool_initialization():
12 | """Test initializing a FunctionTool."""
13 | name = "test_function"
14 | description = "A test function"
15 | parameters = {"type": "object", "properties": {"test_param": {"type": "string"}}}
16 |
17 | def test_func(test_param=None):
18 | """A test function."""
19 | return {"result": f"Test result with {test_param}"}
20 |
21 | tool = FunctionTool(name, description, parameters, func=test_func)
22 |
23 | assert tool.name == name
24 | assert tool.description == description
25 | assert tool.parameters == parameters
26 | assert tool.func == test_func
27 |
28 |
29 | def test_function_tool_call():
30 | """Test calling a FunctionTool."""
31 |
32 | def test_func(test_param=None):
33 | """A test function."""
34 | return {"result": f"Test result with {test_param}"}
35 |
36 | tool = FunctionTool(
37 | name="test_function",
38 | description="A test function",
39 | parameters={"type": "object", "properties": {}},
40 | func=test_func,
41 | )
42 |
43 | result = tool(test_param="test_value")
44 |
45 | assert result == {"result": "Test result with test_value"}
46 |
47 |
48 | def test_function_tool_call_no_implementation():
49 | """Test calling a FunctionTool with no implementation."""
50 | tool = FunctionTool(
51 | name="test_function",
52 | description="A test function",
53 | parameters={"type": "object", "properties": {}},
54 | )
55 |
56 | with pytest.raises(NotImplementedError, match="This tool has no implementation"):
57 | tool()
58 |
59 |
60 | def test_function_tool_get_declaration():
61 | """Test getting the declaration of a FunctionTool."""
62 |
63 | # Define a simple test function
64 | def test_func(test_param=None):
65 | """A test function."""
66 | return {"result": f"Test result with {test_param}"}
67 |
68 | expected_declaration = {
69 | "name": "test_function",
70 | "description": "A test function",
71 | "parameters": {
72 | "type": "object",
73 | "properties": {
74 | "test_param": {"type": "string", "description": "Test parameter"}
75 | },
76 | },
77 | }
78 |
79 | with patch(
80 | "dasshh.core.tools.function_tool.function_to_dict",
81 | return_value=expected_declaration,
82 | ) as mock_fn_to_dict:
83 | tool = FunctionTool(
84 | name="test_function",
85 | description="A test function",
86 | parameters={"type": "object", "properties": {}},
87 | func=test_func,
88 | )
89 |
90 | declaration = tool.get_declaration()
91 |
92 | mock_fn_to_dict.assert_called_once_with(test_func)
93 |
94 | assert declaration == expected_declaration
95 |
--------------------------------------------------------------------------------
/tests/unit/data/__init__.py:
--------------------------------------------------------------------------------
1 | """Tests for the data module."""
2 |
--------------------------------------------------------------------------------
/tests/unit/data/conftest.py:
--------------------------------------------------------------------------------
1 | """
2 | Test fixtures for the data module.
3 | """
4 |
5 | import os
6 | import tempfile
7 | import pytest
8 | from sqlalchemy import create_engine
9 | from sqlalchemy.orm import sessionmaker
10 |
11 | from dasshh.data.client import Base, DBClient
12 | from dasshh.data.session import SessionService
13 |
14 |
15 | @pytest.fixture
16 | def test_db_file():
17 | """Create a temporary database file."""
18 | fd, path = tempfile.mkstemp()
19 | try:
20 | yield path
21 | finally:
22 | os.close(fd)
23 | os.unlink(path)
24 |
25 |
26 | @pytest.fixture
27 | def test_db_client(monkeypatch, test_db_file):
28 | """Create a test database client with a temporary database."""
29 |
30 | class TestDBClient(DBClient):
31 | def __init__(self, db_path):
32 | self.db_path = db_path
33 | self.engine = create_engine(f"sqlite:///{self.db_path}")
34 | self.DatabaseSessionFactory = sessionmaker(bind=self.engine)
35 | Base.metadata.create_all(bind=self.engine)
36 |
37 | client = TestDBClient(test_db_file)
38 | return client
39 |
40 |
41 | @pytest.fixture
42 | def test_session_service(test_db_client):
43 | """Create a test session service."""
44 | return SessionService(test_db_client)
45 |
--------------------------------------------------------------------------------
/tests/unit/data/test_client.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for the database client.
3 | """
4 |
5 | from sqlalchemy import Engine
6 | from sqlalchemy.orm import Session
7 |
8 | from dasshh.data.client import DBClient
9 |
10 |
11 | def test_db_client_initialization():
12 | """Test creating a DBClient instance."""
13 | client = DBClient()
14 | assert client.db_path.parent.exists()
15 | assert isinstance(client.engine, Engine)
16 | assert client.DatabaseSessionFactory is not None
17 |
18 |
19 | def test_db_client_get_db(test_db_client):
20 | """Test getting a database session."""
21 | db = test_db_client.get_db()
22 | assert db is not None
23 | assert isinstance(db, Session)
24 |
25 |
26 | def test_db_client_get_db_as_context_manager(test_db_client):
27 | """Test using the database session as a context manager."""
28 | with test_db_client.get_db() as db:
29 | assert db is not None
30 | assert isinstance(db, Session)
31 |
--------------------------------------------------------------------------------
/tests/unit/data/test_models.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for database models.
3 | """
4 |
5 | import uuid
6 | from datetime import datetime
7 |
8 | from dasshh.data.models import StorageSession, StorageEvent
9 |
10 |
11 | def test_storage_session_model():
12 | """Test the StorageSession model structure."""
13 | session_id = str(uuid.uuid4())
14 | now = datetime.now()
15 | session = StorageSession(
16 | id=session_id, detail="Test Session", created_at=now, updated_at=now
17 | )
18 |
19 | assert session.id == session_id
20 | assert session.detail == "Test Session"
21 | assert session.created_at == now
22 | assert session.updated_at == now
23 |
24 | assert hasattr(session, "events")
25 |
26 |
27 | def test_storage_event_model():
28 | """Test the StorageEvent model structure."""
29 | event_id = str(uuid.uuid4())
30 | session_id = str(uuid.uuid4())
31 | invocation_id = str(uuid.uuid4())
32 | now = datetime.now()
33 | content = {"message": "Test event"}
34 |
35 | event = StorageEvent(
36 | id=event_id,
37 | invocation_id=invocation_id,
38 | session_id=session_id,
39 | created_at=now,
40 | content=content,
41 | )
42 |
43 | assert event.id == event_id
44 | assert event.invocation_id == invocation_id
45 | assert event.session_id == session_id
46 | assert event.created_at == now
47 | assert event.content == content
48 |
--------------------------------------------------------------------------------
/tests/unit/data/test_session.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for the session service.
3 | """
4 |
5 | import uuid
6 |
7 | from dasshh.data.models import StorageSession
8 |
9 |
10 | def test_new_session(test_session_service):
11 | """Test creating a new session."""
12 | session = test_session_service.new_session(detail="Test Session")
13 | assert session is not None
14 | assert isinstance(session, StorageSession)
15 | assert session.detail == "Test Session"
16 |
17 |
18 | def test_get_session(test_session_service):
19 | """Test getting a session by ID."""
20 | session = test_session_service.new_session(detail="Test Session")
21 | retrieved_session = test_session_service.get_session(session_id=session.id)
22 | assert retrieved_session is not None
23 | assert retrieved_session.id == session.id
24 | assert retrieved_session.detail == "Test Session"
25 |
26 |
27 | def test_get_nonexistent_session(test_session_service):
28 | """Test getting a session that doesn't exist."""
29 | session = test_session_service.get_session(session_id=str(uuid.uuid4()))
30 | assert session is None
31 |
32 |
33 | def test_update_session(test_session_service):
34 | """Test updating a session."""
35 | session = test_session_service.new_session(detail="Test Session")
36 |
37 | test_session_service.update_session(
38 | session_id=session.id,
39 | detail="Updated Session",
40 | )
41 |
42 | updated_session = test_session_service.get_session(session_id=session.id)
43 |
44 | assert updated_session is not None
45 | assert updated_session.detail == "Updated Session"
46 |
47 |
48 | def test_list_sessions(test_session_service):
49 | """Test listing sessions."""
50 | session1 = test_session_service.new_session(detail="Test Session 1")
51 | session2 = test_session_service.new_session(detail="Test Session 2")
52 |
53 | sessions = test_session_service.list_sessions()
54 | assert len(sessions) >= 2
55 | assert any(s.id == session1.id for s in sessions)
56 | assert any(s.id == session2.id for s in sessions)
57 |
58 |
59 | def test_list_sessions_with_include_events(test_session_service):
60 | """Test listing sessions with include_events parameter."""
61 | session = test_session_service.new_session(detail="Test Session")
62 |
63 | invocation_id = str(uuid.uuid4())
64 | content = {"message": "Test event"}
65 | test_session_service.add_event(
66 | invocation_id=invocation_id,
67 | session_id=session.id,
68 | content=content,
69 | )
70 |
71 | sessions_with_events = test_session_service.list_sessions(include_events=True)
72 |
73 | assert len(sessions_with_events) > 0
74 | assert any(s.id == session.id for s in sessions_with_events)
75 |
76 | sessions_without_events = test_session_service.list_sessions(include_events=False)
77 | assert len(sessions_without_events) > 0
78 |
79 |
80 | def test_delete_session(test_session_service):
81 | """Test deleting a session."""
82 | # Create a new session
83 | session = test_session_service.new_session(detail="Test Session")
84 |
85 | test_session_service.delete_session(session_id=session.id)
86 |
87 | deleted_session = test_session_service.get_session(session_id=session.id)
88 |
89 | assert deleted_session is None
90 |
91 |
92 | def test_delete_nonexistent_session(test_session_service):
93 | """Test deleting a session that doesn't exist."""
94 | nonexistent_id = str(uuid.uuid4())
95 |
96 | test_session_service.delete_session(session_id=nonexistent_id)
97 |
98 |
99 | def test_get_recent_session(test_session_service):
100 | """Test getting the most recent session."""
101 | session1 = test_session_service.new_session(detail="Test Session 1")
102 | test_session_service.new_session(detail="Test Session 2")
103 |
104 | recent_session = test_session_service.get_recent_session()
105 |
106 | assert recent_session is not None
107 | assert recent_session.id == session1.id
108 |
109 |
110 | def test_add_event(test_session_service):
111 | """Test adding an event to a session."""
112 | session = test_session_service.new_session(detail="Test Session")
113 |
114 | invocation_id = str(uuid.uuid4())
115 | content = {"message": "Test event"}
116 | test_session_service.add_event(
117 | invocation_id=invocation_id,
118 | session_id=session.id,
119 | content=content,
120 | )
121 |
122 | events = test_session_service.get_events(session_id=session.id)
123 |
124 | assert len(events) == 1
125 | assert events[0].invocation_id == invocation_id
126 | assert events[0].session_id == session.id
127 | assert events[0].content == content
128 |
--------------------------------------------------------------------------------
/tests/unit/test_main.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for the main CLI entry point.
3 | """
4 |
5 | from unittest.mock import patch
6 |
7 | from dasshh.__main__ import main
8 |
9 |
10 | def test_version_option(cli_runner):
11 | """Test the --version option."""
12 | result = cli_runner.invoke(main, ["--version"])
13 | assert result.exit_code == 0
14 |
15 |
16 | @patch("dasshh.ui.app.Dasshh")
17 | def test_main_no_command(mock_dasshh, cli_runner):
18 | """Test invoking the main CLI without a subcommand."""
19 | mock_app_instance = mock_dasshh.return_value
20 | result = cli_runner.invoke(main)
21 | assert mock_dasshh.called
22 | assert mock_app_instance.run.called
23 | assert result.exit_code == 0
24 |
--------------------------------------------------------------------------------
/tests/unit/ui/__init__.py:
--------------------------------------------------------------------------------
1 | """Tests for the UI module."""
2 |
--------------------------------------------------------------------------------
/tests/unit/ui/components/__init__.py:
--------------------------------------------------------------------------------
1 | """Tests for the UI components."""
2 |
--------------------------------------------------------------------------------
/tests/unit/ui/components/chat/__init__.py:
--------------------------------------------------------------------------------
1 | """Tests for the chat components."""
2 |
--------------------------------------------------------------------------------
/tests/unit/ui/components/chat/test_action.py:
--------------------------------------------------------------------------------
1 | """Tests for the Action component."""
2 |
3 | from unittest.mock import MagicMock, patch, PropertyMock
4 |
5 | from rich.console import Group
6 | from rich.syntax import Syntax
7 | from rich.text import Text
8 |
9 | from dasshh.ui.components.chat.action import Action
10 |
11 |
12 | def test_action_initialization():
13 | """Test Action initialization."""
14 | action = Action(
15 | invocation_id="test_invocation",
16 | tool_call_id="test_tool_call",
17 | name="test_tool",
18 | args='{"param": "value"}',
19 | result='{"result": "success"}',
20 | )
21 |
22 | assert action.invocation_id == "test_invocation"
23 | assert action.tool_call_id == "test_tool_call"
24 | assert action.name == "test_tool"
25 | assert action.args == '{"param": "value"}'
26 | assert action.result == '{"result": "success"}'
27 |
28 |
29 | @patch("dasshh.ui.components.chat.action.Syntax")
30 | @patch.object(Action, "app", new_callable=PropertyMock)
31 | def test_action_render_with_result(mock_app_prop, mock_syntax):
32 | """Test rendering an action with a result."""
33 | mock_app = MagicMock()
34 | mock_app.get_css_variables.return_value = {"panel": "#123456"}
35 | mock_app_prop.return_value = mock_app
36 |
37 | mock_args_syntax = MagicMock(spec=Syntax)
38 | mock_result_syntax = MagicMock(spec=Syntax)
39 | mock_syntax.side_effect = [mock_args_syntax, mock_result_syntax]
40 |
41 | action = Action(
42 | invocation_id="test_invocation",
43 | tool_call_id="test_tool_call",
44 | name="test_tool",
45 | args='{"param": "value"}',
46 | result='{"result": "success"}',
47 | )
48 |
49 | rendered = action.render()
50 |
51 | assert mock_syntax.call_count == 2
52 | args_call, result_call = mock_syntax.call_args_list
53 |
54 | assert args_call[0][0] == '{"param": "value"}'
55 | assert args_call[0][1] == "json"
56 | assert args_call[1]["background_color"] == "#123456"
57 | assert result_call[0][0] == '{"result": "success"}'
58 | assert result_call[0][1] == "json"
59 | assert result_call[1]["background_color"] == "#123456"
60 |
61 | assert isinstance(rendered, Group)
62 | assert len(rendered.renderables) == 5
63 |
64 | assert isinstance(rendered.renderables[0], Text)
65 | assert "Using tool: test_tool" in rendered.renderables[0].plain
66 |
67 | assert rendered.renderables[1] == mock_args_syntax
68 |
69 | assert isinstance(rendered.renderables[2], Text)
70 | assert rendered.renderables[2].plain == ""
71 |
72 | assert isinstance(rendered.renderables[3], Text)
73 | assert "Result: test_tool" in rendered.renderables[3].plain
74 | assert rendered.renderables[4] == mock_result_syntax
75 |
76 |
77 | @patch("dasshh.ui.components.chat.action.Syntax")
78 | @patch.object(Action, "app", new_callable=PropertyMock)
79 | def test_action_render_without_result(mock_app_prop, mock_syntax):
80 | """Test rendering an action without a result."""
81 | mock_app = MagicMock()
82 | mock_app.get_css_variables.return_value = {"panel": "#123456"}
83 | mock_app_prop.return_value = mock_app
84 | mock_args_syntax = MagicMock(spec=Syntax)
85 | mock_syntax.return_value = mock_args_syntax
86 |
87 | action = Action(
88 | invocation_id="test_invocation",
89 | tool_call_id="test_tool_call",
90 | name="test_tool",
91 | args='{"param": "value"}',
92 | result="",
93 | )
94 |
95 | rendered = action.render()
96 |
97 | mock_syntax.assert_called_once_with(
98 | '{"param": "value"}', "json", background_color="#123456", word_wrap=True
99 | )
100 |
101 | assert isinstance(rendered, Group)
102 | assert len(rendered.renderables) == 2
103 |
104 | assert isinstance(rendered.renderables[0], Text)
105 | assert "Using tool: test_tool" in rendered.renderables[0].plain
106 | assert rendered.renderables[1] == mock_args_syntax
107 |
--------------------------------------------------------------------------------
/tests/unit/ui/components/chat/test_message.py:
--------------------------------------------------------------------------------
1 | """Tests for the ChatMessage component."""
2 |
3 | from rich.console import Group
4 | from rich.markdown import Markdown
5 | from rich.text import Text
6 |
7 | from dasshh.ui.components.chat.message import ChatMessage
8 |
9 |
10 | def test_chat_message_initialization_user():
11 | """Test ChatMessage initialization with user role."""
12 | message = ChatMessage(invocation_id="test_id", role="user", content="Hello, world!")
13 |
14 | assert message.invocation_id == "test_id"
15 | assert message.role == "you"
16 | assert message.content == "Hello, world!"
17 | assert "user" in message.classes
18 |
19 |
20 | def test_chat_message_initialization_assistant():
21 | """Test ChatMessage initialization with assistant role."""
22 | message = ChatMessage(
23 | invocation_id="test_id", role="assistant", content="Hello, I'm the assistant!"
24 | )
25 |
26 | assert message.invocation_id == "test_id"
27 | assert message.role == "dasshh"
28 | assert message.content == "Hello, I'm the assistant!"
29 | assert "assistant" in message.classes
30 |
31 |
32 | def test_chat_message_render_user():
33 | """Test rendering a user message."""
34 | message = ChatMessage(invocation_id="test_id", role="user", content="Hello, world!")
35 |
36 | rendered = message.render()
37 |
38 | assert isinstance(rendered, Group)
39 | assert len(rendered.renderables) == 2
40 |
41 | title = rendered.renderables[0]
42 | assert isinstance(title, Text)
43 | assert "You" in title.plain
44 |
45 | content = rendered.renderables[1]
46 | assert isinstance(content, Markdown)
47 | assert content.markup == "Hello, world!"
48 |
49 |
50 | def test_chat_message_render_assistant():
51 | """Test rendering an assistant message."""
52 | message = ChatMessage(
53 | invocation_id="test_id", role="assistant", content="Hello, I'm the assistant!"
54 | )
55 | rendered = message.render()
56 |
57 | assert isinstance(rendered, Group)
58 | assert len(rendered.renderables) == 2
59 |
60 | title = rendered.renderables[0]
61 | assert isinstance(title, Text)
62 | assert "Dasshh" in title.plain
63 |
64 | content = rendered.renderables[1]
65 | assert isinstance(content, Markdown)
66 | assert content.markup == "Hello, I'm the assistant!"
67 |
68 |
69 | def test_chat_message_empty_assistant():
70 | """Test rendering an empty assistant message (typing indicator)."""
71 | message = ChatMessage(invocation_id="test_id", role="dasshh", content="")
72 | message.role = "assistant"
73 | rendered = message.render()
74 |
75 | assert isinstance(rendered, Group)
76 | assert len(rendered.renderables) == 2
77 |
--------------------------------------------------------------------------------
/tests/unit/ui/components/test_navbar.py:
--------------------------------------------------------------------------------
1 | """Tests for the Navbar component."""
2 |
3 | from unittest.mock import MagicMock, patch
4 |
5 | import pytest
6 | from textual.containers import Horizontal
7 |
8 | from dasshh.ui.components.navbar import Navbar, NavItem, Logo
9 | from dasshh.ui.events import ChangeView
10 |
11 |
12 | @pytest.fixture
13 | def mock_query():
14 | """Mock for query_one and query methods."""
15 | mock = MagicMock()
16 | mock.add_class = MagicMock()
17 | mock.remove_class = MagicMock()
18 | return mock
19 |
20 |
21 | class TestNavItem:
22 | """Tests for the NavItem component."""
23 |
24 | def test_initialization(self):
25 | """Test NavItem initialization."""
26 | nav_item = NavItem(route="test", label="Test", icon="T")
27 | assert nav_item.route == "test"
28 | # The rendered text should include the icon and label
29 | assert nav_item.renderable == "T Test"
30 |
31 | def test_on_click(self):
32 | """Test NavItem click event posts ChangeView message."""
33 | nav_item = NavItem(route="test", label="Test")
34 | nav_item.post_message = MagicMock()
35 | nav_item.add_class = MagicMock()
36 |
37 | nav_item.on_click()
38 |
39 | nav_item.add_class.assert_called_once_with("active")
40 | nav_item.post_message.assert_called_once()
41 |
42 | # Check that the posted message is ChangeView with correct route
43 | posted_message = nav_item.post_message.call_args[0][0]
44 | assert isinstance(posted_message, ChangeView)
45 | assert posted_message.view == "test"
46 |
47 |
48 | class TestLogo:
49 | """Tests for the Logo component."""
50 |
51 | def test_render(self):
52 | """Test Logo renders correctly."""
53 | logo = Logo()
54 | rendered = logo.render()
55 |
56 | assert "Dasshh 🗲" in rendered
57 | assert isinstance(rendered, str)
58 |
59 |
60 | class TestNavbar:
61 | """Tests for the Navbar component."""
62 |
63 | @patch("dasshh.ui.components.navbar.Navbar.compose")
64 | def test_compose(self, mock_compose):
65 | """Test Navbar composition."""
66 | navbar = Navbar()
67 |
68 | # Mock the compose method to avoid Textual app dependencies
69 | mock_logo = MagicMock(spec=Logo)
70 | mock_horizontal = MagicMock(spec=Horizontal)
71 | mock_horizontal.id = "nav-items"
72 |
73 | mock_compose.return_value = [mock_logo, mock_horizontal]
74 |
75 | # Get the components yielded by compose
76 | components = list(navbar.compose())
77 |
78 | # Check that we have a Logo and a Horizontal container
79 | assert len(components) == 2
80 | assert components[0] == mock_logo
81 | assert components[1] == mock_horizontal
82 | assert components[1].id == "nav-items"
83 |
84 | @patch("dasshh.ui.components.navbar.Navbar.query_one")
85 | def test_on_mount(self, mock_query_one):
86 | """Test on_mount adds active class to chat NavItem."""
87 | navbar = Navbar()
88 | mock_chat_item = MagicMock()
89 | mock_query_one.return_value = mock_chat_item
90 |
91 | navbar.on_mount()
92 |
93 | mock_query_one.assert_called_once_with("#chat")
94 | mock_chat_item.add_class.assert_called_once_with("active")
95 |
96 | @patch("dasshh.ui.components.navbar.Navbar.query")
97 | @patch("dasshh.ui.components.navbar.Navbar.query_one")
98 | def test_change_view(self, mock_query_one, mock_query):
99 | """Test change_view event handler."""
100 | navbar = Navbar()
101 |
102 | # Mock NavItems returned by query
103 | mock_items = [MagicMock(), MagicMock(), MagicMock()]
104 | mock_query.return_value = mock_items
105 |
106 | # Mock the selected NavItem
107 | mock_selected_item = MagicMock()
108 | mock_query_one.return_value = mock_selected_item
109 |
110 | # Create a ChangeView event
111 | event = ChangeView("settings")
112 |
113 | # Call the event handler
114 | navbar.change_view(event)
115 |
116 | # Check that remove_class was called on all NavItems
117 | for item in mock_items:
118 | item.remove_class.assert_called_once_with("active")
119 |
120 | # Check that query_one was called with the correct selector
121 | mock_query_one.assert_called_once_with("#settings")
122 |
123 | # Check that add_class was called on the selected NavItem
124 | mock_selected_item.add_class.assert_called_once_with("active")
125 |
--------------------------------------------------------------------------------
/tests/unit/ui/test_events.py:
--------------------------------------------------------------------------------
1 | """Tests for the UI events module."""
2 |
3 | from dasshh.ui.events import (
4 | ChangeView,
5 | NewMessage,
6 | NewSession,
7 | LoadSession,
8 | DeleteSession,
9 | AssistantResponseStart,
10 | AssistantResponseUpdate,
11 | AssistantResponseComplete,
12 | AssistantResponseError,
13 | AssistantToolCallStart,
14 | AssistantToolCallComplete,
15 | AssistantToolCallError,
16 | )
17 |
18 |
19 | def test_change_view_event():
20 | """Test initialization of ChangeView event."""
21 | event = ChangeView("chat")
22 | assert event.view == "chat"
23 |
24 |
25 | def test_new_message_event():
26 | """Test initialization of NewMessage event."""
27 | message = "Hello, world!"
28 | event = NewMessage(message)
29 | assert event.message == message
30 |
31 |
32 | def test_new_session_event():
33 | """Test initialization of NewSession event."""
34 | event = NewSession()
35 | assert isinstance(event, NewSession)
36 |
37 |
38 | def test_load_session_event():
39 | """Test initialization of LoadSession event."""
40 | session_id = "test_session_id"
41 | event = LoadSession(session_id)
42 | assert event.session_id == session_id
43 |
44 |
45 | def test_delete_session_event():
46 | """Test initialization of DeleteSession event."""
47 | session_id = "test_session_id"
48 | event = DeleteSession(session_id)
49 | assert event.session_id == session_id
50 |
51 |
52 | def test_assistant_response_start_event():
53 | """Test initialization of AssistantResponseStart event."""
54 | invocation_id = "test_invocation_id"
55 | event = AssistantResponseStart(invocation_id)
56 | assert event.invocation_id == invocation_id
57 |
58 |
59 | def test_assistant_response_update_event():
60 | """Test initialization of AssistantResponseUpdate event."""
61 | invocation_id = "test_invocation_id"
62 | content = "Partial response content"
63 | event = AssistantResponseUpdate(invocation_id, content)
64 | assert event.invocation_id == invocation_id
65 | assert event.content == content
66 |
67 |
68 | def test_assistant_response_complete_event():
69 | """Test initialization of AssistantResponseComplete event."""
70 | invocation_id = "test_invocation_id"
71 | content = "Complete response content"
72 | event = AssistantResponseComplete(invocation_id, content)
73 | assert event.invocation_id == invocation_id
74 | assert event.content == content
75 |
76 |
77 | def test_assistant_response_error_event():
78 | """Test initialization of AssistantResponseError event."""
79 | invocation_id = "test_invocation_id"
80 | error = "Error message"
81 | event = AssistantResponseError(invocation_id, error)
82 | assert event.invocation_id == invocation_id
83 | assert event.error == error
84 |
85 |
86 | def test_assistant_tool_call_start_event():
87 | """Test initialization of AssistantToolCallStart event."""
88 | invocation_id = "test_invocation_id"
89 | tool_call_id = "test_tool_call_id"
90 | tool_name = "test_tool"
91 | args = '{"param": "value"}'
92 |
93 | event = AssistantToolCallStart(invocation_id, tool_call_id, tool_name, args)
94 |
95 | assert event.invocation_id == invocation_id
96 | assert event.tool_call_id == tool_call_id
97 | assert event.tool_name == tool_name
98 | assert event.args == args
99 |
100 |
101 | def test_assistant_tool_call_complete_event():
102 | """Test initialization of AssistantToolCallComplete event."""
103 | invocation_id = "test_invocation_id"
104 | tool_call_id = "test_tool_call_id"
105 | tool_name = "test_tool"
106 | result = '{"result": "success"}'
107 |
108 | event = AssistantToolCallComplete(invocation_id, tool_call_id, tool_name, result)
109 |
110 | assert event.invocation_id == invocation_id
111 | assert event.tool_call_id == tool_call_id
112 | assert event.tool_name == tool_name
113 | assert event.result == result
114 |
115 |
116 | def test_assistant_tool_call_error_event():
117 | """Test initialization of AssistantToolCallError event."""
118 | invocation_id = "test_invocation_id"
119 | tool_call_id = "test_tool_call_id"
120 | tool_name = "test_tool"
121 | error = "Tool call error message"
122 |
123 | event = AssistantToolCallError(invocation_id, tool_call_id, tool_name, error)
124 |
125 | assert event.invocation_id == invocation_id
126 | assert event.tool_call_id == tool_call_id
127 | assert event.tool_name == tool_name
128 | assert event.error == error
129 |
--------------------------------------------------------------------------------
/tests/unit/ui/test_types.py:
--------------------------------------------------------------------------------
1 | """Tests for the UI types module."""
2 |
3 | import datetime
4 | import pytest
5 | from pydantic import ValidationError
6 |
7 | from dasshh.ui.types import UIMessage, UIAction, UISession
8 |
9 |
10 | def test_ui_message_initialization():
11 | """Test that UIMessage can be initialized with valid parameters."""
12 | message = UIMessage(invocation_id="test_id", role="user", content="Hello, world!")
13 |
14 | assert message.invocation_id == "test_id"
15 | assert message.role == "user"
16 | assert message.content == "Hello, world!"
17 |
18 |
19 | def test_ui_message_default_invocation_id():
20 | """Test that UIMessage initializes with empty string as default invocation_id."""
21 | message = UIMessage(role="assistant", content="Hello!")
22 |
23 | assert message.invocation_id == ""
24 | assert message.role == "assistant"
25 | assert message.content == "Hello!"
26 |
27 |
28 | def test_ui_message_invalid_role():
29 | """Test that UIMessage validates role field."""
30 | with pytest.raises(ValidationError):
31 | UIMessage(role="invalid_role", content="Hello!")
32 |
33 |
34 | def test_ui_action_initialization():
35 | """Test that UIAction can be initialized with valid parameters."""
36 | action = UIAction(
37 | tool_call_id="tool_1",
38 | invocation_id="inv_1",
39 | name="test_tool",
40 | args='{\n "param": "value"\n}',
41 | result='{\n "result": "success"\n}',
42 | )
43 |
44 | assert action.tool_call_id == "tool_1"
45 | assert action.invocation_id == "inv_1"
46 | assert action.name == "test_tool"
47 | assert action.args == '{\n "param": "value"\n}'
48 | assert action.result == '{\n "result": "success"\n}'
49 |
50 |
51 | def test_ui_session_initialization():
52 | """Test that UISession can be initialized with valid parameters."""
53 | now = datetime.datetime.now()
54 |
55 | session = UISession(
56 | id="session_1",
57 | detail="Test session",
58 | created_at=now,
59 | updated_at=now,
60 | messages=[UIMessage(role="user", content="Hello")],
61 | actions=[
62 | UIAction(
63 | tool_call_id="tool_1",
64 | invocation_id="inv_1",
65 | name="test_tool",
66 | args="{}",
67 | result="{}",
68 | )
69 | ],
70 | )
71 |
72 | assert session.id == "session_1"
73 | assert session.detail == "Test session"
74 | assert session.created_at == now
75 | assert session.updated_at == now
76 | assert len(session.messages) == 1
77 | assert len(session.actions) == 1
78 | assert session.messages[0].role == "user"
79 | assert session.actions[0].name == "test_tool"
80 |
--------------------------------------------------------------------------------