Spaces:
Sleeping
Sleeping
reset
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .editorconfig +0 -52
- .flake8 +0 -65
- .gitattributes +0 -35
- .github/ISSUE_TEMPLATE/bug_report.md +0 -42
- .github/ISSUE_TEMPLATE/config.yml +0 -14
- .github/ISSUE_TEMPLATE/documentation.md +0 -36
- .github/ISSUE_TEMPLATE/feature_request.md +0 -42
- .github/PULL_REQUEST_TEMPLATE.md +0 -62
- .github/workflows/deploy.yml +0 -145
- .github/workflows/lint.yml +0 -98
- .github/workflows/test.yml +0 -102
- .pre-commit-config.yaml +0 -170
- CHANGELOG.md +0 -69
- CONTRIBUTING.md +0 -223
- Gradio_UI.py +66 -289
- LICENSE +0 -21
- README.md +27 -675
- SECURITY.md +0 -283
- STYLE_GUIDE.md +0 -220
- agent.json +0 -156
- app.py +54 -581
- config.py +0 -451
- docs/api.md +0 -385
- docs/api_reference.md +0 -143
- docs/error_handling.md +0 -201
- docs/errors.md +0 -434
- docs/getting_started.md +0 -202
- docs/logging.md +0 -231
- docs/security_quickstart.md +0 -130
- docs/user_guide.md +0 -813
- instructions +202 -0
- prompts.yaml +20 -329
- pyproject.toml +0 -107
- pyrightconfig.json +0 -32
- pytest.ini +0 -45
- rate_limiter.py +0 -32
- requirements-dev.txt +0 -0
- requirements.txt +6 -48
- scripts/run_tests.py +0 -96
- setup.py +0 -148
- test_app.py +0 -124
- tests/integration/test_assistant.py +0 -402
- tests/test_api_logging.py +0 -220
- tests/test_api_optimization.py +0 -209
- tests/test_cache.py +0 -197
- tests/test_cache_invalidation.py +0 -221
- tests/test_error_recovery.py +0 -212
- tests/test_errors.py +0 -204
- tests/test_gradio_ui.py +0 -92
- tests/test_health.py +0 -136
.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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 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 |
-
|
| 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.
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
self,
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
],
|
| 211 |
-
|
| 212 |
-
""
|
| 213 |
-
|
| 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 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
chatbot = gr.Chatbot(
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
avatar_images=(
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
),
|
| 274 |
-
resizeable=True,
|
| 275 |
-
scale=1,
|
| 276 |
)
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
[upload_status, file_uploads_log],
|
| 285 |
)
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
demo.launch(
|
| 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 |
-
|
| 19 |
-
|
| 20 |
-
# AI Assistant Framework
|
| 21 |
-
|
| 22 |
-
[](https://github.com/yourusername/ai-assistant/actions/workflows/test.yml)
|
| 23 |
-
[](https://github.com/yourusername/ai-assistant/actions/workflows/test.yml)
|
| 24 |
-
[](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 |
-
|
| 31 |
-
-
|
| 32 |
-
-
|
| 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 |
-
|
| 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 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
API_RETRY_MAX_WAIT=30.0
|
| 610 |
-
API_RETRY_ENABLED=true
|
| 611 |
-
```
|
| 612 |
|
| 613 |
-
##
|
| 614 |
|
| 615 |
-
|
|
|
|
|
|
|
|
|
|
| 616 |
|
| 617 |
-
|
| 618 |
-
{
|
| 619 |
-
"config": {
|
| 620 |
-
"base_url": "http://api.example.com"
|
| 621 |
-
}
|
| 622 |
-
}
|
| 623 |
-
```
|
| 624 |
|
| 625 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 626 |
|
| 627 |
-
|
| 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 |
-
|
| 650 |
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 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 |
-
|
| 670 |
|
| 671 |
-
|
| 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 |
-
|
| 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
|
| 42 |
-
import
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
#
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 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 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
|
| 589 |
-
|
| 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 |
-
|
| 2 |
-
You are
|
| 3 |
-
|
| 4 |
-
|
| 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 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
|
|
|
| 316 |
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
"report": |-
|
| 320 |
-
Here is the final answer from your managed agent '{{name}}':
|
| 321 |
-
{{final_answer}}
|
| 322 |
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
|
| 328 |
user: |
|
| 329 |
-
{
|
| 330 |
|
| 331 |
-
|
| 332 |
-
|
|
|
|
| 333 |
|
| 334 |
-
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
pytz>=
|
| 5 |
-
|
| 6 |
-
|
| 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"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|