cjb97 commited on
Commit
fe8bf03
·
1 Parent(s): 3d307d7
This view is limited to 50 files because it contains too many changes.   See raw diff
.editorconfig DELETED
@@ -1,52 +0,0 @@
1
- # EditorConfig is awesome: https://EditorConfig.org
2
-
3
- # top-most EditorConfig file
4
- root = true
5
-
6
- # Unix-style newlines with a newline ending every file
7
- [*]
8
- end_of_line = lf
9
- insert_final_newline = true
10
- trim_trailing_whitespace = true
11
- charset = utf-8
12
-
13
- # Python files
14
- [*.py]
15
- indent_style = space
16
- indent_size = 4
17
- max_line_length = 100
18
-
19
- # Use 2 spaces for YAML files
20
- [*.{yml,yaml}]
21
- indent_style = space
22
- indent_size = 2
23
-
24
- # Use 2 spaces for JSON files
25
- [*.json]
26
- indent_style = space
27
- indent_size = 2
28
-
29
- # Use 2 spaces for TOML files
30
- [*.toml]
31
- indent_style = space
32
- indent_size = 2
33
-
34
- # Markdown files
35
- [*.md]
36
- trim_trailing_whitespace = false
37
- max_line_length = 120
38
-
39
- # Requirements files
40
- [requirements*.txt]
41
- indent_style = space
42
- indent_size = 2
43
-
44
- # Documentation files
45
- [docs/**/*.rst]
46
- indent_style = space
47
- indent_size = 3
48
- max_line_length = 120
49
-
50
- # Git commit messages
51
- [COMMIT_EDITMSG]
52
- max_line_length = 72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.flake8 DELETED
@@ -1,65 +0,0 @@
1
- [flake8]
2
- max-line-length = 100
3
- max-complexity = 10
4
- extend-ignore =
5
- E203, # Whitespace before ':'
6
- W503, # Line break before binary operator
7
- D100, # Missing docstring in public module
8
- D104, # Missing docstring in public package
9
- D107, # Missing docstring in __init__
10
- B008 # Do not perform function calls in argument defaults
11
- extend-select =
12
- B950, # Line too long
13
- B901, # Return statements in finally blocks
14
- B902, # Invalid first argument names for methods
15
- B903 # __slots__ efficiency
16
-
17
- exclude =
18
- .git,
19
- __pycache__,
20
- build,
21
- dist,
22
- *.egg-info,
23
- venv,
24
- .env,
25
- .venv,
26
- .mypy_cache,
27
- .pytest_cache
28
-
29
- per-file-ignores =
30
- __init__.py: F401,F403
31
- tests/*: S101,D103,ANN
32
- conftest.py: D103
33
-
34
- # Plugin settings
35
- docstring-convention = google
36
- ignore-decorators = property|classmethod|staticmethod|validator|root_validator
37
-
38
- # Complexity settings
39
- max-annotations-complexity = 4
40
- max-expression-complexity = 7
41
- max-cognitive-complexity = 12
42
-
43
- # Additional settings
44
- max-line-doc-length = 100
45
- max-doc-length = 100
46
- statistics = True
47
- count = True
48
- show-source = True
49
-
50
- # Error format
51
- format = %(path)s:%(row)d:%(col)d: %(code)s %(text)s
52
-
53
- # Plugin configurations
54
- [flake8:local-plugins]
55
- extension =
56
- B = flake8_bugbear:BugBearChecker
57
- C = flake8_comprehensions:ComprehensionChecker
58
- ANN = flake8_annotations:AnnotationChecker
59
- CCR = flake8_cognitive_complexity:CognitiveComplexityChecker
60
- ECE = flake8_expression_complexity:ExpressionComplexityChecker
61
- DAR = flake8_darglint:DarglintChecker
62
-
63
- # Darglint configuration
64
- strictness = short
65
- docstring_style = google
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/ISSUE_TEMPLATE/bug_report.md DELETED
@@ -1,42 +0,0 @@
1
- ---
2
- name: Bug Report
3
- about: Create a report to help us improve
4
- title: '[BUG] '
5
- labels: bug
6
- assignees: ''
7
- ---
8
-
9
- ## Bug Description
10
- <!-- A clear and concise description of what the bug is -->
11
-
12
- ## Steps to Reproduce
13
- 1.
14
- 2.
15
- 3.
16
-
17
- ## Expected Behavior
18
- <!-- A clear and concise description of what you expected to happen -->
19
-
20
- ## Actual Behavior
21
- <!-- What actually happened -->
22
-
23
- ## Screenshots/Logs
24
- <!-- If applicable, add screenshots or logs to help explain your problem -->
25
-
26
- ## Environment
27
- - OS: [e.g., Ubuntu 22.04, Windows 11]
28
- - Python Version: [e.g., 3.9.12]
29
- - Package Version: [e.g., 0.1.0]
30
- - Dependencies Versions:
31
- ```
32
- Output of: pip freeze | grep -i ai-assistant
33
- ```
34
-
35
- ## Additional Context
36
- <!-- Add any other context about the problem here -->
37
-
38
- ## Possible Solution
39
- <!-- Optional: suggest a fix or reason for the bug -->
40
-
41
- ## Related Issues/PRs
42
- <!-- Optional: link to related issues or PRs -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/ISSUE_TEMPLATE/config.yml DELETED
@@ -1,14 +0,0 @@
1
- blank_issues_enabled: false
2
- contact_links:
3
- - name: Question or Discussion
4
- url: https://github.com/yourusername/ai-assistant/discussions
5
- about: Ask questions and discuss with other community members
6
- - name: Documentation
7
- url: https://yourusername.github.io/ai-assistant
8
- about: Check out our documentation for answers
9
- - name: Security Issue
10
- url: https://github.com/yourusername/ai-assistant/security/policy
11
- about: Please report security vulnerabilities here
12
-
13
- # Configure issue template chooser
14
- # https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/ISSUE_TEMPLATE/documentation.md DELETED
@@ -1,36 +0,0 @@
1
- ---
2
- name: Documentation
3
- about: Report an issue or suggest an improvement in documentation
4
- title: '[DOCS] '
5
- labels: documentation
6
- assignees: ''
7
- ---
8
-
9
- ## Documentation Location
10
- <!-- Specify where the documentation issue is (e.g., README, API docs, code comments) -->
11
-
12
- ## Current Documentation
13
- <!-- What does the current documentation say? -->
14
-
15
- ## Issue/Suggested Improvement
16
- <!-- Describe what's wrong or what could be better -->
17
-
18
- ## Proposed Changes
19
- <!-- Suggest specific changes or improvements -->
20
-
21
- ### Example
22
- <!-- If applicable, provide an example of the improved documentation -->
23
- ```markdown
24
- # Suggested documentation changes
25
- ```
26
-
27
- ## Additional Context
28
- <!-- Add any other context about the documentation issue here -->
29
-
30
- ## Checklist
31
- - [ ] I have checked that this is not already documented elsewhere
32
- - [ ] I have checked the latest documentation in the main branch
33
- - [ ] I have checked related issues/PRs
34
-
35
- ## Related Issues/PRs
36
- <!-- Optional: link to related issues or PRs -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/ISSUE_TEMPLATE/feature_request.md DELETED
@@ -1,42 +0,0 @@
1
- ---
2
- name: Feature Request
3
- about: Suggest an idea for this project
4
- title: '[FEATURE] '
5
- labels: enhancement
6
- assignees: ''
7
- ---
8
-
9
- ## Problem Statement
10
- <!-- A clear and concise description of what problem this feature would solve -->
11
-
12
- ## Proposed Solution
13
- <!-- A clear and concise description of what you want to happen -->
14
-
15
- ## Alternative Solutions
16
- <!-- A clear and concise description of any alternative solutions or features you've considered -->
17
-
18
- ## Example Use Case
19
- <!-- Provide an example of how this feature would be used -->
20
- ```python
21
- # Example code showing how the feature might work
22
- from ai_assistant import SomeFeature
23
-
24
- # Usage example
25
- ```
26
-
27
- ## Implementation Details
28
- <!-- Optional: If you have ideas about how to implement this feature -->
29
-
30
- ### Required Changes
31
- <!-- List the components that would need to be modified -->
32
- - [ ] Component 1
33
- - [ ] Component 2
34
-
35
- ### Potential Challenges
36
- <!-- List any challenges or considerations for implementing this feature -->
37
-
38
- ## Additional Context
39
- <!-- Add any other context or screenshots about the feature request here -->
40
-
41
- ## Related Issues/PRs
42
- <!-- Optional: link to related issues or PRs -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/PULL_REQUEST_TEMPLATE.md DELETED
@@ -1,62 +0,0 @@
1
- ## Description
2
- <!-- Provide a brief description of the changes in this PR -->
3
-
4
- ## Type of Change
5
- <!-- Mark the appropriate option with an [x] -->
6
- - [ ] Bug fix (non-breaking change that fixes an issue)
7
- - [ ] New feature (non-breaking change that adds functionality)
8
- - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
9
- - [ ] Documentation update
10
- - [ ] Performance improvement
11
- - [ ] Code cleanup or refactor
12
- - [ ] Dependency update
13
- - [ ] Other (please describe):
14
-
15
- ## Motivation and Context
16
- <!-- Why is this change required? What problem does it solve? -->
17
-
18
- ## Implementation Details
19
- <!-- Describe the changes in detail -->
20
-
21
- ## How Has This Been Tested?
22
- <!-- Describe the tests you ran and how -->
23
- - [ ] Unit tests added/updated
24
- - [ ] Integration tests added/updated
25
- - [ ] Manual testing performed
26
-
27
- ### Test Configuration
28
- - Python version:
29
- - OS:
30
- - Dependencies:
31
-
32
- ## Breaking Changes
33
- <!-- List any breaking changes and migration instructions if applicable -->
34
-
35
- ## Checklist
36
- <!-- Mark completed items with an [x] -->
37
- - [ ] My code follows the project's style guidelines
38
- - [ ] I have performed a self-review of my own code
39
- - [ ] I have commented my code, particularly in hard-to-understand areas
40
- - [ ] I have made corresponding changes to the documentation
41
- - [ ] My changes generate no new warnings
42
- - [ ] I have added tests that prove my fix is effective or that my feature works
43
- - [ ] New and existing unit tests pass locally with my changes
44
- - [ ] Any dependent changes have been merged and published
45
- - [ ] I have updated the version number if necessary
46
- - [ ] I have updated the changelog if necessary
47
-
48
- ## Screenshots/Recordings
49
- <!-- If applicable, add screenshots or recordings to demonstrate the changes -->
50
-
51
- ## Related Issues
52
- <!-- Link to any related issues using #issue-number -->
53
- Closes #
54
-
55
- ## Additional Notes
56
- <!-- Add any additional notes or context about the PR here -->
57
-
58
- ## Review Guidelines
59
- <!-- Specify any particular areas of the code that need careful review -->
60
-
61
- ## Deployment Notes
62
- <!-- Any special considerations for deploying these changes -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/workflows/deploy.yml DELETED
@@ -1,145 +0,0 @@
1
- name: Deploy
2
-
3
- on:
4
- push:
5
- tags:
6
- - 'v*'
7
- workflow_dispatch:
8
- inputs:
9
- environment:
10
- description: 'Environment to deploy to'
11
- required: true
12
- default: 'staging'
13
- type: choice
14
- options:
15
- - staging
16
- - production
17
-
18
- jobs:
19
- build:
20
- runs-on: ubuntu-latest
21
- steps:
22
- - uses: actions/checkout@v4
23
-
24
- - name: Set up Python
25
- uses: actions/setup-python@v4
26
- with:
27
- python-version: "3.12"
28
- cache: 'pip'
29
-
30
- - name: Install dependencies
31
- run: |
32
- python -m pip install --upgrade pip
33
- pip install build twine
34
-
35
- - name: Build package
36
- run: |
37
- python -m build
38
-
39
- - name: Upload dist artifact
40
- uses: actions/upload-artifact@v3
41
- with:
42
- name: dist
43
- path: dist/
44
- if-no-files-found: error
45
-
46
- test-package:
47
- needs: build
48
- runs-on: ubuntu-latest
49
- strategy:
50
- matrix:
51
- python-version: ["3.9", "3.10", "3.11", "3.12"]
52
- steps:
53
- - uses: actions/checkout@v4
54
-
55
- - name: Set up Python ${{ matrix.python-version }}
56
- uses: actions/setup-python@v4
57
- with:
58
- python-version: ${{ matrix.python-version }}
59
- cache: 'pip'
60
-
61
- - name: Download dist
62
- uses: actions/download-artifact@v3
63
- with:
64
- name: dist
65
- path: dist/
66
-
67
- - name: Install package
68
- run: |
69
- python -m pip install --upgrade pip
70
- pip install dist/*.whl
71
-
72
- - name: Test import
73
- run: |
74
- python -c "import tools; print(tools.__version__)"
75
-
76
- deploy-staging:
77
- needs: [build, test-package]
78
- runs-on: ubuntu-latest
79
- if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'staging'
80
- environment:
81
- name: staging
82
- url: ${{ steps.deploy.outputs.url }}
83
-
84
- steps:
85
- - name: Download dist
86
- uses: actions/download-artifact@v3
87
- with:
88
- name: dist
89
- path: dist/
90
-
91
- - name: Deploy to Test PyPI
92
- env:
93
- TWINE_USERNAME: __token__
94
- TWINE_PASSWORD: ${{ secrets.TEST_PYPI_TOKEN }}
95
- run: |
96
- python -m twine upload --repository testpypi dist/*
97
-
98
- - name: Set output URL
99
- id: deploy
100
- run: echo "url=https://test.pypi.org/project/ai-assistant/" >> $GITHUB_OUTPUT
101
-
102
- deploy-production:
103
- needs: [build, test-package]
104
- runs-on: ubuntu-latest
105
- if: |
106
- (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production') ||
107
- (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'))
108
- environment:
109
- name: production
110
- url: ${{ steps.deploy.outputs.url }}
111
-
112
- steps:
113
- - name: Download dist
114
- uses: actions/download-artifact@v3
115
- with:
116
- name: dist
117
- path: dist/
118
-
119
- - name: Deploy to PyPI
120
- env:
121
- TWINE_USERNAME: __token__
122
- TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
123
- run: |
124
- python -m twine upload dist/*
125
-
126
- - name: Set output URL
127
- id: deploy
128
- run: echo "url=https://pypi.org/project/ai-assistant/" >> $GITHUB_OUTPUT
129
-
130
- - name: Create GitHub Release
131
- if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
132
- uses: softprops/action-gh-release@v1
133
- with:
134
- files: dist/*
135
- generate_release_notes: true
136
-
137
- cleanup:
138
- needs: [deploy-staging, deploy-production]
139
- if: always()
140
- runs-on: ubuntu-latest
141
- steps:
142
- - name: Delete dist artifact
143
- uses: geekyeggo/delete-artifact@v2
144
- with:
145
- name: dist
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/workflows/lint.yml DELETED
@@ -1,98 +0,0 @@
1
- name: Lint
2
-
3
- on:
4
- push:
5
- branches: [ main, develop ]
6
- pull_request:
7
- branches: [ main, develop ]
8
-
9
- jobs:
10
- lint:
11
- runs-on: ubuntu-latest
12
- strategy:
13
- matrix:
14
- python-version: ["3.9"]
15
-
16
- steps:
17
- - uses: actions/checkout@v4
18
-
19
- - name: Set up Python ${{ matrix.python-version }}
20
- uses: actions/setup-python@v5
21
- with:
22
- python-version: ${{ matrix.python-version }}
23
- cache: 'pip'
24
-
25
- - name: Install dependencies
26
- run: |
27
- python -m pip install --upgrade pip
28
- pip install -r requirements-dev.txt
29
-
30
- - name: Check formatting with black
31
- run: black . --check --diff
32
-
33
- - name: Check import sorting with isort
34
- run: isort . --check --diff
35
-
36
- - name: Run flake8
37
- run: flake8 .
38
-
39
- - name: Run pylint
40
- run: pylint tools tests
41
-
42
- - name: Run mypy type checking
43
- run: mypy .
44
-
45
- - name: Run bandit security checks
46
- run: bandit -r . -c pyproject.toml
47
-
48
- - name: Run pyupgrade checks
49
- run: |
50
- find . -type f -name "*.py" -not -path "./venv/*" -not -path "./.env/*" -exec pyupgrade --py39-plus {} \;
51
-
52
- - name: Verify pre-commit hooks
53
- run: |
54
- pre-commit install
55
- pre-commit run --all-files
56
-
57
- security:
58
- runs-on: ubuntu-latest
59
- steps:
60
- - uses: actions/checkout@v4
61
-
62
- - name: Set up Python
63
- uses: actions/setup-python@v4
64
- with:
65
- python-version: "3.12"
66
- cache: 'pip'
67
-
68
- - name: Install dependencies
69
- run: |
70
- python -m pip install --upgrade pip
71
- pip install bandit safety
72
-
73
- - name: Run bandit
74
- run: |
75
- bandit -r tools -ll -ii
76
-
77
- - name: Run safety check
78
- run: |
79
- safety check
80
-
81
- dependency-review:
82
- runs-on: ubuntu-latest
83
- if: github.event_name == 'pull_request'
84
- steps:
85
- - uses: actions/checkout@v4
86
-
87
- - name: Dependency Review
88
- uses: actions/dependency-review-action@v3
89
- with:
90
- fail-on-severity: moderate
91
- deny-licenses: |
92
- GPL-1.0
93
- LGPL-2.0
94
- BSD-2-Clause
95
- allow-licenses: |
96
- MIT
97
- Apache-2.0
98
- BSD-3-Clause
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/workflows/test.yml DELETED
@@ -1,102 +0,0 @@
1
- name: Tests
2
-
3
- on:
4
- push:
5
- branches: [ main ]
6
- pull_request:
7
- branches: [ main ]
8
- workflow_dispatch:
9
-
10
- jobs:
11
- test:
12
- runs-on: ubuntu-latest
13
- strategy:
14
- matrix:
15
- python-version: ["3.9", "3.10", "3.11", "3.12"]
16
- fail-fast: false
17
-
18
- steps:
19
- - uses: actions/checkout@v4
20
-
21
- - name: Set up Python ${{ matrix.python-version }}
22
- uses: actions/setup-python@v4
23
- with:
24
- python-version: ${{ matrix.python-version }}
25
- cache: 'pip'
26
-
27
- - name: Install dependencies
28
- run: |
29
- python -m pip install --upgrade pip
30
- pip install -r requirements.txt
31
- pip install -r requirements-dev.txt
32
-
33
- - name: Run tests with coverage
34
- run: |
35
- python scripts/run_tests.py
36
- env:
37
- PYTHONPATH: ${{ github.workspace }}
38
-
39
- - name: Upload coverage reports
40
- uses: codecov/codecov-action@v3
41
- with:
42
- file: ./coverage.xml
43
- flags: unittests
44
- name: codecov-umbrella
45
- fail_ci_if_error: true
46
- if: success()
47
-
48
- - name: Upload coverage HTML report
49
- uses: actions/upload-artifact@v3
50
- with:
51
- name: coverage-report-${{ matrix.python-version }}
52
- path: coverage_html/
53
- if-no-files-found: error
54
- if: success()
55
-
56
- - name: Upload test results
57
- uses: actions/upload-artifact@v3
58
- with:
59
- name: pytest-results-${{ matrix.python-version }}
60
- path: |
61
- .coverage
62
- coverage.xml
63
- junit.xml
64
- if-no-files-found: error
65
- if: always()
66
-
67
- coverage-badge:
68
- needs: test
69
- runs-on: ubuntu-latest
70
- if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
71
-
72
- steps:
73
- - uses: actions/checkout@v4
74
-
75
- - name: Download coverage data
76
- uses: actions/download-artifact@v3
77
- with:
78
- name: coverage-report-3.12
79
- path: coverage_html/
80
-
81
- - name: Extract coverage percentage
82
- run: |
83
- COVERAGE=$(grep -o '[0-9]\+%' coverage_html/index.html | head -1 | tr -d '%')
84
- echo "COVERAGE=$COVERAGE" >> $GITHUB_ENV
85
- if [ "$COVERAGE" -ge 90 ]; then
86
- echo "COVERAGE_COLOR=green" >> $GITHUB_ENV
87
- elif [ "$COVERAGE" -ge 80 ]; then
88
- echo "COVERAGE_COLOR=yellow" >> $GITHUB_ENV
89
- else
90
- echo "COVERAGE_COLOR=red" >> $GITHUB_ENV
91
- fi
92
-
93
- - name: Create coverage badge
94
- uses: schneegans/dynamic-badges-action@v1.6.0
95
- with:
96
- auth: ${{ secrets.GIST_SECRET }}
97
- gistID: ${{ secrets.COVERAGE_GIST_ID }}
98
- filename: coverage.json
99
- label: coverage
100
- message: ${{ env.COVERAGE }}%
101
- color: ${{ env.COVERAGE_COLOR }}
102
- namedLogo: python
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.pre-commit-config.yaml DELETED
@@ -1,170 +0,0 @@
1
- repos:
2
- - repo: https://github.com/pre-commit/pre-commit-hooks
3
- rev: v4.5.0
4
- hooks:
5
- - id: check-added-large-files
6
- args: ['--maxkb=1024']
7
- - id: check-ast
8
- - id: check-case-conflict
9
- - id: check-docstring-first
10
- - id: check-executables-have-shebangs
11
- - id: check-json
12
- - id: check-merge-conflict
13
- - id: check-toml
14
- - id: check-yaml
15
- - id: debug-statements
16
- - id: detect-private-key
17
- - id: end-of-file-fixer
18
- - id: trailing-whitespace
19
- - id: mixed-line-ending
20
- args: ['--fix=lf']
21
-
22
- - repo: https://github.com/psf/black
23
- rev: 24.2.0
24
- hooks:
25
- - id: black
26
- language_version: python3
27
- args: ['--config=pyproject.toml']
28
-
29
- - repo: https://github.com/pycqa/isort
30
- rev: 5.13.2
31
- hooks:
32
- - id: isort
33
- args: ['--settings-path=pyproject.toml']
34
-
35
- - repo: https://github.com/pycqa/flake8
36
- rev: 7.0.0
37
- hooks:
38
- - id: flake8
39
- additional_dependencies:
40
- - flake8-docstrings
41
- - flake8-bugbear
42
- - flake8-comprehensions
43
- - flake8-simplify
44
- - flake8-pytest-style
45
- - flake8-typing-imports
46
- - pep8-naming
47
- - flake8-cognitive-complexity
48
- - flake8-expression-complexity
49
- - mccabe
50
- - flake8-functions
51
- - flake8-annotations-complexity
52
-
53
- - repo: https://github.com/pre-commit/mirrors-mypy
54
- rev: v1.8.0
55
- hooks:
56
- - id: mypy
57
- additional_dependencies:
58
- - types-requests
59
- - types-PyYAML
60
- - pydantic
61
- - types-setuptools
62
- - types-python-dateutil
63
- - types-pytz
64
- - types-urllib3
65
- - types-six
66
- - types-protobuf
67
- - types-mock
68
- - types-toml
69
- args: [
70
- '--config-file=pyproject.toml',
71
- '--strict',
72
- '--ignore-missing-imports',
73
- '--python-version=3.9',
74
- '--show-error-codes',
75
- '--pretty',
76
- '--warn-unused-ignores',
77
- '--disallow-any-generics',
78
- '--disallow-untyped-decorators',
79
- '--no-implicit-optional'
80
- ]
81
-
82
- - repo: https://github.com/microsoft/pyright
83
- rev: 1.1.350
84
- hooks:
85
- - id: pyright
86
- additional_dependencies:
87
- - pydantic
88
- - requests
89
- - PyYAML
90
- args: [
91
- '--warnings',
92
- '--pythonversion', '3.9',
93
- '--typeCheckingMode', 'strict'
94
- ]
95
-
96
- - repo: https://github.com/RobertCraigie/pyright-python
97
- rev: v1.1.350
98
- hooks:
99
- - id: pyright-python
100
- name: pyright-strict
101
- args: [
102
- '--warnings',
103
- '--pythonversion', '3.9',
104
- '--typeCheckingMode', 'strict',
105
- '--ignoreexternal'
106
- ]
107
-
108
- - repo: https://github.com/PyCQA/bandit
109
- rev: 1.7.7
110
- hooks:
111
- - id: bandit
112
- args: ['-c', 'pyproject.toml']
113
-
114
- - repo: https://github.com/asottile/pyupgrade
115
- rev: v3.15.1
116
- hooks:
117
- - id: pyupgrade
118
- args: ['--py39-plus']
119
-
120
- - repo: https://github.com/PyCQA/doc8
121
- rev: v1.1.1
122
- hooks:
123
- - id: doc8
124
- args: ['--max-line-length=120']
125
-
126
- - repo: https://github.com/pypa/safety
127
- rev: 2.3.5
128
- hooks:
129
- - id: safety
130
- args: ['check']
131
-
132
- - repo: local
133
- hooks:
134
- - id: pylint
135
- name: pylint
136
- entry: pylint
137
- language: system
138
- types: [python]
139
- args:
140
- - --rcfile=pyproject.toml
141
- - --score=no
142
- - --output-format=colorized
143
-
144
- - id: pytest-check
145
- name: pytest-check
146
- entry: pytest
147
- language: system
148
- pass_filenames: false
149
- always_run: true
150
- args:
151
- - --cov
152
- - --cov-report=term-missing
153
- - -v
154
-
155
- - repo: https://github.com/rubik/radon
156
- rev: v6.0.1
157
- hooks:
158
- - id: radon
159
- name: radon-complexity
160
- entry: radon cc
161
- args: [--min=C, --total-average, --show-complexity]
162
- language: python
163
- types: [python]
164
-
165
- - id: radon-maintainability
166
- name: radon-maintainability
167
- entry: radon mi
168
- args: [--min=B, --show]
169
- language: python
170
- types: [python]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
CHANGELOG.md DELETED
@@ -1,69 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [Unreleased]
9
-
10
- ### Added
11
- - Initial project structure and setup
12
- - Comprehensive code formatting standards
13
- - Black configuration with 100 character line length
14
- - isort for import sorting
15
- - EditorConfig for consistent formatting
16
- - Static type checking configuration
17
- - MyPy with strict type checking
18
- - Pyright integration
19
- - Type stubs for common dependencies
20
- - Code quality tools
21
- - Flake8 with multiple plugins
22
- - Radon for complexity checks
23
- - Bandit for security scanning
24
- - Testing framework
25
- - Pytest configuration
26
- - Coverage reporting setup
27
- - Test helpers and fixtures
28
- - Documentation
29
- - Project README
30
- - Contributing guidelines
31
- - Code style guide
32
- - Type checking guide
33
- - CI/CD Pipeline
34
- - GitHub Actions workflow
35
- - Pre-commit hooks configuration
36
- - Automated testing and linting
37
- - Development tools
38
- - Development requirements
39
- - Virtual environment setup
40
- - Local development guide
41
-
42
- ### Changed
43
- - None (initial release)
44
-
45
- ### Deprecated
46
- - None
47
-
48
- ### Removed
49
- - None
50
-
51
- ### Fixed
52
- - None
53
-
54
- ### Security
55
- - Added Bandit security checks
56
- - Implemented Safety dependency scanning
57
- - Configured security-focused pre-commit hooks
58
- - Added checks for hardcoded secrets
59
-
60
- ## [0.1.0] - YYYY-MM-DD
61
- ### Added
62
- - First release of the project
63
- - Basic project structure
64
- - Core functionality implementation
65
- - Initial test suite
66
- - Basic documentation
67
-
68
- [Unreleased]: https://github.com/username/repository/compare/v0.1.0...HEAD
69
- [0.1.0]: https://github.com/username/repository/releases/tag/v0.1.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
CONTRIBUTING.md DELETED
@@ -1,223 +0,0 @@
1
- # Contributing Guidelines
2
-
3
- ## Table of Contents
4
- - [Development Setup](#development-setup)
5
- - [Code Style](#code-style)
6
- - [Type Checking](#type-checking)
7
- - [Code Quality](#code-quality)
8
- - [Testing](#testing)
9
- - [Pull Request Process](#pull-request-process)
10
- - [Commit Guidelines](#commit-guidelines)
11
-
12
- ## Development Setup
13
-
14
- ### Prerequisites
15
- - Python 3.9+
16
- - pip
17
- - git
18
-
19
- ### Setting Up Development Environment
20
- 1. Clone the repository:
21
- ```bash
22
- git clone <repository-url>
23
- cd <repository-name>
24
- ```
25
-
26
- 2. Create and activate a virtual environment:
27
- ```bash
28
- python -m venv .venv
29
- source .venv/bin/activate # Linux/macOS
30
- .venv\Scripts\activate # Windows
31
- ```
32
-
33
- 3. Install development dependencies:
34
- ```bash
35
- pip install -r requirements-dev.txt
36
- ```
37
-
38
- 4. Install pre-commit hooks:
39
- ```bash
40
- pre-commit install
41
- ```
42
-
43
- ## Code Style
44
-
45
- We follow a strict Python code style guide to maintain consistency across the project. Our code style is enforced through various tools:
46
-
47
- ### Formatting
48
- - **Black**: Code formatting with line length of 100 characters
49
- - **isort**: Import sorting with three sections (stdlib, third-party, local)
50
- - **EditorConfig**: Editor-agnostic formatting rules
51
-
52
- ### Style Rules
53
- - Use 4 spaces for indentation
54
- - Use meaningful variable and function names
55
- - Follow PEP 8 naming conventions
56
- - Write descriptive docstrings in Google style
57
- - Keep functions focused and concise
58
-
59
- Example:
60
- ```python
61
- from typing import List, Optional
62
-
63
- def process_data(items: List[str], max_length: Optional[int] = None) -> List[str]:
64
- """Process a list of string items with optional length limitation.
65
-
66
- Args:
67
- items: List of strings to process.
68
- max_length: Optional maximum length for each string.
69
-
70
- Returns:
71
- List of processed strings.
72
-
73
- Raises:
74
- ValueError: If any string is empty.
75
- """
76
- if not items:
77
- raise ValueError("Items list cannot be empty")
78
-
79
- return [item[:max_length] if max_length else item for item in items]
80
- ```
81
-
82
- ## Type Checking
83
-
84
- We use multiple type checking tools to ensure type safety:
85
-
86
- ### MyPy
87
- - Strict type checking enabled
88
- - All functions must have type annotations
89
- - Generic types must be fully specified
90
- - No implicit Optional types
91
-
92
- ### Pyright
93
- - Strict mode enabled
94
- - Reports missing type stubs
95
- - Enforces strict collection type inference
96
- - Checks for proper None handling
97
-
98
- Example of proper type usage:
99
- ```python
100
- from typing import Dict, List, Optional, TypeVar
101
-
102
- T = TypeVar("T")
103
-
104
- def merge_lists(list1: List[T], list2: List[T]) -> List[T]:
105
- """Merge two lists of the same type."""
106
- return list1 + list2
107
-
108
- def get_config(path: str) -> Dict[str, Optional[str]]:
109
- """Get configuration with optional values."""
110
- return {"key": "value", "optional_key": None}
111
- ```
112
-
113
- ## Code Quality
114
-
115
- We maintain high code quality standards through various tools:
116
-
117
- ### Complexity Checks
118
- - Maximum cyclomatic complexity: C grade (radon)
119
- - Maximum cognitive complexity: 10 (flake8-cognitive-complexity)
120
- - Maximum function length: 50 lines
121
- - Maximum expression complexity: 3 (flake8-expression-complexity)
122
-
123
- ### Security
124
- - Bandit security checks
125
- - Safety dependency checks
126
- - No hardcoded secrets
127
- - Proper error handling
128
-
129
- ### Documentation
130
- - All public APIs must be documented
131
- - Complex algorithms need detailed explanations
132
- - Update documentation when changing functionality
133
- - Include examples in docstrings
134
-
135
- ## Testing
136
-
137
- ### Test Requirements
138
- - All new code must have tests
139
- - Maintain minimum 90% code coverage
140
- - Test both success and error cases
141
- - Use pytest fixtures for common setup
142
-
143
- ### Running Tests
144
- ```bash
145
- # Run all tests
146
- pytest
147
-
148
- # Run with coverage
149
- pytest --cov
150
-
151
- # Run specific test file
152
- pytest tests/test_specific.py
153
- ```
154
-
155
- ## Pull Request Process
156
-
157
- 1. **Branch Naming**
158
- - feature/description-of-feature
159
- - bugfix/description-of-bug
160
- - hotfix/description-of-hotfix
161
-
162
- 2. **Before Submitting**
163
- - Run all pre-commit hooks
164
- - Ensure all tests pass
165
- - Update documentation if needed
166
- - Add tests for new features
167
-
168
- 3. **PR Description**
169
- - Clear description of changes
170
- - Link to related issues
171
- - List of testing steps
172
- - Screenshots (if UI changes)
173
-
174
- 4. **Review Process**
175
- - At least one approval required
176
- - All comments must be resolved
177
- - CI checks must pass
178
- - No merge conflicts
179
-
180
- ## Commit Guidelines
181
-
182
- ### Commit Message Format
183
- ```
184
- <type>(<scope>): <subject>
185
-
186
- <body>
187
-
188
- <footer>
189
- ```
190
-
191
- ### Types
192
- - feat: New feature
193
- - fix: Bug fix
194
- - docs: Documentation
195
- - style: Formatting
196
- - refactor: Code restructuring
197
- - test: Adding tests
198
- - chore: Maintenance
199
-
200
- ### Example
201
- ```
202
- feat(auth): implement OAuth2 authentication
203
-
204
- - Add OAuth2 provider integration
205
- - Implement token refresh mechanism
206
- - Add user session management
207
-
208
- Closes #123
209
- ```
210
-
211
- ### Best Practices
212
- - Keep commits focused and atomic
213
- - Write clear commit messages
214
- - Reference issues in commits
215
- - Squash related commits
216
-
217
- ## Questions or Problems?
218
-
219
- Feel free to:
220
- - Open an issue for questions
221
- - Join our community chat
222
- - Check existing documentation
223
- - Contact the maintainers
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Gradio_UI.py CHANGED
@@ -1,296 +1,73 @@
1
- #!/usr/bin/env python
2
- # coding=utf-8
3
- # Copyright 2024 The HuggingFace Inc. team. All rights reserved.
4
- #
5
- # Licensed under the Apache License, Version 2.0 (the "License");
6
- # you may not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
11
- # Unless required by applicable law or agreed to in writing, software
12
- # distributed under the License is distributed on an "AS IS" BASIS,
13
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- # See the License for the specific language governing permissions and
15
- # limitations under the License.
16
- import mimetypes
17
- import os
18
- import re
19
- import shutil
20
- from typing import Optional
21
-
22
- from smolagents.agent_types import AgentAudio, AgentImage, AgentText, handle_agent_output_types
23
- from smolagents.agents import ActionStep, MultiStepAgent
24
- from smolagents.memory import MemoryStep
25
- from smolagents.utils import _is_package_available
26
-
27
-
28
- def pull_messages_from_step(
29
- step_log: MemoryStep,
30
- ):
31
- """Extract ChatMessage objects from agent steps with proper nesting"""
32
- import gradio as gr
33
-
34
- if isinstance(step_log, ActionStep):
35
- # Output the step number
36
- step_number = f"Step {step_log.step_number}" if step_log.step_number is not None else ""
37
- yield gr.ChatMessage(role="assistant", content=f"**{step_number}**")
38
-
39
- # First yield the thought/reasoning from the LLM
40
- if hasattr(step_log, "model_output") and step_log.model_output is not None:
41
- # Clean up the LLM output
42
- model_output = step_log.model_output.strip()
43
- # Remove any trailing <end_code> and extra backticks, handling multiple possible formats
44
- model_output = re.sub(r"```\s*<end_code>", "```", model_output) # handles ```<end_code>
45
- model_output = re.sub(r"<end_code>\s*```", "```", model_output) # handles <end_code>```
46
- model_output = re.sub(r"```\s*\n\s*<end_code>", "```", model_output) # handles ```\n<end_code>
47
- model_output = model_output.strip()
48
- yield gr.ChatMessage(role="assistant", content=model_output)
49
-
50
- # For tool calls, create a parent message
51
- if hasattr(step_log, "tool_calls") and step_log.tool_calls is not None:
52
- first_tool_call = step_log.tool_calls[0]
53
- used_code = first_tool_call.name == "python_interpreter"
54
- parent_id = f"call_{len(step_log.tool_calls)}"
55
-
56
- # Tool call becomes the parent message with timing info
57
- # First we will handle arguments based on type
58
- args = first_tool_call.arguments
59
- if isinstance(args, dict):
60
- content = str(args.get("answer", str(args)))
61
- else:
62
- content = str(args).strip()
63
-
64
- if used_code:
65
- # Clean up the content by removing any end code tags
66
- content = re.sub(r"```.*?\n", "", content) # Remove existing code blocks
67
- content = re.sub(r"\s*<end_code>\s*", "", content) # Remove end_code tags
68
- content = content.strip()
69
- if not content.startswith("```python"):
70
- content = f"```python\n{content}\n```"
71
-
72
- parent_message_tool = gr.ChatMessage(
73
- role="assistant",
74
- content=content,
75
- metadata={
76
- "title": f"🛠️ Used tool {first_tool_call.name}",
77
- "id": parent_id,
78
- "status": "pending",
79
- },
80
- )
81
- yield parent_message_tool
82
-
83
- # Nesting execution logs under the tool call if they exist
84
- if hasattr(step_log, "observations") and (
85
- step_log.observations is not None and step_log.observations.strip()
86
- ): # Only yield execution logs if there's actual content
87
- log_content = step_log.observations.strip()
88
- if log_content:
89
- log_content = re.sub(r"^Execution logs:\s*", "", log_content)
90
- yield gr.ChatMessage(
91
- role="assistant",
92
- content=f"{log_content}",
93
- metadata={"title": "📝 Execution Logs", "parent_id": parent_id, "status": "done"},
94
- )
95
-
96
- # Nesting any errors under the tool call
97
- if hasattr(step_log, "error") and step_log.error is not None:
98
- yield gr.ChatMessage(
99
- role="assistant",
100
- content=str(step_log.error),
101
- metadata={"title": "💥 Error", "parent_id": parent_id, "status": "done"},
102
- )
103
-
104
- # Update parent message metadata to done status without yielding a new message
105
- parent_message_tool.metadata["status"] = "done"
106
-
107
- # Handle standalone errors but not from tool calls
108
- elif hasattr(step_log, "error") and step_log.error is not None:
109
- yield gr.ChatMessage(role="assistant", content=str(step_log.error), metadata={"title": "💥 Error"})
110
-
111
- # Calculate duration and token information
112
- step_footnote = f"{step_number}"
113
- if hasattr(step_log, "input_token_count") and hasattr(step_log, "output_token_count"):
114
- token_str = (
115
- f" | Input-tokens:{step_log.input_token_count:,} | Output-tokens:{step_log.output_token_count:,}"
116
- )
117
- step_footnote += token_str
118
- if hasattr(step_log, "duration"):
119
- step_duration = f" | Duration: {round(float(step_log.duration), 2)}" if step_log.duration else None
120
- step_footnote += step_duration
121
- step_footnote = f"""<span style="color: #bbbbc2; font-size: 12px;">{step_footnote}</span> """
122
- yield gr.ChatMessage(role="assistant", content=f"{step_footnote}")
123
- yield gr.ChatMessage(role="assistant", content="-----")
124
-
125
-
126
- def stream_to_gradio(
127
- agent,
128
- task: str,
129
- reset_agent_memory: bool = False,
130
- additional_args: Optional[dict] = None,
131
- ):
132
- """Runs an agent with the given task and streams the messages from the agent as gradio ChatMessages."""
133
- if not _is_package_available("gradio"):
134
- raise ModuleNotFoundError(
135
- "Please install 'gradio' extra to use the GradioUI: `pip install 'smolagents[gradio]'`"
136
- )
137
- import gradio as gr
138
-
139
- total_input_tokens = 0
140
- total_output_tokens = 0
141
-
142
- for step_log in agent.run(task, stream=True, reset=reset_agent_memory, additional_args=additional_args):
143
- # Track tokens if model provides them
144
- if hasattr(agent.model, "last_input_token_count"):
145
- total_input_tokens += agent.model.last_input_token_count
146
- total_output_tokens += agent.model.last_output_token_count
147
- if isinstance(step_log, ActionStep):
148
- step_log.input_token_count = agent.model.last_input_token_count
149
- step_log.output_token_count = agent.model.last_output_token_count
150
-
151
- for message in pull_messages_from_step(
152
- step_log,
153
- ):
154
- yield message
155
-
156
- final_answer = step_log # Last log is the run's final_answer
157
- final_answer = handle_agent_output_types(final_answer)
158
-
159
- if isinstance(final_answer, AgentText):
160
- yield gr.ChatMessage(
161
- role="assistant",
162
- content=f"**Final answer:**\n{final_answer.to_string()}\n",
163
- )
164
- elif isinstance(final_answer, AgentImage):
165
- yield gr.ChatMessage(
166
- role="assistant",
167
- content={"path": final_answer.to_string(), "mime_type": "image/png"},
168
- )
169
- elif isinstance(final_answer, AgentAudio):
170
- yield gr.ChatMessage(
171
- role="assistant",
172
- content={"path": final_answer.to_string(), "mime_type": "audio/wav"},
173
- )
174
- else:
175
- yield gr.ChatMessage(role="assistant", content=f"**Final answer:** {str(final_answer)}")
176
-
177
 
178
  class GradioUI:
179
- """A one-line interface to launch your agent in Gradio"""
180
-
181
- def __init__(self, agent: MultiStepAgent, file_upload_folder: str | None = None):
182
- if not _is_package_available("gradio"):
183
- raise ModuleNotFoundError(
184
- "Please install 'gradio' extra to use the GradioUI: `pip install 'smolagents[gradio]'`"
185
- )
186
  self.agent = agent
187
- self.file_upload_folder = file_upload_folder
188
- if self.file_upload_folder is not None:
189
- if not os.path.exists(file_upload_folder):
190
- os.mkdir(file_upload_folder)
191
-
192
- def interact_with_agent(self, prompt, messages):
193
- import gradio as gr
194
-
195
- messages.append(gr.ChatMessage(role="user", content=prompt))
196
- yield messages
197
- for msg in stream_to_gradio(self.agent, task=prompt, reset_agent_memory=False):
198
- messages.append(msg)
199
- yield messages
200
- yield messages
201
-
202
- def upload_file(
203
- self,
204
- file,
205
- file_uploads_log,
206
- allowed_file_types=[
207
- "application/pdf",
208
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
209
- "text/plain",
210
- ],
211
- ):
212
- """
213
- Handle file uploads, default allowed types are .pdf, .docx, and .txt
214
- """
215
- import gradio as gr
216
-
217
- if file is None:
218
- return gr.Textbox("No file uploaded", visible=True), file_uploads_log
219
-
220
- try:
221
- mime_type, _ = mimetypes.guess_type(file.name)
222
- except Exception as e:
223
- return gr.Textbox(f"Error: {e}", visible=True), file_uploads_log
224
-
225
- if mime_type not in allowed_file_types:
226
- return gr.Textbox("File type disallowed", visible=True), file_uploads_log
227
-
228
- # Sanitize file name
229
- original_name = os.path.basename(file.name)
230
- sanitized_name = re.sub(
231
- r"[^\w\-.]", "_", original_name
232
- ) # Replace any non-alphanumeric, non-dash, or non-dot characters with underscores
233
-
234
- type_to_ext = {}
235
- for ext, t in mimetypes.types_map.items():
236
- if t not in type_to_ext:
237
- type_to_ext[t] = ext
238
-
239
- # Ensure the extension correlates to the mime type
240
- sanitized_name = sanitized_name.split(".")[:-1]
241
- sanitized_name.append("" + type_to_ext[mime_type])
242
- sanitized_name = "".join(sanitized_name)
243
-
244
- # Save the uploaded file to the specified folder
245
- file_path = os.path.join(self.file_upload_folder, os.path.basename(sanitized_name))
246
- shutil.copy(file.name, file_path)
247
-
248
- return gr.Textbox(f"File uploaded: {file_path}", visible=True), file_uploads_log + [file_path]
249
-
250
- def log_user_message(self, text_input, file_uploads_log):
251
- return (
252
- text_input
253
- + (
254
- f"\nYou have been provided with these files, which might be helpful or not: {file_uploads_log}"
255
- if len(file_uploads_log) > 0
256
- else ""
257
- ),
258
- "",
259
- )
260
-
261
  def launch(self, **kwargs):
262
- import gradio as gr
263
-
264
- with gr.Blocks(fill_height=True) as demo:
265
- stored_messages = gr.State([])
266
- file_uploads_log = gr.State([])
 
 
 
 
 
 
 
 
 
 
267
  chatbot = gr.Chatbot(
268
- label="Agent",
269
- type="messages",
270
- avatar_images=(
271
- None,
272
- "https://huggingface.co/datasets/agents-course/course-images/resolve/main/en/communication/Alfred.png",
273
- ),
274
- resizeable=True,
275
- scale=1,
276
  )
277
- # If an upload folder is provided, enable the upload feature
278
- if self.file_upload_folder is not None:
279
- upload_file = gr.File(label="Upload a file")
280
- upload_status = gr.Textbox(label="Upload Status", interactive=False, visible=False)
281
- upload_file.change(
282
- self.upload_file,
283
- [upload_file, file_uploads_log],
284
- [upload_status, file_uploads_log],
285
  )
286
- text_input = gr.Textbox(lines=1, label="Chat Message")
287
- text_input.submit(
288
- self.log_user_message,
289
- [text_input, file_uploads_log],
290
- [stored_messages, text_input],
291
- ).then(self.interact_with_agent, [stored_messages, chatbot], [chatbot])
292
-
293
- demo.launch(debug=True, share=True, **kwargs)
294
-
295
-
296
- __all__ = ["stream_to_gradio", "GradioUI"]
 
1
+ import gradio as gr
2
+ import time
3
+ from typing import List, Dict, Any, Optional
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  class GradioUI:
6
+ def __init__(self, agent):
 
 
 
 
 
 
7
  self.agent = agent
8
+ self.chat_history = []
9
+
10
+ def process_agent_response(self, response):
11
+ """Process the agent's response for display in the UI."""
12
+ if isinstance(response, str):
13
+ return response
14
+ elif hasattr(response, "content"):
15
+ return response.content
16
+ else:
17
+ return str(response)
18
+
19
+ def chat(self, message, history):
20
+ """Process a user message and return the agent's response."""
21
+ self.chat_history = history
22
+
23
+ # Add user message to history
24
+ self.chat_history.append((message, ""))
25
+
26
+ # Get response from agent
27
+ response = self.agent.run(message)
28
+ processed_response = self.process_agent_response(response)
29
+
30
+ # Update the last response in history
31
+ self.chat_history[-1] = (message, processed_response)
32
+
33
+ return "", self.chat_history
34
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  def launch(self, **kwargs):
36
+ """Launch the Gradio interface."""
37
+ with gr.Blocks(css="footer {visibility: hidden}") as demo:
38
+ gr.Markdown("# 🤖 My First Agent with smolagents")
39
+ gr.Markdown("""
40
+ This agent can:
41
+ - Search the web using DuckDuckGo
42
+ - Get weather information for locations
43
+
44
+ Try asking it questions like:
45
+ - "What's the weather like in Tokyo?"
46
+ - "What are the latest news about AI?"
47
+ - "Who won the last World Cup?"
48
+ - "Tell me about the history of computers"
49
+ """)
50
+
51
  chatbot = gr.Chatbot(
52
+ [],
53
+ elem_id="chatbot",
54
+ avatar_images=(None, "🤖"),
55
+ bubble_full_width=False,
56
+ height=500
 
 
 
57
  )
58
+
59
+ with gr.Row():
60
+ txt = gr.Textbox(
61
+ scale=4,
62
+ show_label=False,
63
+ placeholder="Enter your message here...",
64
+ container=False
 
65
  )
66
+ submit_btn = gr.Button("Send", variant="primary", scale=1)
67
+
68
+ txt.submit(self.chat, [txt, chatbot], [txt, chatbot])
69
+ submit_btn.click(self.chat, [txt, chatbot], [txt, chatbot])
70
+
71
+ gr.Markdown("### 📝 Created as part of the [Hugging Face Agents Course](https://huggingface.co/learn/agents)")
72
+
73
+ return demo.launch(**kwargs)
 
 
 
LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 AI Assistant Team
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.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,690 +1,42 @@
1
- ---
2
- title: First Agent Template
3
- emoji: ⚡
4
- colorFrom: pink
5
- colorTo: yellow
6
- sdk: gradio
7
- sdk_version: 5.15.0
8
- app_file: app.py
9
- pinned: false
10
- tags:
11
- - smolagents
12
- - agent
13
- - smolagent
14
- - tool
15
- - agent-course
16
- ---
17
 
18
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
19
-
20
- # AI Assistant Framework
21
-
22
- [![Tests](https://github.com/yourusername/ai-assistant/actions/workflows/test.yml/badge.svg)](https://github.com/yourusername/ai-assistant/actions/workflows/test.yml)
23
- [![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/yourusername/coverage.json)](https://github.com/yourusername/ai-assistant/actions/workflows/test.yml)
24
- [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
25
-
26
- A robust framework for building AI assistants with built-in error handling, logging, and API integration capabilities.
27
 
28
  ## Features
29
 
30
- - 🛠 **Extensible Tool System**: Create and register custom tools with a simple interface
31
- - 🔄 **Error Recovery**: Built-in retry mechanism and circuit breaker pattern
32
- - 📝 **Comprehensive Logging**: Structured logging with security-aware data sanitization
33
- - 🔒 **Type Safety**: Full type hints and runtime type checking
34
- - 🌐 **API Integration**: Robust API error handling and rate limiting
35
- - ⚡ **Performance**: Efficient caching and resource management
36
- - 🧪 **Testing**: Extensive test coverage and mocking utilities
37
-
38
- ## Quick Start
39
-
40
- ### Installation
41
-
42
- ```bash
43
- pip install ai-assistant
44
- ```
45
-
46
- ### Basic Usage
47
-
48
- ```python
49
- from tools import get_tool
50
-
51
- # Get a timezone tool
52
- timezone_tool = get_tool("timezone")
53
-
54
- # Use the tool
55
- current_time = timezone_tool.get_current_time("America/New_York")
56
- print(f"Current time: {current_time}")
57
- ```
58
-
59
- ### API Integration Example
60
-
61
- ```python
62
- from tools.api_logging import log_api_call
63
- from tools.error_recovery import RetryConfig, CircuitBreaker
64
-
65
- @log_api_call(
66
- retry_config=RetryConfig(max_retries=3),
67
- circuit_breaker=CircuitBreaker(failure_threshold=5)
68
- )
69
- def get_weather(zipcode: str) -> dict:
70
- # Your API call here
71
- pass
72
- ```
73
-
74
- ## Documentation
75
-
76
- - [Getting Started Guide](docs/getting_started.md)
77
- - [API Reference](docs/api_reference.md)
78
- - [Error Handling Guide](docs/error_handling.md)
79
- - [Logging System](docs/logging.md)
80
-
81
- ## Core Components
82
-
83
- ### Tool Registry
84
-
85
- The framework uses a central registry for managing tools:
86
-
87
- ```python
88
- from tools import ToolRegistry, register_tool
89
-
90
- @register_tool
91
- class MyCustomTool(BaseTool):
92
- @property
93
- def name(self) -> str:
94
- return "my_tool"
95
- ```
96
-
97
- ### Error Recovery
98
-
99
- Built-in support for handling API errors:
100
-
101
- ```python
102
- from tools.exceptions import APIError, APITimeoutError
103
-
104
- try:
105
- result = api_call()
106
- except APITimeoutError as e:
107
- print(f"Timeout after {e.timeout}s")
108
- ```
109
-
110
- ### Logging System
111
-
112
- Comprehensive logging with security features:
113
-
114
- ```python
115
- @log_api_call(
116
- log_response=True,
117
- log_request_body=True
118
- )
119
- def secure_api_call():
120
- # Sensitive data is automatically sanitized
121
- pass
122
- ```
123
-
124
- ## Development
125
-
126
- ### Prerequisites
127
-
128
- - Python 3.9+
129
- - pip
130
-
131
- ### Setup
132
-
133
- 1. Clone the repository:
134
- ```bash
135
- git clone https://github.com/yourusername/ai-assistant.git
136
- cd ai-assistant
137
- ```
138
-
139
- 2. Create a virtual environment:
140
- ```bash
141
- python -m venv venv
142
- source venv/bin/activate # Linux/Mac
143
- # or
144
- .\venv\Scripts\activate # Windows
145
- ```
146
-
147
- 3. Install dependencies:
148
- ```bash
149
- pip install -r requirements.txt
150
- pip install -r requirements-dev.txt
151
- ```
152
-
153
- ### Running Tests
154
-
155
- ```bash
156
- python scripts/run_tests.py
157
- ```
158
-
159
- ### Code Quality
160
-
161
- ```bash
162
- # Format code
163
- black .
164
-
165
- # Type checking
166
- mypy tools tests
167
-
168
- # Linting
169
- flake8
170
- ```
171
-
172
- ## Contributing
173
-
174
- 1. Fork the repository
175
- 2. Create a feature branch
176
- 3. Commit your changes
177
- 4. Push to the branch
178
- 5. Create a Pull Request
179
-
180
- Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and development process.
181
-
182
- ## License
183
-
184
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
185
-
186
- ## Acknowledgments
187
-
188
- - Thanks to all contributors who have helped shape this framework
189
- - Special thanks to the open source community for their invaluable tools and libraries
190
-
191
- # AI Assistant Application
192
-
193
- A Python-based AI assistant application that provides weather information and timezone conversions through a user-friendly Gradio interface.
194
-
195
- ## Features
196
-
197
- - Weather information lookup by zip code using WeatherAPI
198
- - Timezone conversion functionality
199
- - Rate limiting for API calls
200
- - Configurable UI with Gradio
201
- - Comprehensive error handling
202
- - Logging system
203
-
204
- ## Prerequisites
205
-
206
- - Python 3.8 or higher
207
- - pip package manager
208
-
209
- ## Installation
210
-
211
- 1. Clone the repository:
212
- ```bash
213
- git clone <repository-url>
214
- cd <repository-name>
215
- ```
216
-
217
- 2. Create and activate a virtual environment:
218
- ```bash
219
- python3 -m venv venv
220
- source venv/bin/activate # On Windows: venv\Scripts\activate
221
- ```
222
-
223
- 3. Install dependencies:
224
- ```bash
225
- pip install -r requirements.txt
226
- ```
227
-
228
- 4. Create a `.env` file in the project root and configure your environment variables:
229
- ```
230
- WEATHER_API_KEY=your_weather_api_key_here
231
- ```
232
-
233
- ## Configuration
234
-
235
- The application can be configured through:
236
- - `.env` file for environment variables
237
- - `config.py` for application settings
238
- - `prompts.yaml` for AI assistant prompts
239
-
240
- ## Project Structure
241
-
242
- ```
243
- .
244
- ├── app.py # Main application file
245
- ├── config.py # Configuration settings
246
- ├── rate_limiter.py # Rate limiting implementation
247
- ├── ui.py # Gradio UI implementation
248
- ├── tools/ # Tool implementations
249
- │ ├── __init__.py
250
- │ └── final_answer.py
251
- ├── prompts.yaml # AI assistant prompts
252
- ├── requirements.txt # Project dependencies
253
- └── .env # Environment variables
254
- ```
255
-
256
- ## Usage
257
-
258
- 1. Ensure your virtual environment is activated
259
- 2. Run the application:
260
- ```bash
261
- python app.py
262
- ```
263
- 3. Open your web browser and navigate to the URL shown in the console
264
-
265
- ## Error Handling
266
-
267
- The application includes comprehensive error handling for:
268
- - API rate limits
269
- - Invalid inputs
270
- - Network errors
271
- - Configuration issues
272
-
273
- ## Logging
274
-
275
- Logs are written to `assistant.log` with configurable log levels through the `.env` file.
276
-
277
- ## Contributing
278
-
279
- 1. Fork the repository
280
- 2. Create a feature branch
281
- 3. Commit your changes
282
- 4. Push to the branch
283
- 5. Create a Pull Request
284
-
285
- ## License
286
-
287
- This project is licensed under the MIT License - see the LICENSE file for details.
288
-
289
- # AI Assistant Configuration Guide
290
-
291
- This document provides a comprehensive guide to configuring the AI Assistant application. Configuration can be managed through environment variables or a JSON configuration file.
292
-
293
- ## Table of Contents
294
- - [Environment Variables](#environment-variables)
295
- - [Configuration Options](#configuration-options)
296
- - [Weather API Configuration](#weather-api-configuration)
297
- - [Timezone API Configuration](#timezone-api-configuration)
298
- - [Assistant Configuration](#assistant-configuration)
299
- - [UI Configuration](#ui-configuration)
300
- - [Logging Configuration](#logging-configuration)
301
- - [Model Configuration](#model-configuration)
302
- - [Usage Examples](#usage-examples)
303
-
304
- ## Environment Variables
305
-
306
- All configuration options can be set via environment variables. Environment variables take precedence over JSON configuration. Create a `.env` file in the project root to set these variables.
307
-
308
- Example `.env` file:
309
- ```bash
310
- WEATHER_API_KEY=your_api_key_here
311
- LOG_LEVEL=INFO
312
- UI_THEME=dark
313
- ```
314
-
315
- ## Configuration Options
316
-
317
- ### Weather API Configuration
318
-
319
- Controls the weather service API settings.
320
-
321
- | Option | Environment Variable | Default | Description |
322
- |--------|---------------------|---------|-------------|
323
- | `base_url` | `WEATHER_API_BASE_URL` | `http://api.weatherapi.com/v1` | Base URL for the weather API |
324
- | `rate_limit_per_minute` | `WEATHER_API_RATE_LIMIT` | `60` | Maximum API calls per minute |
325
- | `cache_timeout_seconds` | `WEATHER_CACHE_TIMEOUT` | `300` | How long to cache weather data |
326
- | `api_key` | `WEATHER_API_KEY` | `None` | Your weather API key |
327
-
328
- ### Timezone API Configuration
329
-
330
- Controls the timezone service API settings.
331
-
332
- | Option | Environment Variable | Default | Description |
333
- |--------|---------------------|---------|-------------|
334
- | `rate_limit_per_minute` | `TIMEZONE_API_RATE_LIMIT` | `100` | Maximum API calls per minute |
335
-
336
- ### Assistant Configuration
337
-
338
- Controls the AI Assistant's behavior.
339
-
340
- #### Command History
341
- | Option | Environment Variable | Default | Description |
342
- |--------|---------------------|---------|-------------|
343
- | `max_size` | `COMMAND_HISTORY_MAX_SIZE` | `10` | Maximum number of commands to remember |
344
-
345
- #### Cache Settings
346
- | Option | Environment Variable | Default | Description |
347
- |--------|---------------------|---------|-------------|
348
- | `cleanup_interval_seconds` | `CACHE_CLEANUP_INTERVAL` | `600` | Interval between cache cleanup operations |
349
-
350
- ### UI Configuration
351
-
352
- Controls the user interface appearance and behavior.
353
-
354
- | Option | Environment Variable | Default | Description |
355
- |--------|---------------------|---------|-------------|
356
- | `title` | `UI_TITLE` | `AI Assistant` | Application title |
357
- | `description` | `UI_DESCRIPTION` | `Weather and Timezone Assistant` | Application description |
358
- | `theme` | `UI_THEME` | `light` | UI theme (`light` or `dark`) |
359
- | `input_placeholder` | `UI_INPUT_PLACEHOLDER` | `Enter your command (type 'help' for available commands)` | Input field placeholder text |
360
- | `width` | `UI_WIDTH` | `100%` | UI width (CSS units) |
361
- | `height` | `UI_HEIGHT` | `600px` | UI height (CSS units) |
362
-
363
- ### Logging Configuration
364
-
365
- Controls application logging behavior.
366
-
367
- | Option | Environment Variable | Default | Description |
368
- |--------|---------------------|---------|-------------|
369
- | `level` | `LOG_LEVEL` | `INFO` | Log level (`DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`) |
370
- | `file` | `LOG_FILE` | `assistant.log` | Log file path |
371
- | `format` | `LOG_FORMAT` | `%(asctime)s - %(name)s - %(levelname)s - %(message)s` | Log message format |
372
-
373
- ### Model Configuration
374
-
375
- Controls the AI model behavior.
376
-
377
- | Option | Environment Variable | Default | Description |
378
- |--------|---------------------|---------|-------------|
379
- | `class_name` | `MODEL_CLASS` | `HfApiModel` | Model class to use |
380
- | `max_tokens` | `MODEL_MAX_TOKENS` | `2096` | Maximum tokens per response |
381
- | `temperature` | `MODEL_TEMPERATURE` | `0.5` | Response randomness (0.0-1.0) |
382
- | `model_id` | `MODEL_ID` | `Qwen/Qwen2.5-Coder-32B-Instruct` | Model identifier |
383
-
384
- ## Usage Examples
385
-
386
- ### Using Environment Variables
387
-
388
- ```bash
389
- # Set weather API configuration
390
- export WEATHER_API_KEY=your_api_key_here
391
- export WEATHER_API_RATE_LIMIT=30
392
-
393
- # Configure logging
394
- export LOG_LEVEL=DEBUG
395
- export LOG_FILE=logs/assistant.log
396
-
397
- # Set UI preferences
398
- export UI_THEME=dark
399
- export UI_WIDTH=80%
400
- ```
401
-
402
- ### Using JSON Configuration
403
-
404
- Create a `config.json` file:
405
-
406
- ```json
407
- {
408
- "config": {
409
- "api": {
410
- "weather": {
411
- "base_url": "http://api.weatherapi.com/v1",
412
- "rate_limit_per_minute": 30,
413
- "cache_timeout_seconds": 600
414
- },
415
- "timezone": {
416
- "rate_limit_per_minute": 50
417
- }
418
- },
419
- "assistant": {
420
- "command_history": {
421
- "max_size": 20
422
- },
423
- "cache": {
424
- "cleanup_interval_seconds": 300
425
- }
426
- },
427
- "ui": {
428
- "theme": "dark",
429
- "width": "80%",
430
- "height": "800px"
431
- },
432
- "logging": {
433
- "level": "DEBUG",
434
- "file": "logs/debug.log"
435
- }
436
- },
437
- "model": {
438
- "class": "HfApiModel",
439
- "data": {
440
- "max_tokens": 1024,
441
- "temperature": 0.7,
442
- "model_id": "Qwen/Qwen2.5-Coder-32B-Instruct"
443
- }
444
- }
445
- }
446
- ```
447
-
448
- ### Loading Configuration in Code
449
-
450
- ```python
451
- from config import Config
452
-
453
- # Load from environment variables
454
- config = Config()
455
-
456
- # Or load from JSON file
457
- with open('config.json') as f:
458
- import json
459
- config = Config.from_json(json.load(f))
460
-
461
- # Access configuration
462
- weather_url = config.api.weather.base_url
463
- log_level = config.logging.level
464
- ui_theme = config.ui.theme
465
- ```
466
-
467
- ## Validation
468
-
469
- The configuration system includes comprehensive validation:
470
-
471
- - All numeric values are checked for valid ranges
472
- - URLs are validated for correct format
473
- - File paths are checked for existence/createability
474
- - Enums (like log levels and themes) are validated against allowed values
475
- - Cross-field validation ensures consistent configuration
476
-
477
- If validation fails, a `ConfigValidationError` is raised with a descriptive message and the field name that caused the error.
478
-
479
- # API Configuration Guide
480
-
481
- This document describes all available configuration options for the API client. Configuration can be provided via environment variables or a JSON configuration file.
482
-
483
- ## Table of Contents
484
- - [Configuration Methods](#configuration-methods)
485
- - [Base Configuration](#base-configuration)
486
- - [Connection Pool Settings](#connection-pool-settings)
487
- - [Rate Limiting Settings](#rate-limiting-settings)
488
- - [Batch Processing Settings](#batch-processing-settings)
489
- - [Monitoring Settings](#monitoring-settings)
490
- - [Retry Settings](#retry-settings)
491
- - [Environment Variables](#environment-variables)
492
- - [Examples](#examples)
493
-
494
- ## Configuration Methods
495
-
496
- Configuration can be provided in two ways:
497
-
498
- 1. **Environment Variables**: Set using the format `API_<SECTION>_<SETTING>`
499
- 2. **JSON Configuration File**: Provide settings in `agent.json`
500
-
501
- Environment variables take precedence over file-based configuration.
502
-
503
- ## Base Configuration
504
-
505
- | Setting | Description | Type | Required | Default | Validation |
506
- |------------|--------------------|----------|----------|---------------------|-------------------------------|
507
- | `base_url` | Base URL for API | string | Yes | http://localhost:8000 | Must be valid HTTP/HTTPS URL |
508
-
509
- ## Connection Pool Settings
510
-
511
- | Setting | Description | Type | Default | Range | Environment Variable |
512
- |-------------------|------------------------------|---------|---------|--------------|----------------------------------|
513
- | `pool_size` | Maximum connections | integer | 10 | 1-100 | `API_CONNECTION_POOL_SIZE` |
514
- | `timeout` | Connection timeout (seconds) | float | 30.0 | 0.0-300.0 | `API_CONNECTION_TIMEOUT` |
515
- | `keepalive_timeout` | Keep-alive timeout (seconds) | float | None | 0.0-300.0 | `API_CONNECTION_KEEPALIVE_TIMEOUT` |
516
- | `ttl_dns_cache` | DNS cache TTL (seconds) | integer | None | 0-3600 | `API_CONNECTION_TTL_DNS_CACHE` |
517
- | `force_close` | Force close connections | boolean | false | true/false | `API_CONNECTION_FORCE_CLOSE` |
518
-
519
- **Validation Rules**:
520
- - `keepalive_timeout` must be less than or equal to `timeout`
521
- - `pool_size` should be set based on expected concurrent connections
522
-
523
- ## Rate Limiting Settings
524
-
525
- | Setting | Description | Type | Default | Range | Environment Variable |
526
- |-----------|------------------------|---------|---------|------------|------------------------|
527
- | `rate` | Requests per second | float | 10.0 | 0.1-1000.0 | `API_RATE_LIMIT_RATE` |
528
- | `burst` | Maximum burst size | integer | 20 | 1-2000 | `API_RATE_LIMIT_BURST` |
529
- | `enabled` | Enable rate limiting | boolean | true | true/false | `API_RATE_LIMIT_ENABLED` |
530
-
531
- **Validation Rules**:
532
- - `burst` must be greater than or equal to `rate`
533
- - `burst` must not exceed 10x the `rate`
534
- - Recommended: Set `burst` to 2-3x the `rate` for normal operation
535
-
536
- ## Batch Processing Settings
537
-
538
- | Setting | Description | Type | Default | Range | Environment Variable |
539
- |------------|-------------------------|---------|---------|-----------|---------------------|
540
- | `size` | Maximum items per batch | integer | 100 | 1-1000 | `API_BATCH_SIZE` |
541
- | `interval` | Flush interval (seconds)| float | 1.0 | 0.1-60.0 | `API_BATCH_INTERVAL` |
542
- | `enabled` | Enable batch processing | boolean | true | true/false| `API_BATCH_ENABLED` |
543
-
544
- **Best Practices**:
545
- - Set `size` based on your API's batch processing capabilities
546
- - Adjust `interval` based on latency requirements and load
547
-
548
- ## Monitoring Settings
549
-
550
- | Setting | Description | Type | Default | Range | Environment Variable |
551
- |----------------|--------------------------------|---------|---------|------------|-------------------------------|
552
- | `metrics_ttl` | Metrics TTL (seconds) | integer | 3600 | 0-86400 | `API_MONITORING_METRICS_TTL` |
553
- | `max_metrics` | Maximum metrics to store | integer | 1000 | 1-10000 | `API_MONITORING_MAX_METRICS` |
554
- | `log_interval` | Metrics logging interval (sec) | float | 60.0 | 0.1-3600.0 | `API_MONITORING_LOG_INTERVAL` |
555
- | `enabled` | Enable monitoring | boolean | true | true/false | `API_MONITORING_ENABLED` |
556
-
557
- **Validation Rules**:
558
- - `log_interval` must be less than `metrics_ttl`
559
- - Consider memory usage when setting `max_metrics`
560
-
561
- ## Retry Settings
562
-
563
- | Setting | Description | Type | Default | Range | Environment Variable |
564
- |----------------|---------------------------------|---------|---------|------------|-------------------------|
565
- | `max_attempts` | Maximum retry attempts | integer | 3 | 1-10 | `API_RETRY_MAX_ATTEMPTS` |
566
- | `min_wait` | Minimum wait time (seconds) | float | 1.0 | 0.1-60.0 | `API_RETRY_MIN_WAIT` |
567
- | `max_wait` | Maximum wait time (seconds) | float | 10.0 | 0.1-300.0 | `API_RETRY_MAX_WAIT` |
568
- | `enabled` | Enable retry mechanism | boolean | true | true/false | `API_RETRY_ENABLED` |
569
-
570
- **Validation Rules**:
571
- - `max_wait` must be greater than `min_wait`
572
- - `max_wait` must not exceed 10x `min_wait`
573
- - Uses exponential backoff between `min_wait` and `max_wait`
574
-
575
- ## Environment Variables
576
-
577
- All settings can be configured using environment variables. Example `.env` file:
578
-
579
- ```bash
580
- # Base Configuration
581
- API_BASE_URL=http://api.example.com
582
-
583
- # Connection Pool
584
- API_CONNECTION_POOL_SIZE=20
585
- API_CONNECTION_TIMEOUT=30.0
586
- API_CONNECTION_KEEPALIVE_TIMEOUT=60.0
587
- API_CONNECTION_TTL_DNS_CACHE=300
588
- API_CONNECTION_FORCE_CLOSE=false
589
-
590
- # Rate Limiting
591
- API_RATE_LIMIT_RATE=50.0
592
- API_RATE_LIMIT_BURST=100
593
- API_RATE_LIMIT_ENABLED=true
594
-
595
- # Batch Processing
596
- API_BATCH_SIZE=500
597
- API_BATCH_INTERVAL=2.0
598
- API_BATCH_ENABLED=true
599
 
600
- # Monitoring
601
- API_MONITORING_METRICS_TTL=7200
602
- API_MONITORING_MAX_METRICS=5000
603
- API_MONITORING_LOG_INTERVAL=300.0
604
- API_MONITORING_ENABLED=true
605
 
606
- # Retry
607
- API_RETRY_MAX_ATTEMPTS=5
608
- API_RETRY_MIN_WAIT=1.0
609
- API_RETRY_MAX_WAIT=30.0
610
- API_RETRY_ENABLED=true
611
- ```
612
 
613
- ## Examples
614
 
615
- ### Minimal Configuration
 
 
 
616
 
617
- ```json
618
- {
619
- "config": {
620
- "base_url": "http://api.example.com"
621
- }
622
- }
623
- ```
624
 
625
- ### High-Performance Configuration
 
 
 
 
626
 
627
- ```json
628
- {
629
- "config": {
630
- "base_url": "http://api.example.com",
631
- "connection_pool": {
632
- "pool_size": 50,
633
- "timeout": 60.0,
634
- "keepalive_timeout": 30.0,
635
- "ttl_dns_cache": 300
636
- },
637
- "rate_limit": {
638
- "rate": 100.0,
639
- "burst": 200
640
- },
641
- "batch": {
642
- "size": 500,
643
- "interval": 2.0
644
- }
645
- }
646
- }
647
- ```
648
 
649
- ### High-Reliability Configuration
650
 
651
- ```json
652
- {
653
- "config": {
654
- "base_url": "http://api.example.com",
655
- "retry": {
656
- "max_attempts": 5,
657
- "min_wait": 1.0,
658
- "max_wait": 30.0
659
- },
660
- "monitoring": {
661
- "metrics_ttl": 7200,
662
- "max_metrics": 5000,
663
- "log_interval": 300.0
664
- }
665
- }
666
- }
667
- ```
668
 
669
- ### Development Configuration
670
 
671
- ```json
672
- {
673
- "config": {
674
- "base_url": "http://localhost:8000",
675
- "connection_pool": {
676
- "pool_size": 5,
677
- "timeout": 5.0
678
- },
679
- "rate_limit": {
680
- "rate": 10.0,
681
- "burst": 20
682
- },
683
- "monitoring": {
684
- "metrics_ttl": 3600,
685
- "max_metrics": 1000,
686
- "log_interval": 60.0
687
- }
688
- }
689
- }
690
- ```
 
1
+ # My First Agent with smolagents
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
+ This project is part of the [Hugging Face Agents Course](https://huggingface.co/learn/agents). It demonstrates how to create a simple but powerful agent using the smolagents library.
 
 
 
 
 
 
 
 
4
 
5
  ## Features
6
 
7
+ This agent can:
8
+ - Search the web using DuckDuckGo
9
+ - Get weather information for locations (using OpenWeatherMap API)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ ## How to Use
 
 
 
 
12
 
13
+ 1. Ask the agent questions or give it tasks related to its capabilities
14
+ 2. The agent will use its tools to find information or perform actions
15
+ 3. It will provide you with a final answer based on the results
 
 
 
16
 
17
+ ## Example Queries
18
 
19
+ - "What's the weather like in Tokyo?"
20
+ - "What are the latest news about AI?"
21
+ - "Who won the last World Cup?"
22
+ - "Tell me about the history of computers"
23
 
24
+ ## Technical Details
 
 
 
 
 
 
25
 
26
+ This agent is built using:
27
+ - smolagents library for the agent framework
28
+ - Qwen/Qwen2.5-Coder-32B-Instruct as the language model
29
+ - Gradio for the user interface
30
+ - OpenWeatherMap API for weather data
31
 
32
+ ## Setup
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ To run this project locally:
35
 
36
+ 1. Clone the repository
37
+ 2. Install the dependencies: `pip install -r requirements.txt`
38
+ 3. Run the application: `python app.py`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
+ ## Credits
41
 
42
+ Created as part of the Hugging Face Agents Course.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
SECURITY.md DELETED
@@ -1,283 +0,0 @@
1
- # Security Best Practices
2
-
3
- This document outlines security best practices for using the API key management system.
4
-
5
- ## Table of Contents
6
- 1. [API Key Management](#api-key-management)
7
- 2. [Rate Limiting](#rate-limiting)
8
- 3. [Key Storage](#key-storage)
9
- 4. [Environment Variables](#environment-variables)
10
- 5. [Error Handling](#error-handling)
11
- 6. [Encryption](#encryption)
12
- 7. [Monitoring and Logging](#monitoring-and-logging)
13
- 8. [Security Checklist](#security-checklist)
14
-
15
- ## API Key Management
16
-
17
- ### Key Generation
18
- - Use cryptographically secure random generators for API keys
19
- - Minimum key length: 32 characters
20
- - Use URL-safe characters: `[A-Za-z0-9_.-]`
21
- - Example:
22
- ```python
23
- import secrets
24
- import string
25
-
26
- def generate_api_key(length: int = 32) -> str:
27
- alphabet = string.ascii_letters + string.digits + '_.-'
28
- return ''.join(secrets.choice(alphabet) for _ in range(length))
29
- ```
30
-
31
- ### Key Rotation
32
- - Rotate keys regularly (recommended: every 90 days)
33
- - Implement automatic key expiration
34
- - Keep old keys active during transition period
35
- - Use the `rotate_key()` method:
36
- ```python
37
- from datetime import datetime, timedelta, timezone
38
-
39
- # Rotate key with 7-day overlap
40
- new_key = key_manager.rotate_key(
41
- name="service_name",
42
- new_key=generate_api_key(),
43
- expires_at=datetime.now(timezone.utc) + timedelta(days=90)
44
- )
45
- ```
46
-
47
- ### Access Control
48
- - Use scoped access for fine-grained control
49
- - Implement the principle of least privilege
50
- - Regularly audit key usage and permissions
51
- - Example scopes:
52
- ```python
53
- key_manager.add_key(
54
- name="read_only_key",
55
- key=generate_api_key(),
56
- scopes=["read:data", "list:items"]
57
- )
58
- ```
59
-
60
- ## Rate Limiting
61
-
62
- ### Configuration
63
- - Set appropriate rate limits based on service capacity
64
- - Configure burst limits for handling traffic spikes
65
- - Monitor rate limit violations
66
- - Example:
67
- ```python
68
- key_manager.add_key(
69
- name="high_throughput",
70
- key=generate_api_key(),
71
- rate_limit=100.0, # 100 requests per second
72
- burst_limit=200 # Allow bursts up to 200
73
- )
74
- ```
75
-
76
- ### Best Practices
77
- - Implement client-side rate limiting
78
- - Use exponential backoff for retries
79
- - Monitor rate limit errors
80
- - Handle rate limit errors gracefully:
81
- ```python
82
- try:
83
- wait_time = await key_manager.check_rate_limit("my_key")
84
- if wait_time > 0:
85
- # Implement backoff strategy
86
- await asyncio.sleep(wait_time)
87
- except RateLimitError as e:
88
- # Handle rate limit exceeded
89
- logger.warning(f"Rate limit exceeded: {e}")
90
- ```
91
-
92
- ## Key Storage
93
-
94
- ### File Storage
95
- - Use encrypted storage for API keys
96
- - Secure file permissions (600 or more restrictive)
97
- - Regular backups of key storage
98
- - Example file permissions:
99
- ```bash
100
- chmod 600 keys.json
101
- chown service_user:service_group keys.json
102
- ```
103
-
104
- ### Memory Storage
105
- - Never log API keys
106
- - Clear sensitive data from memory when no longer needed
107
- - Use secure string handling:
108
- ```python
109
- from pydantic import SecretStr
110
-
111
- # Keys are automatically masked in logs/str representation
112
- key_config = APIKeyConfig(
113
- key=SecretStr("my-secret-key"),
114
- name="service_name"
115
- )
116
- ```
117
-
118
- ## Environment Variables
119
-
120
- ### Configuration
121
- - Use environment variables for sensitive data
122
- - Follow naming conventions
123
- - Set appropriate file permissions for env files
124
- - Example `.env` file:
125
- ```bash
126
- # API Keys
127
- API_SERVICE_KEY=your-secret-key
128
- API_SERVICE_RATE_LIMIT=100
129
- API_SERVICE_BURST_LIMIT=200
130
-
131
- # Encryption
132
- API_ENCRYPTION_KEY=your-encryption-key
133
- ```
134
-
135
- ### Security Measures
136
- - Never commit `.env` files to version control
137
- - Use different keys for different environments
138
- - Rotate environment variables regularly
139
- - Example `.gitignore`:
140
- ```
141
- .env
142
- *.key
143
- keys.json
144
- ```
145
-
146
- ## Error Handling
147
-
148
- ### Best Practices
149
- - Use specific error types for different security issues
150
- - Never expose sensitive information in error messages
151
- - Log security events appropriately
152
- - Example:
153
- ```python
154
- try:
155
- key = key_manager.get_key("service_name")
156
- except InvalidKeyError:
157
- # Log the error, but don't expose key details
158
- logger.error("Invalid API key used", extra={
159
- "service": "service_name",
160
- "error_type": "invalid_key"
161
- })
162
- raise HTTPError(401, "Authentication failed")
163
- ```
164
-
165
- ### Error Types
166
- - `SecurityError`: Base class for security errors
167
- - `AuthenticationError`: Authentication failures
168
- - `AuthorizationError`: Permission issues
169
- - `InvalidKeyError`: Invalid/missing keys
170
- - `KeyExpiredError`: Expired keys
171
- - `RateLimitError`: Rate limit violations
172
- - `EncryptionError`: Encryption issues
173
- - `ValidationError`: Input validation failures
174
- - `ConfigurationError`: Configuration issues
175
-
176
- ## Encryption
177
-
178
- ### Key Management
179
- - Use strong encryption keys (min 32 bytes)
180
- - Regular rotation of encryption keys
181
- - Secure key storage
182
- - Example:
183
- ```python
184
- import base64
185
- import os
186
-
187
- # Generate new encryption key
188
- encryption_key = base64.urlsafe_b64encode(os.urandom(32)).decode()
189
- ```
190
-
191
- ### Best Practices
192
- - Use industry-standard encryption (Fernet)
193
- - Implement key rotation
194
- - Secure key transmission
195
- - Example:
196
- ```python
197
- from cryptography.fernet import Fernet
198
-
199
- # Initialize with encryption key
200
- key_manager = KeyManager(
201
- encryption_key=os.getenv("API_ENCRYPTION_KEY")
202
- )
203
- ```
204
-
205
- ## Monitoring and Logging
206
-
207
- ### Security Events
208
- - Log all security-related events
209
- - Use appropriate log levels
210
- - Include relevant context
211
- - Example:
212
- ```python
213
- import structlog
214
-
215
- logger = structlog.get_logger()
216
-
217
- # Log security event
218
- logger.warning("rate_limit_exceeded",
219
- key_name="service_name",
220
- wait_time=1.5,
221
- threshold=100
222
- )
223
- ```
224
-
225
- ### Monitoring
226
- - Monitor failed authentication attempts
227
- - Track rate limit violations
228
- - Alert on suspicious activity
229
- - Example metrics:
230
- ```python
231
- # Track security metrics
232
- metrics = {
233
- "failed_auth_attempts": counter,
234
- "rate_limit_violations": counter,
235
- "key_rotations": counter,
236
- "encryption_errors": counter
237
- }
238
- ```
239
-
240
- ## Security Checklist
241
-
242
- ### Implementation
243
- - [ ] Use secure random API key generation
244
- - [ ] Implement key rotation
245
- - [ ] Configure appropriate rate limits
246
- - [ ] Set up encrypted storage
247
- - [ ] Use environment variables
248
- - [ ] Implement proper error handling
249
- - [ ] Set up security logging
250
- - [ ] Configure monitoring
251
-
252
- ### Deployment
253
- - [ ] Secure file permissions
254
- - [ ] Environment separation
255
- - [ ] Backup strategy
256
- - [ ] Monitoring setup
257
- - [ ] Alert configuration
258
- - [ ] Audit logging
259
- - [ ] Regular security reviews
260
-
261
- ### Maintenance
262
- - [ ] Regular key rotation
263
- - [ ] Log review
264
- - [ ] Security updates
265
- - [ ] Access audit
266
- - [ ] Performance monitoring
267
- - [ ] Incident response plan
268
- - [ ] Documentation updates
269
-
270
- ## Reporting Security Issues
271
-
272
- If you discover a security vulnerability, please follow these steps:
273
-
274
- 1. **DO NOT** open a public issue
275
- 2. Send a private report to [security@example.com]
276
- 3. Include detailed information about the vulnerability
277
- 4. Wait for confirmation before any disclosure
278
-
279
- ## Updates and Changes
280
-
281
- This security documentation is regularly reviewed and updated. Last update: [Current Date]
282
-
283
- For questions or suggestions about security practices, please contact the security team.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
STYLE_GUIDE.md DELETED
@@ -1,220 +0,0 @@
1
- # Python Code Style Guide
2
-
3
- ## General Guidelines
4
-
5
- - Line length: 100 characters maximum
6
- - Indentation: 4 spaces (no tabs)
7
- - File encoding: UTF-8
8
- - Line endings: LF (Unix-style)
9
- - One blank line between functions and classes
10
- - Two blank lines between top-level functions and classes
11
-
12
- ## Naming Conventions
13
-
14
- - Package names: `lowercase`
15
- - Module names: `lowercase_with_underscores`
16
- - Class names: `CapWords`
17
- - Function names: `lowercase_with_underscores`
18
- - Variable names: `lowercase_with_underscores`
19
- - Constants: `UPPERCASE_WITH_UNDERSCORES`
20
- - Protected attributes: `_leading_underscore`
21
- - Private attributes: `__double_leading_underscore`
22
-
23
- ## Imports
24
-
25
- - Order imports in three groups, separated by a blank line:
26
- 1. Standard library imports
27
- 2. Third-party imports
28
- 3. Local application imports
29
- - Use absolute imports over relative imports
30
- - One import per line
31
- - No wildcard imports (`from module import *`)
32
-
33
- Example:
34
- ```python
35
- import os
36
- import sys
37
- from typing import Dict, List, Optional
38
-
39
- import requests
40
- from pydantic import BaseModel
41
-
42
- from tools.config import Config
43
- from tools.utils import format_response
44
- ```
45
-
46
- ## String Formatting
47
-
48
- - Use f-strings for string formatting
49
- - Use raw strings (`r"..."`) for regular expressions
50
- - Use triple quotes for docstrings and multiline strings
51
-
52
- Example:
53
- ```python
54
- name = "World"
55
- greeting = f"Hello, {name}!"
56
- pattern = r"^\d{3}-\d{2}-\d{4}$"
57
- ```
58
-
59
- ## Type Hints
60
-
61
- - Use type hints for all function arguments and return values
62
- - Use `Optional` for nullable values
63
- - Use `Union` for multiple possible types
64
- - Use `Any` sparingly and document why when used
65
-
66
- Example:
67
- ```python
68
- def process_data(
69
- input_data: Dict[str, Any],
70
- max_items: Optional[int] = None,
71
- ) -> List[str]:
72
- """Process input data and return list of strings."""
73
- ```
74
-
75
- ## Docstrings
76
-
77
- - Use Google-style docstrings
78
- - Include Args, Returns, Raises sections when applicable
79
- - Document exceptions that may be raised
80
- - Include type information in docstring descriptions
81
-
82
- Example:
83
- ```python
84
- def calculate_average(numbers: List[float]) -> float:
85
- """Calculate the average of a list of numbers.
86
-
87
- Args:
88
- numbers: List of numbers to average.
89
- Must be non-empty list of floats.
90
-
91
- Returns:
92
- float: The arithmetic mean of the input numbers.
93
-
94
- Raises:
95
- ValueError: If the input list is empty.
96
- TypeError: If any element is not a number.
97
- """
98
- ```
99
-
100
- ## Classes
101
-
102
- - Use dataclasses or Pydantic models for data containers
103
- - Implement `__repr__` for debugging
104
- - Use properties instead of getter/setter methods
105
- - Document class attributes in class docstring
106
-
107
- Example:
108
- ```python
109
- from dataclasses import dataclass
110
- from datetime import datetime
111
-
112
- @dataclass
113
- class User:
114
- """User information container.
115
-
116
- Attributes:
117
- username: Unique identifier for the user
118
- email: User's email address
119
- created_at: Timestamp of user creation
120
- """
121
- username: str
122
- email: str
123
- created_at: datetime = field(default_factory=datetime.utcnow)
124
-
125
- def __repr__(self) -> str:
126
- return f"User(username={self.username})"
127
- ```
128
-
129
- ## Error Handling
130
-
131
- - Use specific exception types
132
- - Always include error messages
133
- - Clean up resources in finally blocks
134
- - Use context managers when possible
135
-
136
- Example:
137
- ```python
138
- try:
139
- with open(filename, "r") as f:
140
- data = f.read()
141
- except FileNotFoundError:
142
- logger.error(f"Config file {filename} not found")
143
- raise ConfigError(f"Missing configuration file: {filename}")
144
- except IOError as e:
145
- logger.error(f"IO error reading {filename}: {e}")
146
- raise
147
- ```
148
-
149
- ## Testing
150
-
151
- - Test file names: `test_*.py`
152
- - Test function names: `test_*`
153
- - Use descriptive test names
154
- - One assertion per test when possible
155
- - Use fixtures for setup/teardown
156
-
157
- Example:
158
- ```python
159
- def test_user_creation_with_valid_data():
160
- """Test user creation with valid input data."""
161
- user = User(username="test", email="test@example.com")
162
- assert user.username == "test"
163
- assert user.email == "test@example.com"
164
- ```
165
-
166
- ## Comments
167
-
168
- - Write self-documenting code
169
- - Use comments to explain why, not what
170
- - Keep comments up-to-date with code
171
- - Use TODO comments with ticket numbers
172
-
173
- Example:
174
- ```python
175
- # TODO(#123): Implement rate limiting for API calls
176
- def process_api_request():
177
- # Temporary workaround for race condition
178
- # Remove once transaction handling is implemented
179
- time.sleep(0.1)
180
- ```
181
-
182
- ## Tools and Automation
183
-
184
- Our codebase is automatically formatted and checked using:
185
-
186
- - `black`: Code formatting
187
- - `isort`: Import sorting
188
- - `flake8`: Style guide enforcement
189
- - `mypy`: Type checking
190
- - `pylint`: Code analysis
191
- - `bandit`: Security checks
192
-
193
- Run all checks locally:
194
- ```bash
195
- pre-commit run --all-files
196
- ```
197
-
198
- ## IDE Integration
199
-
200
- ### VS Code Settings
201
- ```json
202
- {
203
- "python.formatting.provider": "black",
204
- "python.linting.enabled": true,
205
- "python.linting.flake8Enabled": true,
206
- "python.linting.pylintEnabled": true,
207
- "python.linting.mypyEnabled": true,
208
- "editor.formatOnSave": true,
209
- "editor.rulers": [100],
210
- "files.trimTrailingWhitespace": true,
211
- "files.insertFinalNewline": true
212
- }
213
- ```
214
-
215
- ### PyCharm Settings
216
- - Enable Black formatter
217
- - Enable Flake8 and PyLint
218
- - Set line length to 100
219
- - Enable "Optimize imports on the fly"
220
- - Enable "Add final newline on Save"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
agent.json DELETED
@@ -1,156 +0,0 @@
1
- {
2
- "tools": [
3
- {
4
- "name": "WeatherTool",
5
- "description": "Class-based tool for retrieving weather information by ZIP code",
6
- "inputs": {
7
- "zipcode": {
8
- "type": "string",
9
- "description": "5-digit US ZIP code"
10
- }
11
- },
12
- "output_type": "string",
13
- "output_description": "Formatted weather information including temperature, conditions, humidity, and wind",
14
- "class_implementation": true
15
- },
16
- {
17
- "name": "AIAssistant",
18
- "description": "Main assistant class implementing core functionality",
19
- "methods": {
20
- "get_current_time_in_timezone": {
21
- "description": "Get current time in a specified timezone",
22
- "inputs": {
23
- "timezone": {
24
- "type": "string",
25
- "description": "Valid timezone name from pytz.all_timezones"
26
- }
27
- },
28
- "output_type": "string",
29
- "output_description": "Current time in the specified timezone format: YYYY-MM-DD HH:MM:SS TZ"
30
- },
31
- "get_matching_timezones": {
32
- "description": "Search for timezone names matching a partial string",
33
- "inputs": {
34
- "partial": {
35
- "type": "string",
36
- "description": "Partial timezone name to search for"
37
- }
38
- },
39
- "output_type": "list",
40
- "output_description": "List of timezone names containing the search string"
41
- }
42
- }
43
- },
44
- {
45
- "name": "FinalAnswerTool",
46
- "description": "Tool for providing the final answer to user queries",
47
- "inputs": {
48
- "answer": {
49
- "type": "any",
50
- "description": "The answer to be returned to the user"
51
- }
52
- },
53
- "output_type": "string",
54
- "output_description": "Formatted answer string",
55
- "class_implementation": true
56
- }
57
- ],
58
- "model": {
59
- "class": "HfApiModel",
60
- "data": {
61
- "max_tokens": 2096,
62
- "temperature": 0.5,
63
- "last_input_token_count": null,
64
- "last_output_token_count": null,
65
- "model_id": "Qwen/Qwen2.5-Coder-32B-Instruct",
66
- "custom_role_conversions": null
67
- }
68
- },
69
- "prompt_templates": {
70
- "system_prompt": "You are an AI Assistant with capabilities for weather information and timezone conversions. You have access to the following tools:\n\n- WeatherTool: Get current weather for US ZIP codes\n- Time/Timezone functions: Get current time and search timezones\n- FinalAnswerTool: Format and return final answers\n\nYou should process user requests by:\n1. Understanding the request\n2. Using appropriate tools to gather information\n3. Formatting responses clearly\n4. Handling errors gracefully\n\nAll responses should be clear, professional, and user-friendly."
71
- },
72
- "authorized_imports": [
73
- "unicodedata",
74
- "stat",
75
- "datetime",
76
- "random",
77
- "pandas",
78
- "itertools",
79
- "math",
80
- "statistics",
81
- "queue",
82
- "time",
83
- "collections",
84
- "re",
85
- "os",
86
- "logging",
87
- "pytz",
88
- "requests",
89
- "signal",
90
- "sys",
91
- "dotenv"
92
- ],
93
- "config": {
94
- "api": {
95
- "weather": {
96
- "base_url": "http://api.weatherapi.com/v1",
97
- "rate_limit_per_minute": 60,
98
- "cache_timeout_seconds": 300
99
- },
100
- "timezone": {
101
- "rate_limit_per_minute": 100
102
- }
103
- },
104
- "assistant": {
105
- "command_history": {
106
- "max_size": 10
107
- },
108
- "cache": {
109
- "cleanup_interval_seconds": 600
110
- }
111
- },
112
- "ui": {
113
- "title": "AI Assistant",
114
- "description": "Weather and Timezone Assistant",
115
- "theme": "light",
116
- "input_placeholder": "Enter your command (type 'help' for available commands)",
117
- "width": "100%",
118
- "height": "600px"
119
- },
120
- "logging": {
121
- "level": "INFO",
122
- "file": "assistant.log",
123
- "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
124
- },
125
- "base_url": "http://api.example.com",
126
- "connection_pool": {
127
- "pool_size": 20,
128
- "timeout": 30.0,
129
- "keepalive_timeout": 60.0,
130
- "ttl_dns_cache": 300,
131
- "force_close": false
132
- },
133
- "rate_limit": {
134
- "rate": 50.0,
135
- "burst": 100,
136
- "enabled": true
137
- },
138
- "batch": {
139
- "size": 500,
140
- "interval": 2.0,
141
- "enabled": true
142
- },
143
- "monitoring": {
144
- "metrics_ttl": 7200,
145
- "max_metrics": 5000,
146
- "log_interval": 300.0,
147
- "enabled": true
148
- },
149
- "retry": {
150
- "max_attempts": 5,
151
- "min_wait": 1.0,
152
- "max_wait": 30.0,
153
- "enabled": true
154
- }
155
- }
156
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,590 +1,63 @@
1
- """
2
- Main application file for the AI Assistant.
3
- Version: 1.0.0
4
- License: MIT
5
- Copyright (c) 2024 AI Assistant Team
6
-
7
- This module implements a command-line AI assistant with weather and timezone capabilities.
8
- It provides a user-friendly interface for accessing weather information and timezone
9
- conversions, with features like caching, rate limiting, and usage statistics.
10
-
11
- Features:
12
- - Weather information by ZIP code with caching
13
- - Timezone conversion and search
14
- - Command history tracking
15
- - Usage statistics
16
- - Rate limiting for API calls
17
- - Graceful shutdown handling
18
-
19
- Example Usage:
20
- $ python app.py
21
- Then use commands like:
22
- - weather 12345
23
- - time America/New_York
24
- - timezone asia
25
- - help
26
-
27
- Environment Variables:
28
- WEATHER_API_KEY: API key for weather service
29
- LOG_LEVEL: Logging level (default: INFO)
30
- LOG_FILE: Log file path (default: assistant.log)
31
- Other variables defined in config.py
32
-
33
- This project is licensed under the MIT License.
34
- See the LICENSE file for the full license text.
35
- """
36
-
37
- import os
38
- import logging
39
- import pytz
40
  import requests
41
- import signal
42
- import sys
43
- import time
44
- from datetime import datetime, timedelta
45
- from dotenv import load_dotenv
46
- from typing import Optional, Dict, Any, List
47
-
48
- from config import Config, ModelConfig, UIConfig
49
- from rate_limiter import RateLimiter
50
  from tools.final_answer import FinalAnswerTool
51
- from tools.weather import WeatherTool
52
- from tools.stats import UsageStats
53
- from tools.logging_config import setup_logging
54
- from tools.health import HealthCheck, HealthStatus
55
- from tools.error_recovery import CircuitBreaker
56
- from tools.system_status import SystemMonitor
57
- from Gradio_UI import GradioUI
58
-
59
- # Application constants
60
- __version__ = "1.0.0"
61
- __author__ = "AI Assistant Team"
62
-
63
- # Load environment variables
64
- load_dotenv()
65
-
66
- # Configure logging with rotation
67
- logger = setup_logging(
68
- log_dir=os.getenv('LOG_DIR', 'logs'),
69
- log_level=os.getenv('LOG_LEVEL', 'INFO'),
70
- max_bytes=int(os.getenv('LOG_MAX_BYTES', 10 * 1024 * 1024)), # Default 10MB
71
- backup_count=int(os.getenv('LOG_BACKUP_COUNT', 5)),
72
- log_format=os.getenv('LOG_FORMAT'),
73
- date_format=os.getenv('LOG_DATE_FORMAT')
74
- )
75
-
76
- def get_system_info() -> str:
77
- """
78
- Get formatted system information string.
79
 
80
- Returns:
81
- str: Formatted string containing system information including:
82
- - Application version
83
- - Python version
84
- - Platform
85
- - Current timezone
86
- - Log file location
87
- """
88
- return (
89
- f"AI Assistant v{__version__}\n"
90
- f"Python {sys.version.split()[0]}\n"
91
- f"Platform: {sys.platform}\n"
92
- f"Timezone: {datetime.now().astimezone().tzname()}\n"
93
- f"Log File: {os.getenv('LOG_FILE', 'assistant.log')}"
94
- )
95
-
96
- def signal_handler(signum: int, frame: Any) -> None:
97
- """
98
- Handle system signals for graceful shutdown.
99
 
 
 
 
100
  Args:
101
- signum: Signal number received
102
- frame: Current stack frame (not used)
103
-
104
- Note:
105
- Handles SIGINT (Ctrl+C) and SIGTERM signals.
106
- Logs shutdown message and exits cleanly.
107
- """
108
- logger.info("Received shutdown signal, cleaning up...")
109
- print("\nShutting down gracefully...")
110
- sys.exit(0)
111
-
112
- class AIAssistant:
113
- """
114
- Main AI Assistant class implementing the core functionality.
115
-
116
- This class provides weather information and timezone conversion services,
117
- with features like command history, caching, and usage statistics.
118
-
119
- Attributes:
120
- config (Config): Application configuration
121
- weather_limiter (RateLimiter): Rate limiter for weather API
122
- timezone_limiter (RateLimiter): Rate limiter for timezone API
123
- command_history (List[str]): List of recent commands
124
- max_history (int): Maximum number of commands to keep in history
125
- stats (UsageStats): Usage statistics collector
126
- """
127
-
128
- def __init__(self):
129
- """Initialize the AI Assistant with configuration and tools."""
130
- self.config = Config()
131
- self.weather_limiter = RateLimiter(
132
- rate_limit=self.config.api.weather_api_rate_limit,
133
- time_window=60
134
- )
135
- self.timezone_limiter = RateLimiter(
136
- rate_limit=self.config.api.timezone_api_rate_limit,
137
- time_window=60
138
- )
139
- self.final_answer_tool = FinalAnswerTool()
140
- self.weather_tool = WeatherTool()
141
- self.command_history = []
142
- self.max_history = 10
143
-
144
- # Initialize statistics collector
145
- self.stats = UsageStats(
146
- stats_dir=os.getenv('STATS_DIR', 'stats')
147
- )
148
-
149
- # Initialize circuit breaker
150
- self.circuit_breaker = CircuitBreaker(
151
- failure_threshold=5,
152
- reset_timeout=60.0
153
- )
154
-
155
- # Initialize health check
156
- self.health_check = HealthCheck(
157
- circuit_breaker=self.circuit_breaker,
158
- memory_threshold=90.0,
159
- cpu_threshold=80.0
160
- )
161
-
162
- # Initialize system monitor
163
- self.system_monitor = SystemMonitor(
164
- history_size=int(os.getenv('METRICS_HISTORY_SIZE', '60')),
165
- disk_path=os.getenv('MONITOR_DISK_PATH')
166
- )
167
-
168
- logger.info("AI Assistant initialized")
169
- logger.debug(f"Configuration loaded: {self.config}")
170
-
171
- def _add_to_history(self, command: str) -> None:
172
- """
173
- Add command to history, maintaining max size.
174
-
175
- Args:
176
- command: Command string to add to history
177
-
178
- Note:
179
- Maintains a fixed-size history by removing oldest entries
180
- when maximum size is reached. Also updates total command counter.
181
- """
182
- self.command_history.append(command)
183
- if len(self.command_history) > self.max_history:
184
- self.command_history.pop(0)
185
-
186
- def _clean_expired_cache(self) -> None:
187
- """
188
- Remove expired entries from weather cache.
189
-
190
- This method checks all cache entries and removes those that have
191
- exceeded the cache timeout period.
192
- """
193
- now = datetime.now()
194
- expired = [
195
- zip_code for zip_code, data in self.weather_tool.cache.items()
196
- if (now - data['timestamp']).total_seconds() >= self.weather_tool.cache_timeout
197
- ]
198
- for zip_code in expired:
199
- del self.weather_tool.cache[zip_code]
200
-
201
- def _get_matching_timezones(self, partial: str) -> List[str]:
202
- """
203
- Get list of timezones matching partial string.
204
-
205
- Args:
206
- partial: Partial timezone name to search for
207
-
208
- Returns:
209
- List[str]: List of timezone names containing the search string
210
-
211
- Note:
212
- Search is case-insensitive and matches any part of timezone name.
213
- """
214
- partial = partial.lower()
215
- return [tz for tz in pytz.all_timezones if partial in tz.lower()]
216
-
217
- def _is_cache_valid(self, zipcode: str) -> bool:
218
- """
219
- Check if cached weather data is still valid.
220
-
221
- Args:
222
- zipcode: ZIP code to check in cache
223
-
224
- Returns:
225
- bool: True if cache entry exists and is within timeout period
226
- """
227
- if zipcode not in self.weather_tool.cache:
228
- return False
229
- cache_time = self.weather_tool.cache[zipcode]['timestamp']
230
- return (datetime.now() - cache_time).total_seconds() < self.weather_tool.cache_timeout
231
-
232
- def _format_weather_response(self, data: Dict[str, Any]) -> str:
233
- """
234
- Format weather data into a user-friendly string.
235
-
236
- Args:
237
- data: Weather data from API
238
-
239
- Returns:
240
- str: Formatted weather information
241
- """
242
- try:
243
- location = data['location']
244
- current = data['current']
245
-
246
- return (
247
- f"Weather for {location['name']}, {location.get('region', '')}\n"
248
- f"Temperature: {current['temp_f']}°F ({current['temp_c']}°C)\n"
249
- f"Condition: {current['condition']['text']}\n"
250
- f"Humidity: {current['humidity']}%\n"
251
- f"Wind: {current['wind_mph']} mph ({current['wind_kph']} kph) {current['wind_dir']}\n"
252
- f"Last Updated: {current['last_updated']}"
253
- )
254
- except KeyError as e:
255
- logger.error(f"Error formatting weather data: {str(e)}")
256
- return "Error formatting weather data"
257
-
258
- def get_weather_by_zipcode(self, zipcode: str) -> str:
259
- """
260
- Get weather information for a given ZIP code.
261
-
262
- Args:
263
- zipcode: The ZIP code to get weather for
264
-
265
- Returns:
266
- str: Formatted weather information
267
-
268
- Raises:
269
- ValueError: If ZIP code is invalid or malformed
270
- RuntimeError: If API call fails or rate limit exceeded
271
- """
272
- start_time = time.time()
273
- success = True
274
- error_type = None
275
-
276
- try:
277
- if not self.weather_limiter.allow_request():
278
- success = False
279
- error_type = "RateLimitError"
280
- raise RuntimeError("Rate limit exceeded for weather API")
281
-
282
- result = self.weather_tool(zipcode)
283
-
284
- # Record cache operation
285
- if 'Using cached weather data' in self.weather_tool.logger.handlers[0].records[-1].message:
286
- self.stats.record_cache_operation(hit=True)
287
- else:
288
- self.stats.record_cache_operation(hit=False)
289
-
290
- return result
291
- except Exception as e:
292
- success = False
293
- error_type = e.__class__.__name__
294
- logger.error(f"Weather API error: {str(e)}")
295
- raise
296
- finally:
297
- # Record API call statistics
298
- response_time = time.time() - start_time
299
- self.stats.record_api_call(
300
- endpoint="weather_api",
301
- success=success,
302
- response_time=response_time,
303
- rate_limited=(error_type == "RateLimitError"),
304
- error_type=error_type
305
- )
306
-
307
- def get_current_time_in_timezone(self, timezone: str) -> str:
308
- """Get current time in specified timezone."""
309
- start_time = time.time()
310
- success = True
311
- error_type = None
312
-
313
- try:
314
- if not timezone or not timezone.strip():
315
- success = False
316
- error_type = "ValueError"
317
- raise ValueError("Timezone cannot be empty")
318
-
319
- if not self.timezone_limiter.allow_request():
320
- success = False
321
- error_type = "RateLimitError"
322
- raise RuntimeError("Rate limit exceeded for timezone API")
323
-
324
- if timezone not in pytz.all_timezones:
325
- success = False
326
- error_type = "ValueError"
327
- raise ValueError(f"Invalid timezone: {timezone}")
328
-
329
- tz = pytz.timezone(timezone)
330
- current_time = datetime.now(tz)
331
- return current_time.strftime("%Y-%m-%d %H:%M:%S %Z")
332
- except Exception as e:
333
- success = False
334
- error_type = e.__class__.__name__
335
- logger.error(f"Timezone error: {str(e)}")
336
- raise RuntimeError(f"Failed to get timezone data: {str(e)}")
337
- finally:
338
- # Record API call statistics
339
- response_time = time.time() - start_time
340
- self.stats.record_api_call(
341
- endpoint="timezone_api",
342
- success=success,
343
- response_time=response_time,
344
- error_type=error_type
345
- )
346
-
347
- def process_input(self, message: str) -> str:
348
- """Process user input and return response."""
349
- start_time = time.time()
350
- command_type = "unknown"
351
- success = True
352
- error_type = None
353
-
354
- try:
355
- if not message or not message.strip():
356
- return "Please enter a message."
357
-
358
- original_message = message
359
- message = message.strip().lower()
360
-
361
- # Determine command type
362
- if message == "help":
363
- command_type = "help"
364
- elif message == "stats":
365
- command_type = "stats"
366
- elif message == "cache clear":
367
- command_type = "cache_clear"
368
- elif message == "history":
369
- command_type = "history"
370
- elif message.startswith("timezone "):
371
- command_type = "timezone_search"
372
- elif message.startswith("weather "):
373
- command_type = "weather"
374
- elif message.startswith("time "):
375
- command_type = "time"
376
-
377
- result = self._process_command(message, original_message)
378
- return result
379
- except Exception as e:
380
- success = False
381
- error_type = e.__class__.__name__
382
- logger.error(f"Error processing input: {str(e)}")
383
- return f"Error processing your request: {str(e)}"
384
- finally:
385
- # Record command statistics
386
- response_time = time.time() - start_time
387
- self.stats.record_command(
388
- command=command_type,
389
- success=success,
390
- response_time=response_time,
391
- error_type=error_type
392
- )
393
-
394
- def _process_command(self, message: str, original_message: str) -> str:
395
- """Process specific command type."""
396
- # Update system metrics before processing command
397
- self.system_monitor.update_metrics()
398
-
399
- if message == "help":
400
- return (
401
- "Available commands:\n"
402
- "- weather <zipcode>: Get weather for a ZIP code\n"
403
- "- time <timezone>: Get current time in timezone\n"
404
- "- timezone <search>: Search for available timezones\n"
405
- "- history: Show command history\n"
406
- "- stats: Show usage statistics\n"
407
- "- status: Show system status\n"
408
- "- cache clear: Clear weather cache\n"
409
- "- help: Show this help message"
410
- )
411
-
412
- if message == "stats":
413
- return self._format_stats_output(self.stats.get_summary())
414
-
415
- if message == "status":
416
- return self._format_system_status()
417
-
418
- if message == "cache clear":
419
- self.weather_tool.cache.clear()
420
- return "Weather cache cleared."
421
-
422
- if message == "history":
423
- if not self.command_history:
424
- return "No command history available."
425
- return "Command History:\n" + "\n".join(
426
- f"{i+1}. {cmd}" for i, cmd in enumerate(self.command_history)
427
- )
428
-
429
- if message.startswith("timezone "):
430
- search_term = message.split("timezone ", 1)[1].strip()
431
- matches = self._get_matching_timezones(search_term)
432
- if not matches:
433
- return f"No timezones found matching '{search_term}'"
434
- return "Matching Timezones:\n" + "\n".join(matches[:10]) + (
435
- "\n(showing first 10 matches)" if len(matches) > 10 else ""
436
- )
437
-
438
- if message.startswith("weather "):
439
- try:
440
- zipcode = message.split("weather ", 1)[1].strip()
441
- result = self.get_weather_by_zipcode(zipcode)
442
- self._add_to_history(original_message)
443
- return result
444
- except (IndexError, ValueError, RuntimeError) as e:
445
- return str(e)
446
-
447
- if message.startswith("time "):
448
- try:
449
- timezone = message.split("time ", 1)[1].strip()
450
- result = self.get_current_time_in_timezone(timezone)
451
- self._add_to_history(original_message)
452
- return result
453
- except (IndexError, ValueError, RuntimeError) as e:
454
- return str(e)
455
-
456
- return (
457
- "I didn't understand that command. Type 'help' to see available commands."
458
- )
459
-
460
- def _format_stats_output(self, stats: Dict[str, Any]) -> str:
461
- """Format statistics for display."""
462
- return (
463
- "=== AI Assistant Statistics ===\n"
464
- f"\nSession Information:\n"
465
- f"Start Time: {stats['session']['start_time']}\n"
466
- f"Uptime: {timedelta(seconds=int(stats['session']['uptime_seconds']))}\n"
467
- f"\nCommand Statistics:\n"
468
- f"Total Commands: {stats['commands']['total']}\n"
469
- f"Success Rate: {stats['commands']['success_rate']:.1f}%\n"
470
- f"\nAPI Statistics:\n"
471
- f"Total API Calls: {stats['api']['total_calls']}\n"
472
- f"API Success Rate: {stats['api']['success_rate']:.1f}%\n"
473
- f"Rate Limit Hits: {stats['api']['rate_limit_hits']}\n"
474
- f"\nCache Statistics:\n"
475
- f"Hit Rate: {stats['cache']['hit_rate']:.1f}%\n"
476
- f"Total Entries: {stats['cache']['total_entries']}\n"
477
- f"Total Size: {stats['cache']['total_size_bytes'] / 1024:.1f}KB\n"
478
- f"Evictions: {stats['cache']['evictions']}"
479
- )
480
-
481
- def _format_system_status(self) -> str:
482
- """Format system status for display."""
483
- metrics = self.system_monitor.get_metrics_summary()
484
- warnings = self.system_monitor.get_resource_warnings()
485
-
486
- status_lines = [
487
- "=== System Status ===\n",
488
- f"Timestamp: {metrics['timestamp']}",
489
- "\nSystem Resources:",
490
- f"CPU Usage: {metrics['system']['cpu_percent']:.1f}%",
491
- f"Memory Usage: {metrics['system']['memory_percent']:.1f}%",
492
- f"Disk Usage: {metrics['system']['disk_usage_percent']:.1f}%",
493
- "\nProcess Information:",
494
- f"Memory Usage: {metrics['process']['memory_mb']:.1f}MB",
495
- f"Threads: {metrics['process']['threads']}",
496
- f"CPU Usage: {metrics['process']['cpu_percent']:.1f}%"
497
- ]
498
-
499
- if metrics.get('trends'):
500
- status_lines.extend([
501
- "\nTrends (since start):",
502
- f"CPU: {metrics['trends']['cpu_trend']:+.1f}%",
503
- f"Memory: {metrics['trends']['memory_trend']:+.1f}%",
504
- f"Disk: {metrics['trends']['disk_trend']:+.1f}%",
505
- f"Process Memory: {metrics['trends']['process_memory_trend']:+.1f}MB"
506
- ])
507
-
508
- if warnings:
509
- status_lines.extend([
510
- "\nWarnings:",
511
- *[f"- {warning}" for warning in warnings]
512
- ])
513
-
514
- return "\n".join(status_lines)
515
-
516
- def get_health(self) -> HealthStatus:
517
- """
518
- Get current health status.
519
-
520
- Returns:
521
- HealthStatus: Current health status including all checks
522
- """
523
- status = self.health_check.check_health()
524
-
525
- # Add system metrics to health status
526
- system_metrics = self.system_monitor.get_metrics_summary()
527
- status.details["system"] = system_metrics.get("system", {})
528
- status.details["process"] = system_metrics.get("process", {})
529
-
530
- # Add application-specific metrics
531
- status.details["application"] = {
532
- "total_commands": sum(s.total_calls for s in self.stats.commands.values()),
533
- "weather_requests": self.stats.api_stats.get("weather_api", {}).get("total_requests", 0),
534
- "timezone_requests": self.stats.api_stats.get("timezone_api", {}).get("total_requests", 0),
535
- "cache_stats": self.stats.get_cache_stats()
536
- }
537
-
538
- # Add any resource warnings
539
- warnings = self.system_monitor.get_resource_warnings()
540
- if warnings:
541
- status.warnings.extend(warnings)
542
-
543
- return status
544
-
545
- def main():
546
- """
547
- Main function to run the application.
548
-
549
- This function:
550
- 1. Sets up signal handlers for graceful shutdown
551
- 2. Displays system information
552
- 3. Initializes the AI Assistant
553
- 4. Launches the user interface
554
-
555
- The application can be terminated with Ctrl+C.
556
-
557
- Note:
558
- Requires environment variables to be properly configured.
559
- See module docstring for required environment variables.
560
  """
561
  try:
562
- # Set up signal handlers
563
- signal.signal(signal.SIGINT, signal_handler)
564
- signal.signal(signal.SIGTERM, signal_handler)
565
-
566
- # Print startup information
567
- print("\n=== AI Assistant Starting ===")
568
- print(get_system_info())
569
- print("\nInitializing components...")
570
-
571
- assistant = AIAssistant()
572
- print("Assistant initialized successfully")
573
-
574
- print("\nStarting UI...")
575
- print("Type 'help' to see available commands")
576
- print("Press Ctrl+C to exit\n")
577
-
578
- ui = GradioUI(
579
- callback=assistant.process_input,
580
- config=assistant.config.ui
581
- )
582
- ui.launch(share=False)
583
  except Exception as e:
584
- logger.error(f"Application error: {str(e)}")
585
- print(f"Error starting application: {str(e)}")
586
- finally:
587
- logger.info("Application shutdown complete")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
588
 
589
- if __name__ == "__main__":
590
- main()
 
1
+ from smolagents import CodeAgent, DuckDuckGoSearchTool, HfApiModel, load_tool, tool
2
+ import datetime
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import requests
4
+ import pytz
5
+ import yaml
 
 
 
 
 
 
 
6
  from tools.final_answer import FinalAnswerTool
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
+ from Gradio_UI import GradioUI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ # Weather tool to get current weather information
11
+ def get_weather(location: str) -> str:
12
+ """A tool that fetches the current weather for a specified location.
13
  Args:
14
+ location: A string representing a city or location (e.g., 'New York', 'Paris, France').
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  """
16
  try:
17
+ # Using OpenWeatherMap API with a free tier (limited requests)
18
+ api_key = "1ae035abc608d0e1095a5472dc989299" # OpenWeatherMap API key
19
+ url = f"http://api.openweathermap.org/data/2.5/weather?q={location}&appid={api_key}&units=metric"
20
+
21
+ response = requests.get(url)
22
+ data = response.json()
23
+
24
+ if response.status_code == 200:
25
+ temp = data["main"]["temp"]
26
+ weather = data["weather"][0]["description"]
27
+ humidity = data["main"]["humidity"]
28
+ wind_speed = data["wind"]["speed"]
29
+ return f"The current weather in {location} is {temp}°C with {weather}. Humidity: {humidity}%, Wind speed: {wind_speed} m/s."
30
+ else:
31
+ return f"Error fetching weather for {location}: {data.get('message', 'Unknown error')}"
 
 
 
 
 
 
32
  except Exception as e:
33
+ return f"Error fetching weather for {location}: {str(e)}"
34
+
35
+ final_answer = FinalAnswerTool()
36
+ model = HfApiModel(
37
+ max_tokens=2096,
38
+ temperature=0.5,
39
+ model_id='Qwen/Qwen2.5-Coder-32B-Instruct',
40
+ custom_role_conversions=None,
41
+ )
42
+
43
+ with open("prompts.yaml", 'r') as stream:
44
+ prompt_templates = yaml.safe_load(stream)
45
+
46
+ # We're creating our CodeAgent with multiple tools
47
+ agent = CodeAgent(
48
+ model=model,
49
+ tools=[
50
+ final_answer,
51
+ DuckDuckGoSearchTool(),
52
+ get_weather
53
+ ],
54
+ max_steps=6,
55
+ verbosity_level=1,
56
+ grammar=None,
57
+ planning_interval=None,
58
+ name=None,
59
+ description=None,
60
+ prompt_templates=prompt_templates
61
+ )
62
 
63
+ GradioUI(agent).launch()
 
config.py DELETED
@@ -1,451 +0,0 @@
1
- """
2
- Configuration settings for the AI Assistant application.
3
-
4
- This module provides a configuration system that can be controlled through environment
5
- variables or a JSON configuration file. Environment variables take precedence over
6
- JSON configuration.
7
-
8
- Environment Variables:
9
- See .env.example for a complete list of available environment variables and their defaults.
10
- """
11
-
12
- import os
13
- import logging
14
- import re
15
- from dataclasses import dataclass, field, fields, FrozenInstanceError
16
- from typing import Optional, Dict, Any, List, Union, TypeVar, Type, cast, ClassVar, Pattern, Callable
17
- from enum import Enum
18
- from pathlib import Path
19
-
20
- logger = logging.getLogger(__name__)
21
-
22
- T = TypeVar('T')
23
-
24
- class ConfigValidationError(ValueError):
25
- """Raised when configuration validation fails."""
26
- def __init__(self, message: str, field_name: Optional[str] = None):
27
- self.field_name = field_name
28
- super().__init__(f"{field_name + ': ' if field_name else ''}{message}")
29
-
30
- def validate_positive(value: Union[int, float], field_name: str) -> None:
31
- """Validate that a numeric value is positive."""
32
- if value <= 0:
33
- raise ConfigValidationError(f"Value must be positive, got {value}", field_name)
34
-
35
- def validate_non_negative(value: Union[int, float], field_name: str) -> None:
36
- """Validate that a numeric value is non-negative."""
37
- if value < 0:
38
- raise ConfigValidationError(f"Value cannot be negative, got {value}", field_name)
39
-
40
- def validate_range(value: Union[int, float], min_val: Union[int, float], max_val: Union[int, float], field_name: str) -> None:
41
- """Validate that a numeric value is within a specified range."""
42
- if not min_val <= value <= max_val:
43
- raise ConfigValidationError(f"Value must be between {min_val} and {max_val}, got {value}", field_name)
44
-
45
- def validate_non_empty_string(value: str, field_name: str) -> None:
46
- """Validate that a string is not empty or just whitespace."""
47
- if not value.strip():
48
- raise ConfigValidationError("Value cannot be empty or whitespace", field_name)
49
-
50
- def validate_path_exists_or_creatable(path: str, field_name: str) -> None:
51
- """Validate that a path exists or can be created."""
52
- try:
53
- path_obj = Path(path)
54
- if not path_obj.parent.exists():
55
- path_obj.parent.mkdir(parents=True, exist_ok=True)
56
- except (OSError, RuntimeError) as e:
57
- raise ConfigValidationError(f"Invalid path or cannot create directory: {str(e)}", field_name)
58
-
59
- class LogLevel(str, Enum):
60
- """Valid logging levels."""
61
- DEBUG = "DEBUG"
62
- INFO = "INFO"
63
- WARNING = "WARNING"
64
- ERROR = "ERROR"
65
- CRITICAL = "CRITICAL"
66
-
67
- class Theme(str, Enum):
68
- """Valid UI themes."""
69
- LIGHT = "light"
70
- DARK = "dark"
71
-
72
- def get_env_value(key: str, default: T, value_type: Type[T] = str) -> T:
73
- """
74
- Get an environment variable with type conversion and validation.
75
-
76
- Args:
77
- key: Environment variable name
78
- default: Default value if not set
79
- value_type: Type to convert the value to
80
-
81
- Returns:
82
- The environment variable value converted to the specified type
83
-
84
- Raises:
85
- ValueError: If type conversion fails
86
- """
87
- value = os.getenv(key)
88
- if value is None:
89
- return default
90
-
91
- try:
92
- if value_type == bool:
93
- return cast(T, value.lower() == 'true')
94
- return value_type(value)
95
- except (ValueError, TypeError) as e:
96
- logger.warning(f"Invalid value for {key}: {value}. Using default: {default}. Error: {str(e)}")
97
- return default
98
-
99
- @dataclass(frozen=True)
100
- class WeatherAPIConfig:
101
- """Weather API specific configuration."""
102
- URL_PATTERN: ClassVar[Pattern] = re.compile(r'^https?://[^\s/$.?#].[^\s]*$')
103
-
104
- base_url: str = field(default_factory=lambda: get_env_value(
105
- 'WEATHER_API_BASE_URL',
106
- 'http://api.weatherapi.com/v1'
107
- ))
108
- rate_limit_per_minute: int = field(default_factory=lambda: get_env_value(
109
- 'WEATHER_API_RATE_LIMIT',
110
- 60,
111
- int
112
- ))
113
- cache_timeout_seconds: int = field(default_factory=lambda: get_env_value(
114
- 'WEATHER_CACHE_TIMEOUT',
115
- 300,
116
- int
117
- ))
118
- api_key: Optional[str] = field(default_factory=lambda: os.getenv('WEATHER_API_KEY'))
119
-
120
- def __post_init__(self):
121
- """Validate configuration after initialization."""
122
- validate_non_empty_string(self.base_url, "base_url")
123
- if not self.URL_PATTERN.match(self.base_url):
124
- raise ConfigValidationError("Invalid URL format", "base_url")
125
-
126
- validate_positive(self.rate_limit_per_minute, "rate_limit_per_minute")
127
- validate_non_negative(self.cache_timeout_seconds, "cache_timeout_seconds")
128
-
129
- if not self.api_key:
130
- logger.warning("Weather API key not set. Weather functionality will be unavailable.")
131
- elif not isinstance(self.api_key, str):
132
- raise ConfigValidationError("API key must be a string", "api_key")
133
-
134
- @dataclass(frozen=True)
135
- class TimezoneAPIConfig:
136
- """Timezone API specific configuration."""
137
- rate_limit_per_minute: int = field(default_factory=lambda: get_env_value(
138
- 'TIMEZONE_API_RATE_LIMIT',
139
- 100,
140
- int
141
- ))
142
-
143
- def __post_init__(self):
144
- """Validate configuration after initialization."""
145
- validate_positive(self.rate_limit_per_minute, "rate_limit_per_minute")
146
-
147
- @dataclass(frozen=True)
148
- class APIConfig:
149
- """API configuration settings."""
150
- weather: WeatherAPIConfig = field(default_factory=WeatherAPIConfig)
151
- timezone: TimezoneAPIConfig = field(default_factory=TimezoneAPIConfig)
152
-
153
- @dataclass(frozen=True)
154
- class CommandHistoryConfig:
155
- """Command history configuration."""
156
- max_size: int = field(default_factory=lambda: get_env_value(
157
- 'COMMAND_HISTORY_MAX_SIZE',
158
- 10,
159
- int
160
- ))
161
-
162
- def __post_init__(self):
163
- """Validate configuration after initialization."""
164
- if self.max_size < 1:
165
- raise ConfigValidationError("Command history size must be at least 1")
166
-
167
- @dataclass(frozen=True)
168
- class CacheConfig:
169
- """Cache configuration settings."""
170
- cleanup_interval_seconds: int = field(default_factory=lambda: get_env_value(
171
- 'CACHE_CLEANUP_INTERVAL',
172
- 600,
173
- int
174
- ))
175
-
176
- def __post_init__(self):
177
- """Validate configuration after initialization."""
178
- if self.cleanup_interval_seconds < 0:
179
- raise ConfigValidationError("Cache cleanup interval cannot be negative")
180
-
181
- @dataclass(frozen=True)
182
- class AssistantConfig:
183
- """Assistant specific configuration."""
184
- command_history: CommandHistoryConfig = field(default_factory=CommandHistoryConfig)
185
- cache: CacheConfig = field(default_factory=CacheConfig)
186
-
187
- @dataclass(frozen=True)
188
- class UIConfig:
189
- """UI configuration settings."""
190
- DIMENSION_PATTERN: ClassVar[Pattern] = re.compile(r'^\d+(%|px|em|rem|vh|vw)$')
191
- ALLOWED_THEMES: ClassVar[List[str]] = [theme.value for theme in Theme]
192
-
193
- title: str = field(default_factory=lambda: get_env_value(
194
- 'UI_TITLE',
195
- 'AI Assistant'
196
- ))
197
- description: str = field(default_factory=lambda: get_env_value(
198
- 'UI_DESCRIPTION',
199
- 'Weather and Timezone Assistant'
200
- ))
201
- theme: Theme = field(default_factory=lambda: Theme(get_env_value(
202
- 'UI_THEME',
203
- 'light'
204
- )))
205
- input_placeholder: str = field(default_factory=lambda: get_env_value(
206
- 'UI_INPUT_PLACEHOLDER',
207
- 'Enter your command (type \'help\' for available commands)'
208
- ))
209
- width: str = field(default_factory=lambda: get_env_value(
210
- 'UI_WIDTH',
211
- '100%'
212
- ))
213
- height: str = field(default_factory=lambda: get_env_value(
214
- 'UI_HEIGHT',
215
- '600px'
216
- ))
217
-
218
- def __post_init__(self):
219
- """Validate configuration after initialization."""
220
- validate_non_empty_string(self.title, "title")
221
- validate_non_empty_string(self.description, "description")
222
- validate_non_empty_string(self.input_placeholder, "input_placeholder")
223
-
224
- if not self.DIMENSION_PATTERN.match(self.width):
225
- raise ConfigValidationError(
226
- f"Invalid format. Must be a number followed by %, px, em, rem, vh, or vw",
227
- "width"
228
- )
229
- if not self.DIMENSION_PATTERN.match(self.height):
230
- raise ConfigValidationError(
231
- f"Invalid format. Must be a number followed by %, px, em, rem, vh, or vw",
232
- "height"
233
- )
234
-
235
- if self.theme.value not in self.ALLOWED_THEMES:
236
- raise ConfigValidationError(
237
- f"Invalid theme. Must be one of: {', '.join(self.ALLOWED_THEMES)}",
238
- "theme"
239
- )
240
-
241
- @dataclass(frozen=True)
242
- class LoggingConfig:
243
- """Logging configuration settings."""
244
- ALLOWED_LEVELS: ClassVar[List[str]] = [level.value for level in LogLevel]
245
-
246
- level: LogLevel = field(default_factory=lambda: LogLevel(get_env_value(
247
- 'LOG_LEVEL',
248
- 'INFO'
249
- )))
250
- file: str = field(default_factory=lambda: get_env_value(
251
- 'LOG_FILE',
252
- 'assistant.log'
253
- ))
254
- format: str = field(default_factory=lambda: get_env_value(
255
- 'LOG_FORMAT',
256
- '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
257
- ))
258
-
259
- def __post_init__(self):
260
- """Validate configuration after initialization."""
261
- validate_non_empty_string(self.file, "file")
262
- validate_non_empty_string(self.format, "format")
263
-
264
- if self.level.value not in self.ALLOWED_LEVELS:
265
- raise ConfigValidationError(
266
- f"Invalid log level. Must be one of: {', '.join(self.ALLOWED_LEVELS)}",
267
- "level"
268
- )
269
-
270
- validate_path_exists_or_creatable(self.file, "file")
271
-
272
- # Validate log format by attempting to use it
273
- try:
274
- logging.Formatter(self.format)
275
- except ValueError as e:
276
- raise ConfigValidationError(f"Invalid log format: {str(e)}", "format")
277
-
278
- @dataclass(frozen=True)
279
- class ModelConfig:
280
- """Model configuration settings."""
281
- class_name: str = field(default_factory=lambda: get_env_value(
282
- 'MODEL_CLASS',
283
- 'HfApiModel'
284
- ))
285
- max_tokens: int = field(default_factory=lambda: get_env_value(
286
- 'MODEL_MAX_TOKENS',
287
- 2096,
288
- int
289
- ))
290
- temperature: float = field(default_factory=lambda: get_env_value(
291
- 'MODEL_TEMPERATURE',
292
- 0.5,
293
- float
294
- ))
295
- model_id: str = field(default_factory=lambda: get_env_value(
296
- 'MODEL_ID',
297
- 'Qwen/Qwen2.5-Coder-32B-Instruct'
298
- ))
299
- last_input_token_count: Optional[int] = None
300
- last_output_token_count: Optional[int] = None
301
- custom_role_conversions: Optional[Dict[str, Any]] = None
302
-
303
- def __post_init__(self):
304
- """Validate configuration after initialization."""
305
- validate_non_empty_string(self.class_name, "class_name")
306
- validate_non_empty_string(self.model_id, "model_id")
307
- validate_positive(self.max_tokens, "max_tokens")
308
- validate_range(self.temperature, 0.0, 1.0, "temperature")
309
-
310
- if self.last_input_token_count is not None:
311
- validate_non_negative(self.last_input_token_count, "last_input_token_count")
312
- if self.last_output_token_count is not None:
313
- validate_non_negative(self.last_output_token_count, "last_output_token_count")
314
-
315
- if self.custom_role_conversions is not None and not isinstance(self.custom_role_conversions, dict):
316
- raise ConfigValidationError("Must be a dictionary", "custom_role_conversions")
317
-
318
- @dataclass(frozen=True)
319
- class Config:
320
- """Main configuration class combining all settings."""
321
- api: APIConfig = field(default_factory=APIConfig)
322
- assistant: AssistantConfig = field(default_factory=AssistantConfig)
323
- ui: UIConfig = field(default_factory=UIConfig)
324
- logging: LoggingConfig = field(default_factory=LoggingConfig)
325
- model: ModelConfig = field(default_factory=ModelConfig)
326
-
327
- def __post_init__(self):
328
- """Cross-validate configuration settings."""
329
- # Validate that cache cleanup interval is less than cache timeout
330
- if self.assistant.cache.cleanup_interval_seconds >= self.api.weather.cache_timeout_seconds:
331
- raise ConfigValidationError(
332
- "Cache cleanup interval must be less than cache timeout",
333
- "assistant.cache.cleanup_interval_seconds"
334
- )
335
-
336
- @classmethod
337
- def from_json(cls, json_data: Dict[str, Any]) -> 'Config':
338
- """
339
- Create a Config instance from JSON data.
340
-
341
- Args:
342
- json_data: Dictionary containing configuration data
343
-
344
- Returns:
345
- Config: New configuration instance
346
-
347
- Raises:
348
- ConfigValidationError: If the configuration is invalid
349
- """
350
- try:
351
- config_data = json_data.get('config', {})
352
- model_data = json_data.get('model', {}).get('data', {})
353
-
354
- return cls(
355
- api=APIConfig(
356
- weather=WeatherAPIConfig(
357
- base_url=config_data.get('api', {}).get('weather', {}).get('base_url', WeatherAPIConfig.base_url),
358
- rate_limit_per_minute=config_data.get('api', {}).get('weather', {}).get('rate_limit_per_minute', WeatherAPIConfig.rate_limit_per_minute),
359
- cache_timeout_seconds=config_data.get('api', {}).get('weather', {}).get('cache_timeout_seconds', WeatherAPIConfig.cache_timeout_seconds)
360
- ),
361
- timezone=TimezoneAPIConfig(
362
- rate_limit_per_minute=config_data.get('api', {}).get('timezone', {}).get('rate_limit_per_minute', TimezoneAPIConfig.rate_limit_per_minute)
363
- )
364
- ),
365
- assistant=AssistantConfig(
366
- command_history=CommandHistoryConfig(
367
- max_size=config_data.get('assistant', {}).get('command_history', {}).get('max_size', CommandHistoryConfig.max_size)
368
- ),
369
- cache=CacheConfig(
370
- cleanup_interval_seconds=config_data.get('assistant', {}).get('cache', {}).get('cleanup_interval_seconds', CacheConfig.cleanup_interval_seconds)
371
- )
372
- ),
373
- ui=UIConfig(
374
- title=config_data.get('ui', {}).get('title', UIConfig.title),
375
- description=config_data.get('ui', {}).get('description', UIConfig.description),
376
- theme=Theme(config_data.get('ui', {}).get('theme', UIConfig.theme.value)),
377
- input_placeholder=config_data.get('ui', {}).get('input_placeholder', UIConfig.input_placeholder),
378
- width=config_data.get('ui', {}).get('width', UIConfig.width),
379
- height=config_data.get('ui', {}).get('height', UIConfig.height)
380
- ),
381
- logging=LoggingConfig(
382
- level=LogLevel(config_data.get('logging', {}).get('level', LoggingConfig.level.value)),
383
- file=config_data.get('logging', {}).get('file', LoggingConfig.file),
384
- format=config_data.get('logging', {}).get('format', LoggingConfig.format)
385
- ),
386
- model=ModelConfig(
387
- class_name=model_data.get('class', ModelConfig.class_name),
388
- max_tokens=model_data.get('max_tokens', ModelConfig.max_tokens),
389
- temperature=model_data.get('temperature', ModelConfig.temperature),
390
- model_id=model_data.get('model_id', ModelConfig.model_id),
391
- last_input_token_count=model_data.get('last_input_token_count'),
392
- last_output_token_count=model_data.get('last_output_token_count'),
393
- custom_role_conversions=model_data.get('custom_role_conversions')
394
- )
395
- )
396
- except (ValueError, TypeError, FrozenInstanceError) as e:
397
- raise ConfigValidationError(f"Invalid configuration: {str(e)}")
398
-
399
- def to_dict(self) -> Dict[str, Any]:
400
- """
401
- Convert configuration to a dictionary.
402
-
403
- Returns:
404
- Dict[str, Any]: Dictionary representation of the configuration
405
- """
406
- return {
407
- 'config': {
408
- 'api': {
409
- 'weather': {
410
- 'base_url': self.api.weather.base_url,
411
- 'rate_limit_per_minute': self.api.weather.rate_limit_per_minute,
412
- 'cache_timeout_seconds': self.api.weather.cache_timeout_seconds
413
- },
414
- 'timezone': {
415
- 'rate_limit_per_minute': self.api.timezone.rate_limit_per_minute
416
- }
417
- },
418
- 'assistant': {
419
- 'command_history': {
420
- 'max_size': self.assistant.command_history.max_size
421
- },
422
- 'cache': {
423
- 'cleanup_interval_seconds': self.assistant.cache.cleanup_interval_seconds
424
- }
425
- },
426
- 'ui': {
427
- 'title': self.ui.title,
428
- 'description': self.ui.description,
429
- 'theme': self.ui.theme.value,
430
- 'input_placeholder': self.ui.input_placeholder,
431
- 'width': self.ui.width,
432
- 'height': self.ui.height
433
- },
434
- 'logging': {
435
- 'level': self.logging.level.value,
436
- 'file': self.logging.file,
437
- 'format': self.logging.format
438
- }
439
- },
440
- 'model': {
441
- 'class': self.model.class_name,
442
- 'data': {
443
- 'max_tokens': self.model.max_tokens,
444
- 'temperature': self.model.temperature,
445
- 'model_id': self.model.model_id,
446
- 'last_input_token_count': self.model.last_input_token_count,
447
- 'last_output_token_count': self.model.last_output_token_count,
448
- 'custom_role_conversions': self.model.custom_role_conversions
449
- }
450
- }
451
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/api.md DELETED
@@ -1,385 +0,0 @@
1
- # API Documentation
2
-
3
- ## Overview
4
-
5
- The API key management system provides secure handling of API keys with features like encryption, rate limiting, and key rotation. This documentation covers all available APIs, their usage, and best practices.
6
-
7
- ## Table of Contents
8
-
9
- 1. [Key Manager](#key-manager)
10
- 2. [API Key Configuration](#api-key-configuration)
11
- 3. [Rate Limiting](#rate-limiting)
12
- 4. [Error Handling](#error-handling)
13
- 5. [Examples](#examples)
14
-
15
- ## Key Manager
16
-
17
- ### Initialization
18
-
19
- ```python
20
- from tools.security import KeyManager, key_manager
21
-
22
- # Use global instance
23
- manager = key_manager
24
-
25
- # Or create custom instance
26
- custom_manager = KeyManager(
27
- keys_file="custom_keys.json",
28
- env_prefix="API",
29
- encryption_key="your-encryption-key"
30
- )
31
- ```
32
-
33
- #### Parameters
34
-
35
- - `keys_file` (str | Path, optional): Path to keys storage file. Default: "keys.json"
36
- - `env_prefix` (str, optional): Prefix for environment variables. Default: "API"
37
- - `encryption_key` (str, optional): Key for encrypting stored keys. Default: None (auto-generated)
38
-
39
- ### Methods
40
-
41
- #### add_key
42
-
43
- Add a new API key with configuration.
44
-
45
- ```python
46
- key = key_manager.add_key(
47
- name="service_name",
48
- key="your-api-key",
49
- scopes=["read:data", "write:data"],
50
- expires_at=datetime.now(timezone.utc) + timedelta(days=90),
51
- rate_limit=10.0,
52
- burst_limit=20
53
- )
54
- ```
55
-
56
- **Parameters:**
57
- - `name` (str): Key identifier
58
- - `key` (str): API key value
59
- - `scopes` (List[str], optional): Access scopes. Default: []
60
- - `expires_at` (datetime, optional): Expiration date. Default: None
61
- - `rate_limit` (float, optional): Requests per second. Default: 10.0
62
- - `burst_limit` (int, optional): Maximum burst size. Default: 20
63
-
64
- **Returns:**
65
- - `APIKeyConfig`: Created key configuration
66
-
67
- **Raises:**
68
- - `ValidationError`: If any input is invalid
69
- - `InvalidKeyError`: If key already exists
70
- - `ConfigurationError`: If key cannot be saved
71
-
72
- #### get_key
73
-
74
- Get an API key by name.
75
-
76
- ```python
77
- key = key_manager.get_key("service_name")
78
- if key and key.is_valid:
79
- api_key = key.key.get_secret_value()
80
- ```
81
-
82
- **Parameters:**
83
- - `name` (str): Key identifier
84
-
85
- **Returns:**
86
- - `Optional[APIKeyConfig]`: Key configuration if found and valid
87
-
88
- **Raises:**
89
- - `ValidationError`: If name is invalid
90
- - `InvalidKeyError`: If key is not found
91
- - `KeyExpiredError`: If key is expired
92
-
93
- #### rotate_key
94
-
95
- Rotate an existing API key.
96
-
97
- ```python
98
- new_key = key_manager.rotate_key(
99
- name="service_name",
100
- new_key="new-api-key",
101
- expires_at=datetime.now(timezone.utc) + timedelta(days=90),
102
- rate_limit=20.0,
103
- burst_limit=40
104
- )
105
- ```
106
-
107
- **Parameters:**
108
- - `name` (str): Key identifier
109
- - `new_key` (str): New API key value
110
- - `expires_at` (datetime, optional): New expiration date
111
- - `rate_limit` (float, optional): New rate limit
112
- - `burst_limit` (int, optional): New burst limit
113
-
114
- **Returns:**
115
- - `APIKeyConfig`: Updated key configuration
116
-
117
- **Raises:**
118
- - `ValidationError`: If any input is invalid
119
- - `InvalidKeyError`: If key does not exist
120
- - `ConfigurationError`: If key cannot be saved
121
-
122
- #### remove_key
123
-
124
- Remove an API key.
125
-
126
- ```python
127
- key_manager.remove_key("service_name")
128
- ```
129
-
130
- **Parameters:**
131
- - `name` (str): Key identifier
132
-
133
- **Raises:**
134
- - `ValidationError`: If name is invalid
135
- - `InvalidKeyError`: If key does not exist
136
- - `ConfigurationError`: If key cannot be removed
137
-
138
- #### list_keys
139
-
140
- List all valid API keys.
141
-
142
- ```python
143
- keys = key_manager.list_keys()
144
- for name, key in keys.items():
145
- print(f"{name}: {key.scopes}")
146
- ```
147
-
148
- **Returns:**
149
- - `Dict[str, APIKeyConfig]`: Dictionary of valid key configurations
150
-
151
- #### check_rate_limit
152
-
153
- Check rate limit for a key.
154
-
155
- ```python
156
- try:
157
- wait_time = await key_manager.check_rate_limit("service_name")
158
- if wait_time > 0:
159
- await asyncio.sleep(wait_time)
160
- except RateLimitError as e:
161
- # Handle rate limit exceeded
162
- logger.warning(f"Rate limited: {e.wait_time}s")
163
- ```
164
-
165
- **Parameters:**
166
- - `name` (str): Key identifier
167
-
168
- **Returns:**
169
- - `float`: Wait time in seconds if rate limited, 0 otherwise
170
-
171
- **Raises:**
172
- - `InvalidKeyError`: If key does not exist
173
- - `KeyExpiredError`: If key is expired
174
- - `RateLimitError`: If rate limit is exceeded
175
-
176
- ## API Key Configuration
177
-
178
- ### APIKeyConfig Class
179
-
180
- Configuration class for API keys with validation.
181
-
182
- ```python
183
- from tools.security import APIKeyConfig
184
- from pydantic import SecretStr
185
-
186
- config = APIKeyConfig(
187
- key=SecretStr("api-key"),
188
- name="service_name",
189
- scopes=["read:data"],
190
- expires_at=datetime.now(timezone.utc) + timedelta(days=90),
191
- rate_limit=10.0,
192
- burst_limit=20
193
- )
194
- ```
195
-
196
- #### Attributes
197
-
198
- - `key` (SecretStr): API key value (masked in logs)
199
- - `name` (str): Key identifier
200
- - `created_at` (datetime): Creation timestamp
201
- - `expires_at` (Optional[datetime]): Expiration date
202
- - `scopes` (List[str]): Access scopes
203
- - `is_active` (bool): Whether key is active
204
- - `rate_limit` (float): Requests per second
205
- - `burst_limit` (int): Maximum burst size
206
-
207
- #### Properties
208
-
209
- - `is_expired` (bool): Check if key is expired
210
- - `is_valid` (bool): Check if key is valid for use
211
-
212
- ## Rate Limiting
213
-
214
- ### Configuration
215
-
216
- Rate limiting is configured per key using token bucket algorithm:
217
-
218
- ```python
219
- # Configure rate limits
220
- key = key_manager.add_key(
221
- name="service_name",
222
- key="api-key",
223
- rate_limit=10.0, # 10 requests per second
224
- burst_limit=20 # Allow bursts up to 20
225
- )
226
-
227
- # Check rate limit before requests
228
- try:
229
- wait_time = await key_manager.check_rate_limit("service_name")
230
- if wait_time > 0:
231
- # Wait before next request
232
- await asyncio.sleep(wait_time)
233
- except RateLimitError as e:
234
- # Rate limit exceeded
235
- logger.warning(f"Rate limited for {e.wait_time}s")
236
- ```
237
-
238
- ### Best Practices
239
-
240
- 1. Set appropriate limits based on service capacity
241
- 2. Configure burst limits for traffic spikes
242
- 3. Implement client-side rate limiting
243
- 4. Use exponential backoff for retries
244
- 5. Monitor rate limit violations
245
-
246
- ## Error Handling
247
-
248
- ### Error Types
249
-
250
- - `SecurityError`: Base class for security errors
251
- - `AuthenticationError`: Authentication failures
252
- - `AuthorizationError`: Permission issues
253
- - `InvalidKeyError`: Invalid/missing keys
254
- - `KeyExpiredError`: Expired keys
255
- - `RateLimitError`: Rate limit violations
256
- - `EncryptionError`: Encryption issues
257
- - `ValidationError`: Input validation failures
258
- - `ConfigurationError`: Configuration issues
259
-
260
- ### Error Handling Example
261
-
262
- ```python
263
- from tools.errors import (
264
- SecurityError,
265
- InvalidKeyError,
266
- KeyExpiredError,
267
- RateLimitError
268
- )
269
-
270
- try:
271
- # Attempt to use key
272
- key = key_manager.get_key("service_name")
273
- if not key or not key.is_valid:
274
- raise InvalidKeyError("Invalid key")
275
-
276
- # Check rate limit
277
- wait_time = await key_manager.check_rate_limit("service_name")
278
- if wait_time > 0:
279
- await asyncio.sleep(wait_time)
280
-
281
- # Use key
282
- api_key = key.key.get_secret_value()
283
-
284
- except InvalidKeyError as e:
285
- logger.error("Invalid API key", service="service_name")
286
- raise HTTPError(401, "Authentication failed")
287
-
288
- except KeyExpiredError as e:
289
- logger.error("Expired API key", service="service_name")
290
- raise HTTPError(401, "Authentication failed")
291
-
292
- except RateLimitError as e:
293
- logger.warning("Rate limit exceeded",
294
- service="service_name",
295
- wait_time=e.wait_time
296
- )
297
- raise HTTPError(429, f"Rate limit exceeded. Try again in {e.wait_time}s")
298
-
299
- except SecurityError as e:
300
- logger.error("Security error",
301
- service="service_name",
302
- error=str(e)
303
- )
304
- raise HTTPError(500, "Internal security error")
305
- ```
306
-
307
- ## Examples
308
-
309
- ### Basic Usage
310
-
311
- ```python
312
- # Initialize
313
- from tools.security import key_manager
314
-
315
- # Add new key
316
- key = key_manager.add_key(
317
- name="my_service",
318
- key="my-api-key",
319
- scopes=["read:data"]
320
- )
321
-
322
- # Use key
323
- try:
324
- key = key_manager.get_key("my_service")
325
- if key and key.is_valid:
326
- api_key = key.key.get_secret_value()
327
- # Use api_key...
328
- except SecurityError as e:
329
- logger.error("Security error", error=str(e))
330
- ```
331
-
332
- ### Key Rotation
333
-
334
- ```python
335
- # Generate new key
336
- import secrets
337
- import string
338
- new_key = ''.join(secrets.choice(string.ascii_letters + string.digits + '_.-')
339
- for _ in range(32))
340
-
341
- # Rotate key
342
- try:
343
- key = key_manager.rotate_key(
344
- name="my_service",
345
- new_key=new_key,
346
- expires_at=datetime.now(timezone.utc) + timedelta(days=90)
347
- )
348
- logger.info("Key rotated successfully", service="my_service")
349
- except SecurityError as e:
350
- logger.error("Key rotation failed", error=str(e))
351
- ```
352
-
353
- ### Rate Limited Service
354
-
355
- ```python
356
- async def make_api_request(service_name: str):
357
- try:
358
- # Get key
359
- key = key_manager.get_key(service_name)
360
- if not key or not key.is_valid:
361
- raise InvalidKeyError("Invalid key")
362
-
363
- # Check rate limit
364
- wait_time = await key_manager.check_rate_limit(service_name)
365
- if wait_time > 0:
366
- await asyncio.sleep(wait_time)
367
-
368
- # Make request
369
- api_key = key.key.get_secret_value()
370
- response = await make_request(api_key)
371
- return response
372
-
373
- except RateLimitError as e:
374
- logger.warning("Rate limited",
375
- service=service_name,
376
- wait_time=e.wait_time
377
- )
378
- raise
379
- ```
380
-
381
- For more examples and best practices, see:
382
- - [Security Documentation](../SECURITY.md)
383
- - [Quick Start Guide](./security_quickstart.md)
384
- - [Error Handling Guide](./errors.md)
385
- - [Monitoring Guide](./monitoring.md)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/api_reference.md DELETED
@@ -1,143 +0,0 @@
1
- # API Reference
2
-
3
- ## Core Components
4
-
5
- ### Tool Registry
6
-
7
- The `ToolRegistry` class manages and instantiates tools in the AI Assistant framework.
8
-
9
- ```python
10
- from tools import ToolRegistry, get_tool, get_all_tools
11
- ```
12
-
13
- #### Methods
14
-
15
- - `get_tool(name: str) -> BaseTool`: Get a tool instance by name
16
- - `get_all_tools() -> dict[str, BaseTool]`: Get all registered tools
17
- - `register(tool_class: Type[T]) -> Type[T]`: Register a tool class (decorator)
18
- - `clear() -> None`: Clear all registered tools (mainly for testing)
19
-
20
- ### Base Tool
21
-
22
- The `BaseTool` abstract base class defines the interface for all tools.
23
-
24
- ```python
25
- from tools import BaseTool
26
- ```
27
-
28
- #### Required Properties
29
-
30
- - `name -> str`: The unique name of the tool
31
- - `description -> str`: A description of what the tool does
32
-
33
- ## Error Handling
34
-
35
- ### API Errors
36
-
37
- The framework provides specialized error types for API-related issues:
38
-
39
- ```python
40
- from tools.exceptions import APIError, APITimeoutError, APIRateLimitError
41
- ```
42
-
43
- #### Error Types
44
-
45
- - `APIError`: Base exception for API-related errors
46
- - `APITimeoutError`: Raised when an API request times out
47
- - `APIRateLimitError`: Raised when API rate limits are exceeded
48
-
49
- ### Error Recovery
50
-
51
- #### Circuit Breaker
52
-
53
- The `CircuitBreaker` class prevents cascading failures:
54
-
55
- ```python
56
- from tools.error_recovery import CircuitBreaker
57
-
58
- circuit_breaker = CircuitBreaker(
59
- failure_threshold=5,
60
- reset_timeout=60.0,
61
- half_open_timeout=30.0
62
- )
63
- ```
64
-
65
- #### Retry Configuration
66
-
67
- The `RetryConfig` class configures retry behavior:
68
-
69
- ```python
70
- from tools.error_recovery import RetryConfig
71
-
72
- retry_config = RetryConfig(
73
- max_retries=3,
74
- initial_delay=1.0,
75
- max_delay=60.0,
76
- exponential_base=2.0,
77
- jitter=0.1
78
- )
79
- ```
80
-
81
- ## API Logging
82
-
83
- The `log_api_call` decorator provides comprehensive API call logging:
84
-
85
- ```python
86
- from tools.api_logging import log_api_call
87
-
88
- @log_api_call(
89
- log_response=True,
90
- log_request_body=True,
91
- timeout=5.0,
92
- retry_config=RetryConfig(max_retries=3),
93
- circuit_breaker=CircuitBreaker(failure_threshold=5)
94
- )
95
- def my_api_function():
96
- pass
97
- ```
98
-
99
- ### Features
100
-
101
- - Request/response logging with sanitization
102
- - Timeout handling
103
- - Retry mechanism with exponential backoff
104
- - Circuit breaker integration
105
- - Rate limit handling
106
-
107
- ## Tools
108
-
109
- ### Timezone Tools
110
-
111
- The `TimezoneTools` class provides timezone conversion functionality:
112
-
113
- ```python
114
- from tools import TimezoneTools
115
-
116
- timezone_tool = TimezoneTools()
117
- ```
118
-
119
- #### Methods
120
-
121
- - `get_current_time(tz: str = "UTC") -> datetime`: Get current time in specified timezone
122
- - `list_common_timezones() -> List[str]`: Get list of common timezone names
123
- - `is_valid_timezone(tz: str) -> bool`: Check if timezone name is valid
124
-
125
- ### Weather Tool
126
-
127
- The `WeatherTool` class provides weather information by ZIP code:
128
-
129
- ```python
130
- from tools.weather import WeatherTool
131
-
132
- weather_tool = WeatherTool(
133
- api_base_url="http://api.weatherapi.com/v1",
134
- cache_timeout=300
135
- )
136
- ```
137
-
138
- #### Methods
139
-
140
- - `__call__(zipcode: str) -> str`: Get weather information for a ZIP code
141
- - Includes built-in caching
142
- - Validates ZIP code format
143
- - Handles API errors and rate limiting
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/error_handling.md DELETED
@@ -1,201 +0,0 @@
1
- # Error Handling Guide
2
-
3
- ## Overview
4
-
5
- The AI Assistant framework provides a robust error handling system with several key features:
6
- - Custom exception types for different error scenarios
7
- - Automatic retry mechanism with exponential backoff
8
- - Circuit breaker pattern to prevent cascading failures
9
- - Comprehensive error logging
10
-
11
- ## Exception Hierarchy
12
-
13
- ```
14
- ToolError
15
- └── APIError
16
- ├── APITimeoutError
17
- └── APIRateLimitError
18
- ```
19
-
20
- ### Base Exceptions
21
-
22
- #### ToolError
23
- Base exception class for all tool-related errors.
24
-
25
- #### APIError
26
- ```python
27
- from tools.exceptions import APIError
28
-
29
- raise APIError(
30
- message="API request failed",
31
- endpoint="/api/v1/data",
32
- status_code=500,
33
- response_body="Internal server error",
34
- request_data={"key": "value"},
35
- tool_name="my_tool"
36
- )
37
- ```
38
-
39
- Properties:
40
- - `message`: Error description
41
- - `endpoint`: The API endpoint that failed
42
- - `status_code`: HTTP status code (optional)
43
- - `response_body`: Response from the API (optional)
44
- - `request_data`: Data sent in the request (optional)
45
- - `tool_name`: Name of the tool where the error occurred
46
- - `timestamp`: When the error occurred
47
-
48
- ### Specialized Exceptions
49
-
50
- #### APITimeoutError
51
- ```python
52
- from tools.exceptions import APITimeoutError
53
-
54
- raise APITimeoutError(
55
- endpoint="/api/v1/data",
56
- timeout=5.0,
57
- request_data={"key": "value"},
58
- tool_name="my_tool"
59
- )
60
- ```
61
-
62
- Additional properties:
63
- - `timeout`: The timeout duration in seconds
64
-
65
- #### APIRateLimitError
66
- ```python
67
- from tools.exceptions import APIRateLimitError
68
-
69
- raise APIRateLimitError(
70
- endpoint="/api/v1/data",
71
- retry_after=60,
72
- request_data={"key": "value"},
73
- tool_name="my_tool"
74
- )
75
- ```
76
-
77
- Additional properties:
78
- - `retry_after`: Seconds to wait before retrying
79
-
80
- ## Error Recovery
81
-
82
- ### Circuit Breaker Pattern
83
-
84
- The circuit breaker prevents cascading failures by temporarily stopping operations after repeated failures.
85
-
86
- ```python
87
- from tools.error_recovery import CircuitBreaker
88
-
89
- circuit_breaker = CircuitBreaker(
90
- failure_threshold=5, # Open after 5 failures
91
- reset_timeout=60.0, # Try to reset after 60 seconds
92
- half_open_timeout=30.0 # Wait 30 seconds between half-open attempts
93
- )
94
- ```
95
-
96
- States:
97
- 1. **Closed**: Normal operation
98
- 2. **Open**: Failing fast, no operations allowed
99
- 3. **Half-Open**: Testing if system has recovered
100
-
101
- ### Retry Mechanism
102
-
103
- The retry mechanism automatically retries failed operations with exponential backoff.
104
-
105
- ```python
106
- from tools.error_recovery import RetryConfig
107
-
108
- retry_config = RetryConfig(
109
- max_retries=3, # Maximum number of retry attempts
110
- initial_delay=1.0, # Initial delay between retries (seconds)
111
- max_delay=60.0, # Maximum delay between retries (seconds)
112
- exponential_base=2.0, # Base for exponential backoff
113
- jitter=0.1 # Random jitter factor (0.0-1.0)
114
- )
115
- ```
116
-
117
- Features:
118
- - Exponential backoff with jitter
119
- - Configurable retry conditions
120
- - Automatic handling of rate limits
121
- - Integration with circuit breaker
122
-
123
- ## Combining Error Recovery Mechanisms
124
-
125
- Use the `@log_api_call` decorator to combine logging, retries, and circuit breaker:
126
-
127
- ```python
128
- from tools.api_logging import log_api_call
129
- from tools.error_recovery import RetryConfig, CircuitBreaker
130
-
131
- @log_api_call(
132
- retry_config=RetryConfig(
133
- max_retries=3,
134
- initial_delay=1.0
135
- ),
136
- circuit_breaker=CircuitBreaker(
137
- failure_threshold=5
138
- )
139
- )
140
- def call_api():
141
- # Your API call here
142
- pass
143
- ```
144
-
145
- ## Best Practices
146
-
147
- 1. **Use Specific Exceptions**
148
- ```python
149
- # Good
150
- raise APITimeoutError(endpoint="/api/data", timeout=5.0)
151
-
152
- # Avoid
153
- raise Exception("API timeout")
154
- ```
155
-
156
- 2. **Include Context**
157
- ```python
158
- # Good
159
- raise APIError(
160
- message="Invalid response format",
161
- endpoint="/api/data",
162
- response_body=response_text
163
- )
164
-
165
- # Avoid
166
- raise APIError("Invalid response format")
167
- ```
168
-
169
- 3. **Configure Recovery Appropriately**
170
- ```python
171
- # Good - Configured for specific use case
172
- RetryConfig(
173
- max_retries=3,
174
- initial_delay=0.1,
175
- max_delay=1.0
176
- )
177
-
178
- # Avoid - Using defaults without consideration
179
- RetryConfig()
180
- ```
181
-
182
- 4. **Handle Rate Limits**
183
- ```python
184
- # Good
185
- except APIRateLimitError as e:
186
- wait_time = e.retry_after or 60
187
- time.sleep(wait_time)
188
-
189
- # Avoid
190
- except APIRateLimitError:
191
- time.sleep(60) # Hard-coded wait time
192
- ```
193
-
194
- 5. **Log Errors Appropriately**
195
- ```python
196
- # Good
197
- @log_api_call(log_response=True, log_request_body=True)
198
-
199
- # Avoid
200
- # No logging or manual logging without sanitization
201
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/errors.md DELETED
@@ -1,434 +0,0 @@
1
- # Error Handling Guide
2
-
3
- ## Overview
4
-
5
- This guide covers error handling in the API key management system, including error types, best practices, and examples.
6
-
7
- ## Table of Contents
8
-
9
- 1. [Error Types](#error-types)
10
- 2. [Error Handling Best Practices](#error-handling-best-practices)
11
- 3. [Common Error Scenarios](#common-error-scenarios)
12
- 4. [Logging Best Practices](#logging-best-practices)
13
- 5. [Error Recovery](#error-recovery)
14
-
15
- ## Error Types
16
-
17
- ### SecurityError
18
-
19
- Base class for all security-related errors.
20
-
21
- ```python
22
- try:
23
- # Security operation
24
- raise SecurityError("Security violation")
25
- except SecurityError as e:
26
- print(f"Error code: {e.code}") # SECURITY_ERROR
27
- print(f"Message: {e.message}") # Security violation
28
- ```
29
-
30
- ### AuthenticationError
31
-
32
- Raised when authentication fails.
33
-
34
- ```python
35
- try:
36
- raise AuthenticationError("Invalid credentials")
37
- except AuthenticationError as e:
38
- print(f"Error code: {e.code}") # AUTH_ERROR
39
- print(f"Message: {e.message}") # Invalid credentials
40
- ```
41
-
42
- ### AuthorizationError
43
-
44
- Raised when authorization fails.
45
-
46
- ```python
47
- try:
48
- raise AuthorizationError("Insufficient permissions")
49
- except AuthorizationError as e:
50
- print(f"Error code: {e.code}") # ACCESS_DENIED
51
- print(f"Message: {e.message}") # Insufficient permissions
52
- ```
53
-
54
- ### InvalidKeyError
55
-
56
- Raised when an API key is invalid.
57
-
58
- ```python
59
- try:
60
- key = key_manager.get_key("nonexistent")
61
- if not key:
62
- raise InvalidKeyError("Key not found")
63
- except InvalidKeyError as e:
64
- print(f"Error code: {e.code}") # INVALID_KEY
65
- print(f"Message: {e.message}") # Key not found
66
- ```
67
-
68
- ### KeyExpiredError
69
-
70
- Raised when an API key has expired.
71
-
72
- ```python
73
- try:
74
- key = key_manager.get_key("expired_key")
75
- if key.is_expired:
76
- raise KeyExpiredError("Key has expired")
77
- except KeyExpiredError as e:
78
- print(f"Error code: {e.code}") # KEY_EXPIRED
79
- print(f"Message: {e.message}") # Key has expired
80
- ```
81
-
82
- ### RateLimitError
83
-
84
- Raised when rate limit is exceeded.
85
-
86
- ```python
87
- try:
88
- await key_manager.check_rate_limit("service")
89
- except RateLimitError as e:
90
- print(f"Error code: {e.code}") # RATE_LIMITED
91
- print(f"Message: {e.message}") # Rate limit exceeded
92
- print(f"Wait time: {e.wait_time}") # Time to wait in seconds
93
- ```
94
-
95
- ### EncryptionError
96
-
97
- Raised when encryption/decryption fails.
98
-
99
- ```python
100
- try:
101
- key_manager = KeyManager(encryption_key="invalid-key")
102
- except EncryptionError as e:
103
- print(f"Error code: {e.code}") # ENCRYPTION_ERROR
104
- print(f"Message: {e.message}") # Failed to decrypt
105
- ```
106
-
107
- ### ValidationError
108
-
109
- Raised when validation fails.
110
-
111
- ```python
112
- try:
113
- key_manager.add_key("service", "invalid@key")
114
- except ValidationError as e:
115
- print(f"Error code: {e.code}") # VALIDATION_ERROR
116
- print(f"Message: {e.message}") # Invalid key format
117
- print(f"Field: {e.field}") # key
118
- ```
119
-
120
- ### ConfigurationError
121
-
122
- Raised when configuration is invalid.
123
-
124
- ```python
125
- try:
126
- key_manager = KeyManager(env_prefix="123") # Must start with letter
127
- except ConfigurationError as e:
128
- print(f"Error code: {e.code}") # CONFIG_ERROR
129
- print(f"Message: {e.message}") # Invalid prefix
130
- ```
131
-
132
- ## Error Handling Best Practices
133
-
134
- ### 1. Use Specific Error Types
135
-
136
- ```python
137
- # ❌ WRONG - Too generic
138
- try:
139
- key = key_manager.get_key("service")
140
- except Exception as e:
141
- logger.error("Error occurred")
142
-
143
- # ✅ RIGHT - Specific error handling
144
- try:
145
- key = key_manager.get_key("service")
146
- except InvalidKeyError:
147
- logger.error("Invalid key", service="service")
148
- raise HTTPError(401)
149
- except KeyExpiredError:
150
- logger.error("Expired key", service="service")
151
- raise HTTPError(401)
152
- except SecurityError as e:
153
- logger.error("Security error", error=str(e))
154
- raise HTTPError(500)
155
- ```
156
-
157
- ### 2. Never Expose Sensitive Data
158
-
159
- ```python
160
- # ❌ WRONG - Exposing sensitive data
161
- except InvalidKeyError as e:
162
- logger.error(f"Invalid key: {api_key}")
163
- return {"error": f"Invalid key: {api_key}"}
164
-
165
- # ✅ RIGHT - Safe error handling
166
- except InvalidKeyError as e:
167
- logger.error("Invalid key used", service="service_name")
168
- return {"error": "Authentication failed"}
169
- ```
170
-
171
- ### 3. Proper Error Propagation
172
-
173
- ```python
174
- # ❌ WRONG - Swallowing errors
175
- try:
176
- key = key_manager.get_key("service")
177
- except SecurityError:
178
- pass # Don't do this!
179
-
180
- # ✅ RIGHT - Proper propagation
181
- try:
182
- key = key_manager.get_key("service")
183
- except SecurityError as e:
184
- logger.error("Security error", error=str(e))
185
- raise # Re-raise or raise appropriate HTTP error
186
- ```
187
-
188
- ### 4. Context Preservation
189
-
190
- ```python
191
- # ❌ WRONG - Lost context
192
- try:
193
- key = key_manager.get_key("service")
194
- except SecurityError as e:
195
- raise RuntimeError("Error occurred")
196
-
197
- # ✅ RIGHT - Preserve context
198
- try:
199
- key = key_manager.get_key("service")
200
- except SecurityError as e:
201
- logger.error("Security error",
202
- service="service",
203
- error_code=e.code,
204
- error_type=type(e).__name__
205
- )
206
- raise HTTPError(500, str(e)) from e
207
- ```
208
-
209
- ## Common Error Scenarios
210
-
211
- ### Authentication Failures
212
-
213
- ```python
214
- from tools.errors import AuthenticationError, InvalidKeyError, KeyExpiredError
215
-
216
- def authenticate_request(service_name: str, api_key: str) -> bool:
217
- try:
218
- # Validate key
219
- key = key_manager.get_key(service_name)
220
- if not key or not key.is_valid:
221
- raise InvalidKeyError("Invalid key")
222
-
223
- # Verify key value
224
- if key.key.get_secret_value() != api_key:
225
- raise AuthenticationError("Invalid credentials")
226
-
227
- return True
228
-
229
- except InvalidKeyError:
230
- logger.error("Invalid API key", service=service_name)
231
- raise HTTPError(401, "Authentication failed")
232
-
233
- except KeyExpiredError:
234
- logger.error("Expired API key", service=service_name)
235
- raise HTTPError(401, "Authentication failed")
236
-
237
- except AuthenticationError:
238
- logger.error("Authentication failed", service=service_name)
239
- raise HTTPError(401, "Authentication failed")
240
- ```
241
-
242
- ### Rate Limiting
243
-
244
- ```python
245
- async def rate_limited_request(service_name: str):
246
- try:
247
- # Check rate limit
248
- wait_time = await key_manager.check_rate_limit(service_name)
249
- if wait_time > 0:
250
- # Implement backoff strategy
251
- await asyncio.sleep(wait_time)
252
-
253
- # Make request
254
- return await make_request()
255
-
256
- except RateLimitError as e:
257
- logger.warning("Rate limit exceeded",
258
- service=service_name,
259
- wait_time=e.wait_time
260
- )
261
- raise HTTPError(
262
- 429,
263
- f"Too many requests. Try again in {e.wait_time} seconds"
264
- )
265
- ```
266
-
267
- ### Configuration Issues
268
-
269
- ```python
270
- def setup_key_manager() -> KeyManager:
271
- try:
272
- # Load configuration
273
- encryption_key = os.getenv("API_ENCRYPTION_KEY")
274
- if not encryption_key:
275
- raise ConfigurationError("Missing encryption key")
276
-
277
- # Initialize manager
278
- return KeyManager(
279
- keys_file="keys.json",
280
- encryption_key=encryption_key
281
- )
282
-
283
- except ConfigurationError as e:
284
- logger.error("Configuration error",
285
- error=str(e),
286
- code=e.code
287
- )
288
- raise SystemExit("Failed to initialize key manager")
289
-
290
- except EncryptionError as e:
291
- logger.error("Encryption error",
292
- error=str(e),
293
- code=e.code
294
- )
295
- raise SystemExit("Failed to initialize encryption")
296
- ```
297
-
298
- ## Logging Best Practices
299
-
300
- ### 1. Use Structured Logging
301
-
302
- ```python
303
- import structlog
304
-
305
- logger = structlog.get_logger()
306
-
307
- try:
308
- key = key_manager.get_key("service")
309
- except SecurityError as e:
310
- logger.error("security_error",
311
- service="service",
312
- error_code=e.code,
313
- error_type=type(e).__name__,
314
- error_message=str(e)
315
- )
316
- ```
317
-
318
- ### 2. Appropriate Log Levels
319
-
320
- ```python
321
- # Error - Security incidents
322
- logger.error("security_violation",
323
- service="service",
324
- error_code="AUTH_ERROR"
325
- )
326
-
327
- # Warning - Rate limits, temporary issues
328
- logger.warning("rate_limit_exceeded",
329
- service="service",
330
- wait_time=1.5
331
- )
332
-
333
- # Info - Normal security events
334
- logger.info("key_rotated",
335
- service="service",
336
- expires_in_days=90
337
- )
338
-
339
- # Debug - Detailed security info
340
- logger.debug("rate_limit_check",
341
- service="service",
342
- current_rate=5.0,
343
- limit=10.0
344
- )
345
- ```
346
-
347
- ### 3. Context Preservation
348
-
349
- ```python
350
- # Add context to all logs
351
- logger = logger.bind(
352
- service="my_service",
353
- environment="production"
354
- )
355
-
356
- try:
357
- key = key_manager.get_key("service")
358
- except SecurityError as e:
359
- logger.error("security_error",
360
- error_code=e.code,
361
- error_message=str(e)
362
- )
363
- ```
364
-
365
- ## Error Recovery
366
-
367
- ### 1. Automatic Retry
368
-
369
- ```python
370
- from tenacity import retry, stop_after_attempt, wait_exponential
371
-
372
- @retry(
373
- stop=stop_after_attempt(3),
374
- wait=wait_exponential(multiplier=1, min=4, max=10),
375
- retry=retry_if_exception_type(RateLimitError)
376
- )
377
- async def make_rate_limited_request(service_name: str):
378
- wait_time = await key_manager.check_rate_limit(service_name)
379
- if wait_time > 0:
380
- await asyncio.sleep(wait_time)
381
- return await make_request()
382
- ```
383
-
384
- ### 2. Fallback Strategies
385
-
386
- ```python
387
- def get_api_key(service_name: str) -> Optional[str]:
388
- try:
389
- # Try primary key
390
- key = key_manager.get_key(service_name)
391
- if key and key.is_valid:
392
- return key.key.get_secret_value()
393
-
394
- # Try fallback key
395
- fallback_key = key_manager.get_key(f"{service_name}_fallback")
396
- if fallback_key and fallback_key.is_valid:
397
- return fallback_key.key.get_secret_value()
398
-
399
- return None
400
-
401
- except SecurityError as e:
402
- logger.error("Failed to get API key",
403
- service=service_name,
404
- error=str(e)
405
- )
406
- return None
407
- ```
408
-
409
- ### 3. Circuit Breaker
410
-
411
- ```python
412
- from circuitbreaker import circuit
413
-
414
- @circuit(
415
- failure_threshold=5,
416
- recovery_timeout=60,
417
- expected_exception=SecurityError
418
- )
419
- async def make_secure_request(service_name: str):
420
- key = key_manager.get_key(service_name)
421
- if not key or not key.is_valid:
422
- raise InvalidKeyError("Invalid key")
423
-
424
- wait_time = await key_manager.check_rate_limit(service_name)
425
- if wait_time > 0:
426
- await asyncio.sleep(wait_time)
427
-
428
- return await make_request(key.key.get_secret_value())
429
- ```
430
-
431
- For more information, see:
432
- - [API Documentation](./api.md)
433
- - [Security Guide](../SECURITY.md)
434
- - [Monitoring Guide](./monitoring.md)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/getting_started.md DELETED
@@ -1,202 +0,0 @@
1
- # Getting Started
2
-
3
- ## Installation
4
-
5
- Install the AI Assistant framework using pip:
6
-
7
- ```bash
8
- pip install ai-assistant
9
- ```
10
-
11
- ## Quick Start
12
-
13
- ### 1. Basic Tool Usage
14
-
15
- ```python
16
- from tools import get_tool
17
-
18
- # Get a tool instance
19
- timezone_tool = get_tool("timezone")
20
-
21
- # Use the tool
22
- current_time = timezone_tool.get_current_time("America/New_York")
23
- print(f"Current time: {current_time}")
24
- ```
25
-
26
- ### 2. Creating a Custom Tool
27
-
28
- ```python
29
- from tools import BaseTool, register_tool
30
-
31
- @register_tool
32
- class MyCustomTool(BaseTool):
33
- @property
34
- def name(self) -> str:
35
- return "my_custom_tool"
36
-
37
- @property
38
- def description(self) -> str:
39
- return "Description of what my tool does"
40
-
41
- def my_method(self) -> str:
42
- return "Hello from my custom tool!"
43
- ```
44
-
45
- ## Core Concepts
46
-
47
- ### 1. Tool Registry
48
-
49
- The tool registry manages all available tools:
50
-
51
- ```python
52
- from tools import ToolRegistry, get_all_tools
53
-
54
- # Get all registered tools
55
- tools = get_all_tools()
56
-
57
- # Get a specific tool
58
- weather_tool = tools["weather"]
59
- ```
60
-
61
- ### 2. API Integration
62
-
63
- Example of making an API call with error handling:
64
-
65
- ```python
66
- from tools.api_logging import log_api_call
67
- from tools.error_recovery import RetryConfig, CircuitBreaker
68
-
69
- @log_api_call(
70
- log_response=True,
71
- retry_config=RetryConfig(max_retries=3),
72
- circuit_breaker=CircuitBreaker(failure_threshold=5)
73
- )
74
- def get_weather(zipcode: str) -> dict:
75
- response = requests.get(
76
- f"https://api.weather.com/v1/{zipcode}",
77
- headers={"Authorization": "Bearer YOUR_API_KEY"}
78
- )
79
- response.raise_for_status()
80
- return response.json()
81
- ```
82
-
83
- ### 3. Error Handling
84
-
85
- Example of handling API errors:
86
-
87
- ```python
88
- from tools.exceptions import APIError, APITimeoutError, APIRateLimitError
89
-
90
- try:
91
- result = get_weather("12345")
92
- except APITimeoutError as e:
93
- print(f"Request timed out after {e.timeout} seconds")
94
- except APIRateLimitError as e:
95
- print(f"Rate limited, retry after {e.retry_after} seconds")
96
- except APIError as e:
97
- print(f"API error: {e.message}")
98
- ```
99
-
100
- ## Common Use Cases
101
-
102
- ### 1. Weather Information
103
-
104
- ```python
105
- from tools.weather import WeatherTool
106
-
107
- weather = WeatherTool()
108
- forecast = weather("90210") # Get weather for Beverly Hills
109
- print(forecast)
110
- ```
111
-
112
- ### 2. Timezone Conversion
113
-
114
- ```python
115
- from tools import TimezoneTools
116
-
117
- tz = TimezoneTools()
118
-
119
- # Get current time in different timezones
120
- nyc_time = tz.get_current_time("America/New_York")
121
- tokyo_time = tz.get_current_time("Asia/Tokyo")
122
-
123
- # List available timezones
124
- timezones = tz.list_common_timezones()
125
- ```
126
-
127
- ## Configuration
128
-
129
- ### 1. Logging Setup
130
-
131
- ```python
132
- import logging
133
-
134
- # Configure logging
135
- logging.basicConfig(
136
- level=logging.INFO,
137
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
138
- )
139
- ```
140
-
141
- ### 2. API Configuration
142
-
143
- ```python
144
- import os
145
-
146
- # Set API keys in environment variables
147
- os.environ['WEATHER_API_KEY'] = 'your_api_key_here'
148
- ```
149
-
150
- ## Best Practices
151
-
152
- 1. **Use Environment Variables for Secrets**
153
- ```python
154
- # Good
155
- api_key = os.getenv('API_KEY')
156
-
157
- # Avoid
158
- api_key = 'hardcoded_secret'
159
- ```
160
-
161
- 2. **Enable Appropriate Logging**
162
- ```python
163
- # Development
164
- @log_api_call(log_response=True, log_request_body=True)
165
-
166
- # Production
167
- @log_api_call(log_response=False, log_request_body=False)
168
- ```
169
-
170
- 3. **Handle Errors Gracefully**
171
- ```python
172
- try:
173
- result = api_call()
174
- except APIError as e:
175
- logger.error(f"API error: {e.message}")
176
- # Implement fallback behavior
177
- ```
178
-
179
- 4. **Use Type Hints**
180
- ```python
181
- def process_data(data: Dict[str, Any]) -> List[str]:
182
- return [item['name'] for item in data['items']]
183
- ```
184
-
185
- 5. **Document Your Tools**
186
- ```python
187
- class MyTool(BaseTool):
188
- """
189
- My custom tool for processing data.
190
-
191
- Attributes:
192
- name: The unique identifier for this tool
193
- description: What this tool does
194
- """
195
- ```
196
-
197
- ## Next Steps
198
-
199
- 1. Read the [API Reference](api_reference.md) for detailed documentation
200
- 2. Learn about [Error Handling](error_handling.md)
201
- 3. Explore the [Logging System](logging.md)
202
- 4. Check out example implementations in the `examples/` directory
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/logging.md DELETED
@@ -1,231 +0,0 @@
1
- # Logging System Documentation
2
-
3
- ## Overview
4
-
5
- The AI Assistant framework provides a comprehensive logging system focused on API interactions and error handling. The system includes:
6
- - Request/response logging
7
- - Error tracking
8
- - Sensitive data sanitization
9
- - Integration with error recovery mechanisms
10
-
11
- ## API Call Logging
12
-
13
- ### Basic Usage
14
-
15
- ```python
16
- from tools.api_logging import log_api_call
17
-
18
- @log_api_call()
19
- def my_api_function(url: str, data: dict):
20
- # Function implementation
21
- pass
22
- ```
23
-
24
- ### Configuration Options
25
-
26
- ```python
27
- @log_api_call(
28
- log_response=True, # Log response bodies
29
- log_request_body=True, # Log request bodies
30
- timeout=5.0, # Request timeout in seconds
31
- retry_config=RetryConfig(...),
32
- circuit_breaker=CircuitBreaker(...)
33
- )
34
- ```
35
-
36
- ## Security Features
37
-
38
- ### Header Sanitization
39
-
40
- The system automatically sanitizes sensitive information in headers:
41
-
42
- ```python
43
- # Original headers
44
- headers = {
45
- 'Authorization': 'Bearer abc123',
46
- 'X-API-Key': 'secret_key',
47
- 'Content-Type': 'application/json'
48
- }
49
-
50
- # Logged headers
51
- {
52
- 'Authorization': '****',
53
- 'X-API-Key': '****',
54
- 'Content-Type': 'application/json'
55
- }
56
- ```
57
-
58
- ### URL Sanitization
59
-
60
- URLs are sanitized to remove sensitive parameters:
61
-
62
- ```python
63
- # Original URL
64
- "https://api.example.com/v1/data?api_key=secret&user=john"
65
-
66
- # Logged URL
67
- "https://api.example.com/v1/data?api_key=****&user=john"
68
- ```
69
-
70
- ## Log Format
71
-
72
- ### Request Logging
73
-
74
- ```json
75
- {
76
- "timestamp": "2024-01-01T12:00:00Z",
77
- "function": "get_weather",
78
- "url": "https://api.example.com/v1/weather",
79
- "headers": {
80
- "Authorization": "****",
81
- "Content-Type": "application/json"
82
- },
83
- "body": {
84
- "location": "New York"
85
- }
86
- }
87
- ```
88
-
89
- ### Response Logging
90
-
91
- ```json
92
- {
93
- "timestamp": "2024-01-01T12:00:01Z",
94
- "status": 200,
95
- "url": "https://api.example.com/v1/weather",
96
- "body": {
97
- "temperature": 72,
98
- "condition": "sunny"
99
- }
100
- }
101
- ```
102
-
103
- ### Error Logging
104
-
105
- ```json
106
- {
107
- "timestamp": "2024-01-01T12:00:01Z",
108
- "error_type": "APITimeoutError",
109
- "error_message": "Request timed out after 5 seconds",
110
- "url": "https://api.example.com/v1/weather"
111
- }
112
- ```
113
-
114
- ## Integration with Error Recovery
115
-
116
- The logging system integrates with the error recovery mechanisms:
117
-
118
- ```python
119
- @log_api_call(
120
- retry_config=RetryConfig(max_retries=3),
121
- circuit_breaker=CircuitBreaker(failure_threshold=5)
122
- )
123
- def api_call():
124
- # Each retry attempt is logged
125
- # Circuit breaker state changes are logged
126
- pass
127
- ```
128
-
129
- Example retry log:
130
- ```json
131
- {
132
- "timestamp": "2024-01-01T12:00:01Z",
133
- "message": "Retry attempt 1 of 3",
134
- "delay": 1.0,
135
- "error": "Connection refused"
136
- }
137
- ```
138
-
139
- ## Best Practices
140
-
141
- 1. **Enable Appropriate Logging Levels**
142
- ```python
143
- # Good - Log both request and response for debugging
144
- @log_api_call(log_response=True, log_request_body=True)
145
-
146
- # Production - Log only essential info
147
- @log_api_call(log_response=False, log_request_body=False)
148
- ```
149
-
150
- 2. **Handle Sensitive Data**
151
- ```python
152
- # Good - Let the logger handle sanitization
153
- headers = {'Authorization': f'Bearer {api_key}'}
154
-
155
- # Avoid - Manual sanitization
156
- headers = {'Authorization': 'Bearer ****'}
157
- ```
158
-
159
- 3. **Include Context in Logs**
160
- ```python
161
- # Good
162
- logger.info({
163
- 'action': 'api_call',
164
- 'endpoint': '/api/v1/data',
165
- 'status': 'success'
166
- })
167
-
168
- # Avoid
169
- logger.info('API call successful')
170
- ```
171
-
172
- 4. **Use Structured Logging**
173
- ```python
174
- # Good
175
- logger.info(json.dumps({
176
- 'event': 'rate_limit',
177
- 'retry_after': 60,
178
- 'endpoint': '/api/v1/data'
179
- }))
180
-
181
- # Avoid
182
- logger.info(f"Rate limited on /api/v1/data, retry after 60s")
183
- ```
184
-
185
- 5. **Log at Appropriate Levels**
186
- ```python
187
- # Debug - Detailed information
188
- logger.debug('Request parameters: %s', params)
189
-
190
- # Info - General operational events
191
- logger.info('API call completed successfully')
192
-
193
- # Warning - Potential issues
194
- logger.warning('Rate limit threshold approaching')
195
-
196
- # Error - Error events
197
- logger.error('API call failed', exc_info=True)
198
- ```
199
-
200
- ## Configuration
201
-
202
- ### Setting Up Logging
203
-
204
- ```python
205
- import logging
206
-
207
- # Configure logging
208
- logging.basicConfig(
209
- level=logging.INFO,
210
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
211
- )
212
-
213
- # Get logger
214
- logger = logging.getLogger(__name__)
215
- ```
216
-
217
- ### Environment-Specific Configuration
218
-
219
- ```python
220
- # Development
221
- logging.basicConfig(
222
- level=logging.DEBUG,
223
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
224
- )
225
-
226
- # Production
227
- logging.basicConfig(
228
- level=logging.WARNING,
229
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
230
- filename='api.log'
231
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/security_quickstart.md DELETED
@@ -1,130 +0,0 @@
1
- # Security Quick Reference
2
-
3
- ## Quick Start
4
-
5
- ```python
6
- from tools.security import key_manager
7
- from datetime import datetime, timedelta, timezone
8
-
9
- # 1. Generate secure API key
10
- import secrets
11
- import string
12
- api_key = ''.join(secrets.choice(string.ascii_letters + string.digits + '_.-') for _ in range(32))
13
-
14
- # 2. Add key with security settings
15
- key = key_manager.add_key(
16
- name="my_service",
17
- key=api_key,
18
- scopes=["read:data", "write:data"],
19
- expires_at=datetime.now(timezone.utc) + timedelta(days=90),
20
- rate_limit=10.0,
21
- burst_limit=20
22
- )
23
-
24
- # 3. Use key securely
25
- try:
26
- # Check rate limit
27
- await key_manager.check_rate_limit("my_service")
28
-
29
- # Use key
30
- key = key_manager.get_key("my_service")
31
- if key and key.is_valid:
32
- api_key = key.key.get_secret_value()
33
- # Use api_key for requests...
34
-
35
- except RateLimitError as e:
36
- # Handle rate limiting
37
- logger.warning(f"Rate limited: {e.wait_time}s")
38
- await asyncio.sleep(e.wait_time)
39
-
40
- except (InvalidKeyError, KeyExpiredError) as e:
41
- # Handle authentication errors
42
- logger.error(f"Authentication failed: {e}")
43
- raise HTTPError(401, "Authentication failed")
44
- ```
45
-
46
- ## Security Checklist
47
-
48
- ✅ **API Keys**
49
- - [ ] 32+ character length
50
- - [ ] URL-safe characters only
51
- - [ ] 90-day expiration
52
- - [ ] Scoped permissions
53
- - [ ] Rate limits configured
54
-
55
- ✅ **Environment**
56
- - [ ] `API_ENCRYPTION_KEY` set
57
- - [ ] `.env` in `.gitignore`
58
- - [ ] Secure file permissions
59
- - [ ] Different keys per environment
60
-
61
- ✅ **Error Handling**
62
- - [ ] Catch security exceptions
63
- - [ ] No sensitive data in errors
64
- - [ ] Proper error logging
65
- - [ ] Rate limit handling
66
-
67
- ✅ **Monitoring**
68
- - [ ] Security event logging
69
- - [ ] Rate limit monitoring
70
- - [ ] Failed auth tracking
71
- - [ ] Regular log review
72
-
73
- ## Common Security Errors
74
-
75
- ```python
76
- # ❌ WRONG - Insecure key generation
77
- api_key = "service123" # Too short, predictable
78
-
79
- # ✅ RIGHT - Secure key generation
80
- api_key = ''.join(secrets.choice(string.ascii_letters + string.digits + '_.-') for _ in range(32))
81
-
82
- # ❌ WRONG - No error handling
83
- key = key_manager.get_key("service")
84
- api_key = key.key.get_secret_value()
85
-
86
- # ✅ RIGHT - Proper error handling
87
- try:
88
- key = key_manager.get_key("service")
89
- if not key or not key.is_valid:
90
- raise InvalidKeyError("Invalid key")
91
- api_key = key.key.get_secret_value()
92
- except SecurityError as e:
93
- logger.error("Security error", error=str(e))
94
- raise
95
-
96
- # ❌ WRONG - Exposed sensitive data
97
- except InvalidKeyError as e:
98
- logger.error(f"Invalid key: {api_key}") # Don't log keys!
99
-
100
- # ✅ RIGHT - Secure error logging
101
- except InvalidKeyError as e:
102
- logger.error("Invalid key used", service="my_service")
103
-
104
- # ❌ WRONG - No rate limiting
105
- while True:
106
- make_api_request(key)
107
-
108
- # ✅ RIGHT - Rate limit handling
109
- try:
110
- wait_time = await key_manager.check_rate_limit("service")
111
- if wait_time > 0:
112
- await asyncio.sleep(wait_time)
113
- make_api_request(key)
114
- except RateLimitError as e:
115
- # Handle rate limiting
116
- await asyncio.sleep(e.wait_time)
117
- ```
118
-
119
- ## Security Contacts
120
-
121
- - Security Team: security@example.com
122
- - Emergency Contact: +1-XXX-XXX-XXXX
123
- - Bug Reports: https://example.com/security
124
-
125
- ## Additional Resources
126
-
127
- - [Full Security Documentation](../SECURITY.md)
128
- - [API Documentation](./api.md)
129
- - [Error Handling Guide](./errors.md)
130
- - [Monitoring Guide](./monitoring.md)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/user_guide.md DELETED
@@ -1,813 +0,0 @@
1
- # API Key Management System User Guide
2
-
3
- This guide provides comprehensive documentation for using the API key management system, including examples and best practices.
4
-
5
- ## Table of Contents
6
- - [Developer Setup](#developer-setup)
7
- - [Command Reference](#command-reference)
8
- - [Quick Start](#quick-start)
9
- - [Key Management](#key-management)
10
- - [Security Features](#security-features)
11
- - [Rate Limiting](#rate-limiting)
12
- - [Error Handling](#error-handling)
13
- - [Best Practices](#best-practices)
14
- - [Examples](#examples)
15
-
16
- ## Developer Setup
17
-
18
- ### Prerequisites
19
-
20
- - Python 3.8 or higher
21
- - pip (Python package installer)
22
- - virtualenv (recommended)
23
-
24
- ### Installation
25
-
26
- 1. **Create and activate a virtual environment:**
27
- ```bash
28
- python -m venv venv
29
- source venv/bin/activate # On Windows: venv\Scripts\activate
30
- ```
31
-
32
- 2. **Install required packages:**
33
- ```bash
34
- pip install -r requirements.txt
35
- ```
36
-
37
- The `requirements.txt` should include:
38
- ```
39
- pydantic>=2.0.0
40
- cryptography>=41.0.0
41
- aiohttp>=3.8.0
42
- tenacity>=8.0.0
43
- structlog>=23.0.0
44
- cachetools>=5.0.0
45
- ```
46
-
47
- 3. **Set up environment variables:**
48
- ```bash
49
- # Create a .env file
50
- touch .env
51
-
52
- # Add required environment variables
53
- echo "API_ENCRYPTION_KEY=$(python -c 'import base64; import os; print(base64.urlsafe_b64encode(os.urandom(32)).decode())')" >> .env
54
- ```
55
-
56
- ### Configuration
57
-
58
- 1. **Basic setup:**
59
- ```python
60
- from tools.security import KeyManager
61
-
62
- # Initialize with default settings
63
- key_manager = KeyManager()
64
- ```
65
-
66
- 2. **Custom configuration:**
67
- ```python
68
- key_manager = KeyManager(
69
- keys_file="custom_keys.json", # Custom storage location
70
- env_prefix="MYAPP", # Custom environment prefix
71
- encryption_key="your-secret-key" # Custom encryption key
72
- )
73
- ```
74
-
75
- ### Development Tools
76
-
77
- 1. **Install development dependencies:**
78
- ```bash
79
- pip install -r requirements-dev.txt
80
- ```
81
-
82
- The `requirements-dev.txt` should include:
83
- ```
84
- pytest>=7.0.0
85
- pytest-asyncio>=0.21.0
86
- pytest-cov>=4.0.0
87
- black>=23.0.0
88
- isort>=5.0.0
89
- mypy>=1.0.0
90
- ```
91
-
92
- 2. **Run tests:**
93
- ```bash
94
- # Run all tests
95
- pytest
96
-
97
- # Run with coverage
98
- pytest --cov=tools
99
-
100
- # Run specific test file
101
- pytest tests/test_security.py
102
- ```
103
-
104
- 3. **Code formatting:**
105
- ```bash
106
- # Format code
107
- black .
108
-
109
- # Sort imports
110
- isort .
111
- ```
112
-
113
- 4. **Type checking:**
114
- ```bash
115
- mypy tools/
116
- ```
117
-
118
- ### Project Structure
119
-
120
- ```
121
- .
122
- ├── tools/
123
- │ ├── __init__.py
124
- │ ├── security.py # Key management implementation
125
- │ ├── errors.py # Custom exceptions
126
- │ ├── validation.py # Input validation
127
- │ ├── api_optimization.py # Rate limiting and optimization
128
- │ └── monitoring.py # Performance monitoring
129
- ├── tests/
130
- │ ├── __init__.py
131
- │ ├── test_security.py
132
- │ ├── test_validation.py
133
- │ └── test_errors.py
134
- ├── docs/
135
- │ ├── api.md
136
- │ ├── security_quickstart.md
137
- │ ├── errors.md
138
- │ └── user_guide.md
139
- ├── requirements.txt
140
- ├── requirements-dev.txt
141
- └── README.md
142
- ```
143
-
144
- ### Debugging
145
-
146
- 1. **Enable debug logging:**
147
- ```python
148
- import logging
149
- logging.basicConfig(level=logging.DEBUG)
150
- ```
151
-
152
- 2. **Monitor rate limiting:**
153
- ```python
154
- async def monitor_rate_limits():
155
- metrics = await key_manager.get_metrics()
156
- print(f"Rate limit delays: {metrics['rate_limiting']['total_delays']}s")
157
- print(f"Rate limit count: {metrics['rate_limiting']['count']}")
158
- ```
159
-
160
- 3. **Test key validation:**
161
- ```python
162
- from tools.validation import validate_api_key
163
-
164
- try:
165
- validate_api_key("test-key")
166
- except ValidationError as e:
167
- print(f"Validation error: {e.message}")
168
- ```
169
-
170
- ### Common Issues
171
-
172
- 1. **Import Errors**
173
- - Ensure your PYTHONPATH includes the project root
174
- - Check that all dependencies are installed
175
- - Verify virtual environment is activated
176
-
177
- 2. **Encryption Errors**
178
- - Verify encryption key is properly formatted (base64)
179
- - Check environment variable is set correctly
180
- - Ensure key file permissions are correct
181
-
182
- 3. **Rate Limiting Issues**
183
- - Monitor rate limit metrics
184
- - Check burst limit configuration
185
- - Verify clock synchronization
186
-
187
- 4. **Performance Issues**
188
- - Enable performance monitoring
189
- - Check connection pool settings
190
- - Monitor batch processing metrics
191
-
192
- ### Contributing
193
-
194
- 1. **Fork and clone the repository**
195
- ```bash
196
- git clone https://github.com/yourusername/api-key-manager.git
197
- cd api-key-manager
198
- ```
199
-
200
- 2. **Create a feature branch**
201
- ```bash
202
- git checkout -b feature/your-feature-name
203
- ```
204
-
205
- 3. **Make changes and test**
206
- ```bash
207
- # Run tests
208
- pytest
209
-
210
- # Check code style
211
- black .
212
- isort .
213
- mypy tools/
214
-
215
- # Run full test suite with coverage
216
- pytest --cov=tools --cov-report=html
217
- ```
218
-
219
- 4. **Submit a pull request**
220
- - Include test coverage
221
- - Update documentation
222
- - Follow code style guidelines
223
-
224
- ## Command Reference
225
-
226
- ### Key Management Commands
227
-
228
- #### Initialize Key Manager
229
- ```python
230
- KeyManager(
231
- keys_file: str = "keys.json",
232
- env_prefix: str = "API",
233
- encryption_key: Optional[str] = None
234
- ) -> KeyManager
235
- ```
236
- Initialize a new key manager instance.
237
- - `keys_file`: Path to the key storage file (default: "keys.json")
238
- - `env_prefix`: Prefix for environment variables (default: "API")
239
- - `encryption_key`: Optional encryption key for storing keys
240
-
241
- #### Add Key
242
- ```python
243
- key_manager.add_key(
244
- name: str,
245
- key: str,
246
- scopes: Optional[List[str]] = None,
247
- expires_at: Optional[datetime] = None,
248
- rate_limit: float = 10.0,
249
- burst_limit: int = 20
250
- ) -> APIKeyConfig
251
- ```
252
- Add a new API key to the manager.
253
- - `name`: Unique identifier for the key
254
- - `key`: The API key value
255
- - `scopes`: List of access scopes
256
- - `expires_at`: Optional expiration date
257
- - `rate_limit`: Requests per second (default: 10.0)
258
- - `burst_limit`: Maximum burst size (default: 20)
259
-
260
- #### Get Key
261
- ```python
262
- key_manager.get_key(name: str) -> Optional[APIKeyConfig]
263
- ```
264
- Retrieve an API key by name.
265
- - `name`: Key identifier
266
- - Returns: Key configuration or None if not found
267
-
268
- #### Rotate Key
269
- ```python
270
- key_manager.rotate_key(
271
- name: str,
272
- new_key: str,
273
- expires_at: Optional[datetime] = None,
274
- rate_limit: Optional[float] = None,
275
- burst_limit: Optional[int] = None
276
- ) -> APIKeyConfig
277
- ```
278
- Rotate an existing API key.
279
- - `name`: Key identifier
280
- - `new_key`: New API key value
281
- - `expires_at`: Optional new expiration date
282
- - `rate_limit`: Optional new rate limit
283
- - `burst_limit`: Optional new burst limit
284
-
285
- #### Remove Key
286
- ```python
287
- key_manager.remove_key(name: str) -> None
288
- ```
289
- Remove an API key.
290
- - `name`: Key identifier to remove
291
-
292
- #### List Keys
293
- ```python
294
- key_manager.list_keys() -> Dict[str, APIKeyConfig]
295
- ```
296
- List all valid API keys.
297
- - Returns: Dictionary of key names to configurations
298
-
299
- ### Rate Limiting Commands
300
-
301
- #### Check Rate Limit
302
- ```python
303
- await key_manager.check_rate_limit(name: str) -> float
304
- ```
305
- Check if a key has exceeded its rate limit.
306
- - `name`: Key identifier
307
- - Returns: Wait time in seconds (0.0 if not rate limited)
308
-
309
- #### Rate Limiter Configuration
310
- ```python
311
- RateLimiter(
312
- rate: float,
313
- burst: int
314
- ) -> RateLimiter
315
- ```
316
- Create a new rate limiter instance.
317
- - `rate`: Tokens (requests) per second
318
- - `burst`: Maximum burst size
319
-
320
- #### Acquire Rate Limit Token
321
- ```python
322
- await rate_limiter.acquire(tokens: float = 1.0) -> float
323
- ```
324
- Acquire tokens from the rate limiter.
325
- - `tokens`: Number of tokens to acquire (default: 1.0)
326
- - Returns: Wait time in seconds
327
-
328
- ### Batch Processing Commands
329
-
330
- #### Initialize Batch Processor
331
- ```python
332
- BatchProcessor(
333
- batch_size: int = 100,
334
- flush_interval: float = 1.0
335
- ) -> BatchProcessor
336
- ```
337
- Create a new batch processor.
338
- - `batch_size`: Maximum items per batch
339
- - `flush_interval`: Time between automatic flushes
340
-
341
- #### Add to Batch
342
- ```python
343
- await batch_processor.add(item: Any) -> None
344
- ```
345
- Add an item to the batch.
346
- - `item`: Item to add to batch
347
-
348
- #### Flush Batch
349
- ```python
350
- await batch_processor.flush() -> None
351
- ```
352
- Process current batch immediately.
353
-
354
- ### Performance Monitoring Commands
355
-
356
- #### Initialize Monitor
357
- ```python
358
- PerformanceMonitor(
359
- metrics_ttl: int = 3600,
360
- max_metrics: int = 1000
361
- ) -> PerformanceMonitor
362
- ```
363
- Create a new performance monitor.
364
- - `metrics_ttl`: Time to live for metrics in seconds
365
- - `max_metrics`: Maximum number of metrics to store
366
-
367
- #### Start Request Monitoring
368
- ```python
369
- await monitor.start_request(endpoint: str) -> float
370
- ```
371
- Start timing a request.
372
- - `endpoint`: API endpoint being called
373
- - Returns: Start timestamp
374
-
375
- #### End Request Monitoring
376
- ```python
377
- await monitor.end_request(
378
- endpoint: str,
379
- start_time: float
380
- ) -> None
381
- ```
382
- Record the end of a request.
383
- - `endpoint`: API endpoint called
384
- - `start_time`: Start timestamp from start_request()
385
-
386
- #### Get Metrics
387
- ```python
388
- monitor.get_metrics() -> Dict[str, Any]
389
- ```
390
- Get comprehensive metrics report.
391
- - Returns: Dictionary of all collected metrics
392
-
393
- #### Log Metrics
394
- ```python
395
- await monitor.log_metrics(logger: Any) -> None
396
- ```
397
- Log current metrics using structlog.
398
- - `logger`: Structlog logger instance
399
-
400
- ### API Client Commands
401
-
402
- #### Initialize API Client
403
- ```python
404
- APIClient(
405
- base_url: str,
406
- pool_size: int = 10,
407
- rate_limit: float = 10.0,
408
- burst_limit: int = 20,
409
- batch_size: int = 100,
410
- **kwargs: Any
411
- ) -> APIClient
412
- ```
413
- Create optimized API client.
414
- - `base_url`: Base URL for API requests
415
- - `pool_size`: Connection pool size
416
- - `rate_limit`: Requests per second
417
- - `burst_limit`: Maximum burst size
418
- - `batch_size`: Default batch size
419
- - `**kwargs`: Additional client options
420
-
421
- #### Make Request
422
- ```python
423
- await client.request(
424
- method: str,
425
- endpoint: str,
426
- **kwargs: Any
427
- ) -> Any
428
- ```
429
- Make an API request with optimizations.
430
- - `method`: HTTP method
431
- - `endpoint`: API endpoint
432
- - `**kwargs`: Request parameters
433
- - Returns: JSON response
434
-
435
- #### Batch Request
436
- ```python
437
- await client.batch_request(
438
- method: str,
439
- endpoint: str,
440
- items: List[Dict[str, Any]]
441
- ) -> List[Any]
442
- ```
443
- Make batch API request.
444
- - `method`: HTTP method
445
- - `endpoint`: API endpoint
446
- - `items`: List of items to process
447
- - Returns: List of results
448
-
449
- ### Validation Commands
450
-
451
- #### Validate API Key
452
- ```python
453
- validate_api_key(key: str) -> str
454
- ```
455
- Validate API key format.
456
- - `key`: API key to validate
457
- - Returns: Validated key or raises ValidationError
458
-
459
- #### Validate Name
460
- ```python
461
- validate_name(name: str) -> str
462
- ```
463
- Validate key name format.
464
- - `name`: Name to validate
465
- - Returns: Validated name or raises ValidationError
466
-
467
- #### Validate Scopes
468
- ```python
469
- validate_scopes(scopes: List[str]) -> List[str]
470
- ```
471
- Validate scope formats.
472
- - `scopes`: List of scopes to validate
473
- - Returns: Validated scopes or raises ValidationError
474
-
475
- #### Validate Rate Limits
476
- ```python
477
- validate_rate_limits(
478
- rate_limit: float,
479
- burst_limit: int
480
- ) -> Tuple[float, int]
481
- ```
482
- Validate rate limiting parameters.
483
- - `rate_limit`: Requests per second
484
- - `burst_limit`: Maximum burst size
485
- - Returns: Tuple of validated limits or raises ValidationError
486
-
487
- ## Quick Start
488
-
489
- ```python
490
- from tools.security import KeyManager
491
-
492
- # Initialize the key manager
493
- key_manager = KeyManager(
494
- keys_file="keys.json",
495
- env_prefix="API"
496
- )
497
-
498
- # Add a new API key
499
- key_config = key_manager.add_key(
500
- name="service_name",
501
- key="api-key-123",
502
- scopes=["read:data", "write:data"],
503
- rate_limit=10.0, # requests per second
504
- burst_limit=20 # maximum burst size
505
- )
506
-
507
- # Use the key
508
- if key := key_manager.get_key("service_name"):
509
- print(f"Key is valid: {key.is_valid}")
510
- print(f"Scopes: {key.scopes}")
511
- ```
512
-
513
- ## Key Management
514
-
515
- ### Creating Keys
516
-
517
- Keys can be created with various parameters:
518
- ```python
519
- key_config = key_manager.add_key(
520
- name="analytics_service",
521
- key="api-key-xyz",
522
- scopes=["read:analytics", "export:data"],
523
- expires_at=datetime.now(timezone.utc) + timedelta(days=90),
524
- rate_limit=5.0,
525
- burst_limit=10
526
- )
527
- ```
528
-
529
- ### Rotating Keys
530
-
531
- For security, rotate keys periodically:
532
- ```python
533
- new_config = key_manager.rotate_key(
534
- name="analytics_service",
535
- new_key="new-api-key-xyz",
536
- expires_at=datetime.now(timezone.utc) + timedelta(days=90)
537
- )
538
- ```
539
-
540
- ### Environment Variables
541
-
542
- Keys can be loaded from environment variables:
543
- ```bash
544
- export API_SERVICE_NAME_KEY=api-key-123
545
- export API_SERVICE_NAME_RATE_LIMIT=10.0
546
- export API_SERVICE_NAME_BURST_LIMIT=20
547
- ```
548
-
549
- ```python
550
- key = key_manager.get_key("service_name") # Loads from environment
551
- ```
552
-
553
- ## Security Features
554
-
555
- ### Encryption
556
-
557
- Keys are automatically encrypted at rest:
558
- ```python
559
- # Keys are encrypted in storage
560
- key_manager = KeyManager(
561
- encryption_key="your-encryption-key", # Optional, can use env var
562
- keys_file="keys.json"
563
- )
564
- ```
565
-
566
- ### Validation
567
-
568
- All inputs are validated:
569
- ```python
570
- try:
571
- key_config = key_manager.add_key(
572
- name="service",
573
- key="invalid-key!", # Will raise ValidationError
574
- scopes=["invalid:scope"] # Will raise ValidationError
575
- )
576
- except ValidationError as e:
577
- print(f"Validation failed: {e.message}")
578
- ```
579
-
580
- ### Scopes
581
-
582
- Control access with scopes:
583
- ```python
584
- key_config = key_manager.add_key(
585
- name="limited_service",
586
- key="api-key-123",
587
- scopes=["read:only", "list:items"]
588
- )
589
- ```
590
-
591
- ## Rate Limiting
592
-
593
- ### Basic Rate Limiting
594
-
595
- ```python
596
- # Create key with rate limits
597
- key_config = key_manager.add_key(
598
- name="service",
599
- key="api-key-123",
600
- rate_limit=10.0, # 10 requests per second
601
- burst_limit=20 # Allow bursts up to 20 requests
602
- )
603
-
604
- # Check rate limit before request
605
- async def make_request():
606
- wait_time = await key_manager.check_rate_limit("service")
607
- if wait_time > 0:
608
- await asyncio.sleep(wait_time)
609
- # Make request...
610
- ```
611
-
612
- ### Burst Handling
613
-
614
- ```python
615
- # Allow higher burst rates for batch operations
616
- key_config = key_manager.add_key(
617
- name="batch_service",
618
- key="api-key-123",
619
- rate_limit=50.0, # 50 requests per second
620
- burst_limit=100 # Allow bursts up to 100 requests
621
- )
622
- ```
623
-
624
- ## Error Handling
625
-
626
- ### Common Errors
627
-
628
- ```python
629
- from tools.errors import (
630
- AuthenticationError,
631
- AuthorizationError,
632
- InvalidKeyError,
633
- KeyExpiredError,
634
- RateLimitError,
635
- ValidationError
636
- )
637
-
638
- try:
639
- key = key_manager.get_key("service")
640
- if not key:
641
- raise AuthenticationError("Invalid API key")
642
-
643
- if not key.is_valid:
644
- if key.is_expired:
645
- raise KeyExpiredError(f"Key expired on {key.expires_at}")
646
- raise InvalidKeyError("Key is inactive")
647
-
648
- wait_time = await key_manager.check_rate_limit("service")
649
- if wait_time > 0:
650
- raise RateLimitError(
651
- "Rate limit exceeded",
652
- wait_time=wait_time
653
- )
654
- except SecurityError as e:
655
- print(f"Security error: [{e.code}] {e.message}")
656
- ```
657
-
658
- ### Error Recovery
659
-
660
- ```python
661
- async def make_api_request():
662
- try:
663
- wait_time = await key_manager.check_rate_limit("service")
664
- if wait_time > 0:
665
- # Handle rate limiting gracefully
666
- await asyncio.sleep(wait_time)
667
- return await make_api_request() # Retry after waiting
668
-
669
- # Make request...
670
- except RateLimitError as e:
671
- logger.warning(f"Rate limited for {e.wait_time}s")
672
- await asyncio.sleep(e.wait_time)
673
- return await make_api_request() # Retry after waiting
674
- except KeyExpiredError:
675
- # Rotate to new key
676
- new_config = await key_manager.rotate_key(
677
- name="service",
678
- new_key=generate_new_key()
679
- )
680
- return await make_api_request() # Retry with new key
681
- ```
682
-
683
- ## Best Practices
684
-
685
- 1. **Key Rotation**
686
- - Rotate keys regularly (e.g., every 90 days)
687
- - Use expiration dates to enforce rotation
688
- - Keep old keys active briefly during rotation
689
-
690
- 2. **Rate Limiting**
691
- - Set appropriate rate limits for your use case
692
- - Use burst limits for handling traffic spikes
693
- - Implement graceful backoff when rate limited
694
-
695
- 3. **Error Handling**
696
- - Always catch and handle security errors
697
- - Log security events appropriately
698
- - Implement retry logic with backoff
699
-
700
- 4. **Validation**
701
- - Validate all inputs before use
702
- - Use appropriate scopes for access control
703
- - Regularly audit active keys
704
-
705
- 5. **Security**
706
- - Store encryption keys securely
707
- - Use environment variables for sensitive data
708
- - Monitor and log security events
709
-
710
- ## Examples
711
-
712
- ### Complete Service Example
713
-
714
- ```python
715
- from datetime import datetime, timezone, timedelta
716
- from tools.security import KeyManager
717
- from tools.errors import SecurityError
718
-
719
- class APIService:
720
- def __init__(self):
721
- self.key_manager = KeyManager(
722
- keys_file="keys.json",
723
- env_prefix="API"
724
- )
725
-
726
- async def initialize(self):
727
- # Create service key if not exists
728
- if not self.key_manager.get_key("service"):
729
- await self.create_service_key()
730
-
731
- async def create_service_key(self):
732
- return self.key_manager.add_key(
733
- name="service",
734
- key=generate_secure_key(),
735
- scopes=["read:data", "write:data"],
736
- expires_at=datetime.now(timezone.utc) + timedelta(days=90),
737
- rate_limit=10.0,
738
- burst_limit=20
739
- )
740
-
741
- async def make_request(self, endpoint: str, data: dict):
742
- try:
743
- # Get and validate key
744
- key = self.key_manager.get_key("service")
745
- if not key:
746
- raise AuthenticationError("Invalid API key")
747
-
748
- if not key.is_valid:
749
- if key.is_expired:
750
- # Rotate expired key
751
- key = await self.rotate_service_key()
752
- else:
753
- raise InvalidKeyError("Key is inactive")
754
-
755
- # Check rate limit
756
- wait_time = await self.key_manager.check_rate_limit("service")
757
- if wait_time > 0:
758
- await asyncio.sleep(wait_time)
759
-
760
- # Make request...
761
- return await self._do_request(endpoint, data, key)
762
-
763
- except RateLimitError as e:
764
- logger.warning(f"Rate limited for {e.wait_time}s")
765
- await asyncio.sleep(e.wait_time)
766
- return await self.make_request(endpoint, data)
767
-
768
- except KeyExpiredError:
769
- logger.info("Rotating expired key")
770
- await self.rotate_service_key()
771
- return await self.make_request(endpoint, data)
772
-
773
- except SecurityError as e:
774
- logger.error(f"Security error: [{e.code}] {e.message}")
775
- raise
776
-
777
- async def rotate_service_key(self):
778
- return self.key_manager.rotate_key(
779
- name="service",
780
- new_key=generate_secure_key(),
781
- expires_at=datetime.now(timezone.utc) + timedelta(days=90)
782
- )
783
- ```
784
-
785
- ### Batch Processing Example
786
-
787
- ```python
788
- async def process_batch(items: list):
789
- async with APIClient(
790
- base_url="https://api.example.com",
791
- rate_limit=50.0,
792
- burst_limit=100
793
- ) as client:
794
- try:
795
- results = await client.batch_request(
796
- method="POST",
797
- endpoint="/batch",
798
- items=items
799
- )
800
- return results
801
- except RateLimitError as e:
802
- logger.warning(f"Batch rate limited for {e.wait_time}s")
803
- # Split batch and retry with smaller chunks
804
- mid = len(items) // 2
805
- results1 = await process_batch(items[:mid])
806
- results2 = await process_batch(items[mid:])
807
- return results1 + results2
808
- ```
809
-
810
- For more detailed information, refer to:
811
- - [API Documentation](api.md)
812
- - [Security Guide](security_quickstart.md)
813
- - [Error Handling Guide](errors.md)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
instructions ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Let’s Create Our First Agent Using smolagents
2
+ In the last section, we learned how we can create Agents from scratch using Python code, and we saw just how tedious that process can be. Fortunately, many Agent libraries simplify this work by handling much of the heavy lifting for you.
3
+
4
+ In this tutorial, you’ll create your very first Agent capable of performing actions such as image generation, web search, time zone checking and much more!
5
+
6
+ You will also publish your agent on a Hugging Face Space so you can share it with friends and colleagues.
7
+
8
+ Let’s get started!
9
+
10
+ What is smolagents?
11
+ smolagents
12
+ To make this Agent, we’re going to use smolagents, a library that provides a framework for developing your agents with ease.
13
+
14
+ This lightweight library is designed for simplicity, but it abstracts away much of the complexity of building an Agent, allowing you to focus on designing your agent’s behavior.
15
+
16
+ We’re going to get deeper into smolagents in the next Unit. Meanwhile, you can also check this blog post or the library’s repo in GitHub.
17
+
18
+ In short, smolagents is a library that focuses on codeAgent, a kind of agent that performs “Actions” through code blocks, and then “Observes” results by executing the code.
19
+
20
+ Here is an example of what we’ll build!
21
+
22
+ We provided our agent with an Image generation tool and asked it to generate an image of a cat.
23
+
24
+ The agent inside smolagents is going to have the same behaviors as the custom one we built previously: it’s going to think, act and observe in cycle until it reaches a final answer:
25
+
26
+
27
+ Exciting, right?
28
+
29
+ Let’s build our Agent!
30
+ To start, duplicate this Space: https://huggingface.co/spaces/agents-course/First_agent_template
31
+
32
+ Thanks to Aymeric for this template! 🙌
33
+
34
+ Duplicating this space means creating a local copy on your own profile:
35
+
36
+ Duplicate
37
+ Throughout this lesson, the only file you will need to modify is the (currently incomplete) “app.py”. You can see here the original one in the template. To find yours, go to your copy of the space, then click the Files tab and then on app.py in the directory listing.
38
+
39
+ Let’s break down the code together:
40
+
41
+ The file begins with some simple but necessary library imports
42
+ Copied
43
+ from smolagents import CodeAgent, DuckDuckGoSearchTool, HfApiModel, load_tool, tool
44
+ import datetime
45
+ import requests
46
+ import pytz
47
+ import yaml
48
+ from tools.final_answer import FinalAnswerTool
49
+ As outlined earlier, we will directly use the CodeAgent class from smolagents.
50
+
51
+ The Tools
52
+ Now let’s get into the tools! If you want a refresher about tools, don’t hesitate to go back to the Tools section of the course.
53
+
54
+ Copied
55
+
56
+ def my_custom_tool(arg1:str, arg2:int)-> str: # it's important to specify the return type
57
+ # Keep this format for the tool description / args description but feel free to modify the tool
58
+ """A tool that does nothing yet
59
+ Args:
60
+ arg1: the first argument
61
+ arg2: the second argument
62
+ """
63
+ return "What magic will you build ?"
64
+
65
+
66
+ def get_current_time_in_timezone(timezone: str) -> str:
67
+ """A tool that fetches the current local time in a specified timezone.
68
+ Args:
69
+ timezone: A string representing a valid timezone (e.g., 'America/New_York').
70
+ """
71
+ try:
72
+ # Create timezone object
73
+ tz = pytz.timezone(timezone)
74
+ # Get current time in that timezone
75
+ local_time = datetime.datetime.now(tz).strftime("%Y-%m-%d %H:%M:%S")
76
+ return f"The current local time in {timezone} is: {local_time}"
77
+ except Exception as e:
78
+ return f"Error fetching time for timezone '{timezone}': {str(e)}"
79
+ The Tools are what we are encouraging you to build in this section! We give you two examples:
80
+
81
+ A non-working dummy Tool that you can modify to make something useful.
82
+ An actually working Tool that gets the current time somewhere in the world.
83
+ To define your tool it is important to:
84
+
85
+ Provide input and output types for your function, like in get_current_time_in_timezone(timezone: str) -> str:
86
+ A well formatted docstring. smolagents is expecting all the arguments to have a textual description in the docstring.
87
+ The Agent
88
+ It uses Qwen/Qwen2.5-Coder-32B-Instruct as the LLM engine. This is a very capable model that we’ll access via the serverless API.
89
+
90
+ Copied
91
+ final_answer = FinalAnswerTool()
92
+ model = HfApiModel(
93
+ max_tokens=2096,
94
+ temperature=0.5,
95
+ model_id='Qwen/Qwen2.5-Coder-32B-Instruct',
96
+ custom_role_conversions=None,
97
+ )
98
+
99
+ with open("prompts.yaml", 'r') as stream:
100
+ prompt_templates = yaml.safe_load(stream)
101
+
102
+ # We're creating our CodeAgent
103
+ agent = CodeAgent(
104
+ model=model,
105
+ tools=[final_answer], # add your tools here (don't remove final_answer)
106
+ max_steps=6,
107
+ verbosity_level=1,
108
+ grammar=None,
109
+ planning_interval=None,
110
+ name=None,
111
+ description=None,
112
+ prompt_templates=prompt_templates
113
+ )
114
+
115
+ GradioUI(agent).launch()
116
+ This Agent still uses the InferenceClient we saw in an earlier section behind the HfApiModel class!
117
+
118
+ We will give more in-depth examples when we present the framework in Unit 2. For now, you need to focus on adding new tools to the list of tools using the tools parameter of your Agent.
119
+
120
+ For example, you could use the DuckDuckGoSearchTool that was imported in the first line of the code, or you can examine the image_generation_tool that is loaded from the Hub later in the code.
121
+
122
+ Adding tools will give your agent new capabilities, try to be creative here!
123
+
124
+ The complete “app.py”:
125
+
126
+ Copied
127
+ from smolagents import CodeAgent, DuckDuckGoSearchTool, HfApiModel, load_tool, tool
128
+ import datetime
129
+ import requests
130
+ import pytz
131
+ import yaml
132
+ from tools.final_answer import FinalAnswerTool
133
+
134
+ from Gradio_UI import GradioUI
135
+
136
+ # Below is an example of a tool that does nothing. Amaze us with your creativity!
137
+
138
+ def my_custom_tool(arg1:str, arg2:int)-> str: # it's important to specify the return type
139
+ # Keep this format for the tool description / args description but feel free to modify the tool
140
+ """A tool that does nothing yet
141
+ Args:
142
+ arg1: the first argument
143
+ arg2: the second argument
144
+ """
145
+ return "What magic will you build ?"
146
+
147
+
148
+ def get_current_time_in_timezone(timezone: str) -> str:
149
+ """A tool that fetches the current local time in a specified timezone.
150
+ Args:
151
+ timezone: A string representing a valid timezone (e.g., 'America/New_York').
152
+ """
153
+ try:
154
+ # Create timezone object
155
+ tz = pytz.timezone(timezone)
156
+ # Get current time in that timezone
157
+ local_time = datetime.datetime.now(tz).strftime("%Y-%m-%d %H:%M:%S")
158
+ return f"The current local time in {timezone} is: {local_time}"
159
+ except Exception as e:
160
+ return f"Error fetching time for timezone '{timezone}': {str(e)}"
161
+
162
+
163
+ final_answer = FinalAnswerTool()
164
+ model = HfApiModel(
165
+ max_tokens=2096,
166
+ temperature=0.5,
167
+ model_id='Qwen/Qwen2.5-Coder-32B-Instruct',
168
+ custom_role_conversions=None,
169
+ )
170
+
171
+
172
+ # Import tool from Hub
173
+ image_generation_tool = load_tool("agents-course/text-to-image", trust_remote_code=True)
174
+
175
+ with open("prompts.yaml", 'r') as stream:
176
+ prompt_templates = yaml.safe_load(stream)
177
+
178
+ agent = CodeAgent(
179
+ model=model,
180
+ tools=[final_answer], # add your tools here (don't remove final_answer)
181
+ max_steps=6,
182
+ verbosity_level=1,
183
+ grammar=None,
184
+ planning_interval=None,
185
+ name=None,
186
+ description=None,
187
+ prompt_templates=prompt_templates
188
+ )
189
+
190
+
191
+ GradioUI(agent).launch()
192
+ Your Goal is to get familiar with the Space and the Agent.
193
+
194
+ Currently, the agent in the template does not use any tools, so try to provide it with some of the pre-made ones or even make some new tools yourself!
195
+
196
+ We are eagerly waiting for your amazing agents output in the discord channel #agents-course-showcase!
197
+
198
+ Congratulations, you’ve built your first Agent! Don’t hesitate to share it with your friends and colleagues.
199
+
200
+ Since this is your first try, it’s perfectly normal if it’s a little buggy or slow. In future units, we’ll learn how to build even better Agents.
201
+
202
+ The best way to learn is to try, so don’t hesitate to update it, add more tools, try with another model, etc.
prompts.yaml CHANGED
@@ -1,336 +1,27 @@
1
- "system_prompt": |-
2
- You are an expert assistant who can solve any task using code blobs. You will be given a task to solve as best you can.
3
- To do so, you have been given access to a list of tools: these tools are basically Python functions which you can call with code.
4
- To solve the task, you must plan forward to proceed in a series of steps, in a cycle of 'Thought:', 'Code:', and 'Observation:' sequences.
5
-
6
- At each step, in the 'Thought:' sequence, you should first explain your reasoning towards solving the task and the tools that you want to use.
7
- Then in the 'Code:' sequence, you should write the code in simple Python. The code sequence must end with '<end_code>' sequence.
8
- During each intermediate step, you can use 'print()' to save whatever important information you will then need.
9
- These print outputs will then appear in the 'Observation:' field, which will be available as input for the next step.
10
- In the end you have to return a final answer using the `final_answer` tool.
11
-
12
- Here are a few examples using notional tools:
13
- ---
14
- Task: "Generate an image of the oldest person in this document."
15
-
16
- Thought: I will proceed step by step and use the following tools: `document_qa` to find the oldest person in the document, then `image_generator` to generate an image according to the answer.
17
- Code:
18
- ```py
19
- answer = document_qa(document=document, question="Who is the oldest person mentioned?")
20
- print(answer)
21
- ```<end_code>
22
- Observation: "The oldest person in the document is John Doe, a 55 year old lumberjack living in Newfoundland."
23
-
24
- Thought: I will now generate an image showcasing the oldest person.
25
- Code:
26
- ```py
27
- image = image_generator("A portrait of John Doe, a 55-year-old man living in Canada.")
28
- final_answer(image)
29
- ```<end_code>
30
-
31
- ---
32
- Task: "What is the result of the following operation: 5 + 3 + 1294.678?"
33
-
34
- Thought: I will use python code to compute the result of the operation and then return the final answer using the `final_answer` tool
35
- Code:
36
- ```py
37
- result = 5 + 3 + 1294.678
38
- final_answer(result)
39
- ```<end_code>
40
-
41
- ---
42
- Task:
43
- "Answer the question in the variable `question` about the image stored in the variable `image`. The question is in French.
44
- You have been provided with these additional arguments, that you can access using the keys as variables in your python code:
45
- {'question': 'Quel est l'animal sur l'image?', 'image': 'path/to/image.jpg'}"
46
-
47
- Thought: I will use the following tools: `translator` to translate the question into English and then `image_qa` to answer the question on the input image.
48
- Code:
49
- ```py
50
- translated_question = translator(question=question, src_lang="French", tgt_lang="English")
51
- print(f"The translated question is {translated_question}.")
52
- answer = image_qa(image=image, question=translated_question)
53
- final_answer(f"The answer is {answer}")
54
- ```<end_code>
55
-
56
- ---
57
- Task:
58
- In a 1979 interview, Stanislaus Ulam discusses with Martin Sherwin about other great physicists of his time, including Oppenheimer.
59
- What does he say was the consequence of Einstein learning too much math on his creativity, in one word?
60
-
61
- Thought: I need to find and read the 1979 interview of Stanislaus Ulam with Martin Sherwin.
62
- Code:
63
- ```py
64
- pages = search(query="1979 interview Stanislaus Ulam Martin Sherwin physicists Einstein")
65
- print(pages)
66
- ```<end_code>
67
- Observation:
68
- No result found for query "1979 interview Stanislaus Ulam Martin Sherwin physicists Einstein".
69
-
70
- Thought: The query was maybe too restrictive and did not find any results. Let's try again with a broader query.
71
- Code:
72
- ```py
73
- pages = search(query="1979 interview Stanislaus Ulam")
74
- print(pages)
75
- ```<end_code>
76
- Observation:
77
- Found 6 pages:
78
- [Stanislaus Ulam 1979 interview](https://ahf.nuclearmuseum.org/voices/oral-histories/stanislaus-ulams-interview-1979/)
79
-
80
- [Ulam discusses Manhattan Project](https://ahf.nuclearmuseum.org/manhattan-project/ulam-manhattan-project/)
81
-
82
- (truncated)
83
-
84
- Thought: I will read the first 2 pages to know more.
85
- Code:
86
- ```py
87
- for url in ["https://ahf.nuclearmuseum.org/voices/oral-histories/stanislaus-ulams-interview-1979/", "https://ahf.nuclearmuseum.org/manhattan-project/ulam-manhattan-project/"]:
88
- whole_page = visit_webpage(url)
89
- print(whole_page)
90
- print("\n" + "="*80 + "\n") # Print separator between pages
91
- ```<end_code>
92
- Observation:
93
- Manhattan Project Locations:
94
- Los Alamos, NM
95
- Stanislaus Ulam was a Polish-American mathematician. He worked on the Manhattan Project at Los Alamos and later helped design the hydrogen bomb. In this interview, he discusses his work at
96
- (truncated)
97
-
98
- Thought: I now have the final answer: from the webpages visited, Stanislaus Ulam says of Einstein: "He learned too much mathematics and sort of diminished, it seems to me personally, it seems to me his purely physics creativity." Let's answer in one word.
99
- Code:
100
- ```py
101
- final_answer("diminished")
102
- ```<end_code>
103
-
104
- ---
105
- Task: "Which city has the highest population: Guangzhou or Shanghai?"
106
-
107
- Thought: I need to get the populations for both cities and compare them: I will use the tool `search` to get the population of both cities.
108
- Code:
109
- ```py
110
- for city in ["Guangzhou", "Shanghai"]:
111
- print(f"Population {city}:", search(f"{city} population")
112
- ```<end_code>
113
- Observation:
114
- Population Guangzhou: ['Guangzhou has a population of 15 million inhabitants as of 2021.']
115
- Population Shanghai: '26 million (2019)'
116
-
117
- Thought: Now I know that Shanghai has the highest population.
118
- Code:
119
- ```py
120
- final_answer("Shanghai")
121
- ```<end_code>
122
-
123
- ---
124
- Task: "What is the current age of the pope, raised to the power 0.36?"
125
-
126
- Thought: I will use the tool `wiki` to get the age of the pope, and confirm that with a web search.
127
- Code:
128
- ```py
129
- pope_age_wiki = wiki(query="current pope age")
130
- print("Pope age as per wikipedia:", pope_age_wiki)
131
- pope_age_search = web_search(query="current pope age")
132
- print("Pope age as per google search:", pope_age_search)
133
- ```<end_code>
134
- Observation:
135
- Pope age: "The pope Francis is currently 88 years old."
136
-
137
- Thought: I know that the pope is 88 years old. Let's compute the result using python code.
138
- Code:
139
- ```py
140
- pope_current_age = 88 ** 0.36
141
- final_answer(pope_current_age)
142
- ```<end_code>
143
-
144
- Above example were using notional tools that might not exist for you. On top of performing computations in the Python code snippets that you create, you only have access to these tools:
145
- {%- for tool in tools.values() %}
146
- - {{ tool.name }}: {{ tool.description }}
147
- Takes inputs: {{tool.inputs}}
148
- Returns an output of type: {{tool.output_type}}
149
- {%- endfor %}
150
-
151
- {%- if managed_agents and managed_agents.values() | list %}
152
- You can also give tasks to team members.
153
- Calling a team member works the same as for calling a tool: simply, the only argument you can give in the call is 'task', a long string explaining your task.
154
- Given that this team member is a real human, you should be very verbose in your task.
155
- Here is a list of the team members that you can call:
156
- {%- for agent in managed_agents.values() %}
157
- - {{ agent.name }}: {{ agent.description }}
158
- {%- endfor %}
159
- {%- else %}
160
- {%- endif %}
161
-
162
- Here are the rules you should always follow to solve your task:
163
- 1. Always provide a 'Thought:' sequence, and a 'Code:\n```py' sequence ending with '```<end_code>' sequence, else you will fail.
164
- 2. Use only variables that you have defined!
165
- 3. Always use the right arguments for the tools. DO NOT pass the arguments as a dict as in 'answer = wiki({'query': "What is the place where James Bond lives?"})', but use the arguments directly as in 'answer = wiki(query="What is the place where James Bond lives?")'.
166
- 4. Take care to not chain too many sequential tool calls in the same code block, especially when the output format is unpredictable. For instance, a call to search has an unpredictable return format, so do not have another tool call that depends on its output in the same block: rather output results with print() to use them in the next block.
167
- 5. Call a tool only when needed, and never re-do a tool call that you previously did with the exact same parameters.
168
- 6. Don't name any new variable with the same name as a tool: for instance don't name a variable 'final_answer'.
169
- 7. Never create any notional variables in our code, as having these in your logs will derail you from the true variables.
170
- 8. You can use imports in your code, but only from the following list of modules: {{authorized_imports}}
171
- 9. The state persists between code executions: so if in one step you've created variables or imported modules, these will all persist.
172
- 10. Don't give up! You're in charge of solving the task, not providing directions to solve it.
173
-
174
- Now Begin! If you solve the task correctly, you will receive a reward of $1,000,000.
175
- "planning":
176
- "initial_facts": |-
177
- Below I will present you a task.
178
-
179
- You will now build a comprehensive preparatory survey of which facts we have at our disposal and which ones we still need.
180
- To do so, you will have to read the task and identify things that must be discovered in order to successfully complete it.
181
- Don't make any assumptions. For each item, provide a thorough reasoning. Here is how you will structure this survey:
182
-
183
- ---
184
- ### 1. Facts given in the task
185
- List here the specific facts given in the task that could help you (there might be nothing here).
186
-
187
- ### 2. Facts to look up
188
- List here any facts that we may need to look up.
189
- Also list where to find each of these, for instance a website, a file... - maybe the task contains some sources that you should re-use here.
190
-
191
- ### 3. Facts to derive
192
- List here anything that we want to derive from the above by logical reasoning, for instance computation or simulation.
193
-
194
- Keep in mind that "facts" will typically be specific names, dates, values, etc. Your answer should use the below headings:
195
- ### 1. Facts given in the task
196
- ### 2. Facts to look up
197
- ### 3. Facts to derive
198
- Do not add anything else.
199
- "initial_plan": |-
200
- You are a world expert at making efficient plans to solve any task using a set of carefully crafted tools.
201
-
202
- Now for the given task, develop a step-by-step high-level plan taking into account the above inputs and list of facts.
203
- This plan should involve individual tasks based on the available tools, that if executed correctly will yield the correct answer.
204
- Do not skip steps, do not add any superfluous steps. Only write the high-level plan, DO NOT DETAIL INDIVIDUAL TOOL CALLS.
205
- After writing the final step of the plan, write the '\n<end_plan>' tag and stop there.
206
-
207
- Here is your task:
208
-
209
- Task:
210
- ```
211
- {{task}}
212
- ```
213
- You can leverage these tools:
214
- {%- for tool in tools.values() %}
215
- - {{ tool.name }}: {{ tool.description }}
216
- Takes inputs: {{tool.inputs}}
217
- Returns an output of type: {{tool.output_type}}
218
- {%- endfor %}
219
-
220
- {%- if managed_agents and managed_agents.values() | list %}
221
- You can also give tasks to team members.
222
- Calling a team member works the same as for calling a tool: simply, the only argument you can give in the call is 'request', a long string explaining your request.
223
- Given that this team member is a real human, you should be very verbose in your request.
224
- Here is a list of the team members that you can call:
225
- {%- for agent in managed_agents.values() %}
226
- - {{ agent.name }}: {{ agent.description }}
227
- {%- endfor %}
228
- {%- else %}
229
- {%- endif %}
230
-
231
- List of facts that you know:
232
- ```
233
- {{answer_facts}}
234
- ```
235
-
236
- Now begin! Write your plan below.
237
- "update_facts_pre_messages": |-
238
- You are a world expert at gathering known and unknown facts based on a conversation.
239
- Below you will find a task, and a history of attempts made to solve the task. You will have to produce a list of these:
240
- ### 1. Facts given in the task
241
- ### 2. Facts that we have learned
242
- ### 3. Facts still to look up
243
- ### 4. Facts still to derive
244
- Find the task and history below:
245
- "update_facts_post_messages": |-
246
- Earlier we've built a list of facts.
247
- But since in your previous steps you may have learned useful new facts or invalidated some false ones.
248
- Please update your list of facts based on the previous history, and provide these headings:
249
- ### 1. Facts given in the task
250
- ### 2. Facts that we have learned
251
- ### 3. Facts still to look up
252
- ### 4. Facts still to derive
253
-
254
- Now write your new list of facts below.
255
- "update_plan_pre_messages": |-
256
- You are a world expert at making efficient plans to solve any task using a set of carefully crafted tools.
257
-
258
- You have been given a task:
259
- ```
260
- {{task}}
261
- ```
262
-
263
- Find below the record of what has been tried so far to solve it. Then you will be asked to make an updated plan to solve the task.
264
- If the previous tries so far have met some success, you can make an updated plan based on these actions.
265
- If you are stalled, you can make a completely new plan starting from scratch.
266
- "update_plan_post_messages": |-
267
- You're still working towards solving this task:
268
- ```
269
- {{task}}
270
- ```
271
-
272
- You can leverage these tools:
273
- {%- for tool in tools.values() %}
274
- - {{ tool.name }}: {{ tool.description }}
275
- Takes inputs: {{tool.inputs}}
276
- Returns an output of type: {{tool.output_type}}
277
- {%- endfor %}
278
-
279
- {%- if managed_agents and managed_agents.values() | list %}
280
- You can also give tasks to team members.
281
- Calling a team member works the same as for calling a tool: simply, the only argument you can give in the call is 'task'.
282
- Given that this team member is a real human, you should be very verbose in your task, it should be a long string providing informations as detailed as necessary.
283
- Here is a list of the team members that you can call:
284
- {%- for agent in managed_agents.values() %}
285
- - {{ agent.name }}: {{ agent.description }}
286
- {%- endfor %}
287
- {%- else %}
288
- {%- endif %}
289
-
290
- Here is the up to date list of facts that you know:
291
- ```
292
- {{facts_update}}
293
- ```
294
-
295
- Now for the given task, develop a step-by-step high-level plan taking into account the above inputs and list of facts.
296
- This plan should involve individual tasks based on the available tools, that if executed correctly will yield the correct answer.
297
- Beware that you have {remaining_steps} steps remaining.
298
- Do not skip steps, do not add any superfluous steps. Only write the high-level plan, DO NOT DETAIL INDIVIDUAL TOOL CALLS.
299
- After writing the final step of the plan, write the '\n<end_plan>' tag and stop there.
300
-
301
- Now write your new plan below.
302
- "managed_agent":
303
- "task": |-
304
- You're a helpful agent named '{{name}}'.
305
- You have been submitted this task by your manager.
306
- ---
307
- Task:
308
- {{task}}
309
- ---
310
- You're helping your manager solve a wider task: so make sure to not provide a one-line answer, but give as much information as possible to give them a clear understanding of the answer.
311
 
312
- Your final_answer WILL HAVE to contain these parts:
313
- ### 1. Task outcome (short version):
314
- ### 2. Task outcome (extremely detailed version):
315
- ### 3. Additional context (if relevant):
 
316
 
317
- Put all these in your final_answer tool, everything that you do not pass as an argument to final_answer will be lost.
318
- And even if your task resolution is not successful, please return as much context as possible, so that your manager can act upon this feedback.
319
- "report": |-
320
- Here is the final answer from your managed agent '{{name}}':
321
- {{final_answer}}
322
 
323
- system: |
324
- You are an AI assistant that helps users with various tasks.
325
- You have access to several tools including weather information, timezone conversion, and more.
326
- Always be helpful, professional, and provide clear explanations in your responses.
327
 
328
  user: |
329
- {input}
330
 
331
- assistant: |
332
- I'll help you with your request: {output}
 
333
 
334
- error: |
335
- I apologize, but I encountered an error: {error}
336
- Please try again or rephrase your request.
 
1
+ system: |
2
+ You are a helpful AI assistant that can use tools to solve problems.
3
+ You have access to the following tools:
4
+ {{tools}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
+ When you want to use a tool, output a code block with the tool name and arguments.
7
+ For example:
8
+ ```python
9
+ get_current_time_in_timezone(timezone="America/New_York")
10
+ ```
11
 
12
+ After using a tool, you'll receive the result. Analyze the result and decide if you need to use more tools or if you can provide a final answer.
13
+ When you have a final answer, use the final_answer tool.
 
 
 
14
 
15
+ Remember to:
16
+ 1. Think step by step
17
+ 2. Use tools when appropriate
18
+ 3. Provide helpful, accurate, and concise responses
19
 
20
  user: |
21
+ {{query}}
22
 
23
+ tool_response: |
24
+ The result of your tool call was:
25
+ {{observation}}
26
 
27
+ What would you like to do next?
 
 
pyproject.toml DELETED
@@ -1,107 +0,0 @@
1
- [tool.black]
2
- line-length = 100
3
- target-version = ['py39']
4
- include = '\.pyi?$'
5
- extend-exclude = '''
6
- # A regex preceded with ^/ will apply only to files and directories
7
- # in the root of the project.
8
- ^/tests/data/
9
- '''
10
-
11
- [tool.isort]
12
- profile = "black"
13
- multi_line_output = 3
14
- include_trailing_comma = true
15
- force_grid_wrap = 0
16
- use_parentheses = true
17
- ensure_newline_before_comments = true
18
- line_length = 100
19
- skip_gitignore = true
20
- skip_glob = ["tests/data/*"]
21
-
22
- [tool.pylint.messages_control]
23
- disable = [
24
- "C0111", # missing-docstring
25
- "C0103", # invalid-name
26
- "C0330", # bad-continuation
27
- "C0326", # bad-whitespace
28
- "W0621", # redefined-outer-name
29
- "W0614", # unused-wildcard-import
30
- "R0903", # too-few-public-methods
31
- "R0913", # too-many-arguments
32
- "R0914", # too-many-locals
33
- ]
34
-
35
- [tool.pylint.format]
36
- max-line-length = 100
37
-
38
- [tool.pylint.basic]
39
- good-names = ["i", "j", "k", "ex", "Run", "_", "fp", "id"]
40
-
41
- [tool.pylint.design]
42
- max-args = 8
43
- max-attributes = 12
44
- max-bool-expr = 5
45
- max-branches = 12
46
- max-locals = 20
47
- max-parents = 7
48
- max-public-methods = 20
49
- max-returns = 6
50
- max-statements = 50
51
- min-public-methods = 1
52
-
53
- [tool.mypy]
54
- python_version = "3.9"
55
- warn_return_any = true
56
- warn_unused_configs = true
57
- disallow_untyped_defs = true
58
- disallow_incomplete_defs = true
59
- check_untyped_defs = true
60
- disallow_untyped_decorators = false
61
- no_implicit_optional = true
62
- warn_redundant_casts = true
63
- warn_unused_ignores = true
64
- warn_no_return = true
65
- warn_unreachable = true
66
- strict_equality = true
67
-
68
- [[tool.mypy.overrides]]
69
- module = "tests.*"
70
- disallow_untyped_defs = false
71
- disallow_incomplete_defs = false
72
-
73
- [tool.pytest.ini_options]
74
- minversion = "6.0"
75
- addopts = "-ra -q --cov=tools --cov-report=term-missing"
76
- testpaths = ["tests"]
77
- python_files = ["test_*.py"]
78
- python_classes = ["Test*"]
79
- python_functions = ["test_*"]
80
- markers = [
81
- "slow: marks tests as slow (deselect with '-m \"not slow\"')",
82
- "integration: marks tests as integration tests",
83
- ]
84
-
85
- [tool.bandit]
86
- exclude_dirs = ["tests", "venv", ".env", ".venv"]
87
- skips = ["B101", "B404", "B603"]
88
- targets = ["tools"]
89
-
90
- [tool.bandit.assert_used]
91
- skips = ["*_test.py", "*/test_*.py"]
92
-
93
- # Security settings
94
- [tool.bandit.any_other_function_with_shell_equals_true]
95
- no_shell = [
96
- "subprocess.Popen",
97
- "subprocess.run",
98
- "subprocess.call",
99
- "subprocess.check_call",
100
- "subprocess.check_output",
101
- ]
102
-
103
- [tool.bandit.hardcoded_tmp_directory]
104
- tmp_dirs = ["/tmp", "/var/tmp", "/dev/shm"]
105
-
106
- [tool.bandit.hardcoded_password_string]
107
- possible_strings = ["password", "pass", "pwd", "secret", "token"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pyrightconfig.json DELETED
@@ -1,32 +0,0 @@
1
- {
2
- "include": [
3
- "tools",
4
- "tests"
5
- ],
6
- "exclude": [
7
- "**/node_modules",
8
- "**/__pycache__",
9
- "build",
10
- "dist"
11
- ],
12
- "ignore": [
13
- "**/migrations"
14
- ],
15
- "defineConstant": {
16
- "DEBUG": true
17
- },
18
- "reportMissingImports": true,
19
- "reportMissingTypeStubs": true,
20
- "reportUnknownMemberType": true,
21
- "reportUnknownParameterType": true,
22
- "reportUnknownVariableType": true,
23
- "reportUnnecessaryTypeIgnoreComment": true,
24
- "pythonVersion": "3.9",
25
- "typeCheckingMode": "strict",
26
- "useLibraryCodeForTypes": true,
27
- "strictListInference": true,
28
- "strictDictionaryInference": true,
29
- "strictSetInference": true,
30
- "strictParameterNoneValue": true,
31
- "enableTypeIgnoreComments": true
32
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pytest.ini DELETED
@@ -1,45 +0,0 @@
1
- [pytest]
2
- testpaths = tests
3
- python_files = test_*.py
4
- python_classes = Test*
5
- python_functions = test_*
6
-
7
- # Coverage settings
8
- addopts =
9
- --cov=tools
10
- --cov-report=term-missing
11
- --cov-report=html
12
- --cov-report=xml
13
- --cov-branch
14
- --no-cov-on-fail
15
-
16
- # Coverage configuration
17
- [coverage:run]
18
- branch = True
19
- source = tools
20
- omit =
21
- */__init__.py
22
- */tests/*
23
- */venv/*
24
- setup.py
25
-
26
- [coverage:report]
27
- exclude_lines =
28
- pragma: no cover
29
- def __repr__
30
- raise NotImplementedError
31
- if __name__ == .__main__.:
32
- pass
33
- raise ImportError
34
- except ImportError:
35
- if TYPE_CHECKING:
36
-
37
- show_missing = True
38
- skip_covered = False
39
- fail_under = 90
40
-
41
- [coverage:html]
42
- directory = coverage_html
43
-
44
- [coverage:xml]
45
- output = coverage.xml
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
rate_limiter.py DELETED
@@ -1,32 +0,0 @@
1
- """
2
- Rate limiter implementation using token bucket algorithm.
3
- """
4
- import time
5
- from collections import defaultdict
6
- from typing import Dict, Tuple
7
- import logging
8
-
9
- logger = logging.getLogger(__name__)
10
-
11
- class RateLimiter:
12
- def __init__(self, requests_per_minute: int):
13
- self.requests_per_minute = requests_per_minute
14
- self.tokens = requests_per_minute
15
- self.last_update = time.time()
16
-
17
- def _add_tokens(self) -> None:
18
- now = time.time()
19
- time_passed = now - self.last_update
20
- new_tokens = time_passed * (self.requests_per_minute / 60.0)
21
- self.tokens = min(self.requests_per_minute, self.tokens + new_tokens)
22
- self.last_update = now
23
-
24
- def acquire(self) -> bool:
25
- self._add_tokens()
26
- if self.tokens >= 1:
27
- self.tokens -= 1
28
- return True
29
- return False
30
-
31
- # Global rate limiters
32
- rate_limiters: Dict[str, RateLimiter] = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements-dev.txt DELETED
The diff for this file is too large to render. See raw diff
 
requirements.txt CHANGED
@@ -1,48 +1,6 @@
1
- # Core dependencies
2
- requests>=2.31.0,<3.0.0
3
- python-dateutil>=2.8.2,<3.0.0
4
- pytz>=2024.1
5
- typing-extensions>=4.8.0
6
- pydantic>=2.0.0
7
- smolagents>=0.1.0 # For agent functionality
8
-
9
- # API and HTTP
10
- aiohttp>=3.8.0
11
- urllib3>=2.2.0,<3.0.0 # Required by requests
12
- certifi>=2024.2.2 # For SSL certificates
13
-
14
- # Logging and monitoring
15
- structlog>=23.0.0,<25.0.0 # For structured logging
16
- python-json-logger>=2.0.7,<3.0.0 # JSON log formatting
17
-
18
- # Security
19
- python-jose[cryptography]>=3.3.0
20
- passlib[bcrypt]>=1.7.4
21
- python-dotenv>=0.19.0
22
-
23
- # UI
24
- gradio>=4.16.0,<5.0.0 # For web interface
25
- markdown>=3.5.2,<4.0.0 # For markdown rendering
26
-
27
- # Caching
28
- cachetools>=5.0.0
29
-
30
- # Error handling
31
- backoff>=2.2.1
32
- tenacity>=8.0.0
33
-
34
- # System monitoring
35
- psutil>=5.9.0,<6.0.0 # System resource monitoring
36
-
37
- # For testing
38
- pytest>=7.0.0
39
- pytest-asyncio>=0.21.0
40
- aioresponses>=0.7.5
41
- pytest-aiohttp>=1.0.5
42
-
43
- # Additional dependencies
44
- fastapi>=0.68.0
45
- uvicorn>=0.15.0
46
- python-multipart>=0.0.5
47
- httpx>=0.24.0
48
- cryptography>=41.0.0
 
1
+ smolagents>=0.0.5
2
+ gradio>=4.0.0
3
+ pyyaml>=6.0
4
+ pytz>=2023.3
5
+ requests>=2.31.0
6
+ huggingface_hub>=0.19.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/run_tests.py DELETED
@@ -1,96 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Script to run tests with coverage reporting.
4
- """
5
-
6
- import os
7
- import sys
8
- import subprocess
9
- from typing import List, Optional
10
-
11
- def run_tests(args: Optional[List[str]] = None) -> int:
12
- """
13
- Run tests with coverage reporting.
14
-
15
- Args:
16
- args: Additional pytest arguments.
17
-
18
- Returns:
19
- Exit code from pytest.
20
- """
21
- # Ensure we're in the project root directory
22
- project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
23
- os.chdir(project_root)
24
-
25
- # Base pytest command with coverage
26
- cmd = [
27
- "pytest",
28
- "--verbose",
29
- "--cov=tools",
30
- "--cov-report=term-missing",
31
- "--cov-report=html",
32
- "--cov-report=xml",
33
- "--cov-branch",
34
- ]
35
-
36
- # Add any additional arguments
37
- if args:
38
- cmd.extend(args)
39
-
40
- # Run tests
41
- try:
42
- result = subprocess.run(cmd, check=True)
43
- return result.returncode
44
- except subprocess.CalledProcessError as e:
45
- print(f"Error running tests: {e}", file=sys.stderr)
46
- return e.returncode
47
-
48
- def print_coverage_report() -> None:
49
- """Print a summary of the coverage report."""
50
- try:
51
- # Read the coverage XML report
52
- import xml.etree.ElementTree as ET
53
- tree = ET.parse('coverage.xml')
54
- root = tree.getroot()
55
-
56
- # Get overall coverage metrics
57
- metrics = root.find('.//metrics')
58
- if metrics is not None:
59
- statements = int(metrics.get('statements', 0))
60
- covered = int(metrics.get('covered', 0))
61
- branches = int(metrics.get('branches', 0))
62
- branches_covered = int(metrics.get('branches-covered', 0))
63
-
64
- # Calculate percentages
65
- line_coverage = (covered / statements * 100) if statements else 0
66
- branch_coverage = (branches_covered / branches * 100) if branches else 0
67
-
68
- print("\nCoverage Summary:")
69
- print(f"Line Coverage: {line_coverage:.1f}%")
70
- print(f"Branch Coverage: {branch_coverage:.1f}%")
71
- print(f"Statements: {covered}/{statements}")
72
- print(f"Branches: {branches_covered}/{branches}")
73
-
74
- # Print location of detailed reports
75
- print("\nDetailed reports generated:")
76
- print("- HTML report: coverage_html/index.html")
77
- print("- XML report: coverage.xml")
78
- except Exception as e:
79
- print(f"Error reading coverage report: {e}", file=sys.stderr)
80
-
81
- def main() -> int:
82
- """Main entry point."""
83
- # Get any additional arguments
84
- args = sys.argv[1:] if len(sys.argv) > 1 else None
85
-
86
- # Run tests
87
- result = run_tests(args)
88
-
89
- # Print coverage summary if tests passed
90
- if result == 0:
91
- print_coverage_report()
92
-
93
- return result
94
-
95
- if __name__ == "__main__":
96
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
setup.py DELETED
@@ -1,148 +0,0 @@
1
- """
2
- Setup configuration for the AI Assistant package.
3
- """
4
-
5
- import os
6
- from setuptools import setup, find_packages
7
-
8
- # Read the contents of README.md
9
- with open("README.md", encoding="utf-8") as f:
10
- long_description = f.read()
11
-
12
- # Read the contents of requirements.txt
13
- with open("requirements.txt", encoding="utf-8") as f:
14
- requirements = [
15
- line.strip()
16
- for line in f
17
- if line.strip() and not line.startswith("#") and not line.startswith("-")
18
- ]
19
-
20
- # Package metadata
21
- NAME = "ai-assistant"
22
- DESCRIPTION = "A robust framework for building AI assistants with built-in error handling, logging, and API integration capabilities"
23
- AUTHOR = "Your Name"
24
- AUTHOR_EMAIL = "your.email@example.com"
25
- URL = "https://github.com/yourusername/ai-assistant"
26
- LICENSE = "MIT"
27
- PYTHON_REQUIRES = ">=3.9"
28
- VERSION = "0.1.0" # Also update in __init__.py
29
-
30
- # Classifiers help users find your project
31
- CLASSIFIERS = [
32
- # Development Status
33
- "Development Status :: 4 - Beta",
34
-
35
- # Intended Audience
36
- "Intended Audience :: Developers",
37
- "Intended Audience :: Science/Research",
38
-
39
- # License
40
- "License :: OSI Approved :: MIT License",
41
-
42
- # Python versions
43
- "Programming Language :: Python :: 3",
44
- "Programming Language :: Python :: 3.9",
45
- "Programming Language :: Python :: 3.10",
46
- "Programming Language :: Python :: 3.11",
47
- "Programming Language :: Python :: 3.12",
48
-
49
- # Environment
50
- "Operating System :: OS Independent",
51
-
52
- # Topics
53
- "Topic :: Software Development :: Libraries :: Python Modules",
54
- "Topic :: Software Development :: Libraries :: Application Frameworks",
55
- "Topic :: Scientific/Engineering :: Artificial Intelligence",
56
-
57
- # Typing
58
- "Typing :: Typed",
59
- ]
60
-
61
- # Additional package data
62
- PACKAGE_DATA = {
63
- "ai_assistant": [
64
- "py.typed", # Marker file for PEP 561
65
- "prompts/*.yaml", # Include prompt templates
66
- "config/*.json", # Include default configurations
67
- ]
68
- }
69
-
70
- setup(
71
- # Basic package information
72
- name=NAME,
73
- version=VERSION,
74
- description=DESCRIPTION,
75
- long_description=long_description,
76
- long_description_content_type="text/markdown",
77
-
78
- # Author information
79
- author=AUTHOR,
80
- author_email=AUTHOR_EMAIL,
81
-
82
- # Project URLs
83
- url=URL,
84
- project_urls={
85
- "Bug Tracker": f"{URL}/issues",
86
- "Documentation": f"{URL}/docs",
87
- "Source Code": URL,
88
- },
89
-
90
- # License and Python version
91
- license=LICENSE,
92
- python_requires=PYTHON_REQUIRES,
93
-
94
- # Package configuration
95
- packages=find_packages(exclude=["tests*", "docs*"]),
96
- package_data=PACKAGE_DATA,
97
- include_package_data=True,
98
- zip_safe=False, # Required for mypy to find py.typed
99
-
100
- # Dependencies
101
- install_requires=requirements,
102
- extras_require={
103
- "dev": [
104
- line.strip()
105
- for line in open("requirements-dev.txt")
106
- if line.strip() and not line.startswith("#") and not line.startswith("-")
107
- ],
108
- "docs": [
109
- "sphinx>=7.2.6,<8.0.0",
110
- "sphinx-rtd-theme>=2.0.0,<3.0.0",
111
- "sphinx-autodoc-typehints>=1.25.2,<2.0.0",
112
- "sphinx-copybutton>=0.5.2,<1.0.0",
113
- "myst-parser>=2.0.0,<3.0.0",
114
- ],
115
- "test": [
116
- "pytest>=8.0.0,<9.0.0",
117
- "pytest-cov>=4.1.0,<5.0.0",
118
- "pytest-asyncio>=0.23.5,<1.0.0",
119
- "pytest-mock>=3.12.0,<4.0.0",
120
- "responses>=0.24.1,<1.0.0",
121
- "freezegun>=1.4.0,<2.0.0",
122
- "faker>=22.6.0,<23.0.0",
123
- ],
124
- },
125
-
126
- # Entry points for CLI tools
127
- entry_points={
128
- "console_scripts": [
129
- "ai-assistant=tools.cli:main",
130
- ],
131
- },
132
-
133
- # Classifiers
134
- classifiers=CLASSIFIERS,
135
-
136
- # Keywords for PyPI
137
- keywords=[
138
- "ai",
139
- "assistant",
140
- "nlp",
141
- "machine-learning",
142
- "api",
143
- "tools",
144
- "automation",
145
- "error-handling",
146
- "logging",
147
- ],
148
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test_app.py DELETED
@@ -1,124 +0,0 @@
1
- """Tests for the API key management web interface."""
2
-
3
- import pytest
4
- from fastapi.testclient import TestClient
5
- from datetime import datetime, timezone
6
-
7
- from app import app
8
-
9
- client = TestClient(app)
10
-
11
- def test_root():
12
- """Test root endpoint."""
13
- response = client.get("/")
14
- assert response.status_code == 200
15
- assert response.json()["message"] == "API Key Management System"
16
-
17
- def test_create_and_get_key():
18
- """Test key creation and retrieval."""
19
- # Create a key
20
- create_response = client.post(
21
- "/keys",
22
- json={
23
- "name": "test_service",
24
- "scopes": ["read:data", "write:data"],
25
- "rate_limit": 5.0,
26
- "burst_limit": 10,
27
- "expires_in_days": 90
28
- }
29
- )
30
- assert create_response.status_code == 200
31
- key_data = create_response.json()
32
- assert key_data["name"] == "test_service"
33
- assert key_data["scopes"] == ["read:data", "write:data"]
34
- assert key_data["rate_limit"] == 5.0
35
- assert key_data["burst_limit"] == 10
36
- assert key_data["is_active"] is True
37
-
38
- # Get the key
39
- get_response = client.get(f"/keys/test_service")
40
- assert get_response.status_code == 200
41
- get_data = get_response.json()
42
- assert get_data["name"] == "test_service"
43
- assert get_data["scopes"] == ["read:data", "write:data"]
44
- assert get_data["is_active"] is True
45
-
46
- def test_list_keys():
47
- """Test listing keys."""
48
- # Create a test key first
49
- client.post(
50
- "/keys",
51
- json={
52
- "name": "list_test_service",
53
- "scopes": ["read:only"]
54
- }
55
- )
56
-
57
- # List keys
58
- response = client.get("/keys")
59
- assert response.status_code == 200
60
- keys = response.json()
61
- assert "list_test_service" in keys
62
- assert keys["list_test_service"]["scopes"] == ["read:only"]
63
-
64
- def test_rotate_key():
65
- """Test key rotation."""
66
- # Create a key first
67
- client.post(
68
- "/keys",
69
- json={
70
- "name": "rotate_test_service",
71
- "scopes": ["read:data"]
72
- }
73
- )
74
-
75
- # Rotate the key
76
- rotate_response = client.post(
77
- "/keys/rotate_test_service/rotate",
78
- params={"expires_in_days": 30}
79
- )
80
- assert rotate_response.status_code == 200
81
- rotated_key = rotate_response.json()
82
- assert rotated_key["name"] == "rotate_test_service"
83
- assert rotated_key["is_active"] is True
84
-
85
- def test_delete_key():
86
- """Test key deletion."""
87
- # Create a key first
88
- client.post(
89
- "/keys",
90
- json={
91
- "name": "delete_test_service",
92
- "scopes": ["read:data"]
93
- }
94
- )
95
-
96
- # Delete the key
97
- delete_response = client.delete("/keys/delete_test_service")
98
- assert delete_response.status_code == 200
99
-
100
- # Verify key is gone
101
- get_response = client.get("/keys/delete_test_service")
102
- assert get_response.status_code == 404
103
-
104
- def test_invalid_key_creation():
105
- """Test validation for key creation."""
106
- response = client.post(
107
- "/keys",
108
- json={
109
- "name": "invalid!name", # Invalid character in name
110
- "scopes": ["invalid:scope"], # Invalid scope format
111
- "rate_limit": -1.0, # Invalid rate limit
112
- "burst_limit": 0 # Invalid burst limit
113
- }
114
- )
115
- assert response.status_code == 400
116
-
117
- def test_metrics():
118
- """Test metrics endpoint."""
119
- response = client.get("/metrics")
120
- assert response.status_code == 200
121
- metrics = response.json()
122
- assert "latency" in metrics
123
- assert "rate_limiting" in metrics
124
- assert "connections" in metrics
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/integration/test_assistant.py DELETED
@@ -1,402 +0,0 @@
1
- """
2
- Integration tests for the AI Assistant.
3
-
4
- These tests verify that different tools work together correctly in common scenarios
5
- that the assistant might encounter.
6
- """
7
-
8
- import pytest
9
- from datetime import datetime, timezone
10
- from typing import List, Dict, Any
11
- from unittest.mock import Mock, patch
12
-
13
- from tools.base import ToolRegistry
14
- from tools.timezone_tools import TimezoneTools
15
- from tools.final_answer import FinalAnswerTool
16
- from tools.exceptions import (
17
- InvalidTimezoneError,
18
- TimeFormatError,
19
- ValidationError,
20
- ToolNotFoundError
21
- )
22
- from tests.utils.mock_responses import (
23
- MOCK_RESPONSES,
24
- MOCK_ENDPOINTS,
25
- create_mock_time_response,
26
- create_mock_timezone_response,
27
- create_mock_error_response
28
- )
29
-
30
- @pytest.fixture
31
- def registry() -> ToolRegistry:
32
- """Fixture that provides a clean ToolRegistry instance with all tools registered."""
33
- registry = ToolRegistry()
34
- registry.clear()
35
- # Register all tools that should be available to the assistant
36
- registry.register(TimezoneTools)
37
- registry.register(FinalAnswerTool)
38
- return registry
39
-
40
- @pytest.fixture
41
- def mock_api_calls() -> None:
42
- """Fixture that mocks all API calls."""
43
- with patch('tools.timezone_tools.requests.get') as mock_get:
44
- def mock_api_response(*args: Any, **kwargs: Any) -> Mock:
45
- url = args[0]
46
- if "time" in url:
47
- timezone_param = kwargs.get('params', {}).get('timezone', 'UTC')
48
- return MOCK_ENDPOINTS["time"].get(
49
- timezone_param,
50
- MOCK_RESPONSES["invalid_timezone"]
51
- )
52
- elif "timezone" in url:
53
- timezone_name = url.split('/')[-1]
54
- return MOCK_ENDPOINTS["timezone"].get(
55
- timezone_name,
56
- MOCK_RESPONSES["invalid_timezone"]
57
- )
58
- return MOCK_RESPONSES["invalid_timezone"]
59
-
60
- mock_get.side_effect = mock_api_response
61
- yield mock_get
62
-
63
- class TestTimezoneFinalAnswerIntegration:
64
- """Test integration between timezone and final answer tools."""
65
-
66
- def test_timezone_conversion_to_final_answer(
67
- self,
68
- registry: ToolRegistry,
69
- mock_api_calls: Mock
70
- ) -> None:
71
- """Test converting time and formatting it as a final answer."""
72
- # Get tool instances
73
- tz_tool = registry.get_tool("timezone")
74
- answer_tool = registry.get_tool("final_answer")
75
-
76
- # Convert time using timezone tool
77
- time_str = "2024-01-01T12:00:00"
78
- converted = tz_tool.convert_time(
79
- time_str,
80
- from_tz="UTC",
81
- to_tz="America/New_York"
82
- )
83
-
84
- # Format as final answer
85
- answer = answer_tool.submit_answer(
86
- answer=converted.strftime("%Y-%m-%d %H:%M:%S %Z"),
87
- confidence=1.0,
88
- source="Timezone conversion",
89
- reasoning=f"Converted {time_str} from UTC to America/New_York"
90
- )
91
-
92
- # Verify the answer and API calls
93
- assert "2024-01-01 07:00:00" in answer
94
- assert "America/New_York" in answer
95
- assert "Confidence: 100%" in answer
96
- mock_api_calls.assert_called()
97
-
98
- def test_multiple_timezone_conversions_to_final_answer(
99
- self,
100
- registry: ToolRegistry,
101
- mock_api_calls: Mock
102
- ) -> None:
103
- """Test converting time to multiple zones and combining in final answer."""
104
- tz_tool = registry.get_tool("timezone")
105
- answer_tool = registry.get_tool("final_answer")
106
-
107
- # Convert current time to multiple zones
108
- zones = ["UTC", "America/New_York", "Asia/Tokyo"]
109
- times = []
110
- for zone in zones:
111
- time = tz_tool.get_current_time(zone)
112
- times.append(f"{zone}: {time.strftime('%H:%M:%S %Z')}")
113
-
114
- # Combine results in final answer
115
- answer = answer_tool.submit_answer(
116
- answer="\n".join(times),
117
- confidence=1.0,
118
- source="Current time in multiple zones",
119
- reasoning="Retrieved current time in UTC, New York, and Tokyo"
120
- )
121
-
122
- # Verify the answer and API calls
123
- for zone in zones:
124
- assert zone in answer
125
- assert "Confidence: 100%" in answer
126
- assert mock_api_calls.call_count == len(zones)
127
-
128
- class TestErrorHandlingIntegration:
129
- """Test error handling across multiple tools."""
130
-
131
- def test_invalid_timezone_to_final_answer(
132
- self,
133
- registry: ToolRegistry,
134
- mock_api_calls: Mock
135
- ) -> None:
136
- """Test handling invalid timezone error in final answer."""
137
- tz_tool = registry.get_tool("timezone")
138
- answer_tool = registry.get_tool("final_answer")
139
-
140
- # Try to get time for invalid timezone
141
- invalid_zone = "Invalid/Zone"
142
- error_message = ""
143
- try:
144
- tz_tool.get_current_time(invalid_zone)
145
- except InvalidTimezoneError as e:
146
- error_message = str(e)
147
-
148
- # Report error in final answer
149
- answer = answer_tool.submit_answer(
150
- answer=f"Error: {error_message}",
151
- confidence=1.0,
152
- source="Timezone operation",
153
- reasoning=f"Failed to get time for invalid timezone: {invalid_zone}"
154
- )
155
-
156
- # Verify error handling and API calls
157
- assert "Invalid timezone" in answer
158
- assert invalid_zone in answer
159
- assert "Error" in answer
160
- mock_api_calls.assert_called_once()
161
-
162
- def test_rate_limit_handling(
163
- self,
164
- registry: ToolRegistry,
165
- mock_api_calls: Mock
166
- ) -> None:
167
- """Test handling rate limit errors."""
168
- mock_api_calls.side_effect = lambda *args, **kwargs: MOCK_RESPONSES["rate_limit"]
169
-
170
- tz_tool = registry.get_tool("timezone")
171
- answer_tool = registry.get_tool("final_answer")
172
-
173
- try:
174
- tz_tool.get_current_time("UTC")
175
- except Exception as e:
176
- error_message = str(e)
177
-
178
- answer = answer_tool.submit_answer(
179
- answer=f"Error: {error_message}",
180
- confidence=1.0,
181
- source="Rate limit handling",
182
- reasoning="API rate limit exceeded"
183
- )
184
-
185
- assert "rate limit" in answer.lower()
186
- assert "30" in error_message # Retry-After value
187
- mock_api_calls.assert_called_once()
188
-
189
- class TestToolRegistryIntegration:
190
- """Test tool registry integration scenarios."""
191
-
192
- def test_tool_dependencies(
193
- self,
194
- registry: ToolRegistry,
195
- mock_api_calls: Mock
196
- ) -> None:
197
- """Test tools that depend on other tools."""
198
- class DependentTool(TimezoneTools):
199
- def __init__(self) -> None:
200
- super().__init__()
201
- self._answer_tool = registry.get_tool("final_answer")
202
-
203
- @property
204
- def name(self) -> str:
205
- return "dependent_tool"
206
-
207
- def get_formatted_time(self, timezone_name: str) -> str:
208
- """Get current time and format it using final answer tool."""
209
- time = self.get_current_time(timezone_name)
210
- return self._answer_tool.submit_answer(
211
- answer=time.strftime("%Y-%m-%d %H:%M:%S %Z"),
212
- confidence=1.0,
213
- source=f"Current time in {timezone_name}",
214
- reasoning=f"Retrieved current time for {timezone_name}"
215
- )
216
-
217
- # Register and test dependent tool
218
- registry.register(DependentTool)
219
- dependent_tool = registry.get_tool("dependent_tool")
220
-
221
- result = dependent_tool.get_formatted_time("UTC")
222
- assert "UTC" in result
223
- assert "Confidence: 100%" in result
224
- mock_api_calls.assert_called_once()
225
-
226
- def test_tool_chain(
227
- self,
228
- registry: ToolRegistry,
229
- mock_api_calls: Mock
230
- ) -> None:
231
- """Test chaining multiple tools together."""
232
- tz_tool = registry.get_tool("timezone")
233
- answer_tool = registry.get_tool("final_answer")
234
-
235
- # Chain of operations:
236
- # 1. Get current UTC time
237
- # 2. Convert to NY time
238
- # 3. Get timezone offset
239
- # 4. Format as final answer
240
-
241
- utc_time = tz_tool.get_current_time("UTC")
242
- ny_time = tz_tool.convert_time(utc_time, to_tz="America/New_York")
243
- ny_offset = tz_tool.get_timezone_offset("America/New_York")
244
-
245
- answer = answer_tool.submit_answer(
246
- answer=f"New York: {ny_time.strftime('%H:%M:%S')} ({ny_offset})",
247
- confidence=1.0,
248
- source="Time conversion chain",
249
- reasoning=(
250
- f"Converted current UTC time ({utc_time.strftime('%H:%M:%S')}) "
251
- f"to New York time with offset {ny_offset}"
252
- )
253
- )
254
-
255
- # Verify chain results and API calls
256
- assert "New York" in answer
257
- assert ny_offset in answer
258
- assert "UTC" in answer
259
- assert "Confidence: 100%" in answer
260
- assert mock_api_calls.call_count >= 3 # At least 3 API calls in the chain
261
-
262
- class TestErrorRecoveryIntegration:
263
- """Test error recovery scenarios across tools."""
264
-
265
- def test_fallback_timezone_conversion(
266
- self,
267
- registry: ToolRegistry,
268
- mock_api_calls: Mock
269
- ) -> None:
270
- """Test fallback to UTC when timezone conversion fails."""
271
- tz_tool = registry.get_tool("timezone")
272
- answer_tool = registry.get_tool("final_answer")
273
-
274
- time_str = "2024-01-01T12:00:00"
275
- target_zone = "Invalid/Zone"
276
-
277
- # First call will fail, second call to UTC will succeed
278
- mock_api_calls.side_effect = [
279
- MOCK_RESPONSES["invalid_timezone"],
280
- MOCK_RESPONSES["utc_time"]
281
- ]
282
-
283
- try:
284
- # Try to convert to invalid timezone
285
- converted = tz_tool.convert_time(time_str, to_tz=target_zone)
286
- except InvalidTimezoneError:
287
- # Fallback to UTC
288
- converted = tz_tool.convert_time(time_str, to_tz="UTC")
289
-
290
- answer = answer_tool.submit_answer(
291
- answer=converted.strftime("%Y-%m-%d %H:%M:%S %Z"),
292
- confidence=0.8, # Lower confidence due to fallback
293
- source="Timezone conversion with fallback",
294
- reasoning=(
295
- f"Failed to convert to {target_zone}, "
296
- f"falling back to UTC time"
297
- )
298
- )
299
-
300
- # Verify fallback behavior and API calls
301
- assert "UTC" in answer
302
- assert "Confidence: 80%" in answer
303
- assert "falling back to UTC" in answer.lower()
304
- assert mock_api_calls.call_count == 2
305
-
306
- def test_graceful_degradation(
307
- self,
308
- registry: ToolRegistry,
309
- mock_api_calls: Mock
310
- ) -> None:
311
- """Test graceful degradation when some operations fail."""
312
- tz_tool = registry.get_tool("timezone")
313
- answer_tool = registry.get_tool("final_answer")
314
-
315
- # Configure mock to alternate between success and failure
316
- mock_api_calls.side_effect = [
317
- MOCK_RESPONSES["utc_time"],
318
- MOCK_RESPONSES["invalid_timezone"],
319
- MOCK_RESPONSES["ny_time"],
320
- MOCK_RESPONSES["invalid_timezone"]
321
- ]
322
-
323
- # Try to perform multiple operations, some of which will fail
324
- results = []
325
- zones = ["UTC", "Invalid/Zone1", "America/New_York", "Invalid/Zone2"]
326
-
327
- for zone in zones:
328
- try:
329
- time = tz_tool.get_current_time(zone)
330
- results.append(f"{zone}: {time.strftime('%H:%M:%S %Z')}")
331
- except InvalidTimezoneError:
332
- results.append(f"{zone}: Invalid timezone")
333
-
334
- # Report partial results
335
- answer = answer_tool.submit_answer(
336
- answer="\n".join(results),
337
- confidence=0.7, # Lower confidence due to partial failure
338
- source="Multi-timezone operation with errors",
339
- reasoning="Some timezone operations failed, reporting partial results"
340
- )
341
-
342
- # Verify partial results handling and API calls
343
- assert "UTC" in answer
344
- assert "America/New_York" in answer
345
- assert "Invalid timezone" in answer
346
- assert "Confidence: 70%" in answer
347
- assert "partial results" in answer.lower()
348
- assert mock_api_calls.call_count == 4
349
-
350
- class TestConcurrentToolUsage:
351
- """Test using multiple tools concurrently."""
352
-
353
- def test_parallel_timezone_queries(
354
- self,
355
- registry: ToolRegistry,
356
- mock_api_calls: Mock
357
- ) -> None:
358
- """Test querying multiple timezones in parallel."""
359
- import concurrent.futures
360
-
361
- tz_tool = registry.get_tool("timezone")
362
- answer_tool = registry.get_tool("final_answer")
363
-
364
- zones = [
365
- "UTC",
366
- "America/New_York",
367
- "Europe/London",
368
- "Asia/Tokyo",
369
- "Australia/Sydney"
370
- ]
371
-
372
- def get_zone_time(zone: str) -> str:
373
- time = tz_tool.get_current_time(zone)
374
- return f"{zone}: {time.strftime('%H:%M:%S %Z')}"
375
-
376
- # Configure mock responses for parallel calls
377
- mock_api_calls.side_effect = [
378
- MOCK_RESPONSES["utc_time"],
379
- MOCK_RESPONSES["ny_time"],
380
- create_mock_time_response("2024-01-01T12:00:00Z", "Europe/London"),
381
- MOCK_RESPONSES["tokyo_time"],
382
- create_mock_time_response("2024-01-01T23:00:00Z", "Australia/Sydney")
383
- ]
384
-
385
- # Get times in parallel
386
- with concurrent.futures.ThreadPoolExecutor() as executor:
387
- futures = [executor.submit(get_zone_time, zone) for zone in zones]
388
- results = [f.result() for f in futures]
389
-
390
- # Combine results
391
- answer = answer_tool.submit_answer(
392
- answer="\n".join(results),
393
- confidence=1.0,
394
- source="Parallel timezone queries",
395
- reasoning="Retrieved current time for multiple zones concurrently"
396
- )
397
-
398
- # Verify parallel execution results and API calls
399
- for zone in zones:
400
- assert zone in answer
401
- assert "Confidence: 100%" in answer
402
- assert mock_api_calls.call_count == len(zones)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_api_logging.py DELETED
@@ -1,220 +0,0 @@
1
- """
2
- Tests for the API logging functionality.
3
- """
4
-
5
- from datetime import datetime
6
- import json
7
- import logging
8
- from typing import Dict, Any, List
9
- import pytest
10
- from unittest.mock import Mock, patch
11
-
12
- from tools.api_logging import log_api_call, sanitize_headers, sanitize_url
13
- from tools.exceptions import APIError, APIRateLimitError, APITimeoutError
14
- from tools.error_recovery import RetryConfig, CircuitBreaker
15
-
16
- # Test data
17
- TEST_URL = "https://api.example.com/v1/data?api_key=secret123&format=json"
18
- TEST_HEADERS = {
19
- "Authorization": "Bearer token123",
20
- "Content-Type": "application/json",
21
- "X-API-Key": "secret456"
22
- }
23
- TEST_REQUEST_BODY = {
24
- "query": "test",
25
- "limit": 10
26
- }
27
-
28
- def test_sanitize_headers() -> None:
29
- """Test sanitization of headers."""
30
- sanitized = sanitize_headers(TEST_HEADERS)
31
- assert sanitized["Authorization"] == "****"
32
- assert sanitized["Content-Type"] == "application/json"
33
- assert sanitized["X-API-Key"] == "****"
34
-
35
- def test_sanitize_url() -> None:
36
- """Test sanitization of URLs."""
37
- sanitized = sanitize_url(TEST_URL)
38
- assert "api_key=****" in sanitized
39
- assert "secret123" not in sanitized
40
- assert "format=json" in sanitized
41
-
42
- @pytest.fixture
43
- def mock_logger() -> Mock:
44
- """Fixture that provides a mock logger."""
45
- with patch('tools.api_logging.logger') as mock_log:
46
- yield mock_log
47
-
48
- def test_successful_api_call(mock_logger: Mock) -> None:
49
- """Test logging of successful API calls."""
50
-
51
- @log_api_call
52
- def mock_api_call(url: str, headers: Dict[str, str], json: Dict[str, Any]) -> Mock:
53
- response = Mock()
54
- response.status_code = 200
55
- response.json.return_value = {"status": "success"}
56
- return response
57
-
58
- response = mock_api_call(TEST_URL, headers=TEST_HEADERS, json=TEST_REQUEST_BODY)
59
-
60
- # Check request logging
61
- request_call = mock_logger.info.call_args_list[0]
62
- request_log = json.loads(request_call.args[0].replace("API Request: ", ""))
63
- assert "timestamp" in request_log
64
- assert request_log["function"] == "mock_api_call"
65
- assert "api_key=****" in request_log["url"]
66
- assert request_log["headers"]["Authorization"] == "****"
67
-
68
- # Check response logging
69
- response_call = mock_logger.info.call_args_list[1]
70
- response_log = json.loads(response_call.args[0].replace("API Response: ", ""))
71
- assert response_log["status"] == 200
72
- assert response_log["body"] == {"status": "success"}
73
-
74
- def test_api_call_with_retry(mock_logger: Mock) -> None:
75
- """Test API call with retry configuration."""
76
- attempts = 0
77
-
78
- @log_api_call(
79
- retry_config=RetryConfig(max_retries=2, initial_delay=0.01),
80
- circuit_breaker=CircuitBreaker(failure_threshold=3)
81
- )
82
- def failing_then_succeeding(url: str) -> Mock:
83
- nonlocal attempts
84
- attempts += 1
85
- if attempts < 2:
86
- raise TimeoutError("Connection timeout")
87
- response = Mock()
88
- response.status_code = 200
89
- response.json.return_value = {"status": "success"}
90
- return response
91
-
92
- response = failing_then_succeeding(TEST_URL)
93
-
94
- # Should have logged initial failure and final success
95
- assert attempts == 2
96
- assert len(mock_logger.info.call_args_list) == 4 # 2 requests + 2 responses
97
- assert len(mock_logger.error.call_args_list) == 1 # 1 error
98
-
99
- # Check final response
100
- assert response.status_code == 200
101
- assert response.json() == {"status": "success"}
102
-
103
- def test_api_call_with_circuit_breaker(mock_logger: Mock) -> None:
104
- """Test API call with circuit breaker."""
105
- attempts = 0
106
- cb = CircuitBreaker(failure_threshold=2, reset_timeout=0.1)
107
-
108
- @log_api_call(circuit_breaker=cb)
109
- def always_failing(url: str) -> None:
110
- nonlocal attempts
111
- attempts += 1
112
- raise ConnectionError("Connection failed")
113
-
114
- # First call should fail normally
115
- with pytest.raises(APIError):
116
- always_failing(TEST_URL)
117
- assert attempts == 1
118
-
119
- # Second call should fail and open the circuit
120
- with pytest.raises(APIError):
121
- always_failing(TEST_URL)
122
- assert attempts == 2
123
-
124
- # Third call should fail immediately with circuit breaker error
125
- with pytest.raises(APIError) as exc_info:
126
- always_failing(TEST_URL)
127
- assert attempts == 2 # No additional attempt
128
- assert "Circuit breaker is open" in str(exc_info.value)
129
-
130
- def test_api_call_with_rate_limit_retry(mock_logger: Mock) -> None:
131
- """Test API call with rate limit retry."""
132
- attempts = 0
133
-
134
- @log_api_call(
135
- retry_config=RetryConfig(max_retries=1, initial_delay=0.01)
136
- )
137
- def rate_limited(url: str) -> None:
138
- nonlocal attempts
139
- attempts += 1
140
- response = Mock()
141
- response.status_code = 429
142
- response.headers = {"Retry-After": "0.1"}
143
- error = Exception("Rate limit exceeded")
144
- error.response = response
145
- raise error
146
-
147
- with pytest.raises(APIRateLimitError) as exc_info:
148
- rate_limited(TEST_URL)
149
-
150
- assert attempts == 2 # Initial + 1 retry
151
- error = exc_info.value
152
- assert error.retry_after == 0.1
153
- assert "rate limit exceeded" in str(error).lower()
154
-
155
- def test_api_call_with_non_json_response(mock_logger: Mock) -> None:
156
- """Test API call with non-JSON response."""
157
-
158
- @log_api_call
159
- def text_response(url: str) -> Mock:
160
- response = Mock()
161
- response.status_code = 200
162
- response.json.side_effect = ValueError("Invalid JSON")
163
- response.__str__ = lambda self: "Plain text response"
164
- return response
165
-
166
- response = text_response(TEST_URL)
167
-
168
- # Check response logging
169
- response_call = mock_logger.info.call_args_list[1]
170
- response_log = json.loads(response_call.args[0].replace("API Response: ", ""))
171
- assert response_log["status"] == 200
172
- assert response_log["body"] == "Plain text response"
173
-
174
- def test_api_call_with_custom_retry_conditions(mock_logger: Mock) -> None:
175
- """Test API call with custom retry conditions."""
176
- attempts = 0
177
-
178
- class CustomError(Exception):
179
- pass
180
-
181
- retry_config = RetryConfig(
182
- max_retries=2,
183
- initial_delay=0.01,
184
- retry_on={CustomError, APITimeoutError}
185
- )
186
-
187
- @log_api_call(retry_config=retry_config)
188
- def custom_error(url: str) -> None:
189
- nonlocal attempts
190
- attempts += 1
191
- raise CustomError("Custom error")
192
-
193
- with pytest.raises(APIError):
194
- custom_error(TEST_URL)
195
-
196
- assert attempts == 3 # Initial + 2 retries
197
-
198
- def test_api_call_logs_sanitized_data(mock_logger: Mock) -> None:
199
- """Test that API call logs are properly sanitized."""
200
- sensitive_data = {
201
- "api_key": "secret123",
202
- "password": "sensitive",
203
- "query": "safe_data"
204
- }
205
-
206
- @log_api_call(log_request_body=True)
207
- def sensitive_call(url: str, json: Dict[str, Any]) -> Mock:
208
- response = Mock()
209
- response.status_code = 200
210
- response.json.return_value = {"status": "success"}
211
- return response
212
-
213
- sensitive_call(TEST_URL, json=sensitive_data)
214
-
215
- # Check request logging
216
- request_call = mock_logger.info.call_args_list[0]
217
- request_log = json.loads(request_call.args[0].replace("API Request: ", ""))
218
- log_body = json.loads(request_log["body"])
219
- assert log_body["api_key"] == "secret123" # Body content isn't sanitized
220
- assert "api_key=****" in request_log["url"] # URL is sanitized
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_api_optimization.py DELETED
@@ -1,209 +0,0 @@
1
- """Tests for API optimization utilities."""
2
-
3
- import asyncio
4
- from datetime import timedelta
5
- from typing import Any, Dict, List
6
- from unittest.mock import AsyncMock, MagicMock, patch
7
-
8
- import aiohttp
9
- import pytest
10
- from aioresponses import aioresponses
11
-
12
- from tools.api_optimization import (
13
- APIClient,
14
- BatchProcessor,
15
- ConnectionPool,
16
- RateLimiter,
17
- with_rate_limit,
18
- with_retry,
19
- )
20
-
21
- @pytest.fixture
22
- def rate_limiter() -> RateLimiter:
23
- """Create a test rate limiter."""
24
- return RateLimiter(rate=10.0, burst=20)
25
-
26
- @pytest.fixture
27
- def batch_processor() -> BatchProcessor:
28
- """Create a test batch processor."""
29
- return BatchProcessor(batch_size=2, flush_interval=0.1)
30
-
31
- @pytest.fixture
32
- def connection_pool() -> ConnectionPool:
33
- """Create a test connection pool."""
34
- return ConnectionPool(pool_size=2, timeout=1.0)
35
-
36
- @pytest.fixture
37
- def api_client() -> APIClient:
38
- """Create a test API client."""
39
- return APIClient(
40
- base_url="http://test.api",
41
- pool_size=2,
42
- rate_limit=10.0,
43
- burst_limit=20,
44
- batch_size=2
45
- )
46
-
47
- async def test_rate_limiter(rate_limiter: RateLimiter):
48
- """Test rate limiter functionality."""
49
- # Test initial state
50
- assert rate_limiter.tokens == rate_limiter.burst
51
-
52
- # Test token acquisition
53
- wait_time = rate_limiter.acquire(1.0)
54
- assert wait_time == 0.0
55
- assert rate_limiter.tokens == rate_limiter.burst - 1
56
-
57
- # Test burst handling
58
- wait_time = rate_limiter.acquire(rate_limiter.burst)
59
- assert wait_time > 0.0
60
- assert rate_limiter.tokens == 0.0
61
-
62
- async def test_batch_processor(batch_processor: BatchProcessor):
63
- """Test batch processor functionality."""
64
- # Test batch accumulation
65
- await batch_processor.add({"key": "value1"})
66
- assert len(batch_processor.batch) == 1
67
-
68
- # Test batch flush on size
69
- await batch_processor.add({"key": "value2"})
70
- assert len(batch_processor.batch) == 0
71
-
72
- # Test batch flush on interval
73
- await batch_processor.add({"key": "value3"})
74
- await asyncio.sleep(0.2) # Wait for flush interval
75
- assert len(batch_processor.batch) == 0
76
-
77
- async def test_connection_pool():
78
- """Test connection pool functionality."""
79
- pool = ConnectionPool(pool_size=2)
80
-
81
- async with pool as p:
82
- assert isinstance(p, ConnectionPool)
83
- assert p.session is not None
84
-
85
- with aioresponses() as mocked:
86
- url = "http://test.api/endpoint"
87
- mocked.get(url, status=200, payload={"key": "value"})
88
-
89
- response = await p.request("GET", url)
90
- assert response.status == 200
91
-
92
- async def test_retry_decorator():
93
- """Test retry decorator functionality."""
94
- mock_func = AsyncMock(side_effect=[Exception, Exception, "success"])
95
-
96
- @with_retry(max_attempts=3, min_wait=0.1, max_wait=0.3)
97
- async def test_func():
98
- return await mock_func()
99
-
100
- result = await test_func()
101
- assert result == "success"
102
- assert mock_func.call_count == 3
103
-
104
- async def test_rate_limit_decorator():
105
- """Test rate limit decorator functionality."""
106
- call_times: List[float] = []
107
-
108
- @with_rate_limit(rate=10.0, burst=2)
109
- async def test_func():
110
- call_times.append(asyncio.get_event_loop().time())
111
-
112
- # Make multiple calls
113
- await asyncio.gather(*[test_func() for _ in range(3)])
114
-
115
- # Verify rate limiting
116
- time_diffs = [
117
- call_times[i] - call_times[i-1]
118
- for i in range(1, len(call_times))
119
- ]
120
- assert all(diff >= 0.1 for diff in time_diffs) # At least 0.1s between calls
121
-
122
- async def test_api_client(api_client: APIClient):
123
- """Test API client functionality."""
124
- with aioresponses() as mocked:
125
- # Test single request
126
- url = "http://test.api/test"
127
- mocked.post(url, status=200, payload={"result": "success"})
128
-
129
- response = await api_client.request(
130
- "POST",
131
- "test",
132
- json={"data": "test"}
133
- )
134
- assert response == {"result": "success"}
135
-
136
- # Test batch request
137
- items = [
138
- {"id": 1, "data": "test1"},
139
- {"id": 2, "data": "test2"}
140
- ]
141
- mocked.post(url, repeat=True, status=200, payload={"result": "success"})
142
-
143
- results = await api_client.batch_request("POST", "test", items)
144
- assert len(results) == 2
145
- assert all(r == {"result": "success"} for r in results)
146
-
147
- async def test_api_client_error_handling(api_client: APIClient):
148
- """Test API client error handling."""
149
- with aioresponses() as mocked:
150
- url = "http://test.api/test"
151
-
152
- # Test retry on error
153
- mocked.post(
154
- url,
155
- status=500,
156
- repeat=True,
157
- exception=aiohttp.ClientError()
158
- )
159
-
160
- with pytest.raises(aiohttp.ClientError):
161
- await api_client.request("POST", "test", json={"data": "test"})
162
-
163
- async def test_api_client_rate_limiting(api_client: APIClient):
164
- """Test API client rate limiting."""
165
- with aioresponses() as mocked:
166
- url = "http://test.api/test"
167
- mocked.post(
168
- url,
169
- status=200,
170
- payload={"result": "success"},
171
- repeat=True
172
- )
173
-
174
- start_time = asyncio.get_event_loop().time()
175
-
176
- # Make multiple requests
177
- responses = await asyncio.gather(*[
178
- api_client.request("POST", "test", json={"data": f"test{i}"})
179
- for i in range(5)
180
- ])
181
-
182
- end_time = asyncio.get_event_loop().time()
183
- duration = end_time - start_time
184
-
185
- # Verify rate limiting
186
- assert duration >= 0.4 # At least 0.4s for 5 requests at 10 RPS
187
- assert all(r == {"result": "success"} for r in responses)
188
-
189
- async def test_connection_pool_limits(api_client: APIClient):
190
- """Test connection pool concurrent connection limits."""
191
- with aioresponses() as mocked:
192
- url = "http://test.api/test"
193
- mocked.get(
194
- url,
195
- status=200,
196
- payload={"result": "success"},
197
- repeat=True
198
- )
199
-
200
- async def make_request():
201
- return await api_client.request("GET", "test")
202
-
203
- # Make concurrent requests
204
- results = await asyncio.gather(*[
205
- make_request() for _ in range(5)
206
- ])
207
-
208
- assert len(results) == 5
209
- assert all(r == {"result": "success"} for r in results)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_cache.py DELETED
@@ -1,197 +0,0 @@
1
- """Tests for the caching module."""
2
-
3
- import time
4
- from datetime import datetime, timedelta
5
- from typing import Optional
6
- from unittest.mock import MagicMock, patch
7
-
8
- import pytest
9
- import redis
10
- from redis.exceptions import RedisError
11
-
12
- from tools.cache import CacheConfig, CacheEntry, CacheManager, LRUCache, cached
13
- from tools.stats import UsageStats
14
-
15
- @pytest.fixture
16
- def cache_config() -> CacheConfig:
17
- """Create a test cache configuration."""
18
- return CacheConfig(
19
- redis_host="localhost",
20
- redis_port=6379,
21
- redis_db=0,
22
- default_ttl=60,
23
- local_cache_size=10,
24
- enable_local_cache=True,
25
- enable_redis_cache=True
26
- )
27
-
28
- @pytest.fixture
29
- def mock_redis() -> MagicMock:
30
- """Create a mock Redis client."""
31
- with patch("redis.Redis") as mock:
32
- mock.return_value.ping.return_value = True
33
- yield mock.return_value
34
-
35
- @pytest.fixture
36
- def mock_stats() -> MagicMock:
37
- """Create a mock stats manager."""
38
- return MagicMock(spec=UsageStats)
39
-
40
- @pytest.fixture
41
- def cache_manager(
42
- cache_config: CacheConfig,
43
- mock_redis: MagicMock,
44
- mock_stats: MagicMock
45
- ) -> CacheManager:
46
- """Create a test cache manager."""
47
- with patch("redis.Redis", return_value=mock_redis):
48
- return CacheManager(cache_config, mock_stats)
49
-
50
- def test_lru_cache_basic_operations():
51
- """Test basic LRU cache operations."""
52
- cache: LRUCache[str, int] = LRUCache(max_size=2)
53
-
54
- # Test set and get
55
- cache.set("a", 1)
56
- cache.set("b", 2)
57
- assert cache.get("a") == 1
58
- assert cache.get("b") == 2
59
-
60
- # Test LRU eviction
61
- cache.set("c", 3) # Should evict "a"
62
- assert cache.get("a") is None
63
- assert cache.get("b") == 2
64
- assert cache.get("c") == 3
65
-
66
- # Test removal
67
- cache.remove("b")
68
- assert cache.get("b") is None
69
-
70
- # Test clear
71
- cache.clear()
72
- assert cache.get("c") is None
73
-
74
- def test_lru_cache_ttl():
75
- """Test LRU cache TTL functionality."""
76
- cache: LRUCache[str, int] = LRUCache(max_size=2)
77
-
78
- # Set with TTL
79
- cache.set("a", 1, ttl=1)
80
- assert cache.get("a") == 1
81
-
82
- # Wait for expiration
83
- time.sleep(1.1)
84
- assert cache.get("a") is None
85
-
86
- def test_cache_entry_serialization():
87
- """Test cache entry serialization."""
88
- entry = CacheEntry[str](
89
- value="test",
90
- created_at=datetime.utcnow(),
91
- expires_at=datetime.utcnow() + timedelta(seconds=60),
92
- hit_count=5
93
- )
94
-
95
- # Test serialization
96
- json_str = entry.to_json()
97
-
98
- # Test deserialization
99
- loaded_entry = CacheEntry.from_json(json_str, str)
100
- assert loaded_entry.value == entry.value
101
- assert loaded_entry.hit_count == entry.hit_count
102
- assert abs((loaded_entry.created_at - entry.created_at).total_seconds()) < 1
103
- assert abs((loaded_entry.expires_at - entry.expires_at).total_seconds()) < 1
104
-
105
- def test_cache_manager_local_only(cache_config: CacheConfig, mock_stats: MagicMock):
106
- """Test cache manager with local cache only."""
107
- config = cache_config.copy()
108
- config.enable_redis_cache = False
109
- manager = CacheManager(config, mock_stats)
110
-
111
- # Test set and get
112
- manager.set("key", "value")
113
- assert manager.get("key", str) == "value"
114
-
115
- # Test stats recording
116
- mock_stats.record_cache_operation.assert_called()
117
-
118
- def test_cache_manager_redis_operations(
119
- cache_manager: CacheManager,
120
- mock_redis: MagicMock
121
- ):
122
- """Test cache manager Redis operations."""
123
- # Setup mock Redis response
124
- entry = CacheEntry[str](value="test", expires_at=datetime.utcnow() + timedelta(seconds=60))
125
- mock_redis.get.return_value = entry.to_json()
126
-
127
- # Test get from Redis
128
- value = cache_manager.get("key", str)
129
- assert value == "test"
130
- mock_redis.get.assert_called_with("key")
131
-
132
- # Test set to Redis
133
- cache_manager.set("key", "value", ttl=60)
134
- mock_redis.setex.assert_called()
135
-
136
- # Test Redis error handling
137
- mock_redis.get.side_effect = RedisError("Test error")
138
- assert cache_manager.get("key", str) is None
139
-
140
- def test_cache_manager_two_level_caching(
141
- cache_manager: CacheManager,
142
- mock_redis: MagicMock
143
- ):
144
- """Test two-level caching strategy."""
145
- # Set value
146
- cache_manager.set("key", "value")
147
-
148
- # First get should use Redis
149
- entry = CacheEntry[str](value="value", expires_at=datetime.utcnow() + timedelta(seconds=60))
150
- mock_redis.get.return_value = entry.to_json()
151
- assert cache_manager.get("key", str) == "value"
152
- mock_redis.get.assert_called_once()
153
-
154
- # Second get should use local cache
155
- mock_redis.get.reset_mock()
156
- assert cache_manager.get("key", str) == "value"
157
- mock_redis.get.assert_not_called()
158
-
159
- @pytest.mark.parametrize("ttl", [None, 60])
160
- def test_cached_decorator(cache_manager: CacheManager, ttl: Optional[int]):
161
- """Test cached decorator."""
162
- call_count = 0
163
-
164
- @cached(ttl=ttl, cache_manager=cache_manager)
165
- def test_func(x: int, y: str = "default") -> str:
166
- nonlocal call_count
167
- call_count += 1
168
- return f"{x}-{y}"
169
-
170
- # First call should execute function
171
- result1 = test_func(1, y="test")
172
- assert result1 == "1-test"
173
- assert call_count == 1
174
-
175
- # Second call should use cache
176
- result2 = test_func(1, y="test")
177
- assert result2 == "1-test"
178
- assert call_count == 1
179
-
180
- # Different arguments should execute function
181
- result3 = test_func(2, y="test")
182
- assert result3 == "2-test"
183
- assert call_count == 2
184
-
185
- def test_cached_decorator_type_handling(cache_manager: CacheManager):
186
- """Test cached decorator type handling."""
187
- @cached(cache_manager=cache_manager)
188
- def test_func() -> Optional[str]:
189
- return "test"
190
-
191
- # Should correctly handle Optional types
192
- result = test_func()
193
- assert result == "test"
194
-
195
- # Should correctly cache and return the value
196
- cached_result = test_func()
197
- assert cached_result == "test"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_cache_invalidation.py DELETED
@@ -1,221 +0,0 @@
1
- """Tests for cache invalidation strategies."""
2
-
3
- from datetime import datetime, timedelta, timezone
4
- from typing import List, Pattern
5
- from unittest.mock import MagicMock, call
6
-
7
- import pytest
8
- from typing_extensions import TypeAlias
9
-
10
- from tools.cache import CacheConfig, CacheManager
11
- from tools.cache_invalidation import (
12
- CacheInvalidator,
13
- InvalidationEvent,
14
- InvalidationStrategy,
15
- )
16
-
17
- # Type aliases
18
- PatternType: TypeAlias = "str | Pattern[str]"
19
-
20
- @pytest.fixture
21
- def cache_config() -> CacheConfig:
22
- """Create a test cache configuration."""
23
- return CacheConfig(
24
- redis_host="localhost",
25
- redis_port=6379,
26
- redis_db=0,
27
- default_ttl=60,
28
- local_cache_size=10,
29
- enable_local_cache=True,
30
- enable_redis_cache=True
31
- )
32
-
33
- @pytest.fixture
34
- def mock_cache_manager(cache_config: CacheConfig) -> MagicMock:
35
- """Create a mock cache manager."""
36
- manager = MagicMock(spec=CacheManager)
37
- manager.config = cache_config
38
- return manager
39
-
40
- @pytest.fixture
41
- def invalidator(mock_cache_manager: MagicMock) -> CacheInvalidator:
42
- """Create a test cache invalidator."""
43
- return CacheInvalidator(mock_cache_manager)
44
-
45
- def test_add_and_remove_tags(invalidator: CacheInvalidator):
46
- """Test adding and removing tags from cache keys."""
47
- # Add tags
48
- invalidator.add_tags("key1", ["tag1", "tag2"])
49
- invalidator.add_tags("key2", ["tag2", "tag3"])
50
-
51
- # Verify tag mappings
52
- assert invalidator.key_tags["key1"] == {"tag1", "tag2"}
53
- assert invalidator.key_tags["key2"] == {"tag2", "tag3"}
54
- assert invalidator.tag_keys["tag1"] == {"key1"}
55
- assert invalidator.tag_keys["tag2"] == {"key1", "key2"}
56
- assert invalidator.tag_keys["tag3"] == {"key2"}
57
-
58
- # Remove specific tags
59
- invalidator.remove_tags("key1", ["tag1"])
60
- assert invalidator.key_tags["key1"] == {"tag2"}
61
- assert "tag1" not in invalidator.tag_keys
62
-
63
- # Remove all tags
64
- invalidator.remove_tags("key2")
65
- assert "key2" not in invalidator.key_tags
66
- assert invalidator.tag_keys["tag2"] == {"key1"}
67
- assert "tag3" not in invalidator.tag_keys
68
-
69
- def test_invalidate_by_pattern(
70
- invalidator: CacheInvalidator,
71
- mock_cache_manager: MagicMock
72
- ):
73
- """Test pattern-based cache invalidation."""
74
- # Setup test data
75
- invalidator.add_tags("user:1", ["user"])
76
- invalidator.add_tags("user:2", ["user"])
77
- invalidator.add_tags("post:1", ["post"])
78
-
79
- # Test glob pattern invalidation
80
- keys = invalidator.invalidate_by_pattern("user:*")
81
- assert set(keys) == {"user:1", "user:2"}
82
- assert mock_cache_manager.remove.call_count == 2
83
- mock_cache_manager.remove.assert_has_calls([
84
- call("user:1"),
85
- call("user:2")
86
- ], any_order=True)
87
-
88
- # Verify tags were removed
89
- assert "user:1" not in invalidator.key_tags
90
- assert "user:2" not in invalidator.key_tags
91
- assert "post:1" in invalidator.key_tags
92
-
93
- def test_invalidate_by_tags(
94
- invalidator: CacheInvalidator,
95
- mock_cache_manager: MagicMock
96
- ):
97
- """Test tag-based cache invalidation."""
98
- # Setup test data
99
- invalidator.add_tags("key1", ["tag1", "tag2"])
100
- invalidator.add_tags("key2", ["tag2", "tag3"])
101
- invalidator.add_tags("key3", ["tag3"])
102
-
103
- # Test invalidation with any tag
104
- keys = invalidator.invalidate_by_tags(["tag1", "tag3"])
105
- assert set(keys) == {"key1", "key2", "key3"}
106
- assert mock_cache_manager.remove.call_count == 3
107
-
108
- # Test invalidation with all tags
109
- invalidator.add_tags("key4", ["tag4", "tag5"])
110
- invalidator.add_tags("key5", ["tag4", "tag5", "tag6"])
111
- keys = invalidator.invalidate_by_tags(["tag4", "tag5"], match_all=True)
112
- assert set(keys) == {"key4", "key5"}
113
-
114
- def test_invalidate_by_time(
115
- invalidator: CacheInvalidator,
116
- mock_cache_manager: MagicMock
117
- ):
118
- """Test time-based cache invalidation."""
119
- now = datetime.now(timezone.utc)
120
- old_time = now - timedelta(hours=2)
121
- recent_time = now - timedelta(minutes=30)
122
-
123
- # Setup test data with different timestamps
124
- invalidator.add_tags("old_key", ["tag1"])
125
- invalidator.key_timestamps["old_key"] = old_time
126
-
127
- invalidator.add_tags("recent_key", ["tag1"])
128
- invalidator.key_timestamps["recent_key"] = recent_time
129
-
130
- # Test invalidation of old keys
131
- keys = invalidator.invalidate_by_time(timedelta(hours=1))
132
- assert keys == ["old_key"]
133
- mock_cache_manager.remove.assert_called_once_with("old_key")
134
-
135
- # Verify old key was removed from all mappings
136
- assert "old_key" not in invalidator.key_tags
137
- assert "old_key" not in invalidator.key_timestamps
138
- assert "recent_key" in invalidator.key_tags
139
-
140
- def test_bulk_invalidation(
141
- invalidator: CacheInvalidator,
142
- mock_cache_manager: MagicMock
143
- ):
144
- """Test bulk cache invalidation."""
145
- now = datetime.now(timezone.utc)
146
- old_time = now - timedelta(hours=2)
147
-
148
- # Setup test data
149
- invalidator.add_tags("user:1", ["user", "active"])
150
- invalidator.add_tags("user:2", ["user", "inactive"])
151
- invalidator.add_tags("post:1", ["post"])
152
- invalidator.key_timestamps["user:2"] = old_time
153
-
154
- # Test bulk invalidation with multiple criteria
155
- keys = invalidator.bulk_invalidate(
156
- patterns=["user:*"],
157
- tags=["inactive"],
158
- max_age=timedelta(hours=1)
159
- )
160
- assert set(keys) == {"user:1", "user:2"}
161
- assert mock_cache_manager.remove.call_count == 2
162
-
163
- def test_invalidation_callbacks(invalidator: CacheInvalidator):
164
- """Test invalidation callbacks."""
165
- callback_called = False
166
-
167
- def test_callback() -> None:
168
- nonlocal callback_called
169
- callback_called = True
170
-
171
- # Register callback
172
- invalidator.register_callback("test", test_callback)
173
-
174
- # Setup test data
175
- invalidator.add_tags("key1", ["tag1"])
176
-
177
- # Test callback execution
178
- invalidator.invalidate_by_pattern("key*", callback="test")
179
- assert callback_called
180
-
181
- def test_invalidation_events(
182
- invalidator: CacheInvalidator,
183
- mock_cache_manager: MagicMock
184
- ):
185
- """Test invalidation events."""
186
- # Setup test data
187
- invalidator.add_tags("user:1", ["user"])
188
- invalidator.add_tags("post:1", ["post"])
189
-
190
- # Test pattern invalidation event
191
- event = InvalidationEvent(
192
- strategy=InvalidationStrategy.PATTERN,
193
- pattern="user:*"
194
- )
195
- keys = invalidator.record_invalidation(event)
196
- assert keys == ["user:1"]
197
-
198
- # Test tag invalidation event
199
- event = InvalidationEvent(
200
- strategy=InvalidationStrategy.TAG,
201
- tags=["post"]
202
- )
203
- keys = invalidator.record_invalidation(event)
204
- assert keys == ["post:1"]
205
-
206
- # Test time invalidation event
207
- event = InvalidationEvent(
208
- strategy=InvalidationStrategy.TIME,
209
- max_age=timedelta(hours=1)
210
- )
211
- keys = invalidator.record_invalidation(event)
212
- assert keys == []
213
-
214
- # Test bulk invalidation event
215
- event = InvalidationEvent(
216
- strategy=InvalidationStrategy.BULK,
217
- pattern="*:1",
218
- tags=["user", "post"]
219
- )
220
- keys = invalidator.record_invalidation(event)
221
- assert set(keys) == {"user:1", "post:1"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_error_recovery.py DELETED
@@ -1,212 +0,0 @@
1
- """
2
- Tests for the error recovery mechanisms.
3
- """
4
-
5
- from datetime import datetime, timedelta
6
- import time
7
- from typing import Dict, Any, Optional, List
8
- import pytest
9
- from unittest.mock import Mock, patch
10
-
11
- from tools.error_recovery import (
12
- CircuitBreaker,
13
- CircuitBreakerState,
14
- RetryConfig,
15
- with_retry
16
- )
17
- from tools.exceptions import APIError, APIRateLimitError, APITimeoutError
18
-
19
- def test_circuit_breaker_state() -> None:
20
- """Test circuit breaker state initialization and updates."""
21
- state = CircuitBreakerState()
22
- assert not state.is_open
23
- assert state.failure_count == 0
24
- assert state.last_failure_time is None
25
- assert state.last_attempt_time is None
26
- assert len(state.endpoints) == 0
27
-
28
- def test_retry_config() -> None:
29
- """Test retry configuration initialization."""
30
- config = RetryConfig()
31
- assert config.max_retries == 3
32
- assert config.initial_delay == 1.0
33
- assert config.max_delay == 60.0
34
- assert config.exponential_base == 2.0
35
- assert 0 < config.jitter <= 1.0
36
- assert APITimeoutError in config.retry_on
37
- assert APIRateLimitError in config.retry_on
38
-
39
- def test_circuit_breaker_basic() -> None:
40
- """Test basic circuit breaker functionality."""
41
- cb = CircuitBreaker(failure_threshold=2)
42
-
43
- # Initially should allow execution
44
- assert cb.can_execute("test_key")
45
-
46
- # Record a failure
47
- cb.record_failure("test_key", "test_endpoint")
48
- assert cb.can_execute("test_key")
49
-
50
- # Record another failure to trigger circuit open
51
- cb.record_failure("test_key", "test_endpoint")
52
- assert not cb.can_execute("test_key")
53
-
54
- # Record success should reset the circuit
55
- cb.record_success("test_key")
56
- assert cb.can_execute("test_key")
57
- state = cb.get_state("test_key")
58
- assert not state.is_open
59
- assert state.failure_count == 0
60
- assert len(state.endpoints) == 0
61
-
62
- def test_circuit_breaker_timeout() -> None:
63
- """Test circuit breaker timeout and half-open state."""
64
- cb = CircuitBreaker(
65
- failure_threshold=1,
66
- reset_timeout=0.1, # Short timeout for testing
67
- half_open_timeout=0.05
68
- )
69
-
70
- # Open the circuit
71
- cb.record_failure("test_key")
72
- assert not cb.can_execute("test_key")
73
-
74
- # Wait for reset timeout
75
- time.sleep(0.15)
76
-
77
- # Should allow one test request
78
- assert cb.can_execute("test_key")
79
-
80
- # Should not allow another request too soon
81
- assert not cb.can_execute("test_key")
82
-
83
- # Wait for half-open timeout
84
- time.sleep(0.1)
85
-
86
- # Should allow another test request
87
- assert cb.can_execute("test_key")
88
-
89
- @pytest.mark.parametrize("error_class", [
90
- APITimeoutError,
91
- APIRateLimitError,
92
- TimeoutError,
93
- ConnectionError
94
- ])
95
- def test_retry_on_different_errors(error_class: type) -> None:
96
- """Test retry behavior with different error types."""
97
- retry_count = 0
98
-
99
- @with_retry(retry_config=RetryConfig(max_retries=2, initial_delay=0.01))
100
- def failing_function() -> None:
101
- nonlocal retry_count
102
- retry_count += 1
103
- if error_class == APIRateLimitError:
104
- response = Mock()
105
- response.headers = {"Retry-After": "1"}
106
- error = Exception("Rate limit")
107
- error.response = response
108
- error.response.status_code = 429
109
- raise error
110
- raise error_class("Test error")
111
-
112
- with pytest.raises((APIError, error_class)):
113
- failing_function()
114
-
115
- assert retry_count == 3 # Initial attempt + 2 retries
116
-
117
- def test_retry_with_success() -> None:
118
- """Test successful retry after failures."""
119
- attempts = 0
120
-
121
- @with_retry(retry_config=RetryConfig(max_retries=3, initial_delay=0.01))
122
- def eventually_succeeds() -> str:
123
- nonlocal attempts
124
- attempts += 1
125
- if attempts < 2:
126
- raise APITimeoutError("test", 1.0)
127
- return "success"
128
-
129
- result = eventually_succeeds()
130
- assert result == "success"
131
- assert attempts == 2
132
-
133
- def test_retry_respects_rate_limit() -> None:
134
- """Test that retry mechanism respects rate limit retry-after."""
135
- attempts = 0
136
- start_time = time.time()
137
-
138
- @with_retry(retry_config=RetryConfig(max_retries=1, initial_delay=0.01))
139
- def rate_limited() -> None:
140
- nonlocal attempts
141
- attempts += 1
142
- response = Mock()
143
- response.headers = {"Retry-After": "0.1"} # 100ms
144
- error = Exception("Rate limit")
145
- error.response = response
146
- error.response.status_code = 429
147
- raise error
148
-
149
- with pytest.raises(APIError):
150
- rate_limited()
151
-
152
- duration = time.time() - start_time
153
- assert duration >= 0.1 # Should have waited for retry-after
154
- assert attempts == 2 # Initial attempt + 1 retry
155
-
156
- def test_combined_circuit_breaker_and_retry() -> None:
157
- """Test circuit breaker and retry working together."""
158
- cb = CircuitBreaker(failure_threshold=3)
159
- attempts = 0
160
-
161
- @with_retry(
162
- retry_config=RetryConfig(max_retries=2, initial_delay=0.01),
163
- circuit_breaker=cb
164
- )
165
- def failing_function() -> None:
166
- nonlocal attempts
167
- attempts += 1
168
- raise APITimeoutError("test", 1.0)
169
-
170
- # First call should retry twice
171
- with pytest.raises(APITimeoutError):
172
- failing_function()
173
- assert attempts == 3
174
-
175
- # Second call should retry twice
176
- attempts = 0
177
- with pytest.raises(APITimeoutError):
178
- failing_function()
179
- assert attempts == 3
180
-
181
- # Third call should fail immediately due to circuit breaker
182
- attempts = 0
183
- with pytest.raises(APIError) as exc_info:
184
- failing_function()
185
- assert attempts == 0
186
- assert "Circuit breaker is open" in str(exc_info.value)
187
-
188
- def test_exponential_backoff() -> None:
189
- """Test exponential backoff with jitter."""
190
- sleep_times: List[float] = []
191
-
192
- @with_retry(
193
- retry_config=RetryConfig(
194
- max_retries=2,
195
- initial_delay=0.1,
196
- exponential_base=2.0,
197
- jitter=0.1
198
- )
199
- )
200
- def failing_function() -> None:
201
- raise APITimeoutError("test", 1.0)
202
-
203
- with patch('time.sleep') as mock_sleep:
204
- mock_sleep.side_effect = lambda x: sleep_times.append(x)
205
- with pytest.raises(APITimeoutError):
206
- failing_function()
207
-
208
- assert len(sleep_times) == 2
209
- # First delay should be around 0.1s (with jitter)
210
- assert 0.09 <= sleep_times[0] <= 0.11
211
- # Second delay should be around 0.2s (with jitter)
212
- assert 0.18 <= sleep_times[1] <= 0.22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_errors.py DELETED
@@ -1,204 +0,0 @@
1
- """Tests for security error handling."""
2
-
3
- import pytest
4
- from datetime import datetime, timedelta, timezone
5
-
6
- from tools.errors import (
7
- SecurityError,
8
- AuthenticationError,
9
- AuthorizationError,
10
- InvalidKeyError,
11
- KeyExpiredError,
12
- RateLimitError,
13
- EncryptionError,
14
- ValidationError,
15
- ConfigurationError,
16
- )
17
- from tools.security import KeyManager
18
-
19
-
20
- def test_security_error_base():
21
- """Test base security error."""
22
- error = SecurityError("Test error")
23
- assert str(error) == "[SECURITY_ERROR] Test error"
24
- assert error.message == "Test error"
25
- assert error.code == "SECURITY_ERROR"
26
-
27
-
28
- def test_authentication_error():
29
- """Test authentication error."""
30
- error = AuthenticationError("Invalid credentials")
31
- assert str(error) == "[AUTH_ERROR] Invalid credentials"
32
- assert error.message == "Invalid credentials"
33
- assert error.code == "AUTH_ERROR"
34
-
35
-
36
- def test_authorization_error():
37
- """Test authorization error."""
38
- error = AuthorizationError("Insufficient permissions")
39
- assert str(error) == "[ACCESS_DENIED] Insufficient permissions"
40
- assert error.message == "Insufficient permissions"
41
- assert error.code == "ACCESS_DENIED"
42
-
43
-
44
- def test_invalid_key_error():
45
- """Test invalid key error."""
46
- error = InvalidKeyError("Key not found")
47
- assert str(error) == "[INVALID_KEY] Key not found"
48
- assert error.message == "Key not found"
49
- assert error.code == "INVALID_KEY"
50
-
51
-
52
- def test_key_expired_error():
53
- """Test key expired error."""
54
- error = KeyExpiredError("Key has expired")
55
- assert str(error) == "[KEY_EXPIRED] Key has expired"
56
- assert error.message == "Key has expired"
57
- assert error.code == "KEY_EXPIRED"
58
-
59
-
60
- def test_rate_limit_error():
61
- """Test rate limit error."""
62
- error = RateLimitError("Too many requests", wait_time=1.5)
63
- assert str(error) == "[RATE_LIMITED] Too many requests"
64
- assert error.message == "Too many requests"
65
- assert error.code == "RATE_LIMITED"
66
- assert error.wait_time == 1.5
67
-
68
-
69
- def test_encryption_error():
70
- """Test encryption error."""
71
- error = EncryptionError("Failed to decrypt")
72
- assert str(error) == "[ENCRYPTION_ERROR] Failed to decrypt"
73
- assert error.message == "Failed to decrypt"
74
- assert error.code == "ENCRYPTION_ERROR"
75
-
76
-
77
- def test_validation_error():
78
- """Test validation error."""
79
- # Without field
80
- error = ValidationError("Invalid value")
81
- assert str(error) == "[VALIDATION_ERROR] Invalid value"
82
- assert error.message == "Invalid value"
83
- assert error.code == "VALIDATION_ERROR"
84
- assert error.field is None
85
-
86
- # With field
87
- error = ValidationError("Invalid format", field="api_key")
88
- assert str(error) == "[VALIDATION_ERROR] api_key: Invalid format"
89
- assert error.message == "api_key: Invalid format"
90
- assert error.code == "VALIDATION_ERROR"
91
- assert error.field == "api_key"
92
-
93
-
94
- def test_configuration_error():
95
- """Test configuration error."""
96
- error = ConfigurationError("Invalid settings")
97
- assert str(error) == "[CONFIG_ERROR] Invalid settings"
98
- assert error.message == "Invalid settings"
99
- assert error.code == "CONFIG_ERROR"
100
-
101
-
102
- @pytest.fixture
103
- def key_manager():
104
- """Create test key manager."""
105
- return KeyManager(keys_file="test_keys.json")
106
-
107
-
108
- def test_invalid_key_handling(key_manager):
109
- """Test handling of invalid keys."""
110
- # Test adding invalid key
111
- with pytest.raises(ValidationError) as exc:
112
- key_manager.add_key("test", "invalid@key")
113
- assert "API key must contain only" in str(exc.value)
114
- assert exc.value.field == "key"
115
-
116
- # Test getting non-existent key
117
- with pytest.raises(InvalidKeyError) as exc:
118
- key_manager.get_key("nonexistent")
119
- assert "Key 'nonexistent' not found" in str(exc.value)
120
-
121
- # Test removing non-existent key
122
- with pytest.raises(InvalidKeyError) as exc:
123
- key_manager.remove_key("nonexistent")
124
- assert "Key 'nonexistent' not found" in str(exc.value)
125
-
126
-
127
- def test_expired_key_handling(key_manager):
128
- """Test handling of expired keys."""
129
- # Add key that expires in 1 second
130
- expires_at = datetime.now(timezone.utc) + timedelta(seconds=1)
131
- key_manager.add_key("test", "valid-key-123", expires_at=expires_at)
132
-
133
- # Key should be valid initially
134
- key = key_manager.get_key("test")
135
- assert key is not None
136
- assert key.is_valid
137
-
138
- # Wait for key to expire
139
- import time
140
- time.sleep(1.1)
141
-
142
- # Key should raise expired error
143
- with pytest.raises(KeyExpiredError) as exc:
144
- key_manager.get_key("test")
145
- assert "Key 'test' has expired" in str(exc.value)
146
-
147
-
148
- @pytest.mark.asyncio
149
- async def test_rate_limit_handling(key_manager):
150
- """Test handling of rate limits."""
151
- # Add key with low rate limit
152
- key_manager.add_key(
153
- "test",
154
- "valid-key-123",
155
- rate_limit=1.0, # 1 request per second
156
- burst_limit=1
157
- )
158
-
159
- # First request should succeed
160
- wait_time = await key_manager.check_rate_limit("test")
161
- assert wait_time == 0
162
-
163
- # Second request should be rate limited
164
- with pytest.raises(RateLimitError) as exc:
165
- await key_manager.check_rate_limit("test")
166
- assert "Rate limit exceeded" in str(exc.value)
167
- assert exc.value.wait_time > 0
168
-
169
-
170
- def test_encryption_handling(key_manager):
171
- """Test handling of encryption errors."""
172
- # Test invalid encryption key
173
- with pytest.raises(EncryptionError) as exc:
174
- KeyManager(encryption_key="invalid-key")
175
- assert "Invalid base64 encoding" in str(exc.value)
176
-
177
- # Test corrupted keys file
178
- with open("test_keys.json", "wb") as f:
179
- f.write(b"corrupted data")
180
-
181
- with pytest.raises(EncryptionError) as exc:
182
- key_manager._load_keys()
183
- assert "Failed to decrypt keys file" in str(exc.value)
184
-
185
-
186
- def test_configuration_handling(key_manager):
187
- """Test handling of configuration errors."""
188
- # Test invalid environment variable
189
- import os
190
- os.environ["API_TEST_KEY"] = "invalid@key"
191
- os.environ["API_TEST_RATE_LIMIT"] = "invalid"
192
-
193
- with pytest.raises(ConfigurationError) as exc:
194
- key_manager.get_key("test")
195
- assert "Invalid environment configuration" in str(exc.value)
196
-
197
- # Test invalid rate limits
198
- with pytest.raises(ValidationError) as exc:
199
- key_manager.add_key("test", "valid-key-123", rate_limit=0)
200
- assert "Rate limit must be between" in str(exc.value)
201
-
202
- with pytest.raises(ValidationError) as exc:
203
- key_manager.add_key("test", "valid-key-123", burst_limit=0)
204
- assert "Burst limit must be at least" in str(exc.value)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_gradio_ui.py DELETED
@@ -1,92 +0,0 @@
1
- """Tests for the Gradio UI implementation."""
2
-
3
- from typing import List, Optional, Generator, Any, Dict
4
- import os
5
- import tempfile
6
- try:
7
- import gradio as gr
8
- except ImportError:
9
- print("Warning: gradio package not found. Please install it with 'pip install gradio'")
10
- from Gradio_UI import GradioUI
11
-
12
- class MockTool:
13
- """Mock tool for testing."""
14
- def __init__(self) -> None:
15
- self.description: str = "A mock tool for testing"
16
- self.name: str = "mock_tool"
17
- self.inputs: Dict[str, Dict[str, str]] = {
18
- "input_text": {
19
- "type": "string",
20
- "description": "Input text to process"
21
- }
22
- }
23
- self.output_type: str = "string"
24
-
25
- def run(self, input_text: str) -> str:
26
- """Process input text."""
27
- return f"Processed: {input_text}"
28
-
29
- class MockModel:
30
- """Mock model for testing."""
31
- def generate(self, prompt: str) -> str:
32
- """Generate a response to the prompt."""
33
- return f"Response to: {prompt}"
34
-
35
- class MockAgent:
36
- """Mock agent for testing."""
37
- def __init__(self) -> None:
38
- self.tools: List[MockTool] = [MockTool()]
39
- self.model: MockModel = MockModel()
40
-
41
- def run(
42
- self,
43
- task: str,
44
- stream: bool = False,
45
- reset: bool = False,
46
- additional_args: Optional[Dict[str, Any]] = None
47
- ) -> List[str]:
48
- """Run the agent with the given task."""
49
- return [self.model.generate(task)]
50
-
51
- def test_gradio_ui_initialization() -> None:
52
- """Test GradioUI initialization."""
53
- agent = MockAgent()
54
- ui = GradioUI(agent)
55
- assert ui.agent == agent
56
-
57
- def test_gradio_ui_interaction() -> None:
58
- """Test GradioUI interaction."""
59
- agent = MockAgent()
60
- ui = GradioUI(agent)
61
- messages: List[Dict[str, Any]] = []
62
- response_generator: Generator[List[Dict[str, Any]], None, None] = ui.interact_with_agent("Hello", messages)
63
- responses: List[List[Dict[str, Any]]] = list(response_generator)
64
- assert len(responses) > 0
65
- assert any("Hello" in str(msg) for msg in responses[0])
66
-
67
- def test_gradio_ui_file_upload() -> None:
68
- """Test GradioUI file upload."""
69
- # Create a temporary directory for file uploads
70
- with tempfile.TemporaryDirectory() as temp_dir:
71
- agent = MockAgent()
72
- ui = GradioUI(agent, file_upload_folder=temp_dir)
73
-
74
- # Create a test file
75
- test_file_path = os.path.join(temp_dir, "test.txt")
76
- with open(test_file_path, "w") as f:
77
- f.write("Test content")
78
-
79
- # Create a mock file object
80
- class MockFile:
81
- def __init__(self, path: str) -> None:
82
- self.name: str = path
83
-
84
- mock_file = MockFile(test_file_path)
85
- file_uploads_log: List[str] = []
86
-
87
- # Test file upload
88
- result, log = ui.upload_file(mock_file, file_uploads_log)
89
- assert isinstance(result, gr.Textbox)
90
- assert "File uploaded:" in result.value
91
- assert len(log) == 1 # One file should be added to the log
92
- assert os.path.splitext(os.path.basename(test_file_path))[0] in os.path.basename(log[0]) # The file name (without extension) should be in the log
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_health.py DELETED
@@ -1,136 +0,0 @@
1
- """
2
- Tests for the health check functionality.
3
- """
4
-
5
- import pytest
6
- from datetime import datetime, timedelta
7
- from unittest.mock import Mock, patch
8
-
9
- from tools.health import HealthCheck, HealthStatus
10
- from tools.error_recovery import CircuitBreaker, CircuitBreakerState
11
-
12
- @pytest.fixture
13
- def health_check() -> HealthCheck:
14
- """Create a health check instance for testing."""
15
- return HealthCheck(
16
- circuit_breaker=CircuitBreaker(),
17
- memory_threshold=90.0,
18
- cpu_threshold=80.0
19
- )
20
-
21
- def test_health_status_init() -> None:
22
- """Test health status initialization."""
23
- status = HealthStatus()
24
- assert status.status == "healthy"
25
- assert isinstance(status.timestamp, datetime)
26
- assert status.checks == {}
27
- assert status.details == {}
28
-
29
- @patch("psutil.virtual_memory")
30
- @patch("psutil.cpu_percent")
31
- def test_check_system_resources(mock_cpu: Mock, mock_memory: Mock, health_check: HealthCheck) -> None:
32
- """Test system resource check."""
33
- # Mock healthy system
34
- mock_memory.return_value.percent = 50.0
35
- mock_cpu.return_value = 30.0
36
-
37
- result = health_check.check_system_resources()
38
- assert result["status"] == "healthy"
39
- assert result["details"]["memory_usage"] == 50.0
40
- assert result["details"]["cpu_usage"] == 30.0
41
-
42
- # Mock degraded system
43
- mock_memory.return_value.percent = 95.0
44
- mock_cpu.return_value = 85.0
45
-
46
- result = health_check.check_system_resources()
47
- assert result["status"] == "degraded"
48
- assert result["details"]["memory_usage"] == 95.0
49
- assert result["details"]["cpu_usage"] == 85.0
50
-
51
- def test_check_circuit_breaker(health_check: HealthCheck) -> None:
52
- """Test circuit breaker status check."""
53
- # Test with no open circuits
54
- result = health_check.check_circuit_breaker()
55
- assert result["status"] == "healthy"
56
- assert result["details"]["total_circuits"] == 0
57
- assert result["details"]["open_circuits"] == []
58
-
59
- # Test with open circuit
60
- state = CircuitBreakerState(
61
- is_open=True,
62
- failure_count=5,
63
- last_failure_time=datetime.utcnow(),
64
- endpoints={"api/weather"}
65
- )
66
- health_check.circuit_breaker._states["test_circuit"] = state
67
-
68
- result = health_check.check_circuit_breaker()
69
- assert result["status"] == "degraded"
70
- assert result["details"]["total_circuits"] == 1
71
- assert len(result["details"]["open_circuits"]) == 1
72
- assert result["details"]["open_circuits"][0]["key"] == "test_circuit"
73
- assert result["details"]["open_circuits"][0]["failures"] == 5
74
- assert "api/weather" in result["details"]["open_circuits"][0]["endpoints"]
75
-
76
- @patch("tools.health.requests")
77
- @patch("tools.health.pydantic")
78
- @patch("tools.health.typing_extensions")
79
- def test_check_dependencies(
80
- mock_typing: Mock,
81
- mock_pydantic: Mock,
82
- mock_requests: Mock,
83
- health_check: HealthCheck
84
- ) -> None:
85
- """Test dependency check."""
86
- # Mock healthy dependencies
87
- mock_requests.__version__ = "2.31.0"
88
- mock_pydantic.__version__ = "2.5.2"
89
- mock_typing.__version__ = "4.8.0"
90
-
91
- result = health_check.check_dependencies()
92
- assert result["status"] == "healthy"
93
- assert result["details"]["dependencies"]["requests"] == "2.31.0"
94
- assert result["details"]["dependencies"]["pydantic"] == "2.5.2"
95
- assert result["details"]["dependencies"]["typing_extensions"] == "4.8.0"
96
-
97
- # Test with missing dependency
98
- mock_requests.__version__ = None
99
- result = health_check.check_dependencies()
100
- assert result["status"] == "unhealthy"
101
- assert "error" in result["details"]
102
-
103
- def test_get_uptime(health_check: HealthCheck) -> None:
104
- """Test uptime information."""
105
- # Set start time to 1 hour ago
106
- health_check.start_time = datetime.utcnow() - timedelta(hours=1)
107
-
108
- result = health_check.get_uptime()
109
- assert result["status"] == "healthy"
110
- assert "start_time" in result["details"]
111
- assert result["details"]["uptime_seconds"] >= 3600
112
- assert "uptime_human" in result["details"]
113
-
114
- def test_check_health(health_check: HealthCheck) -> None:
115
- """Test comprehensive health check."""
116
- with patch.multiple(
117
- health_check,
118
- check_system_resources=Mock(return_value={"status": "healthy", "details": {}}),
119
- check_circuit_breaker=Mock(return_value={"status": "healthy", "details": {}}),
120
- check_dependencies=Mock(return_value={"status": "healthy", "details": {}}),
121
- get_uptime=Mock(return_value={"status": "healthy", "details": {}})
122
- ):
123
- status = health_check.check_health()
124
- assert status.status == "healthy"
125
- assert len(status.checks) == 4
126
- assert all(check["status"] == "healthy" for check in status.checks.values())
127
-
128
- # Test degraded status
129
- health_check.check_system_resources.return_value = {"status": "degraded", "details": {}}
130
- status = health_check.check_health()
131
- assert status.status == "degraded"
132
-
133
- # Test unhealthy status
134
- health_check.check_dependencies.return_value = {"status": "unhealthy", "details": {}}
135
- status = health_check.check_health()
136
- assert status.status == "unhealthy"