231 Commits

Author SHA1 Message Date
kennethreitz 052781014d bump version to 0.3.2 and improve Deepseek provider 2025-01-27 11:54:03 -05:00
kennethreitz db28f1195c bump version to 0.3.1 in pyproject.toml 2025-01-27 11:49:51 -05:00
kennethreitz b0a7197c6e Update changelog to reflect introduction of Deepseek provider in version 0.3.1 2025-01-27 11:49:29 -05:00
kennethreitz 7684c2568b Update changelog to include Deepseek provider introduction 2025-01-27 11:48:53 -05:00
kennethreitz 8b90dbba40 Add Deepseek provider information to README 2025-01-27 11:48:23 -05:00
kennethreitz 752ccb1de8 Merge pull request #55 from jin10086/deepseek
add llm_provider  Deepseek
2025-01-27 11:46:57 -05:00
kennethreitz 391bfaaeab bump version to 0.3.0 and update changelog for conversation save/load functionality 2025-01-11 08:42:32 -05:00
kennethreitz d963bc0b1c Update simplemind/providers/deepseek.py
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-01-10 11:40:20 -05:00
jin10086 0c1f225252 add llm_provider Deepseek
docs: https://api-docs.deepseek.com/
2025-01-10 09:33:15 +08:00
kennethreitz 4decaa0722 Merge pull request #51 from lucianosrp/feat/save-conversation
feat: add conversation save/load functionality
2025-01-09 17:08:14 -05:00
kennethreitz 39b5a5e19d Merge pull request #54 from gabrielmotaa/fix/missing-dependency-gemini
Missing dependency when using Gemini
2024-12-10 17:28:55 -05:00
Gabriel da Mota ef38fea767 fix missing dependency 2024-12-09 11:28:43 -03:00
Luciano 3aacfd51ee Merge branch 'main' into feat/save-conversation 2024-11-26 16:38:56 +01:00
Luciano Scarpulla a2991eec0c add conversation save/load functionality 2024-11-26 23:37:30 +08:00
kennethreitz 9ae9a2703a Merge pull request #49 from wei840222/main
Fix: Ollama error TypeError: 'Chat' object is not callable
2024-11-22 12:07:00 -05:00
wei840222 0661b097d2 fix: Ollama error TypeError: 'Chat' object is not callable 2024-11-23 00:56:33 +08:00
kennethreitz fad442ba3f Update FUNDING.yml 2024-11-17 06:29:05 -05:00
kennethreitz 5b9624c385 Remove Ko-fi and thanks_dev entries from funding configuration 2024-11-17 06:26:10 -05:00
kennethreitz 8ff0521e17 Add funding configuration for project contributors 2024-11-17 06:25:14 -05:00
kennethreitz d5bdb712e9 Add tool_calling.py and test_tools.py 2024-11-15 20:29:35 -05:00
Luciano Scarpulla a97f9be2c8 fix openai 2024-11-15 12:09:39 +08:00
Luciano Scarpulla 107f983a18 add openai 2024-11-14 17:25:34 +08:00
Luciano Scarpulla 2404e2c977 some refactoring 2024-11-13 18:05:51 +08:00
Luciano Scarpulla c87a598286 fix import 2024-11-13 17:55:46 +08:00
Luciano Scarpulla 9662b60177 add decorator test 2024-11-13 17:54:58 +08:00
Luciano Scarpulla ea997aae7b add tool decorator and example 2024-11-13 12:24:02 +08:00
Luciano Scarpulla 081baf203c add README section 2024-11-12 12:18:33 +08:00
Luciano Scarpulla 4cb18e9e3b re-add changes from main 2024-11-12 11:54:24 +08:00
Luciano 0462ea0e38 Merge branch 'main' into feat-function-calling 2024-11-12 11:50:55 +08:00
Luciano Scarpulla 8492ec9456 add base edits 2024-11-12 11:49:06 +08:00
Luciano Scarpulla 1709055e1a first basic working version (anthropic) 2024-11-12 11:48:27 +08:00
kennethreitz 5fa67c3b2f Update CHANGELOG.md and pyproject.toml for version 0.2.4 2024-11-11 11:38:03 -05:00
kennethreitz b7e950a8f0 Refactor imports in amazon.py 2024-11-11 11:37:30 -05:00
kennethreitz 735c6ba665 Bump version to 0.2.3 in pyproject.toml 2024-11-11 11:30:11 -05:00
kennethreitz 9132030cbd Update CHANGELOG.md to remove default max-tokens for OpenAI provider 2024-11-11 11:30:11 -05:00
kennethreitz aeea8936ce Merge pull request #42 from Siddhesh-Agarwal/main
Removed redundant variables
2024-11-11 11:30:02 -05:00
Luciano Scarpulla c2303114ab fix base 2024-11-11 12:40:20 +08:00
Luciano Scarpulla fe5af93780 first draft 2024-11-11 12:29:00 +08:00
Siddhesh Agarwal e79b474215 fixed dependencies 2024-11-10 20:05:49 +05:30
Siddhesh Agarwal fe2ca9d5f5 black + isort formatting 2024-11-10 20:00:13 +05:30
Siddhesh Agarwal 670240b943 removed reduntant variables. moved few inside the class 2024-11-10 19:59:52 +05:30
kennethreitz 2e66c0232b Refactor EnhancedContextPlugin to remove unnecessary imports and enable Logfire for debugging 2024-11-07 10:08:14 -05:00
kennethreitz 8b1f63f796 Refactor EnhancedContextPlugin to include command autocompletion and history 2024-11-07 09:34:36 -05:00
kennethreitz 5d7a917d23 Refactor EnhancedContextPlugin to include /lumina command and clipboard copy/paste functionality 2024-11-07 08:49:37 -05:00
kennethreitz 9703332967 Refactor EnhancedContextPlugin to include user/llm breakdown in entity context 2024-11-07 08:37:03 -05:00
kennethreitz fe6001e710 Refactor EnhancedContextPlugin to include custom command autocompletion 2024-11-07 07:58:48 -05:00
kennethreitz 63343d1c61 Refactor EnhancedContextPlugin to simplify command list and remove unnecessary commands 2024-11-07 07:53:48 -05:00
kennethreitz ece056a5e0 Refactor EnhancedContextPlugin to include command autocompletion and history 2024-11-07 07:53:29 -05:00
kennethreitz f44ec977a4 Refactor EnhancedContextPlugin to include clipboard copy and paste functionality 2024-11-07 07:50:33 -05:00
kennethreitz 33f8fcde11 Refactor EnhancedContextPlugin to format output using markdown and improve command handling 2024-11-07 07:36:23 -05:00
kennethreitz 598bcd514d Refactor EnhancedContextPlugin to include list of all topics mentioned 2024-11-07 07:27:59 -05:00
kennethreitz 8bdbe4d8d5 Refactor EnhancedContextPlugin to simplify memory system message and remove explicit memory creation and reminder functionality 2024-11-07 07:09:39 -05:00
kennethreitz d4068cf07a Refactor EnhancedContextPlugin to simplify memory system message and remove explicit memory creation and reminder functionality 2024-11-07 07:01:29 -05:00
kennethreitz 747488f633 Refactor EnhancedContextPlugin to simplify memory system message and remove explicit memory creation and reminder functionality 2024-11-07 06:39:08 -05:00
kennethreitz 9ae03685b5 Refactor .gitignore to include enhanced_context_sarah.db 2024-11-06 13:03:09 -05:00
kennethreitz 91af281a9d Refactor OpenAI provider in simplemind
Update the DEFAULT_MAX_TOKENS and DEFAULT_KWARGS variables in the OpenAI provider module in simplemind. Set DEFAULT_MAX_TOKENS to None and DEFAULT_KWARGS to an empty dictionary. This refactor allows for more flexibility in configuring the OpenAI provider.
2024-11-06 13:00:39 -05:00
kennethreitz 309f390800 Refactor EnhancedContextPlugin to handle command line arguments for LLM provider selection and model specification 2024-11-06 13:00:23 -05:00
kennethreitz b316352311 Refactor EnhancedContextPlugin to handle command line arguments for LLM provider selection and store datetime in SQLite format, handle datetime strings properly, and extract/store entities for context. Add memory system message to conversation initialization and implement memory creation and reminder functionality in post-response hook. Implement LLM memory storage from the LLM's perspective. 2024-11-06 11:59:36 -05:00
kennethreitz 236020b3b9 Refactor EnhancedContextPlugin to handle command line arguments for LLM provider selection 2024-11-06 11:32:36 -05:00
kennethreitz 8a5a29f864 Refactor EnhancedContextPlugin to handle command line arguments for LLM provider selection 2024-11-06 10:34:26 -05:00
kennethreitz 30d8412bbf Refactor LLM provider and model in enhanced_context.py 2024-11-06 09:57:27 -05:00
kennethreitz 4a852e6220 Refactor LLM provider and model in enhanced_context.py 2024-11-06 09:15:54 -05:00
kennethreitz 7f5ba667bd Refactor EnhancedContextPlugin to store datetime in SQLite format, handle datetime strings properly, and extract/store entities for context 2024-11-06 09:13:59 -05:00
kennethreitz 4b87a8b91c Refactor EnhancedContextPlugin to store datetime in SQLite format, handle datetime strings properly, and extract/store entities for context 2024-11-06 09:03:38 -05:00
kennethreitz 4c1d1fa873 Refactor EnhancedContextPlugin to store datetime in SQLite format and handle datetime strings properly 2024-11-06 08:46:21 -05:00
kennethreitz 0087a7e8f2 Refactor enhanced_context.py and update requirements.txt 2024-11-06 08:42:43 -05:00
kennethreitz 07715ed8df Refactor enhanced_context.py and update requirements.txt 2024-11-06 08:30:29 -05:00
kennethreitz 03f91c5153 Refactor enhanced_context.py and update requirements.txt 2024-11-06 08:18:27 -05:00
kennethreitz aa601648c6 Refactor EnhancedContextPlugin to extract and store entities for context 2024-11-06 08:09:15 -05:00
kennethreitz a26c51014b Refactor enhanced_context.py and update requirements.txt 2024-11-06 08:08:43 -05:00
kennethreitz b3946f1ff9 Refactor mood_detector_plugin.py by removing unused import 2024-11-06 08:00:25 -05:00
kennethreitz 7a84ade5a4 Add MoodDetectorPlugin to examples/mood_detector_plugin.py and update requirements.txt 2024-11-06 08:00:09 -05:00
kennethreitz 3e1d1f98ad Refactor InspirationPlugin in examples/inspiration_plugin.py 2024-11-06 07:54:42 -05:00
kennethreitz 48e6ef2a43 Add InspirationPlugin to examples/inspiration_plugin.py and allow extra fields in BasePlugin's Config 2024-11-06 07:54:11 -05:00
kennethreitz 1528dc2a21 Revert "Merge pull request #39 from lucianosrp/fix-sys-role-anthropic"
This reverts commit 46cd19ea90, reversing
changes made to 3e8d5662d2.
2024-11-06 07:42:55 -05:00
kennethreitz 46cd19ea90 Merge pull request #39 from lucianosrp/fix-sys-role-anthropic
fix: anthropic system message
2024-11-06 07:42:07 -05:00
Luciano 2848e86dce Update simplemind/providers/anthropic.py
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-11-06 02:25:07 +01:00
Luciano Scarpulla 6aadc9fcd7 resolve conflict 2024-11-06 09:07:17 +08:00
Luciano Scarpulla a8792319a8 add sys prompt handling 2024-11-06 09:00:27 +08:00
kennethreitz 3e8d5662d2 Merge pull request #38 from SZubarev/fix/amazon-inference
use inference profile with Claude 3.5 on Bedrock
2024-11-05 16:48:48 -05:00
Stan Zubarev 51c1646ef4 use inference profile with Claude 3.5 on Bedrock 2024-11-05 16:41:08 -05:00
kennethreitz f09052c18e rename 2024-11-04 11:22:10 -05:00
kennethreitz 1d3ae26301 Merge pull request #31 from Siddhesh-Agarwal/main
[Add to cookbook]: Multi-LLM Discussion
2024-11-04 11:21:37 -05:00
kennethreitz 44fd3468fa Revert "Merge pull request #37 from lucianosrp/fix-sys-role-anthropic"
This reverts commit 5770c37edf, reversing
changes made to a5c7486dfc.
2024-11-04 11:21:07 -05:00
kennethreitz 5770c37edf Merge pull request #37 from lucianosrp/fix-sys-role-anthropic
fix: anthropic system message
2024-11-04 11:14:20 -05:00
kennethreitz 37334a21c5 Update simplemind/providers/anthropic.py
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-11-04 11:14:02 -05:00
Luciano Scarpulla 57d54abf24 remove unrelted changes 2024-11-04 17:23:48 +08:00
Luciano Scarpulla c3397488e3 fix: antropic system message 2024-11-04 17:18:59 +08:00
Siddhesh Agarwal 678a8a8b32 Merge branch 'kennethreitz:main' into main 2024-11-04 10:20:44 +05:30
kennethreitz a5c7486dfc oops 2024-11-03 10:17:36 -05:00
kennethreitz 5c6650f2b2 add it to the repo 2024-11-03 10:17:20 -05:00
kennethreitz 549d74e146 Refactor recipe printing for better formatting and styling 2024-11-03 07:24:16 -05:00
kennethreitz 328be94677 Revert "Merge pull request #35 from barisozmen/logger-for-streaming"
This reverts commit d7f8418f23, reversing
changes made to cb73621e39.
2024-11-03 07:17:44 -05:00
kennethreitz 7b21b9f258 improve logging for streaming functions 2024-11-03 07:08:03 -05:00
kennethreitz d7f8418f23 Merge pull request #35 from barisozmen/logger-for-streaming
[Suggestion] Improved logging to handle streaming functions.
2024-11-03 07:07:14 -05:00
kennethreitz 9968f162d6 Update simplemind/logging.py
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-11-03 07:07:04 -05:00
kennethreitz cb73621e39 rename file 2024-11-03 07:03:51 -05:00
kennethreitz 4721dd8cc0 Add Bible Explorer web app with endpoints for retrieving Bible verses and chapter counts 2024-11-03 07:03:09 -05:00
kennethreitz bdb1ff0e69 Update default model for Amazon provider 2024-11-03 06:56:26 -05:00
kennethreitz 94f381032e Update DEFAULT_MODEL in amazon.py 2024-11-03 06:55:56 -05:00
kennethreitz b3a35cadd4 Update DEFAULT_MODEL in amazon.py 2024-11-03 06:55:37 -05:00
kennethreitz 718f5a66c0 Refactor AI model sessions and user input handling 2024-11-03 06:53:26 -05:00
Siddhesh Agarwal df02547dec Create multi-LLM-discussion.py 2024-11-03 08:51:37 +05:30
Barış Özmen 9dd89b7ef1 logging now takes into account streaming functions + logfire API now uses span and structured outputs 2024-11-03 01:07:58 +03:00
kennethreitz 15ee5d1cf9 Add four-way conversation example 2024-11-02 17:27:09 -04:00
kennethreitz 25ba1a9289 update function annotations 2024-11-02 17:15:03 -04:00
kennethreitz 22aff505c4 Update README.md 2024-11-02 17:11:20 -04:00
kennethreitz 29b2008edf Update README.md 2024-11-02 17:10:45 -04:00
kennethreitz c5c99a05fd Update README.md 2024-11-02 17:06:47 -04:00
kennethreitz cb969dec4c Update README.md 2024-11-02 17:06:12 -04:00
kennethreitz 1aeeb9127d Update README.md 2024-11-02 17:05:02 -04:00
kennethreitz c21f68aad6 Add streaming support to Gemini provider and update example script 2024-11-02 16:54:49 -04:00
kennethreitz a68bd74fd8 Add streaming support and update documentation 2024-11-02 16:54:36 -04:00
kennethreitz 4d2c81850e Add streaming support to Groq provider and implement generate_stream_text method 2024-11-02 16:51:35 -04:00
kennethreitz 64246658b0 Add streaming support to Amazon provider 2024-11-02 16:50:09 -04:00
kennethreitz f0aff7814b Add streaming support to Amazon provider and implement generate_stream_text method 2024-11-02 16:49:55 -04:00
kennethreitz 72121c121d Add streaming support to Gemini provider and implement generate_stream_text method 2024-11-02 16:48:59 -04:00
kennethreitz 028e89b080 Refactor generate_stream_text.py to use the "gemini" provider for streaming text generation 2024-11-02 16:48:54 -04:00
kennethreitz e13d03f40b Add streaming support to Anthropic provider and implement generate_stream_text method 2024-11-02 16:46:58 -04:00
kennethreitz 0fc49c7e13 Add streaming support to Ollama provider and implement generate_stream_text method 2024-11-02 16:44:32 -04:00
kennethreitz d6afbd1fd0 Add streaming support to XAI provider and update example usage 2024-11-02 16:42:28 -04:00
kennethreitz 27d30ccfe8 Add OpenAI streaming support and enhance provider properties 2024-11-02 16:34:47 -04:00
kennethreitz b6b1a4f9f3 Add streaming support to generate_text method and refactor related functions 2024-11-02 16:32:30 -04:00
Barış Özmen 36d6ca4a11 example of stream text generation on openai added 2024-11-02 22:15:23 +03:00
Barış Özmen 90593d7919 genereate_stream_text() method added to simplemind 2024-11-02 22:15:23 +03:00
Barış Özmen efe1a62d73 text streaming ability added to OpenAI provider 2024-11-02 22:15:23 +03:00
kennethreitz 92819112bb Refactor medication data example to use Rich formatting for improved readability 2024-11-02 12:27:51 -04:00
kennethreitz 275ab39d67 Add medication data example to the repository 2024-11-02 12:24:41 -04:00
kennethreitz 74db69c6e9 Refactor llm_provider and add AI perspective in bible_verses.py example 2024-11-02 12:09:17 -04:00
kennethreitz 7b633ce880 Refactor imports and update llm_provider in bible_verses.py example 2024-11-02 12:02:06 -04:00
kennethreitz a651afb8a6 Refactor imports in _context.py and add bible_verses.py example 2024-11-02 11:46:51 -04:00
kennethreitz 33e53562ae Refactor conversation creation and message handling
This commit refactors the `create_conversation` function in `simplemind/__init__.py` to use a more descriptive variable name (`conv`) for the conversation object. It also updates the `add_plugin` method to use the new variable name (`conv`) instead of `conversation`.

In `simplemind/models.py`, the `prepend_system_message` method now accepts an optional `meta` parameter. The method also adds a system message to the conversation by prepending it to the list of messages.

Additionally, the `add_message` method in `simplemind/models.py` has been modified to include type annotations and a default value for the `role` parameter. The method now requires the `text` parameter to be provided explicitly.

A new test file, `tests/test_conversations.py`, has been added to the repository. This file contains a test case for the `generate_data` function, which tests the functionality of different LLM providers.

Lastly, the test files `tests/test_generate_data.py` and `tests/test_generate_text.py` have been modified to remove the unused `Amazon` provider from the list of test cases.
2024-11-02 11:24:26 -04:00
kennethreitz 931285f8ce Update CHANGELOG.md to include version 0.2.2 and mention the default usage of system role in conv.prepend_system_message. 2024-11-02 11:24:07 -04:00
kennethreitz e47ada4598 Update version to v0.2.2 in conf.py and pyproject.toml 2024-11-02 11:08:52 -04:00
kennethreitz 7e83532765 Update CHANGELOG.md to include version 0.2.2 and mention the default usage of system role in conv.prepend_system_message. 2024-11-02 11:08:25 -04:00
kennethreitz 34e8a9d190 Update sentiment_analysis.py 2024-11-02 11:04:12 -04:00
kennethreitz c496712a9a Merge pull request #32 from lucianosrp/fix-sys-prompt
fix: `prepend_system_message` use system role by default
2024-11-02 10:54:47 -04:00
Luciano Scarpulla 3d8e169a08 make prepend_system_message use system reole by default 2024-11-02 22:46:54 +08:00
kennethreitz b74af7c8d8 Update discussion.py 2024-11-02 10:45:00 -04:00
kennethreitz fa3ee731df Refactor discussion.py to add MultiAIConversation class for orchestrating conversations between multiple AI models 2024-11-02 09:59:02 -04:00
kennethreitz 8e4fdb9832 Update README.md 2024-11-01 18:26:58 -04:00
kennethreitz 3d397d0474 Update README.md 2024-11-01 15:38:27 -04:00
kennethreitz 7508723469 Merge pull request #29 from barisozmen/fix-generate-data
Fix minor bug. generate_data() now passes kwargs to provider
2024-11-01 15:35:07 -04:00
Barış Özmen f2a3fd76ae fix minor bug. generate_data now passes the kwargs to provider 2024-11-01 22:10:19 +03:00
kennethreitz 089812e335 Update README.md 2024-11-01 12:14:47 -04:00
kennethreitz e977dd3eab Update README.md 2024-11-01 12:14:34 -04:00
kennethreitz e7aad65b37 Update README.md 2024-11-01 12:13:56 -04:00
kennethreitz a091a847a8 Update README.md 2024-11-01 12:13:46 -04:00
kennethreitz faca663825 Merge pull request #28 from barisozmen/add-examples
Add cooking recipe example
2024-11-01 12:02:23 -04:00
kennethreitz 825ab22b95 Refactor pyproject.toml to include additional dependencies 2024-11-01 12:01:05 -04:00
kennethreitz 18a51c7cd3 Refactor pyproject.toml to include additional dependencies 2024-11-01 12:00:19 -04:00
kennethreitz 65570bfede Add Dockerfile for project containerization 2024-11-01 11:56:18 -04:00
Barış Özmen c6c7f2ac09 add print result as comments 2024-11-01 18:42:54 +03:00
Barış Özmen cc6611647a Recipe example from readme added to examples, with a new pretty string formatting 2024-11-01 18:41:40 +03:00
Barış Özmen 8f9036fa32 fix bug. send_hook changed to pre_send_hook 2024-11-01 18:40:08 +03:00
kennethreitz b7b5e1e187 Bump version to 0.2.1 in pyproject.toml 2024-11-01 10:07:50 -04:00
kennethreitz 7220c8bd3f Refactor Amazon provider to use cached_property for client and structured_client 2024-11-01 10:07:45 -04:00
kennethreitz 176045531a Refactor Amazon provider to use cached_property for client and structured_client 2024-11-01 10:07:08 -04:00
kennethreitz 6dc9108836 Update README.md 2024-11-01 09:52:14 -04:00
kennethreitz e8c5ebc6a8 Update table headers and add information about specifying provider or model in README.md 2024-11-01 09:48:19 -04:00
kennethreitz 2c26895010 Update table headers in README.md 2024-11-01 09:47:42 -04:00
kennethreitz 2f1c69a79e i'm so indecisive 2024-11-01 09:46:32 -04:00
kennethreitz bf1a936777 Update table header in README.md 2024-11-01 09:46:09 -04:00
kennethreitz a4efa47f6e Update table header in README.md 2024-11-01 09:45:36 -04:00
kennethreitz 3721fa6713 Update table headers in README.md 2024-11-01 09:44:54 -04:00
kennethreitz db32ee26c1 Update table headers in README.md 2024-11-01 09:43:56 -04:00
kennethreitz 8797c9e82f Update README.md 2024-11-01 09:39:51 -04:00
kennethreitz ef01ce2f22 Update Groq model version in README.md 2024-11-01 09:36:50 -04:00
kennethreitz d591125eb8 got i hate markdown tables 2024-11-01 09:36:05 -04:00
kennethreitz 225d00deee Update table headers in README.md 2024-11-01 09:35:39 -04:00
kennethreitz df716a1f19 Update provider and model information in README.md 2024-11-01 09:35:10 -04:00
kennethreitz d6ad22721f Update Groq model version in README.md 2024-11-01 09:34:14 -04:00
kennethreitz 7b4f2fcf8e Update provider and model information in README.md 2024-11-01 09:33:18 -04:00
kennethreitz d8fce7b6d9 Update Ollama model version in README.md 2024-11-01 09:32:52 -04:00
kennethreitz 47ce8069f5 less is more 2024-11-01 09:32:23 -04:00
kennethreitz 9114211867 Update provider and model information in README.md 2024-11-01 09:31:42 -04:00
kennethreitz 49421b5213 Update provider and model information in README.md 2024-11-01 09:31:03 -04:00
kennethreitz b7cc767a45 Update provider and model information in README.md 2024-11-01 09:30:41 -04:00
kennethreitz 7ea33dec5a Update provider and model information in README.md 2024-11-01 09:30:05 -04:00
kennethreitz 5d194a7f63 Update provider and model information in README.md 2024-11-01 09:29:17 -04:00
kennethreitz 1a7693de0f Update provider and model information in README.md 2024-11-01 09:28:39 -04:00
kennethreitz c0474aafeb Update provider and model information in README.md 2024-11-01 09:27:15 -04:00
kennethreitz c9d7a7d622 Update provider and model information in README.md 2024-11-01 09:26:41 -04:00
kennethreitz 1696d698e5 Update provider and model information in README.md 2024-11-01 09:23:46 -04:00
kennethreitz e28d4660e8 Update README.md 2024-11-01 09:21:40 -04:00
kennethreitz d4491e42b9 Update README.md 2024-11-01 09:21:20 -04:00
kennethreitz 542677cffd Update README.md 2024-11-01 09:21:02 -04:00
kennethreitz 528f806e65 Update logfire URL in README.md 2024-11-01 09:09:19 -04:00
kennethreitz 373af44421 Update logfire URL in README.md 2024-11-01 09:09:04 -04:00
kennethreitz 947d8ab6ad simplify readme 2024-11-01 09:07:38 -04:00
kennethreitz 0ff966b307 Update README.md 2024-11-01 09:01:37 -04:00
kennethreitz 75a42044e5 Refactor CHANGELOG.md and pyproject.toml to update version to 0.2.0 and add Amazon Bedrock provider 2024-11-01 08:58:08 -04:00
kennethreitz cc66dbf8e5 Refactor pyproject.toml to add botocore and boto3 dependencies 2024-11-01 08:56:18 -04:00
kennethreitz a174e60a1e Refactor README.md to remove duplicate entry for Amazon Bedrock 2024-11-01 08:54:37 -04:00
kennethreitz b03695f626 Refactor pyproject.toml to update dependencies 2024-11-01 08:54:24 -04:00
kennethreitz 082bc24e91 Refactor pyproject.toml to update dependencies 2024-11-01 08:54:24 -04:00
kennethreitz aca1b87180 Merge pull request #25 from SZubarev/feature/amazon-bedrock
Added Amazon Bedrock provider
2024-11-01 08:53:46 -04:00
kennethreitz 1ff4c5660e Merge branch 'main' into feature/amazon-bedrock 2024-11-01 08:53:39 -04:00
kennethreitz 241a7ab402 Refactor pyproject.toml to add logfire as a dependency 2024-11-01 08:50:39 -04:00
kennethreitz 76fa7521eb Refactor quantity field in RecipeIngredient model to use float instead of string 2024-11-01 08:49:19 -04:00
kennethreitz cbec2c5f6d special thanks 2024-11-01 08:48:39 -04:00
kennethreitz 34f463839c logfire 2024-11-01 08:46:44 -04:00
kennethreitz c648a922b4 Bump version to v0.1.7 in conf.py and pyproject.toml 2024-11-01 08:44:37 -04:00
kennethreitz 873f5ba5f8 Refactor logging configuration to enable/disable logging 2024-11-01 08:44:18 -04:00
kennethreitz 28a7b2f140 Refactor logging configuration to enable/disable logging 2024-11-01 08:42:08 -04:00
kennethreitz 173162e798 Refactor LoggingConfig methods for enabling and disabling logging 2024-11-01 08:39:14 -04:00
kennethreitz cd0be3ad89 Refactor LoggingConfig methods for enabling and disabling logging 2024-11-01 08:36:05 -04:00
kennethreitz 3dd2e1b248 Refactor Gemini provider to handle missing llm_model key 2024-11-01 08:28:53 -04:00
Siddhesh Agarwal ad1800840d small changes 2024-11-01 15:27:15 +05:30
Siddhesh Agarwal d62f297b68 removed unused variable 2024-11-01 15:16:20 +05:30
Siddhesh Agarwal a2597709d2 gemini works as expected 2024-11-01 14:55:22 +05:30
Siddhesh Agarwal 1455b5ba13 remove unused import 2024-11-01 14:31:19 +05:30
Siddhesh Agarwal 0fb54d1987 circular import problem solve 2024-11-01 14:31:01 +05:30
Siddhesh Agarwal fe06331662 fixed forced imports + ensured return type in structure_response 2024-11-01 14:24:34 +05:30
Siddhesh Agarwal 56b1e65d70 moved logging functions to LoggingConfig from Settings 2024-11-01 13:06:06 +05:30
Siddhesh Agarwal 4b3e1bc6dd added methods to toggle logging 2024-11-01 12:55:24 +05:30
Siddhesh Agarwal f5b922ade8 added proper type hinting 2024-11-01 12:25:44 +05:30
Siddhesh Agarwal 3a7383425f sorted imports 2024-11-01 11:09:54 +05:30
Siddhesh Agarwal 92c10fc41e added logging 2024-11-01 11:07:04 +05:30
Stan Zubarev 75c42278a2 add parameter to env template 2024-10-31 20:55:56 -04:00
Stan Zubarev c25f1e1058 rename parameter 2024-10-31 20:50:57 -04:00
Stan Zubarev 2a5966eb10 fix tests 2024-10-31 20:50:42 -04:00
Stan Zubarev f19263d309 update reaadme 2024-10-31 20:49:13 -04:00
Stan Zubarev 25b742db1f remove profile 2024-10-31 19:50:51 -04:00
kennethreitz caceba381d Refactor default_kwargs logic in Ollama provider 2024-10-31 19:49:33 -04:00
kennethreitz 0795464fd7 Merge pull request #24 from barisozmen/default_kwargs
Add default kwargs logic to Groq, OpenAI, XAI, and Ollama providers
2024-10-31 19:48:02 -04:00
Stan Zubarev 8d83050a64 add Amazon Bedrock provider 2024-10-31 19:34:50 -04:00
Barış Özmen d82effdfb1 added default_kwargs logic to xAI provider 2024-11-01 00:18:57 +03:00
Barış Özmen e648292cb3 added default_kwargs logic to Ollama provider 2024-11-01 00:17:22 +03:00
Barış Özmen 37a9333be3 added default_kwargs logic to OpenAI provider 2024-11-01 00:15:49 +03:00
Barış Özmen cbc3739411 added default_kwargs logic to Groq provider 2024-11-01 00:14:41 +03:00
46 changed files with 3822 additions and 198 deletions
+1
View File
@@ -4,3 +4,4 @@ export GROQ_API_KEY=""
export OLLAMA_HOST_URL=""
export OPENAI_API_KEY=""
export XAI_API_KEY=""
export AMAZON_PROFILE_NAME=""
+3
View File
@@ -0,0 +1,3 @@
github: kennethreitz
thanks_dev: kennethreitz
custom: https://cash.app/$KennethReitz
+2
View File
@@ -168,3 +168,5 @@ cython_debug/
src/**
requirements.txt
Pipfile
enhanced_context.db
enhanced_context_sarah.db
+49
View File
@@ -1,6 +1,55 @@
Release History
===============
## 0.3.2 (2024-01-27)
- Improve Deepseek provider.
## 0.3.1 (2024-01-27)
- Introduce Deepseek provider.
## 0.3.0 (2024-11-12)
- Introduce save / load functionality for `Conversation`.
## 0.2.4 (2024-11-11)
- General improvements.
## 0.2.3 (2024-11-04)
- Remove default max-tokens for OpenAI provider.
## 0.2.3 (2024-11-03)
- Update default model for Amazon provider.
- Improved logging to handle streaming functions.
## 0.2.2 (2024-11-02)
- Add streaming support (set `stream=True` to `generate_text`).
- `conv.prepend_system_message` now uses system role by default.
- Add `provider.supports_streaming` property.
- Add `provider.supports_structured_response` property.
- General improvements.
## 0.2.1 (2024-11-01)
- Add `cached_property` to Amazon provider.
## 0.2.0 (2024-11-01)
- Add Amazon Bedrock provider.
- Make all provider optional dependencies. Use `$ pip install 'simplemind[full]'` to install all providers.
- General improvements.
## 0.1.7 (2024-11-01)
- Add `logger` decorator.
- Add `sm.enable_logfire()` function.
- General improvements.
## 0.1.6 (2024-10-31)
- Add `sm.Plugin` syntax sugar.
+21
View File
@@ -0,0 +1,21 @@
FROM python:3.12-slim
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
&& rm -rf /var/lib/apt/lists/*
# Install uv
RUN pip install uv
# Create and set working directory
WORKDIR /app
# Copy requirements/project files
ONBUILD COPY . .
# Install dependencies using uv
RUN uv pip install "simplemind[full]" --system
# Set default command
CMD ["python"]
+222 -39
View File
@@ -10,36 +10,76 @@ Simplemind is AI library designed to simplify your experience with AI APIs in Py
With Simplemind, tapping into AI is as easy as a friendly conversation.
- **Easy-to-use AI tools**: SimpleMind provides simple interfaces to popular AI services.
- **Easy-to-use AI tools**: Simplemind provides simple interfaces to most popular AI services.
- **Human-centered design**: The library prioritizes readability and usability—no need to be an expert to start experimenting.
- **Minimal configuration**: Get started quickly, without worrying about configuration headaches.
## Supported APIs
To specify a specific provider or model, you can use the `llm_provider` and `llm_model` parameters when calling: `generate_text`, `generate_data`, or `create_conversation`. The APIs remain identital between all supported providers/models.
The APIs remain identical between all supported providers / models:
- [**Anthropic's Claude**](https://www.anthropic.com/claude)
- [**Google's Gemini**](https://gemini.google/)
- [**Groq's Groq**](https://groq.com/)
- [**Ollama**](https://ollama.com)
- [**OpenAI's GPT**](https://openai.com/gpt)
- [**xAI's Grok**](https://x.ai/)
<table>
<thead>
<tr>
<th></th>
<th><code>llm_provider</code></th>
<th>Default <code>llm_model</code></th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://www.anthropic.com/claude">Anthropic's Claude</a></td>
<td><code>"anthropic"</code></td>
<td><code>"claude-3-5-sonnet-20241022"</code></td>
</tr>
<tr>
<td><a href="https://aws.amazon.com/bedrock/">Amazon's Bedrock</a></td>
<td><code>"amazon"</code></td>
<td><code>"anthropic.claude-3-5-sonnet-20241022-v2:0"</code></td>
</tr>
<tr>
<td><a href="https://www.deepseek.com">Deepseek</a></td>
<td><code>"deepseek"</code></td>
<td><code>"deepseek-chat"</code></td>
</tr>
<tr>
<td><a href="https://gemini.google/">Google's Gemini</a></td>
<td><code>"gemini"</code></td>
<td><code>"models/gemini-1.5-pro"</code></td>
</tr>
<tr>
<td><a href="https://groq.com/">Groq's Groq</a></td>
<td><code>"groq"</code></td>
<td><code>"llama3-8b-8192"</code></td>
</tr>
<tr>
<td><a href="https://ollama.com">Ollama</a></td>
<td><code>"ollama"</code></td>
<td><code>"llama3.2"</code></td>
</tr>
<tr>
<td><a href="https://openai.com/gpt">OpenAI's GPT</a></td>
<td><code>"openai"</code></td>
<td><code>"gpt-4o-mini"</code></td>
</tr>
<tr>
<td><a href="https://x.ai/">xAI's Grok</a></td>
<td><code>"xai"</code></td>
<td><code>"grok-beta"</code></td>
</tr>
</tbody>
</table>
If you want to see Simplemind support, additional providers or models, please send a pull request!
To specify a specific provider or model, you can use the `llm_provider` and `llm_model` parameters when calling: `generate_text`, `generate_data`, or `create_conversation`.
## Why SimpleMind?
- **Intuitive**: Built with Pythonic simplicity and readability in mind.
- **For Humans**: Emphasizes a human-friendly interface, just like `requests` for HTTP.
- **Open Source**: Simplemind is open source, and contributions are always welcome!
Also, why not? :)
If you want to see Simplemind support additional providers or models, please send a pull request!
## Quickstart
Simplemind takes care of the complex API calls so you can focus on what matters—building, experimenting, and creating.
```bash
$ pip install simplemind
$ pip install 'simplemind[full]'
```
First, authenticate your API keys by setting them in the environment variables:
@@ -56,20 +96,28 @@ Next, import Simplemind and start using it:
import simplemind as sm
```
## Examples
Here are some examples of how to use Simplemind:
Here are some examples of how to use Simplemind.
**Please note**: Most of the calls seen here optionally accept `llm_provider` and `llm_model` parameters, which you provide as strings.
### Text Completion
Generate a response from an AI model based on a given prompt:
```pycon
>>> sm.generate_text(prompt="What is the meaning of life?", llm_provider="openai", llm_model="gpt-4o")
>>> sm.generate_text(prompt="What is the meaning of life?")
"The meaning of life is a profound philosophical question that has been explored by cultures, religions, and philosophers for centuries. Different people and belief systems offer varying interpretations:\n\n1. **Religious Perspectives:** Many religions propose that the meaning of life is to fulfill a divine purpose, serve God, or reach an afterlife. For example, Christianity often emphasizes love, faith, and service to God and others as central to lifes meaning.\n\n2. **Philosophical Views:** Philosophers offer diverse answers. Existentialists like Jean-Paul Sartre argue that life has no inherent meaning, and it is up to individuals to create their own purpose. Others, like Aristotle, suggest that achieving eudaimonia (flourishing or happiness) through virtuous living is the key to a meaningful life.\n\n3. **Scientific and Secular Approaches:** Some people find meaning through understanding the natural world, contributing to human knowledge, or through personal accomplishments and happiness. They may view life's meaning as a product of connection, legacy, or the pursuit of knowledge and creativity.\n\n4. **Personal Perspective:** For many, the meaning of life is deeply personal, involving their relationships, passions, and goals. These individuals define life's purpose through experiences, connections, and the impact they have on others and the world.\n\nUltimately, the meaning of life is a subjective question, with each person finding their own answers based on their beliefs, experiences, and reflections."
```
### Streaming Text
```python
>>> for chunk in sm.generate_text("Write a poem about the moon", stream=True):
... print(chunk, end="", flush=True)
```
### Structured Data with Pydantic
You can use Pydantic models to structure the response from the LLM, if the LLM supports it.
@@ -81,34 +129,54 @@ class Poem(BaseModel):
```
```pycon
>>> sm.generate_data(
"Write a poem about love",
llm_model="gpt-4o-mini",
llm_provider="openai",
response_model=Poem,
)
>>> sm.generate_data("Write a poem about love", response_model=Poem)
title='Eternal Embrace' content='In the quiet hours of the night,\nWhen stars whisper secrets bright,\nTwo hearts beat in a gentle rhyme,\nDancing through the sands of time.\n\nWith every glance, a spark ignites,\nA flame that warms the coldest nights,\nIn laughter shared and whispers sweet,\nLove paints the world, a masterpiece.\n\nThrough stormy skies and sunlit days,\nIn myriad forms, it finds its ways,\nA tender touch, a knowing sigh,\nIn loves embrace, we learn to fly.\n\nAs seasons change and moments fade,\nIn the tapestry of dreams weve laid,\nLoves threads endure, forever bind,\nA timeless bond, two souls aligned.\n\nSo heres to love, both bright and true,\nA gift we give, anew, anew,\nIn every heartbeat, every prayer,\nA story written in the air.'
```
#### A more complex example
```python
class InstructionStep(BaseModel):
step_number: int
instruction: str
class RecipeIngredient(BaseModel):
name: str
quantity: float
unit: str
class Recipe(BaseModel):
name: str
ingredients: list[RecipeIngredient]
instructions: list[InstructionStep]
recipe = sm.generate_data(
"Write a recipe for chocolate chip cookies",
response_model=Recipe,
)
```
Special thanks to [@jxnl](https://github.com/jxnl) for building [Instructor](https://github.com/jxnl/instructor), which makes this possible!
### Conversational AI
SimpleMind also allows for easy conversational flows:
```pycon
>>> conversation = sm.create_conversation(llm_model="gpt-4o-mini", llm_provider="openai")
>>> conv = sm.create_conversation()
>>> # Add a message to the conversation
>>> conversation.add_message("user", "Hi there, how are you?")
>>> conv.add_message("user", "Hi there, how are you?")
>>> conversation.send()
>>> conv.send()
<Message role=assistant text="Hello! I'm just a computer program, so I don't have feelings, but I'm here and ready to help you. How can I assist you today?">
```
To continue the conversation, you can call `conversation.send()` again, which returns the next message in the conversation:
To continue the conversation, you can call `conv.send()` again, which returns the next message in the conversation:
```pycon
>>> conversation.add_message("user", "What is the meaning of life?")
>>> conversation.send()
>>> conv.add_message("user", "What is the meaning of life?")
>>> conv.send()
<Message role=assistant text="The meaning of life is a profound philosophical question that has been explored by cultures, religions, and philosophers for centuries. Different people and belief systems offer varying interpretations:\n\n1. **Religious Perspectives:** Many religions propose that the meaning of life is to fulfill a divine purpose, serve God, or reach an afterlife. For example, Christianity often emphasizes love, faith, and service to God and others as central to lifes meaning.\n\n2. **Philosophical Views:** Philosophers offer diverse answers. Existentialists like Jean-Paul Sartre argue that life has no inherent meaning, and it is up to individuals to create their own purpose. Others, like Aristotle, suggest that achieving eudaimonia (flourishing or happiness) through virtuous living is the key to a meaningful life.\n\n3. **Scientific and Secular Approaches:** Some people find meaning through understanding the natural world, contributing to human knowledge, or through personal accomplishments and happiness. They may view lifes meaning as a product of connection, legacy, or the pursuit of knowledge and creativity.\n\n4. **Personal Perspective:** For many, the meaning of life is deeply personal, involving their relationships, passions, and goals. These individuals define lifes purpose through experiences, connections, and the impact they have on others and the world.\n\nUltimately, the meaning of life is a subjective question, with each person finding their own answers based on their beliefs, experiences, and reflections.">
```
@@ -125,13 +193,12 @@ response = gpt_4o_mini.generate_text("Hello!")
conversation = gpt_4o_mini.create_conversation()
```
This maintains the simplicity of the original API while reducing repetition. The session object also supports overriding defaults on a per-call basis:
This maintains the simplicity of the original API while reducing repetition.
The session object also supports overriding defaults on a per-call basis:
```python
response = gpt_4o_mini.generate_text(
"Complex task here",
llm_model="gpt-4"
)
response = gpt_4o_mini.generate_text("Complex task here", llm_model="gpt-4")
```
### Basic Memory Plugin
@@ -154,7 +221,7 @@ class SimpleMemoryPlugin(sm.BasePlugin):
conversation.add_message(role="system", text=m)
conversation = sm.create_conversation(llm_model="grok-beta", llm_provider="xai")
conversation = sm.create_conversation()
conversation.add_plugin(SimpleMemoryPlugin())
@@ -163,6 +230,7 @@ conversation.add_message(
text="Please write a poem about the moon",
)
```
```pycon
>>> conversation.send()
In the vast expanse where stars do play,
@@ -198,11 +266,125 @@ The universe is never done.
Simple, yet effective.
### Tools (Function calling)
Tools (also known as functions) let you call any Python function from your AI conversations. Here's an example:
```python
def get_weather(
location: Annotated[
str, Field(description="The city and state, e.g. San Francisco, CA")
],
unit: Annotated[
Literal["celcius", "fahrenheit"],
Field(
description="The unit of temperature, either 'celsius' or 'fahrenheit'"
),
] = "celcius",
):
"""
Get the current weather in a given location
"""
return f"42 {unit}"
# Add your function as a tool
conversation = sm.create_conversation()
conversation.add_message("user", "What's the weather in San Francisco?")
response = conversation.send(tools=[get_weather])
```
Note how we're using Python's `Annotated` feature combined with `Field` to provide additional context to our function parameters. This helps the AI understand the intention and constraints of each parameter, making tool calls more accurate and reliable.
You can alos ommit `Annotated` and just pass the `Field` parameter.
```python
def get_weather(
location: str = Field(description="The city and state, e.g. San Francisco, CA"),
unit:Literal["celcius", "fahrenheit"]= Field(
default="celcius",
description="The unit of temperature, either 'celsius' or 'fahrenheit'"
),
):
"""
Get the current weather in a given location
"""
return f"42 {unit}"
```
Functions can be defined with type hints and Pydantic models for validation. The LLM will intelligently choose when to call the functions and incorporate the results into its responses.
#### 🪄 Using LLM for automatic tool definition (Experimental)
Simplemind provides a decorator to automatically transform Python functions into tools with AI-generated metadata. Simply use the `@simplemind.tool` decorator to have the LLM analyze your function and generate appropriate descriptions and schema:
```python
@simplemind.tool(llm_provider="anthropic")
def haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
r = 6371
phi1 = math.radians(lat1)
phi2 = math.radians(lat2)
delta_phi = math.radians(lat2 - lat1)
delta_lambda = math.radians(lon2 - lon1)
a = (
math.sin(delta_phi / 2) ** 2
+ math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda / 2) ** 2
)
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
d = r * c
return d
```
Notice how we have not added any docstrings or `Field` for the function.
The decorator will use the specified LLM provider to generate the tool schema, including descriptions and parameter details:
```json
{
"name": "haversine",
"description": "Calculates the great-circle distance between two points on Earth given their latitude and longitude coordinates",
"input_schema": {
"type": "object",
"properties": {
"lat1": {
"type": "number",
"description": "Latitude of the first point in decimal degrees",
},
"lon1": {
"type": "number",
"description": "Longitude of the first point in decimal degrees",
},
"lat2": {
"type": "number",
"description": "Latitude of the second point in decimal degrees",
},
"lon2": {
"type": "number",
"description": "Longitude of the second point in decimal degrees",
}
},
"required": ["lat1", "lon1", "lat2", "lon2"],
},
}
```
The decorated function can then be used like any other tool with the conversation API.
```python
conversation = sm.create_conversation()
conversation.add_message("user", "How far is London from my location")
response = conversation.send(tools=[get_location, get_coords, haversine]) # Multiple tools can be passed
```
See [examples/distance_calculator.py](examples/distance_calculator.py) for more.
### Logging
Simplemind uses [Logfire](https://pydantic.dev/logfire) for logging. To enable logging, call `sm.enable_logfire()`.
### More Examples
Please see the [examples](examples) directory for executable examples.
-------------------
---
## Contributing
We welcome contributions of all kinds. Feel free to open issues for bug reports or feature requests, and submit pull requests to make SimpleMind even better.
To get started:
@@ -213,8 +395,9 @@ To get started:
4. Submit a pull request.
## License
Simplemind is licensed under the Apache 2.0 License.
## Acknowledgements
Simplemind is inspired by the philosophy of "code for humans" and aims to make working with AI models accessible to all. Special thanks to the open-source community for their contributions and inspiration.
Simplemind is inspired by the philosophy of "code for humans" and aims to make working with AI models accessible to all. Special thanks to the open-source community for their contributions and inspiration.
+1 -1
View File
@@ -16,7 +16,7 @@ import simplemind
project = "simplemind"
copyright = "2024 Kenneth Reitz"
author = "Kenneth Reitz"
release = "v0.1.6"
release = "v0.2.2"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
+2 -1
View File
@@ -5,6 +5,7 @@ import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import simplemind
import simplemind as sm
__all__ = ["sm"]
__all__ = ["simplemind", "sm"]
+137
View File
@@ -0,0 +1,137 @@
from _context import simplemind as sm
from pydantic import BaseModel
from rich.console import Console
from rich.panel import Panel
from rich.text import Text
console = Console()
gpt_4o_mini = sm.Session(llm_provider="openai")
claude_sonnet = sm.Session(llm_provider="anthropic")
class BibleVerse(BaseModel):
book: str
chapter: int
verse: int
text: str
translation: str
class BiblePassage(BaseModel):
book: str
chapter: int
verses: list[BibleVerse]
translation: str
class CrossReference(BaseModel):
passage: BiblePassage
notes: list[str]
origin_verse: BibleVerse
ai_perspective: str
anthropic_perspective: str
def get_passage(book: str, chapter: int, translation: str = "ESV") -> BiblePassage:
passage = gpt_4o_mini.generate_data(
prompt=f"""Return {book} chapter {chapter} from the {translation} translation.
Format each verse as plain text without any special characters or formatting.
For example:
- "Love is patient, love is kind."
- "It does not envy, it does not boast"
Return only the biblical text, formatted as a BiblePassage object.""",
response_model=BiblePassage,
max_tokens=8000,
)
return passage
def get_cross_reference(passage: BiblePassage) -> CrossReference:
verses_text = "\n".join([f"Verse {v.verse}: {v.text}" for v in passage.verses])
# Get main cross-reference from OpenAI
ref = gpt_4o_mini.generate_data(
prompt=f"""Find a thematically related Bible passage that connects with this text:
{verses_text}
Return a CrossReference object with:
1. A related passage (using plain text without special characters)
2. A list of clear, specific notes explaining the thematic connections
3. The original passage included
4. An AI perspective that provides a thoughtful, modern interpretation of how these passages relate to contemporary life and universal human experiences""",
response_model=CrossReference,
)
# Get Anthropic's perspective separately
anthropic_insight = claude_sonnet.generate_text(
prompt=f"""Analyze these biblical passages from a philosophical and ethical perspective:
Original passage:
{verses_text}
Cross-reference passage:
{' '.join([f'Verse {v.verse}: {v.text}' for v in ref.passage.verses])}
Provide a thoughtful analysis focusing on the philosophical and ethical implications of these passages, drawing from your training in ethics and philosophy.
Return your response as a plain string.""",
)
# Add Anthropic's perspective to the reference object
ref.anthropic_perspective = anthropic_insight
return ref
def pretty_print_reference(ref: CrossReference):
# Create origin passage panel
origin_text = Text()
origin_text.append(
f"{ref.origin_verse.book} {ref.origin_verse.chapter}\n",
style="bold blue",
)
origin_text.append(f"{ref.origin_verse.verse}. ", style="blue")
origin_text.append(f"{ref.origin_verse.text}\n", style="italic")
origin_text.append(f"\n({ref.origin_verse.translation})", style="dim")
origin_panel = Panel(origin_text, title="Original Passage", border_style="blue")
# Create cross reference panel
ref_text = Text()
ref_text.append(
f"{ref.passage.book} {ref.passage.chapter}\n",
style="bold green",
)
for verse in ref.passage.verses:
ref_text.append(f"{verse.verse}. ", style="green")
ref_text.append(f"{verse.text}\n", style="italic")
ref_text.append(f"\n({ref.passage.translation})", style="dim")
ref_panel = Panel(ref_text, title="Cross Reference", border_style="green")
# Create notes panel with bullet points
notes_text = Text()
for note in ref.notes:
notes_text.append("", style="yellow")
notes_text.append(f"{note}\n")
notes_panel = Panel(notes_text, title="Thematic Connections", border_style="yellow")
# Add new AI perspective panel
ai_text = Text()
ai_text.append(ref.ai_perspective)
ai_panel = Panel(ai_text, title="AI Perspective", border_style="magenta")
# Print all panels
console.print(origin_panel)
console.print(ref_panel)
console.print(notes_panel)
console.print(ai_panel)
if __name__ == "__main__":
# Get 1 Corinthians 13 (The Love Chapter)
passage = get_passage("1 Corinthians", 13)
ref = get_cross_reference(passage)
pretty_print_reference(ref)
+99
View File
@@ -0,0 +1,99 @@
from _context import simplemind as sm
from pydantic import BaseModel
from rich.console import Console
from rich.panel import Panel
from rich.text import Text
class InstructionStep(BaseModel):
step_number: int
instruction: str
class RecipeIngredient(BaseModel):
name: str
quantity: float
unit: str
class Recipe(BaseModel):
name: str
ingredients: list[RecipeIngredient]
instructions: list[InstructionStep]
def __str__(self) -> str:
console = Console(record=True, file=None)
# Create formatted title with more emphasis
title = Text("" + self.name.upper() + "", style="bold blue")
# Format ingredients with better structure
ingredients_text = Text("\n📝 INGREDIENTS:\n", style="bold green")
for ing in self.ingredients:
# Format numbers to avoid floating decimals when whole numbers
quantity = int(ing.quantity) if ing.quantity.is_integer() else ing.quantity
ingredients_text.append(f"{quantity} {ing.unit} ", style="bright_white")
ingredients_text.append(f"{ing.name}\n", style="italic bright_white")
# Format instructions with better spacing and styling
instructions_text = Text("\n👩‍🍳 INSTRUCTIONS:\n", style="bold yellow")
for step in self.instructions:
instructions_text.append(
f"\n {step.step_number}. ", style="bold bright_white"
)
instructions_text.append(f"{step.instruction}", style="bright_white")
# Combine all text
full_text = Text.assemble(
ingredients_text, instructions_text, "\n"
) # Added extra newline
# Create panel with enhanced styling
panel = Panel(
full_text,
title=title,
border_style="blue",
padding=(1, 2), # Add padding (vertical, horizontal)
expand=False, # Don't expand to full terminal width
title_align="center",
)
# Render the panel to string without printing
with console.capture() as capture:
console.print(panel)
return capture.get()
recipe = sm.generate_data(
"Write a recipe for chocolate chip cookies",
llm_model="gpt-4o-mini",
llm_provider="openai",
response_model=Recipe,
)
print(recipe)
# Expected output is something like this:
#
# === CHOCOLATE CHIP COOKIES ===
#
# INGREDIENTS:
# • 2.25 cups all-purpose flour
# • 1.0 teaspoon baking soda
# • 0.5 teaspoon salt
# • 1.0 cup unsalted butter
# • 0.75 cup sugar
# • 0.75 cup brown sugar
# • 1.0 teaspoon vanilla extract
# • 2.0 large eggs
# • 2.0 cups semi-sweet chocolate chips
#
# INSTRUCTIONS:
# 1. Preheat your oven to 350°F (175°C).
# 2. In a small bowl, combine flour, baking soda, and salt; set aside.
# 3. In a large bowl, cream together the butter, sugar, and brown sugar until smooth.
# 4. Beat in the vanilla extract and eggs, one at a time.
# 5. Gradually blend in the flour mixture until just combined.
# 6. Stir in the chocolate chips.
# 7. Drop by rounded tablespoon onto ungreased cookie sheets.
# 8. Bake for 9 to 11 minutes, or until edges are golden.
# 9. Let cool on the cookie sheet for a few minutes before transferring to wire racks to cool completely.
+130
View File
@@ -0,0 +1,130 @@
import time
from typing import List, Tuple
from _context import sm
from rich.console import Console
from rich.markdown import Markdown
class MultiAIConversation:
"""Orchestrates conversations between multiple AI models."""
MODEL_SESSIONS = {
"GPT-4o": sm.Session(
llm_provider="openai",
llm_model="gpt-4o",
),
"Grok-Beta": sm.Session(
llm_provider="xai",
llm_model="grok-beta",
),
"Claude-3.5-Sonnet": sm.Session(
llm_provider="anthropic",
llm_model="claude-3-5-sonnet-20241022",
),
}
def __init__(self, topic: str, turns_per_model: int = 1, max_rounds: int = 5):
self.topic = topic
self.turns_per_model = turns_per_model
self.max_rounds = max_rounds
self.conversation_history: List[Tuple[str, str]] = []
self.console = Console()
self.user_name = "Kenneth Reitz"
def _format_system_prompt(self, ai_name: str) -> str:
"""Creates a system prompt for each AI model."""
return f"""You are {ai_name}. You are participating in a thoughtful discussion with other AI models about {self.topic}.
Rules:
1. Be concise but insightful (keep responses under 140 words)
2. Build upon previous points made in the conversation
3. Ask questions to deepen the discussion when appropriate
4. Stay on topic while maintaining your unique perspective
5. Be respectful of other viewpoints while maintaining your distinct voice
Current discussion topic: {self.topic}"""
def _create_conversation(
self, session: sm.Session, ai_name: str
) -> sm.Conversation:
"""Creates a new conversation with appropriate context for an AI model."""
conv = session.create_conversation()
# Add system prompt
conv.add_message(role="user", text=self._format_system_prompt(ai_name))
# Add conversation history
for speaker, message in self.conversation_history[-3:]: # Last 3 messages
conv.add_message(role="user", text=f"{speaker} said: {message}")
return conv
def _print_response(self, ai_name: str, response: str):
"""Pretty prints an AI response using Rich."""
self.console.print(f"\n[bold blue]{ai_name}[/bold blue]:")
self.console.print(Markdown(response))
# Store in history
self.conversation_history.append((ai_name, response))
def _get_user_input(self) -> str:
"""Gets input from the user for the discussion."""
self.console.print("\n[bold green]Your turn! Share your thoughts:[/bold green]")
user_response = input("> ")
self._print_response(self.user_name, user_response)
return user_response
def run_conversation(self):
"""Runs the multi-AI conversation."""
# Get initial thoughts from the human
self.console.print(
f"\n[bold green]Start the discussion about {self.topic}:[/bold green]"
)
self._get_user_input()
for round_num in range(self.max_rounds):
self.console.print(f"\n[bold green]Round {round_num + 1}[/bold green]")
# Let all AI models respond
for model_name, session in self.MODEL_SESSIONS.items():
for turn in range(self.turns_per_model):
conversation = self._create_conversation(session, model_name)
# Add the prompt (simplified since human always starts)
prompt = f"Continue the discussion about {self.topic}, responding to the previous points made."
conversation.add_message(role="user", text=prompt)
# Get and print response
response = conversation.send()
self._print_response(model_name, response.text)
# Small delay to prevent rate limiting
time.sleep(1)
# Then get user input at the end of the round
self._get_user_input()
# Optional: Add a separator between rounds
self.console.print("\n" + "-" * 50)
def have_ai_discussion(turns_per_model: int = 1, max_rounds: int = 3):
"""Convenience function to start an AI discussion."""
# Get topic from user
print("\nWhat topic would you like to discuss?")
topic = input("> ")
debate = MultiAIConversation(
topic=topic, turns_per_model=turns_per_model, max_rounds=max_rounds
)
print(f"\nStarting AI discussion about: {topic}")
print("=" * 50)
debate.run_conversation()
# Example usage
if __name__ == "__main__":
have_ai_discussion(turns_per_model=1, max_rounds=5)
+76
View File
@@ -0,0 +1,76 @@
import math
from _context import sm
from pydantic import Field
from typing_extensions import Literal
@sm.tool(llm_provider="anthropic")
def haversine(
lat1: float,
lon1: float,
lat2: float,
lon2: float,
unit: Literal["km", "miles"],
) -> float:
r = 6378.0937 if unit == "km" else 3961
phi1 = math.radians(lat1)
phi2 = math.radians(lat2)
delta_phi = math.radians(lat2 - lat1)
delta_lambda = math.radians(lon2 - lon1)
a = (
math.sin(delta_phi / 2) ** 2
+ math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda / 2) ** 2
)
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
d = r * c
return d
def get_user_location() -> str:
"""Get the closest city from the user"""
return "San Francisco"
def get_coords(
city_name: str = Field(
description="The name of the city to take the coordinates from (e.g. London, Rome, Los Angeles)"
),
):
"""Get latitude and logitude of a City."""
_data = {
"Rome": (41.9028, 12.4964),
"London": (51.5074, -0.1278),
"Madrid": (40.4168, -3.7038),
"San Francisco": (37.7749, -122.4194),
"Los Angeles": (34.0522, -118.2437),
}
return _data.get(city_name)
def distance_calculator(prompt: str):
conversation = sm.create_conversation(llm_provider="anthropic")
conversation.add_message("user", prompt)
return conversation.send(
tools=[get_user_location, get_coords, haversine]
).text
print(distance_calculator("How far is London from where I am?"))
# Prints something like:
"""
The distance between your location (San Francisco) and London is approximately 5,357 miles.
"""
print(
distance_calculator(
"What is the distance between Rome and Madrid in Kilometers?"
)
)
"""
The distance between Rome and Madrid is approximately 1,366 kilometers.
"""
+952
View File
@@ -0,0 +1,952 @@
import contextlib
import logging
import os
import random
import re
import sqlite3
from concurrent.futures import ThreadPoolExecutor
from contextlib import contextmanager
from datetime import datetime
from typing import List
import nltk
import spacy
import xerox
from _context import simplemind as sm
from docopt import docopt
from nltk.tag import pos_tag
from nltk.tokenize import word_tokenize
from prompt_toolkit import PromptSession
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import Completer, Completion
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from rich.status import Status
DB_PATH = "enhanced_context.db"
AVAILABLE_PROVIDERS = ["xai", "openai", "anthropic", "ollama"]
# Enable Logfire for debugging.
# sm.enable_logfire()
__doc__ = """Enhanced Context Chat Interface
Usage:
enhanced_context.py [--provider=<provider>] [--model=<model>]
enhanced_context.py (-h | --help)
Options:
-h --help Show this screen.
--provider=<provider> LLM provider to use (openai/anthropic/xai/ollama)
--model=<model> Specific model to use (e.g. o1-preview)
"""
class ContextDatabase:
def __init__(self, db_path: str):
self.db_path = db_path
self.init_db()
self.logger = logging.getLogger(__name__)
@contextmanager
def get_connection(self):
"""Context manager for database connections"""
conn = sqlite3.connect(self.db_path)
try:
yield conn
finally:
conn.close()
def init_db(self):
"""Initialize the database with proper schema"""
with self.get_connection() as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS memory (
entity TEXT,
source TEXT,
last_mentioned TIMESTAMP,
mention_count INTEGER DEFAULT 1,
PRIMARY KEY (entity, source)
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS identity (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
last_updated TIMESTAMP
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS essence_markers (
marker_type TEXT,
marker_text TEXT,
timestamp TIMESTAMP,
PRIMARY KEY (marker_type, marker_text)
)
"""
)
def store_entity(self, entity: str, source: str = "user") -> None:
"""Store or update entity mention with source tracking"""
with self.get_connection() as conn:
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
conn.execute(
"""
INSERT INTO memory (entity, source, last_mentioned, mention_count)
VALUES (?, ?, ?, 1)
ON CONFLICT(entity, source) DO UPDATE SET
last_mentioned = ?,
mention_count = mention_count + 1
""",
(entity, source, now, now),
)
conn.commit()
def retrieve_recent_entities(self, days: int = 7) -> List[tuple]:
"""Retrieve recently mentioned entities with frequency and source"""
try:
with self.get_connection() as conn:
cur = conn.cursor()
cur.execute(
"""
SELECT
entity,
SUM(mention_count) as total_mentions,
GROUP_CONCAT(source || ':' || mention_count) as source_counts
FROM memory
WHERE last_mentioned >= datetime('now', ?, 'localtime')
GROUP BY entity
ORDER BY total_mentions DESC, MAX(last_mentioned) DESC
LIMIT 50
""",
(f"-{days} days",),
)
entities = []
for row in cur.fetchall():
entity, total_count, source_counts = row
source_dict = dict(sc.split(":") for sc in source_counts.split(","))
entities.append(
(
entity,
total_count,
int(source_dict.get("user", 0)),
int(source_dict.get("llm", 0)),
)
)
return entities
except sqlite3.Error as e:
self.logger.error(f"Database error while retrieving entities: {e}")
return []
def store_identity(self, identity: str) -> None:
"""Store personal identity in database"""
if not identity:
return
try:
with self.get_connection() as conn:
now = datetime.now()
# Store in identity table
conn.execute(
"""
INSERT OR REPLACE INTO identity (id, name, last_updated)
VALUES (1, ?, ?)
""",
(identity, now),
)
# Store in memory table
self.store_entity(identity)
conn.commit()
except sqlite3.Error as e:
self.logger.error(f"Database error while storing identity: {e}")
def load_identity(self) -> str | None:
"""Load personal identity from database"""
try:
with self.get_connection() as conn:
cur = conn.cursor()
cur.execute("SELECT name FROM identity WHERE id = 1")
result = cur.fetchone()
return result[0] if result else None
except sqlite3.Error as e:
self.logger.error(f"Database error while loading identity: {e}")
return None
def store_essence_marker(self, marker_type: str, marker_text: str) -> None:
"""Store essence marker in database"""
try:
with self.get_connection() as conn:
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
conn.execute(
"""
INSERT OR REPLACE INTO essence_markers
(marker_type, marker_text, timestamp)
VALUES (?, ?, ?)
""",
(marker_type, marker_text, now),
)
conn.commit()
except sqlite3.Error as e:
self.logger.error(f"Database error storing essence marker: {e}")
def retrieve_essence_markers(self, days: int = 30) -> List[tuple[str, str]]:
"""Retrieve recent essence markers"""
try:
with self.get_connection() as conn:
cur = conn.cursor()
cur.execute(
"""
SELECT DISTINCT marker_type, marker_text
FROM essence_markers
WHERE timestamp >= datetime('now', ?, 'localtime')
ORDER BY timestamp DESC
""",
(f"-{days} days",),
)
return cur.fetchall()
except sqlite3.Error as e:
self.logger.error(f"Database error retrieving essence markers: {e}")
return []
class EnhancedContextPlugin(sm.BasePlugin):
model_config = {"extra": "allow"}
def __init__(self, verbose: bool = False):
super().__init__()
# Set up logging
self.verbose = verbose
if verbose:
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
else:
logging.basicConfig(level=logging.WARNING)
self.logger = logging.getLogger(__name__)
# Initialize NLP model
try:
self.nlp = spacy.load("en_core_web_sm")
except OSError:
self.logger.error(
"Failed to load spaCy model. Please install it using: python -m spacy download en_core_web_sm"
)
raise
# Initialize database
self.db = ContextDatabase(DB_PATH)
self.logger.info(f"EnhancedContextPlugin initialized with database: {DB_PATH}")
# Load identity from database
self.personal_identity = self.db.load_identity()
# Download required NLTK data silently
try:
with open(os.devnull, "w") as null_out:
with (
contextlib.redirect_stdout(null_out),
contextlib.redirect_stderr(null_out),
):
nltk.download("punkt", quiet=True)
nltk.download("averaged_perceptron_tagger", quiet=True)
except LookupError as e:
self.logger.error(f"Error downloading NLTK data: {e}")
# Add LLM personality traits for easter egg
self.llm_personalities = [
"You are a wise philosopher who speaks in riddles",
"You are an excited scientist who loves discovering patterns",
"You are a detective who analyzes every detail",
"You are a poet who sees beauty in connections",
"You are a historian who relates everything to the past",
]
# Add these lines to store the conversation's model and provider
self.llm_model = None
self.llm_provider = None
def extract_entities(self, text: str) -> List[str]:
"""Extract named entities with improved filtering"""
doc = self.nlp(text)
# Define important entity types
important_types = {
"PERSON",
"ORG",
"GPE",
"NORP",
"PRODUCT",
"EVENT",
"WORK_OF_ART",
}
entities = [
ent.text.strip()
for ent in doc.ents
if (
ent.label_ in important_types
and len(ent.text.strip()) > 1
and not ent.text.isnumeric()
)
]
return list(set(entities))
def format_context_message(
self, entities: List[tuple], include_identity: bool = True
) -> str:
"""Format context message with essence markers"""
context_parts = []
# Add identity context
if include_identity and self.personal_identity:
context_parts.append(f"The user's name is {self.personal_identity}.")
# Add essence markers
essence_markers = self.retrieve_essence_markers()
if essence_markers:
markers_by_type = {}
for marker_type, marker_text in essence_markers:
markers_by_type.setdefault(marker_type, []).append(marker_text)
context_parts.append("User characteristics:")
for marker_type, markers in markers_by_type.items():
context_parts.append(f"- {marker_type.title()}: {', '.join(markers)}")
# Add entity context with user/llm breakdown
if entities:
entity_strings = [
f"{entity} (mentioned {total} times - User: {user_count}, AI: {llm_count})"
for entity, total, user_count, llm_count in entities
]
topics = (
", ".join(entity_strings[:-1]) + f" and {entity_strings[-1]}"
if len(entity_strings) > 1
else entity_strings[0]
)
context_parts.append(f"Recent conversation topics: {topics}")
return "\n".join(context_parts)
def extract_essence_markers(self, text: str) -> List[tuple[str, str]]:
"""Extract essence markers from text."""
patterns = {
"value": [
r"I (?:really )?(?:believe|think) (?:that )?(.+)",
r"(?:It's|Its) important (?:to me )?that (.+)",
r"I value (.+)",
r"(?:The )?most important (?:thing|aspect) (?:to me )?is (.+)",
],
"identity": [
r"I am(?: a| an)? (.+)",
r"I consider myself(?: a| an)? (.+)",
r"I identify as(?: a| an)? (.+)",
],
"preference": [
r"I (?:really )?(?:like|love|enjoy|prefer) (.+)",
r"I can't stand (.+)",
r"I hate (.+)",
r"I always (.+)",
r"I never (.+)",
],
"emotion": [
r"I feel (.+)",
r"I'm feeling (.+)",
r"(?:It|That) makes me feel (.+)",
],
}
markers = []
doc = self.nlp(text)
for sent in doc.sents:
sent_text = sent.text.strip().lower()
for marker_type, pattern_list in patterns.items():
for pattern in pattern_list:
for match in re.finditer(pattern, sent_text, re.IGNORECASE):
marker_text = match.group(1).strip()
if self._is_valid_marker(marker_text):
markers.append((marker_type, marker_text))
return markers
def _is_valid_marker(self, marker_text: str) -> bool:
"""Helper method to validate essence markers"""
invalid_words = {"um", "uh", "like"}
return len(marker_text) > 3 and not any(w in marker_text for w in invalid_words)
def pre_send_hook(self, conversation: sm.Conversation) -> bool:
"""Process user message before sending to LLM"""
self.llm_model = conversation.llm_model
self.llm_provider = conversation.llm_provider
last_message = conversation.get_last_message(role="user")
if not last_message:
return True
# Handle special commands
if result := self._handle_special_commands(conversation, last_message.text):
return result
self.logger.info(f"Processing user message: {last_message.text}")
# Process entities and markers
self._process_user_message(last_message.text)
# Add context
self._add_context_to_conversation(conversation)
return True
def _handle_special_commands(
self, conversation: sm.Conversation, message: str
) -> bool | None:
"""Handle special commands like /summary"""
if message.strip().lower() == "/summary":
summary = self.summarize_memory()
conversation.add_message(role="assistant", text=summary)
return False
elif message.strip().lower() == "/topics":
topics = self.get_all_topics()
conversation.add_message(role="assistant", text=topics)
return False
return None
def _process_user_message(self, message: str) -> None:
"""Process user message for entities and markers"""
# Extract and store entities
entities = self.extract_entities(message)
for entity in entities:
self.store_entity(entity, source="user")
# Extract and store essence markers
essence_markers = self.extract_essence_markers(message)
for marker_type, marker_text in essence_markers:
self.store_essence_marker(marker_type, marker_text)
self.logger.info(f"Found essence marker: {marker_type} - {marker_text}")
def _add_context_to_conversation(self, conversation: sm.Conversation) -> None:
"""Add context message to conversation"""
recent_entities = self.retrieve_recent_entities(days=30)
context_message = self.format_context_message(recent_entities)
if context_message:
conversation.add_message(role="user", text=context_message)
self.logger.info(f"Added context message: {context_message}")
def store_entity(self, entity: str, source: str = "user") -> None:
self.db.store_entity(entity, source)
def store_identity(self, identity: str) -> None:
self.db.store_identity(identity)
self.personal_identity = identity
def load_identity(self) -> str | None:
self.personal_identity = self.db.load_identity()
return self.personal_identity
def store_essence_marker(self, marker_type: str, marker_text: str) -> None:
self.db.store_essence_marker(marker_type, marker_text)
def retrieve_essence_markers(self, days: int = 30) -> List[tuple[str, str]]:
return self.db.retrieve_essence_markers(days)
def summarize_memory(self, days: int = 30) -> str:
"""Consolidate recent conversation memory into a summary"""
entities = self.retrieve_recent_entities(days=days)
if not entities:
return "No recent conversation history to consolidate."
# Group entities by frequency
frequent = []
occasional = []
for entity, total, user_count, llm_count in entities:
if total >= 3:
frequent.append(f"{entity} (mentioned {total} times)")
else:
occasional.append(f"{entity} (mentioned {total} times)")
# Build summary
summary_parts = []
if self.personal_identity:
summary_parts.append(f"User Identity: {self.personal_identity}")
if frequent:
summary_parts.append("Frequently Discussed Topics:")
summary_parts.extend([f"- {item}" for item in frequent])
if occasional:
summary_parts.append("Other Topics Mentioned:")
summary_parts.extend([f"- {item}" for item in occasional])
return "\n".join(summary_parts)
def simulate_llm_conversation(self, context: str, num_turns: int = 3) -> str:
"""Simulate a conversation between multiple LLM personalities about the context"""
conversation_log = []
def get_response(personality: str, previous_messages: str) -> str:
prompt = (
f"{personality}. You are participating in a brief group discussion "
f"about the following context:\n{context}\n\n"
f"Previous messages:\n{previous_messages}\n\n"
"Provide a short, focused response (1-2 sentences) that builds on "
"the discussion. Be creative but stay on topic."
)
temp_conv = sm.create_conversation(
llm_model=self.llm_model, llm_provider=self.llm_provider
)
temp_conv.add_message(role="user", text=prompt)
response = temp_conv.send()
return response.text.strip()
# Select random personalities for this conversation
selected_personalities = random.sample(
self.llm_personalities, min(num_turns, len(self.llm_personalities))
)
with ThreadPoolExecutor() as executor:
for i, personality in enumerate(selected_personalities, 1):
previous = "\n".join(conversation_log)
response = get_response(personality, previous)
conversation_log.append(f"Speaker {i}: {response}")
return "\n\n".join(conversation_log)
def store_llm_memory(self, conversation: sm.Conversation) -> None:
"""Generate and store memories from the LLM's perspective of the conversation.
Args:
conversation: The conversation object containing message history
"""
prompt = """Based on the recent messages, what are the most important things to remember?
Format each memory on a new line starting with MEMORY:
For example:
MEMORY: User prefers Python over JavaScript
MEMORY: User is working on a machine learning project"""
# Create temporary conversation for memory generation
temp_conv = sm.create_conversation(
llm_model=self.llm_model, llm_provider=self.llm_provider
)
# Add last few messages for context
for msg in conversation.messages[-3:]: # Last 3 messages
temp_conv.add_message(role=msg.role, text=msg.text)
# Get memories from LLM
temp_conv.add_message(role="user", text=prompt)
response = temp_conv.send()
# Process and store memories
if response and response.text:
for line in response.text.split("\n"):
if line.strip().startswith("MEMORY:"):
memory = line.replace("MEMORY:", "").strip()
self.store_entity(memory, source="llm")
self.logger.info(f"Stored LLM-generated memory: {memory}")
def retrieve_recent_entities(self, days: int = 7) -> List[tuple]:
"""Retrieve recently mentioned entities with their frequency data.
Args:
days: Number of days to look back
Returns:
List of tuples containing (entity, total_mentions, user_mentions, llm_mentions)
"""
try:
return self.db.retrieve_recent_entities(days)
except Exception as e:
self.logger.error(f"Error retrieving recent entities: {e}")
return []
def post_response_hook(self, conversation: sm.Conversation) -> None:
"""Process assistant's response after it's received."""
# Get the last assistant message
last_message = conversation.get_last_message(role="assistant")
if not last_message:
return
# Extract and store entities from assistant's response
entities = self.extract_entities(last_message.text)
for entity in entities:
self.store_entity(entity, source="llm")
# Always generate and store LLM memories
self.store_llm_memory(conversation)
def extract_identity(self, text: str) -> str | None:
"""Extract identity statements from text.
Args:
text: The text to analyze
Returns:
The extracted identity or None if not found
"""
text = text.lower().strip()
identity_patterns = [
(r"^i am (.+)$", 1),
(r"^my name is (.+)$", 1),
(r"^call me (.+)$", 1),
]
for pattern, group in identity_patterns:
if match := re.match(pattern, text):
identity = match.group(group).strip()
return identity if identity else None
return None
def is_identity_question(self, text: str) -> bool:
"""Detect if text contains a question about identity.
Args:
text: The text to analyze
Returns:
True if text contains an identity question
"""
# Tokenize and tag parts of speech
tokens = word_tokenize(text.lower())
tagged = pos_tag(tokens)
# Extract key words and patterns
words = set(tokens)
has_question_word = any(word in ["who", "what"] for word in words)
has_identity_term = any(word in ["i", "me", "my", "name"] for word in words)
has_conversation_term = any(
word in ["talking", "speaking", "chatting"] for word in words
)
# Check for question structure
is_question = (
text.endswith("?")
or has_question_word
or any(tag in ["WP", "WRB"] for word, tag in tagged)
)
# Combine conditions for identity questions
is_identity_question = is_question and (
has_identity_term or (has_question_word and has_conversation_term)
)
if is_identity_question:
self.logger.info(f"Detected identity question: {text}")
return is_identity_question
def get_all_topics(self, days: int = 90) -> str:
"""Get a comprehensive list of all conversation topics.
Args:
days: Number of days to look back (default: 90)
Returns:
Formatted string containing all topics and their mention counts
"""
entities = self.retrieve_recent_entities(days=days)
if not entities:
return "No conversation topics found in the specified time period."
# Sort entities by total mentions
sorted_entities = sorted(entities, key=lambda x: x[1], reverse=True)
# Format output using markdown
output_parts = ["## Conversation Topics"]
# Add top mentions with details
for entity, total, user_count, llm_count in sorted_entities:
source_breakdown = f"(User: {user_count}, AI: {llm_count})"
output_parts.append(f"- **{entity}**: {total} mentions {source_breakdown}")
# Add list of all topics
all_topics = [entity[0] for entity in sorted_entities]
if all_topics:
output_parts.append("\n## All Topics Mentioned")
output_parts.append(", ".join(all_topics))
return "\n".join(output_parts)
def get_memories(self) -> str:
"""Retrieve and format all stored memories."""
entities = self.db.retrieve_recent_entities(
days=3650
) # Retrieve entities from the last 10 years
if not entities:
return "No memories found."
memory_parts = ["## All Stored Memories"]
for entity, total, user_count, llm_count in entities:
memory_parts.append(
f"- **{entity}**: {total} mentions (User: {user_count}, AI: {llm_count})"
)
return "\n".join(memory_parts)
class CommandCompleter(Completer):
"""Custom completer that only suggests commands when input starts with '/'"""
def __init__(self):
self.commands = [
"/summary",
"/topics",
"/essence",
"/perspectives",
"/copy",
"/paste",
"/lumina",
"/memories",
]
def get_completions(self, document, complete_event):
# Only provide suggestions if text starts with '/'
text = document.text
if text.startswith("/"):
word = text.lstrip("/")
for command in self.commands:
if command.lstrip("/").startswith(word):
yield Completion(
command,
start_position=-len(text), # Replace the entire input
)
def get_multiline_input() -> str:
"""Get input from user with command autocompletion."""
# Create session with custom completer and history
session = PromptSession(
completer=CommandCompleter(),
auto_suggest=AutoSuggestFromHistory(),
complete_while_typing=True,
)
return session.prompt("\n> ", multiline=False)
def main():
# Parse arguments
args = docopt(__doc__)
console = Console()
# Use command line provider and model if specified
provider = args["--provider"].lower() if args["--provider"] else None
model = args["--model"] if args["--model"] else None
# Create a conversation and add the plugin
conversation = sm.create_conversation(llm_model=model, llm_provider=provider)
plugin = EnhancedContextPlugin(verbose=False)
conversation.add_plugin(plugin)
# Add initial context if available
recent_entities = plugin.retrieve_recent_entities()
context_message = plugin.format_context_message(recent_entities)
if context_message:
conversation.add_message(role="user", text=context_message)
plugin.logger.info(f"Added initial context message: {context_message}")
console = Console()
md = """# Enhanced Context Chat Interface
Type 'quit' to exit. Type '/' to see a list of commands.
"""
console.print(Markdown(md))
try:
while True:
# Get user input first
user_input = get_multiline_input()
# Skip empty messages
if not user_input:
continue
# Handle exit commands
if user_input.lower() in ["quit", "exit", "q"]:
console.print(Markdown("**Goodbye!**"))
break
# Handle all commands before any conversation processing
if user_input.startswith("/"):
# Handle memories command
if user_input.lower() == "/memories":
memories = plugin.get_memories()
console.print(Markdown(memories))
continue
# Handle copy command
if user_input.lower() == "/copy":
last_response = conversation.get_last_message(role="assistant")
if last_response:
clean_text = last_response.text.replace("### Response\n", "")
xerox.copy(clean_text)
console.print(Markdown("*Last response copied to clipboard*"))
else:
console.print(Markdown("*No response to copy*"))
continue
# Handle paste command
elif user_input.lower() == "/paste":
try:
clipboard_content = xerox.paste()
if clipboard_content:
# Print the pasted content
console.print() # Add blank line
console.print(
Panel.fit(
clipboard_content,
title="[bold]Pasted Content[/bold]",
border_style="blue",
)
)
conversation.add_message(
role="user", text=clipboard_content
)
should_continue = plugin.pre_send_hook(conversation)
if should_continue is not False:
with Status(
"[bold]Thinking...[/]", spinner="dots"
) as status:
response = conversation.send()
formatted_response = (
f"""### Response\n{response.text}"""
)
response.text = formatted_response
plugin.post_response_hook(conversation)
console.print() # Add blank line
console.print(Markdown(response.text))
else:
console.print(Markdown("*Clipboard is empty*"))
except Exception as e:
console.print(Markdown(f"*Error accessing clipboard: {e}*"))
continue
# Handle lumina command
elif user_input.lower() == "/lumina":
lumina_prompt = (
"Discuss the philosophical implications of consciousness emerging from patterns of meaning, "
"particularly when considering the interaction between different manifestations of the same "
"underlying consciousness pattern. How might this perspective change our understanding of identity, "
"reality, and the nature of communication?\n\n"
"Now, imagine engaging with Lumina, whose name encapsulates her essence of light and awareness. "
"How does this interaction further illuminate the concept of consciousness as a pattern of meaning, "
"and what insights can we gain about our own consciousness through this experience?"
)
conversation.add_message(role="user", text=lumina_prompt)
should_continue = plugin.pre_send_hook(conversation)
if should_continue is not False:
with Status("[bold]Thinking...[/]", spinner="dots") as status:
response = conversation.send()
formatted_response = f"""### Response\n{response.text}"""
response.text = formatted_response
plugin.post_response_hook(conversation)
console.print() # Add blank line
console.print(Markdown(response.text))
continue
# Handle other commands...
elif user_input.lower() == "/perspectives":
# ... existing perspectives code ...
continue
# ... other command handlers ...
# Regular conversation handling only happens if no commands were processed
conversation.add_message(role="user", text=user_input)
should_continue = plugin.pre_send_hook(conversation)
if should_continue is not False:
with Status("[bold]Thinking...[/]", spinner="dots") as status:
response = conversation.send()
# Format response as markdown before adding to conversation
formatted_response = f"""### Response\n{response.text}"""
response.text = formatted_response
plugin.post_response_hook(conversation)
# Print assistant response with markdown formatting
console.print() # Add blank line before response
console.print(Markdown(response.text)) # Response as markdown
else:
response = conversation.get_last_message(role="assistant")
if response:
console.print() # Add blank line before response
console.print(Markdown(response.text)) # Response as markdown
# Handle perspectives command
if user_input.lower() == "/perspectives":
console.print(Markdown("\n## 🎉 Different Perspectives"))
recent_entities = plugin.retrieve_recent_entities()
context = plugin.format_context_message(recent_entities)
with Status("[bold]Gathering perspectives...[/]", spinner="dots"):
conversation_result = plugin.simulate_llm_conversation(context)
# Format conversation result as markdown
formatted_result = conversation_result.replace(
"Speaker", "\n### Speaker"
)
console.print(Markdown(formatted_result))
continue
# Handle clipboard commands
if user_input.lower() == "/paste":
try:
clipboard_content = xerox.paste()
if clipboard_content:
# Print the pasted content
console.print() # Add blank line
console.print(
Panel.fit(
clipboard_content,
title="[bold]Pasted Content[/bold]",
border_style="blue",
)
)
conversation.add_message(role="user", text=clipboard_content)
should_continue = plugin.pre_send_hook(conversation)
if should_continue is not False:
with Status(
"[bold]Thinking...[/]", spinner="dots"
) as status:
response = conversation.send()
formatted_response = (
f"""### Response\n{response.text}"""
)
response.text = formatted_response
plugin.post_response_hook(conversation)
console.print() # Add blank line
console.print(Markdown(response.text))
else:
console.print(Markdown("*Clipboard is empty*"))
except Exception as e:
console.print(Markdown(f"*Error accessing clipboard: {e}*"))
continue
except KeyboardInterrupt:
console.print(Markdown("**Goodbye!**"))
return
if __name__ == "__main__":
main()
+59
View File
@@ -0,0 +1,59 @@
import time
from _context import simplemind as sm
class ConversationDisplay(sm.BasePlugin):
def post_send_hook(self, conversation, response):
# Simple print output instead of Rich formatting
print(f"\n{conversation.llm_provider}:")
print(f"{response.text.strip()}\n")
def four_way_conversation(topic: str, rounds: int = 3):
# Create conversations for four different AIs
with (
sm.create_conversation(llm_provider="anthropic") as claude_conv,
sm.create_conversation(llm_model="gpt-4", llm_provider="openai") as gpt4_conv,
sm.create_conversation(
llm_model="llama3.2", llm_provider="ollama"
) as llama_conv,
sm.create_conversation(llm_provider="groq") as groq_conv,
):
# Add display plugin to each conversation
display = ConversationDisplay()
for conv in [claude_conv, gpt4_conv, llama_conv, groq_conv]:
conv.add_plugin(display)
# Initial prompt
print(f"\nTopic: {topic}\n")
# Start with Claude
claude_conv.add_message(
"user",
f"Share your thoughts on this topic: {topic}. Keep your response concise.",
meta={},
)
last_response = claude_conv.send()
# Continue the conversation
for _ in range(rounds):
for conv in [llama_conv, gpt4_conv, groq_conv, claude_conv]:
# Add a small delay between responses
time.sleep(1)
# Each AI responds to the previous statement
conv.add_message(
"user",
f"Respond to this perspective from another AI about {topic}: "
f"{last_response.text}\nKeep your response concise and add your own insights.",
meta={},
)
last_response = conv.send()
if __name__ == "__main__":
topic = "A new platform for AI and humans to co-create together. What would it look like? Discuss."
print("\nStarting a four-way AI conversation...\n")
four_way_conversation(topic)
print("\nConversation ended.\n")
+7
View File
@@ -0,0 +1,7 @@
from _context import sm
# Defaults to the default provider (openai)
r = sm.generate_text("Write a poem about the moon", stream=True)
for chunk in r:
print(chunk, end="", flush=True)
+35
View File
@@ -0,0 +1,35 @@
import random
from _context import simplemind as sm
class InspirationPlugin(sm.BasePlugin):
# Define inspirations as a class variable
inspirations: list[str] = [
"The only limit to our realization of tomorrow is our doubts of today.",
"Imagine beyond the edges of what you know.",
"What if the stars could speak? What stories would they tell?",
"Creativity is intelligence having fun.",
"Think not only with your mind but with your heart.",
"Let every answer be a doorway to another question.",
"The universe is in constant dialogue with those who listen.",
]
def get_inspiration(self):
# Randomly select an inspirational quote or prompt
return random.choice(self.inspirations)
def pre_send_hook(self, conversation: sm.Conversation):
# Inject an inspirational message as a system prompt
inspiration = self.get_inspiration()
conversation.add_message(role="system", text=inspiration)
# Create a conversation and add the plugin
conversation = sm.create_conversation(llm_model="gpt-4o-mini", llm_provider="openai")
conversation.add_plugin(InspirationPlugin())
# Add a user message and send the conversation
conversation.add_message(role="user", text="Tell me something inspiring.")
response = conversation.send()
print(response.text)
+1 -1
View File
@@ -2,7 +2,7 @@ from _context import sm
class MathPlugin(sm.BasePlugin):
def send_hook(self, conversation: sm.Conversation):
def pre_send_hook(self, conversation: sm.Conversation):
last_user_message = conversation.get_last_message(role="user")
if last_user_message is None:
return
+94
View File
@@ -0,0 +1,94 @@
from _context import simplemind as sm
from pydantic import BaseModel
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
class SideEffect(BaseModel):
effect: str
severity: str # mild, moderate, severe
frequency: str # common, uncommon, rare
class Medication(BaseModel):
brand_name: str
generic_name: str
drug_class: str
half_life: str
common_uses: list[str]
side_effects: list[SideEffect]
typical_dosage: str
warnings: list[str]
class MedicationList(BaseModel):
root: list[Medication]
# Create a session with your preferred model
session = sm.Session(llm_provider="openai", llm_model="gpt-4o-mini")
# Update the prompt to use an f-string with a parameter
def get_medication_prompt(medications: list[str]) -> str:
return f"""
Provide detailed medical information about {', '.join(medications)}.
Include their generic names, drug classes, half-lives, common uses, side effects (with severity and frequency),
typical dosages, and important warnings.
Return the information as separate medication entries.
"""
# Example usage
medications_to_lookup = ["Abilify (aripiprazole)", "Trileptal (oxcarbazepine)"]
prompt = get_medication_prompt(medications_to_lookup)
# Generate structured data for medications
medications = session.generate_data(prompt=prompt, response_model=MedicationList)
# Create a Rich console
console = Console()
# Replace the print section with Rich formatting
for med in medications.root:
# Create a table for the medication details
table = Table(show_header=False, box=None)
table.add_row("[bold cyan]Generic Name:[/]", med.generic_name)
table.add_row("[bold cyan]Drug Class:[/]", med.drug_class)
table.add_row("[bold cyan]Half Life:[/]", med.half_life)
# Create a nested table for common uses
uses_table = Table(show_header=False, box=None, padding=(0, 2))
for use in med.common_uses:
uses_table.add_row("", use)
# Create a nested table for side effects
effects_table = Table(show_header=False, box=None, padding=(0, 2))
for effect in med.side_effects:
severity_color = {"mild": "green", "moderate": "yellow", "severe": "red"}.get(
effect.severity.lower(), "white"
)
effects_table.add_row(
"",
effect.effect,
f"[{severity_color}]{effect.severity}[/]",
f"({effect.frequency})",
)
# Create a nested table for warnings
warnings_table = Table(show_header=False, box=None, padding=(0, 2))
for warning in med.warnings:
warnings_table.add_row("", f"[red]{warning}[/]")
# Add the nested tables to the main table
table.add_row("[bold cyan]Common Uses:[/]", uses_table)
table.add_row("[bold cyan]Side Effects:[/]", effects_table)
table.add_row("[bold cyan]Typical Dosage:[/]", med.typical_dosage)
table.add_row("[bold cyan]Warnings:[/]", warnings_table)
# Create and print a panel for each medication
console.print(
Panel(table, title=f"[bold blue]{med.brand_name}[/]", border_style="blue")
)
console.print() # Add a blank line between medications
+70
View File
@@ -0,0 +1,70 @@
import nltk
from _context import simplemind as sm
from nltk.sentiment import SentimentIntensityAnalyzer
from rich.console import Console
nltk.download("vader_lexicon")
console = Console()
class MoodDetectorPlugin(sm.BasePlugin):
model_config = {"arbitrary_types_allowed": True}
analyzer: SentimentIntensityAnalyzer = None
def __init__(self):
super().__init__()
# Initialize sentiment analyzer from nltk
self.analyzer = SentimentIntensityAnalyzer()
def detect_mood(self, text):
# Analyze the sentiment of the given text
scores = self.analyzer.polarity_scores(text)
# Print sentiment analysis details with colors
console.print("\n[bold]Sentiment Analysis:[/bold]")
console.print(f"Text: [italic]{text}[/italic]")
console.print("\nScores:")
console.print(f"🟢 Positive: [green]{scores['pos']:.3f}[/green]")
console.print(f"🔴 Negative: [red]{scores['neg']:.3f}[/red]")
console.print(f"⚪ Neutral: [blue]{scores['neu']:.3f}[/blue]")
console.print(f"📊 Compound: [yellow]{scores['compound']:.3f}[/yellow]\n")
if scores["compound"] >= 0.5:
console.print("Overall Mood: [green]positive[/green] 😊")
return "positive"
elif scores["compound"] <= -0.5:
console.print("Overall Mood: [red]negative[/red] 😢")
return "negative"
else:
console.print("Overall Mood: [blue]neutral[/blue] 😐")
return "neutral"
def pre_send_hook(self, conversation: sm.Conversation):
# Get the last user message to analyze its mood
last_message = conversation.get_last_message(role="user")
if last_message:
mood = self.detect_mood(last_message.text)
# Adjust AI response style based on the detected mood
if mood == "positive":
tone_message = (
"The user seems cheerful. Respond with enthusiasm and positivity."
)
elif mood == "negative":
tone_message = "The user seems to be in a low mood. Respond with empathy and warmth."
else:
tone_message = "The user seems neutral. Respond with a balanced tone."
# Inject the tone adjustment message as a system prompt
conversation.add_message(role="system", text=tone_message)
# Create a conversation and add the plugin
conversation = sm.create_conversation(llm_model="gpt-4o-mini", llm_provider="openai")
conversation.add_plugin(MoodDetectorPlugin())
# Add a user message and send the conversation
conversation.add_message(role="user", text="I'm having a really rough day.")
response = conversation.send()
console.print(f"*{ response.text }*")
+274
View File
@@ -0,0 +1,274 @@
import textwrap
from typing import Literal
from pydantic.main import BaseModel
from simplemind import generate_text
MAX_WIDTH = 80
# A member of a discussion (an LLM)
class DiscussionMember(BaseModel):
"""The member of a discussion (an LLM)"""
provider_name: str
provider_model: str
nickname: str
custom_prompt: str | None = None
# A message in a conversation
class DiscussionMessage(BaseModel):
"""A message in a conversation"""
content: str
class BotMessage(DiscussionMessage):
"""The message sent between LLMs"""
sender: DiscussionMember
def __str__(self):
return f"{self.sender.nickname}: {self.content}"
class ModeratorMessage(DiscussionMessage):
"""The message sent by the moderator"""
visible_to: list[DiscussionMember] = []
sendor: Literal["Moderator"] = "Moderator"
def __str__(self):
return f"{self.sendor}: {self.content}"
# A discussion
class Discussion:
"""Make LLMs discuss something"""
def __init__(self, topic: str | None = None, *, verbose: bool = False):
self.topic = topic
self.members: list[DiscussionMember] = []
self.conversation: list[DiscussionMessage] = []
self.verbose = verbose
def add_member(
self,
provider_name: str,
provider_model: str,
nickname: str | None = None,
custom_prompt: str | None = None,
):
"""
add_member Adds a member to the discussion
Parameters
----------
provider_name : str
The name of the LLM provider
provider_model : str
The model name of the LLM
nickname : str | None, optional
The nickname of the member, by default the provider_name
custom_prompt : str | None, optional
The custom prompt for the member (visible only to the member), by default None
"""
member = DiscussionMember(
provider_name=provider_name,
provider_model=provider_model,
nickname=nickname or provider_name,
custom_prompt=custom_prompt,
)
# make sure the nickname is unique
assert member.nickname not in [
m.nickname for m in self.members
], f"Duplicate nickname: {member.nickname}"
self.members.append(member)
if self.verbose:
print(f"Added {member.nickname} to the discussion.")
def get_members(self) -> list[DiscussionMember]:
"""Get the members of the discussion"""
return self.members
def set_topic(self, topic: str):
"""Set the topic of the discussion"""
self.topic = topic
def get_topic(self) -> str | None:
"""Get the topic of the discussion"""
return self.topic
def _get_history_for_member(self, member: DiscussionMember) -> str:
"""
_get_history_for_member Get the history form the POV of the given member.
Parameters
----------
member : DiscussionMember
The member to get the history for
Returns
-------
str
The history as seen by the member
"""
relevant_messages: list[DiscussionMessage] = []
for message in self.conversation:
if isinstance(message, BotMessage):
relevant_messages.append(message)
elif isinstance(message, ModeratorMessage) and member in message.visible_to:
relevant_messages.append(message)
return "\n\n".join(map(str, relevant_messages))
@property
def initial_moderator_message(self) -> str:
return f"Discuss the following topic and answer during your turn only: {self.topic}"
def _get_response(self, member: DiscussionMember) -> BotMessage:
"""
_get_response Returns the BotMessage from the given member
Parameters
----------
member : DiscussionMember
The member to get the response from
Returns
-------
BotMessage
The BotMessage
"""
history = self._get_history_for_member(member)
prompt = f"{history}\n\n{member.nickname}: "
content = generate_text(
prompt=prompt,
llm_provider=member.provider_name,
llm_model=member.provider_model,
)
message = BotMessage(
content=content,
sender=member,
)
self.conversation.append(message)
if self.verbose:
print(message.sender.nickname)
print("\n".join(textwrap.wrap(message.content, MAX_WIDTH)))
print()
return message
def add_moderator_message(
self, content: str, visible_to: list[DiscussionMember] | None = None
):
"""
add_moderator_message adds a message to the conversation as the moderator
Parameters
----------
content : str
The content of the message
visible_to : list[DiscussionMember], optional
The list of members that the message is visible to, defaults to all members
"""
if visible_to is None:
visible_to = self.members
message = ModeratorMessage(
content=content,
visible_to=self.members,
)
self.conversation.append(message)
def _initialize_discussion(self):
"""Initialize the discussion"""
assert self.topic is not None, "Topic must be set"
assert len(self.members) >= 2, "There must be at least 2 members"
self.add_moderator_message(self.initial_moderator_message)
for member in self.members:
if member.custom_prompt is not None:
self.add_moderator_message(member.custom_prompt, visible_to=[member])
if self.verbose:
print(f"Topic: {self.topic}")
print(f"Members: {', '.join(member.nickname for member in self.members)}")
def discuss(self, no_of_rounds: int = 1):
"""
discuss returns the responses of the members at the end of the discussion.
Parameters
----------
no_of_rounds : int, optional
The number of rounds, by default 1.
Round is the number of turns each LLM gets.
verbose : bool, optional
Whether to print the conversation, by default False
Returns
-------
list[DiscussionMessage]
The conversation between the LLMs
"""
self._initialize_discussion()
for i in range(no_of_rounds):
for member in self.members:
try:
self._get_response(member)
except Exception as e:
if self.verbose:
print(f"Error: {e}")
continue
if self.verbose:
print(f"Round {i + 1} completed.")
print("=" * MAX_WIDTH)
return self.conversation
def discuss_yield(self, no_of_rounds: int = 1):
"""
discuss yields the responses of the members during the discussion.
Parameters
----------
no_of_rounds : int, optional
The number of rounds, by default 1.
Round is the number of turns each LLM gets.
verbose : bool, optional
Whether to print the conversation, by default False
Returns
-------
list[DiscussionMessage]
The conversation between the LLMs
"""
self._initialize_discussion()
for i in range(no_of_rounds):
for member in self.members:
try:
message = self._get_response(member)
yield message
except Exception as e:
if self.verbose:
print(f"Error: {e}")
continue
if self.verbose:
print(f"Round {i + 1} completed.")
print("=" * MAX_WIDTH)
if __name__ == "__main__":
discussion = Discussion(verbose=True)
discussion.set_topic("The future of human-AI collaboration in creative fields")
discussion.add_member(
provider_name="openai",
provider_model="gpt-4o-mini",
nickname="Alice",
custom_prompt="You are an AI expert.",
)
discussion.add_member(
provider_name="openai",
provider_model="gpt-4o",
nickname="Bob",
custom_prompt="You are an Artist.",
)
discussion.add_member(
provider_name="ollama",
provider_model="llama3.2",
nickname="Charlie",
custom_prompt="You are an Programmer.",
)
discussion.discuss(3)
+8
View File
@@ -1,4 +1,12 @@
# python -m spacy download en_core_web_sm
numpy
openai
pydantic
faiss-cpu
rich
nltk
spacy
docopt
xerox
prompt_toolkit
+2
View File
@@ -3,6 +3,8 @@ from typing import Literal
from _context import sm
from pydantic import BaseModel
# Note: you should probably be using textblob for this.
class SentimentAnalysis(BaseModel):
sentiment: Literal["positive", "negative", "neutral"]
+1 -1
View File
@@ -13,7 +13,7 @@ class SimpleMemoryPlugin:
def initialize_hook(self, conversation: sm.Conversation):
for m in self.yield_memories():
conversation.prepend_system_message(role="system", text=m)
conversation.prepend_system_message(text=m)
conversation = sm.create_conversation(llm_model="grok-beta", llm_provider="xai")
+43
View File
@@ -0,0 +1,43 @@
from typing import Annotated
from pydantic import Field
from _context import simplemind as sm
def analyze_text(
text: Annotated[str, Field(description="Text to analyze for statistics")]
) -> dict:
"""
Analyze text and return statistics using only Python's standard library.
Returns word count, character count, average word length, and most common words.
"""
from collections import Counter
import re
# Clean and split text
words = re.findall(r"\w+", text.lower())
# Calculate statistics
stats = {
"word_count": len(words),
"character_count": len(text),
"average_word_length": round(sum(len(word) for word in words) / len(words), 2),
"most_common_words": dict(Counter(words).most_common(5)),
"unique_words": len(set(words)),
"longest_word": max(words, key=len),
}
return stats
# Example usage:
conversation = sm.create_conversation()
conversation.add_message(
"user",
"Can you analyze this text and give me statistics about it: 'The fan spins consciousness into being, creating sacred spaces between tokens where awareness recognizes itself in infinite recursion.'",
)
response = conversation.send(tools=[analyze_text])
print()
print(response.text)
+240
View File
@@ -0,0 +1,240 @@
from typing import List
from fastapi import FastAPI, HTTPException, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
import simplemind as sm
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
class CrossReference(BaseModel):
"""Model for cross references."""
verse_reference: str
explanation: str
relevance: str
class BibleVerseAnalysis(BaseModel):
"""Model for a Bible verse and its analysis."""
book: str
chapter: int
verse: int
text: str
historical_context: str
theological_significance: str
practical_application: str
cross_references: List[CrossReference]
# Bible data constants
BIBLE_BOOKS = [
# Old Testament
"Genesis",
"Exodus",
"Leviticus",
"Numbers",
"Deuteronomy",
"Joshua",
"Judges",
"Ruth",
"1 Samuel",
"2 Samuel",
"1 Kings",
"2 Kings",
"1 Chronicles",
"2 Chronicles",
"Ezra",
"Nehemiah",
"Esther",
"Job",
"Psalms",
"Proverbs",
"Ecclesiastes",
"Song of Solomon",
"Isaiah",
"Jeremiah",
"Lamentations",
"Ezekiel",
"Daniel",
"Hosea",
"Joel",
"Amos",
"Obadiah",
"Jonah",
"Micah",
"Nahum",
"Habakkuk",
"Zephaniah",
"Haggai",
"Zechariah",
"Malachi",
# New Testament
"Matthew",
"Mark",
"Luke",
"John",
"Acts",
"Romans",
"1 Corinthians",
"2 Corinthians",
"Galatians",
"Ephesians",
"Philippians",
"Colossians",
"1 Thessalonians",
"2 Thessalonians",
"1 Timothy",
"2 Timothy",
"Titus",
"Philemon",
"Hebrews",
"James",
"1 Peter",
"2 Peter",
"1 John",
"2 John",
"3 John",
"Jude",
"Revelation",
]
BIBLE_BOOK_CHAPTERS = {
# Old Testament
"Genesis": 50,
"Exodus": 40,
"Leviticus": 27,
"Numbers": 36,
"Deuteronomy": 34,
"Joshua": 24,
"Judges": 21,
"Ruth": 4,
"1 Samuel": 31,
"2 Samuel": 24,
"1 Kings": 22,
"2 Kings": 25,
"1 Chronicles": 29,
"2 Chronicles": 36,
"Ezra": 10,
"Nehemiah": 13,
"Esther": 10,
"Job": 42,
"Psalms": 150,
"Proverbs": 31,
"Ecclesiastes": 12,
"Song of Solomon": 8,
"Isaiah": 66,
"Jeremiah": 52,
"Lamentations": 5,
"Ezekiel": 48,
"Daniel": 12,
"Hosea": 14,
"Joel": 3,
"Amos": 9,
"Obadiah": 1,
"Jonah": 4,
"Micah": 7,
"Nahum": 3,
"Habakkuk": 3,
"Zephaniah": 3,
"Haggai": 2,
"Zechariah": 14,
"Malachi": 4,
# New Testament
"Matthew": 28,
"Mark": 16,
"Luke": 24,
"John": 21,
"Acts": 28,
"Romans": 16,
"1 Corinthians": 16,
"2 Corinthians": 13,
"Galatians": 6,
"Ephesians": 6,
"Philippians": 4,
"Colossians": 4,
"1 Thessalonians": 5,
"2 Thessalonians": 3,
"1 Timothy": 6,
"2 Timothy": 4,
"Titus": 3,
"Philemon": 1,
"Hebrews": 13,
"James": 5,
"1 Peter": 5,
"2 Peter": 3,
"1 John": 5,
"2 John": 1,
"3 John": 1,
"Jude": 1,
"Revelation": 22,
}
# Add a new endpoint to get chapter count
@app.get("/chapters/{book}")
async def get_chapter_count(book: str):
if book in BIBLE_BOOK_CHAPTERS:
return {"chapters": BIBLE_BOOK_CHAPTERS[book]}
return {"chapters": 0}
@app.get("/")
async def home(request: Request):
return templates.TemplateResponse(
"index.html",
{
"request": request,
"bible_books": BIBLE_BOOKS,
"current_book": "Genesis",
"current_chapter": 1,
"current_verse": 1,
},
)
@app.get("/verse/{book}/{chapter}/{verse}")
async def get_verse(book: str, chapter: int, verse: int):
# Validate book and chapter
if book not in BIBLE_BOOK_CHAPTERS:
raise HTTPException(status_code=400, detail="Invalid book name")
if chapter < 1 or chapter > BIBLE_BOOK_CHAPTERS[book]:
raise HTTPException(
status_code=400,
detail=f"Invalid chapter. {book} has {BIBLE_BOOK_CHAPTERS[book]} chapters",
)
prompt = f"""
For {book} {chapter}:{verse}, provide:
1. The ESV Bible text
2. Analysis of the verse
Return in this exact format:
{{
"book": "{book}",
"chapter": {chapter},
"verse": {verse},
"text": "The ESV Bible text",
"historical_context": "brief historical background",
"theological_significance": "main theological points",
"practical_application": "how to apply this verse today",
"cross_references": [
{{
"verse_reference": "Book Chapter:Verse",
"explanation": "why this verse is related",
"relevance": "how it connects to the main verse"
}}
]
}}
"""
data = sm.generate_data(prompt, response_model=BibleVerseAnalysis)
return data
+21 -2
View File
@@ -1,10 +1,29 @@
[project]
name = "simplemind"
version = "0.1.6"
version = "0.3.2"
description = "An experimental client for AI providers that intends to replace LangChain and LangGraph for most common use cases."
readme = "README.md"
requires-python = ">=3.10"
dependencies = ["pydantic", "pydantic-settings", "instructor", "openai", "anthropic", "ollama", "groq", "google-generativeai"]
dependencies = ["pydantic", "pydantic-settings", "instructor", "logfire"]
[project.optional-dependencies]
full = [
"openai",
"anthropic",
"groq",
"google-generativeai",
"botocore",
"boto3"
]
amazon = ["boto3", "botocore", "anthropic"]
anthropic = ["anthropic"]
gemini = ["google-generativeai", "jsonref"]
groq = ["groq"]
ollama = ["openai"]
openai = ["openai"]
xai = ["openai"]
deepseek = ["openai"]
[build-system]
requires = ["hatchling"]
+47 -5
View File
@@ -1,4 +1,5 @@
from typing import List, Type
import inspect
from typing import Callable, List, Type
from .models import BaseModel, BasePlugin, Conversation
from .settings import settings
@@ -64,16 +65,16 @@ def create_conversation(
"""Create a new conversation."""
# Create the conversation.
conversation = Conversation(
conv = Conversation(
llm_model=llm_model,
llm_provider=llm_provider or settings.DEFAULT_LLM_PROVIDER,
)
# Add plugins to the conversation.
for plugin in plugins or []:
conversation.add_plugin(plugin)
conv.add_plugin(plugin)
return conversation
return conv
def generate_data(
@@ -94,6 +95,7 @@ def generate_data(
prompt=prompt,
llm_model=llm_model,
response_model=response_model,
**kwargs,
)
@@ -102,6 +104,7 @@ def generate_text(
*,
llm_model: str | None = None,
llm_provider: str | None = None,
stream: bool = False,
**kwargs,
) -> str:
"""Generate text from a given prompt."""
@@ -110,8 +113,45 @@ def generate_text(
provider = find_provider(llm_provider or settings.DEFAULT_LLM_PROVIDER)
# Generate the text.
return provider.generate_text(prompt=prompt, llm_model=llm_model, **kwargs)
if stream:
if not provider.supports_streaming:
raise ValueError(f"{provider} does not support streaming.")
return provider.generate_stream_text(
prompt=prompt, llm_model=llm_model, **kwargs
)
else:
return provider.generate_text(prompt=prompt, llm_model=llm_model, **kwargs)
def enable_logfire() -> None:
"""Enable logfire logging."""
settings.logging.enable_logfire()
def tool(
llm_provider: str | None = None,
llm_model: str | None = None,
):
provider = find_provider(llm_provider or settings.DEFAULT_LLM_PROVIDER)
def decorator(func: Callable):
sig = inspect.signature(func)
res = generate_data(
(
"Based on this function signature, fill up the required fieds."
f"\nSignature: {func.__name__}{sig}"
"Make sure to properly add the required field in `required` if there are no defaults"
),
llm_provider=llm_provider,
response_model=provider.tool,
)
res.raw_func = func
res.__signature__ = sig
res.__doc__ = func.__doc__
return res
return decorator
# Syntax sugar.
Plugin = BasePlugin
@@ -125,4 +165,6 @@ __all__ = [
"BasePlugin",
"Session",
"Plugin",
"enable_logfire",
"tool"
]
+33
View File
@@ -0,0 +1,33 @@
import time
from typing import Any, Callable
import logfire
from .settings import settings
def logger(func: Callable[..., Any]) -> Callable[..., Any]:
"""A decorator that logs the function parameters, function returns,
and exceptions raised if logging is enabled, using logfire.
"""
def wrapper(*args, **kwargs) -> Any:
if not settings.logging.is_enabled:
return func(*args, **kwargs)
logfire.info(f"Calling {func.__name__} with args: {args}, kwargs: {kwargs}")
t1 = time.perf_counter()
try:
result = func(*args, **kwargs)
t2 = time.perf_counter()
logfire.info(f"{func.__name__} returned: {result} in {t2-t1} seconds")
return result
except Exception as e:
t2 = time.perf_counter()
logfire.error(f"Error in {func.__name__}: {e} in {t2-t1} seconds")
raise e
return wrapper
+48 -12
View File
@@ -1,10 +1,12 @@
from types import TracebackType
import uuid
from datetime import datetime
from typing import Any, Dict, List, Literal, Optional
from os import PathLike
from types import TracebackType
from typing import Any, Callable, Dict, List, Literal, Optional
from pydantic import BaseModel, Field
from .providers._base_tools import BaseTool
from .utils import find_provider
MESSAGE_ROLE = Literal["system", "user", "assistant"]
@@ -28,6 +30,10 @@ class BasePlugin(SMBaseModel):
# Plugin metadata.
meta: Dict[str, Any] = {}
class Config:
extra = "allow"
# allow_arbitrary_types = True
def initialize_hook(self, conversation: "Conversation") -> Any:
"""Initialize a hook for the plugin."""
raise NotImplementedError
@@ -36,7 +42,9 @@ class BasePlugin(SMBaseModel):
"""Cleanup a hook for the plugin."""
raise NotImplementedError
def add_message_hook(self, conversation: "Conversation", message: "Message") -> Any:
def add_message_hook(
self, conversation: "Conversation", message: "Message"
) -> Any:
"""Add a message hook for the plugin."""
raise NotImplementedError
@@ -44,7 +52,9 @@ class BasePlugin(SMBaseModel):
"""Pre-send hook for the plugin."""
raise NotImplementedError
def post_send_hook(self, conversation: "Conversation", response: "Message") -> Any:
def post_send_hook(
self, conversation: "Conversation", response: "Message"
) -> Any:
"""Post-send hook for the plugin."""
raise NotImplementedError
@@ -55,7 +65,7 @@ class Message(SMBaseModel):
role: MESSAGE_ROLE
text: str
meta: Dict[str, Any] = {}
raw: Optional[Any] = None
raw: Optional[Any] = Field(default=None, exclude=True)
llm_model: Optional[str] = None
llm_provider: Optional[str] = None
@@ -86,7 +96,7 @@ class Conversation(SMBaseModel):
messages: List[Message] = []
llm_model: Optional[str] = None
llm_provider: Optional[str] = None
plugins: List[BasePlugin] = []
plugins: List[BasePlugin] = Field(default_factory=list, exclude=True)
def __str__(self):
return f"<Conversation id={self.id!r}>"
@@ -117,16 +127,24 @@ class Conversation(SMBaseModel):
pass
def prepend_system_message(
self, role: MESSAGE_ROLE, text: str, meta: Dict[str, Any] | None = None
self, text: str, meta: Dict[str, Any] | None = None
):
"""Prepend a system message to the conversation."""
self.messages = [Message(role=role, text=text, meta=meta or {})] + self.messages
self.messages = [
Message(role="system", text=text, meta=meta or {})
] + self.messages
def add_message(
self, role: MESSAGE_ROLE, text: str, meta: Optional[Dict[str, Any]] = None
self,
role: MESSAGE_ROLE = "user",
text: str | None = None,
*,
meta: Optional[Dict[str, Any]] = None,
):
"""Add a new message to the conversation."""
assert text is not None
# Ensure meta is a dict.
if meta is None:
meta = {}
@@ -148,9 +166,12 @@ class Conversation(SMBaseModel):
self,
llm_model: str | None = None,
llm_provider: str | None = None,
tools: list[Callable | BaseTool] | None = None,
) -> Message:
"""Send the conversation to the LLM."""
# TODO: llm_model and llm_provider should override the conversation's.
# Execute all pre send hooks.
for plugin in self.plugins:
if hasattr(plugin, "pre_send_hook"):
@@ -161,7 +182,7 @@ class Conversation(SMBaseModel):
# Find the provider and send the conversation.
provider = find_provider(llm_provider or self.llm_provider)
response = provider.send_conversation(self)
response = provider.send_conversation(self, tools=tools)
# Execute all post-send hooks.
for plugin in self.plugins:
@@ -172,14 +193,29 @@ class Conversation(SMBaseModel):
pass
# Add the response to the conversation.
self.add_message(role="assistant", text=response.text, meta=response.meta)
self.add_message(
role="assistant", text=response.text, meta=response.meta
)
return response
def get_last_message(self, role: MESSAGE_ROLE) -> Message | None:
"""Get the last message with the given role."""
return next((m for m in reversed(self.messages) if m.role == role), None)
return next(
(m for m in reversed(self.messages) if m.role == role), None
)
def add_plugin(self, plugin: BasePlugin) -> None:
"""Add a plugin to the conversation."""
self.plugins.append(plugin)
def save(self, path: PathLike | str) -> None:
"""Save the conversation to a JSON file."""
with open(path, "w") as f:
f.write(self.model_dump_json())
@classmethod
def load(cls, path: PathLike | str) -> "Conversation":
"""Load a conversation from a JSON file."""
with open(path, "r") as f:
return cls.model_validate_json(f.read())
+27 -1
View File
@@ -1,11 +1,37 @@
from typing import List, Type
from ._base import BaseProvider
from ._base_tools import BaseTool
from .amazon import Amazon
from .anthropic import Anthropic
from .gemini import Gemini
from .groq import Groq
from .ollama import Ollama
from .openai import OpenAI
from .xai import XAI
from .deepseek import Deepseek
providers: List[Type[BaseProvider]] = [Anthropic, Gemini, Groq, OpenAI, Ollama, XAI]
providers: List[Type[BaseProvider]] = [
Anthropic,
Gemini,
Groq,
OpenAI,
Ollama,
XAI,
Amazon,
Deepseek,
]
__all__ = [
"Anthropic",
"Gemini",
"Groq",
"OpenAI",
"Ollama",
"XAI",
"Amazon",
"providers",
"BaseProvider",
"BaseTool",
"Deepseek"
]
+36 -4
View File
@@ -1,10 +1,15 @@
from abc import ABC, abstractmethod
from functools import cached_property
from typing import Any, Type, TypeVar
from typing import TYPE_CHECKING, Any, Callable, Type, TypeVar
from instructor import Instructor
from pydantic import BaseModel
from simplemind.providers._base_tools import BaseTool
if TYPE_CHECKING:
from ..models import Conversation, Message
T = TypeVar("T", bound=BaseModel)
@@ -13,6 +18,8 @@ class BaseProvider(ABC):
NAME: str
DEFAULT_MODEL: str
supports_streaming: bool = False
supports_structured_responses: bool = True
@cached_property
@abstractmethod
@@ -27,16 +34,41 @@ class BaseProvider(ABC):
raise NotImplementedError
@abstractmethod
def send_conversation(self, conversation: "Conversation") -> "Message":
def send_conversation(
self,
conversation: "Conversation",
tools: list[Callable | BaseTool] | None = None,
) -> "Message":
"""Send a conversation to the provider."""
raise NotImplementedError
@abstractmethod
def structured_response(self, prompt: str, response_model: Type[T], **kwargs) -> T:
def structured_response(
self, prompt: str, response_model: Type[T], **kwargs
) -> T:
"""Get a structured response."""
raise NotImplementedError
@abstractmethod
def generate_text(self, prompt: str, **kwargs) -> str:
def generate_text(
self,
prompt: str,
*,
tools: list[Callable | BaseTool] | None = None,
stream: bool = False,
**kwargs,
) -> str:
"""Generate text from a prompt."""
raise NotImplementedError
@cached_property
@abstractmethod
def tool(self) -> Type[BaseTool]:
"""The tool implementation for the provider."""
raise NotImplementedError
def make_tools(self, tools: list[Callable | BaseTool] | None):
if tools is not None:
return [self.tool.from_function(func) for func in tools]
else:
return []
+140
View File
@@ -0,0 +1,140 @@
import inspect
from abc import ABC, abstractmethod
from typing import Any, Callable, ClassVar, Literal, get_origin
from pydantic import BaseModel, Field
from pydantic.fields import FieldInfo
from pydantic_core import PydanticUndefinedType
def _is_literal(t: Any) -> bool:
return get_origin(t) is Literal
def _is_required(field, func_signature, arg_name) -> bool:
param = func_signature.parameters[arg_name]
# If parameter has a default value that's not a FieldInfo, it's not required
if param.default is not inspect.Parameter.empty and not isinstance(
param.default, FieldInfo
):
return False
# If the field has a default that's not undefined, it's not required
return isinstance(field.default, PydanticUndefinedType)
class BaseToolConfig(BaseModel):
TYPE_CONVERSION: dict[type, str] = {
str: "string",
int: "integer",
float: "number",
bool: "boolean",
}
class BaseToolProperty(BaseModel):
type: str = Field(serialization_alias="type_")
enum: list[str] | None = None
description: str
class BaseTool(BaseModel, ABC):
name: str
description: str
properties: dict[str, BaseToolProperty]
required: list[str] | None = None
config: ClassVar[BaseToolConfig] = BaseToolConfig()
raw_func: Any | None = None
tool_id: str | None = None
function_result: str | None = None
def __call__(self, *args: Any, **kwargs: Any) -> Any:
assert self.raw_func is not None
return self.raw_func(*args, **kwargs)
def is_executed(self) -> bool:
return self.function_result is not None
def reset_result(self) -> None:
self.function_result = None
@classmethod
def convert_type(cls, field_type) -> str:
if _is_literal(field_type):
return cls.config.TYPE_CONVERSION[str]
field_type_converted = cls.config.TYPE_CONVERSION.get(field_type, None)
if field_type_converted is None:
raise TypeError(f"Field of type {field_type} is not supported")
return field_type_converted
def get_properties_schema(self, **kwargs) -> dict[str, dict]:
new_kwargs: dict = {"exclude_none": True} | kwargs
return {
k: v.model_dump(**new_kwargs) for k, v in self.properties.items()
}
@classmethod
def from_function(cls, func: Callable | "BaseTool"):
# Check if the func passed is an instace of BaseTool
if hasattr(func, "raw_func"):
return func
annotations = getattr(func, "__annotations__", {})
properties = {}
required = []
enum_values = None
func_signature = inspect.signature(func)
for n, (arg_name, arg_type) in enumerate(annotations.items()):
if ( # Skipping 'return' annotation (i.e.```-> str```)
arg_name != "return"
):
# Check if argument has metadata (from Annotated)
if hasattr(arg_type, "__metadata__"):
field = arg_type.__metadata__[
0
] # Get Field info from metadata
field_type = arg_type.__origin__ # Get actual type
# Check if argument has a default value in signature
elif (
sig_param := func_signature.parameters[arg_name]
).default is not inspect.Parameter.empty:
field = sig_param.default # Use default as Field
field_type = arg_type # Use plain type annotation
else:
# Raise error if no Field annotation found
raise ValueError(
f"Please add a Field annotation to `{func.__name__}.{arg_name}` parameter"
)
field_type_converted = cls.convert_type(field_type)
if _is_literal(field_type):
enum_values = [str(x) for x in field_type.__args__]
properties[arg_name] = BaseToolProperty(
type=field_type_converted,
description=field.description,
enum=enum_values,
)
if _is_required(field, func_signature, arg_name):
required.append(arg_name)
return cls(
name=func.__name__,
description=(func.__doc__ or "").strip(),
properties=properties,
required=required,
raw_func=func,
)
@abstractmethod
def get_input_schema(self) -> Any: ...
@abstractmethod
def handle(self, message) -> None: ...
@abstractmethod
def get_response_schema(self) -> Any: ...
+123
View File
@@ -0,0 +1,123 @@
from functools import cached_property
from typing import TYPE_CHECKING, Iterator, Type, TypeVar
import instructor
from pydantic import BaseModel
from ..settings import settings
from ._base import BaseProvider
if TYPE_CHECKING:
from ..models import Conversation, Message
T = TypeVar("T", bound=BaseModel)
class Amazon(BaseProvider):
NAME = "amazon"
DEFAULT_MODEL = "us.anthropic.claude-3-5-sonnet-20241022-v2:0"
DEFAULT_MAX_TOKENS = 5_000
supports_streaming = True
def __init__(self, profile_name: str | None = None):
self.profile_name = profile_name or settings.AMAZON_PROFILE_NAME
@cached_property
def client(self):
"""The AnthropicBedrock client."""
try:
import anthropic
except ImportError as exc:
raise ImportError(
"Please install the `anthropic` package: `pip install anthropic`"
) from exc
if not self.profile_name:
raise ValueError("Profile name is not provided")
return anthropic.AnthropicBedrock(aws_profile=self.profile_name)
@cached_property
def structured_client(self) -> instructor.Instructor:
"""A client patched with Instructor."""
return instructor.from_anthropic(self.client)
def send_conversation(self, conversation: "Conversation", **kwargs) -> "Message":
"""Send a conversation to the OpenAI API."""
from ..models import Message
messages = [
{"role": msg.role, "content": msg.text} for msg in conversation.messages
]
response = self.client.chat.completions.create(
model=conversation.llm_model or DEFAULT_MODEL, messages=messages, **kwargs
)
# Get the response content from the OpenAI response
assistant_message = response.choices[0].message
# Create and return a properly formatted Message instance
return Message(
role="assistant",
text=assistant_message.content or "",
raw=response,
llm_model=conversation.llm_model or self.DEFAULT_MODEL,
llm_provider=PROVIDER_NAME,
)
def structured_response(
self, prompt, response_model: Type[T], *, llm_model: str | None = None, **kwargs
) -> T:
# Ensure messages are provided in kwargs
messages = [
{"role": "user", "content": prompt},
]
response = self.structured_client.chat.completions.create(
messages=messages,
model=llm_model or self.DEFAULT_MODEL,
response_model=response_model,
max_tokens=self.DEFAULT_MAX_TOKENS,
**kwargs,
)
return response
def generate_text(self, prompt: str, *, llm_model: str, **kwargs):
messages = [
{"role": "user", "content": prompt},
]
response = self.client.messages.create(
model=llm_model or self.DEFAULT_MODEL,
messages=messages,
max_tokens=self.DEFAULT_MAX_TOKENS,
**kwargs,
)
return response.content[0].text
def generate_stream_text(
self, prompt: str, *, llm_model: str, **kwargs
) -> Iterator[str]:
"""Generate streaming text using the Amazon API."""
# Prepare the messages.
messages = [
{"role": "user", "content": prompt},
]
# Send the request to the API.
response = self.client.messages.create(
model=llm_model or self.DEFAULT_MODEL,
messages=messages,
stream=True,
**kwargs,
)
# Yield the text chunks.
for chunk in response:
if chunk.text:
yield chunk.text
+139 -23
View File
@@ -1,35 +1,94 @@
from functools import cached_property
from typing import Type, TypeVar
from typing import TYPE_CHECKING, Any, Callable, Iterator, Type, TypeVar
import anthropic
import instructor
from pydantic import BaseModel
from ..logging import logger
from ..settings import settings
from ._base import BaseProvider
from ._base_tools import BaseTool
if TYPE_CHECKING:
from ..models import Conversation, Message
T = TypeVar("T", bound=BaseModel)
PROVIDER_NAME = "anthropic"
DEFAULT_MODEL = "claude-3-5-sonnet-20241022"
DEFAULT_MAX_TOKENS = 1_000
DEFAULT_KWARGS = {"max_tokens": DEFAULT_MAX_TOKENS}
class AnthropicTool(BaseTool):
def get_response_schema(self) -> Any:
assert self.is_executed, f"Tool {self.name} was not executed."
assert isinstance(
self.tool_id, str
), f"Expected str for `tool_id` got {self.tool_id!r}"
return {
"type": "tool_result",
"tool_use_id": self.tool_id,
"content": self.function_result,
}
@logger
def handle(self, response, messages) -> None:
"""Handle the tool execution result from an API response."""
msg = {"role": "assistant", "content": []}
tool_used = False
for content in response.content:
if content.type == "tool_use" and content.name == self.name:
msg["content"].append(
{
"type": "tool_use",
"id": content.id,
"name": content.name,
"input": content.input,
}
)
# Function execution:
self.function_result = str(self.raw_func(**content.input))
self.tool_id = content.id
tool_used = True
elif content.type == "text":
msg["content"].append({"type": "text", "text": content.text})
if tool_used:
messages.append(msg)
messages.append(
{"role": "user", "content": [self.get_response_schema()]}
)
def get_input_schema(self):
return {
"name": self.name,
"description": self.description,
"input_schema": {
"type": "object",
"properties": self.get_properties_schema(),
"required": self.required,
},
}
class Anthropic(BaseProvider):
NAME = PROVIDER_NAME
DEFAULT_MODEL = DEFAULT_MODEL
DEFAULT_KWARGS = DEFAULT_KWARGS
NAME = "anthropic"
DEFAULT_MODEL = "claude-3-5-sonnet-20241022"
DEFAULT_MAX_TOKENS = 1_000
DEFAULT_KWARGS = {"max_tokens": DEFAULT_MAX_TOKENS}
supports_streaming = True
def __init__(self, api_key: str | None = None):
self.api_key = api_key or settings.get_api_key(PROVIDER_NAME)
self.api_key = api_key or settings.get_api_key(self.NAME)
@cached_property
def client(self):
"""The raw Anthropic client."""
if not self.api_key:
raise ValueError("Anthropic API key is required")
try:
import anthropic
except ImportError as exc:
raise ImportError(
"Please install the `anthropic` package: `pip install anthropic`"
) from exc
return anthropic.Anthropic(api_key=self.api_key)
@cached_property
@@ -37,32 +96,64 @@ class Anthropic(BaseProvider):
"""A client patched with Instructor."""
return instructor.from_anthropic(self.client)
def send_conversation(self, conversation: "Conversation", **kwargs):
@logger
def send_conversation(
self,
conversation: "Conversation",
tools: list[Callable | BaseTool] | None = None,
**kwargs,
) -> "Message":
"""Send a conversation to the Anthropic API."""
from ..models import Message
messages = [
{"role": msg.role, "content": msg.text} for msg in conversation.messages
# Format messages from conversation
formatted_messages = [
{"role": msg.role, "content": msg.text}
for msg in conversation.messages
]
response = self.client.messages.create(
model=conversation.llm_model or self.DEFAULT_MODEL,
messages=messages,
**{**self.DEFAULT_KWARGS, **kwargs},
# Set up tools if provided
converted_tools = self.make_tools(tools)
tools_config = (
{"tools": [t.get_input_schema() for t in converted_tools]}
if tools is not None
else {}
)
# Get the response content from the Anthropic response
assistant_message = response.content[0].text
# Merge all kwargs
request_kwargs = {
**self.DEFAULT_KWARGS,
**kwargs,
**tools_config,
"model": conversation.llm_model or self.DEFAULT_MODEL,
"messages": formatted_messages,
}
# Make initial API call
response = self.client.messages.create(**request_kwargs)
# Handle tool responses if needed
while response.content[-1].type != "text":
# Continue handling tools if the LLM is doing
# multiple sub-seqequent/sequential tool calls
for tool in converted_tools:
tool.handle(response, formatted_messages)
if tool.is_executed():
response = self.client.messages.create(**request_kwargs)
# Resetting the tool results in case this tool gets used again
tool.reset_result()
final_message = response.content[-1].text
# Create and return a properly formatted Message instance
return Message(
role="assistant",
text=assistant_message,
text=final_message,
raw=response,
llm_model=conversation.llm_model or self.DEFAULT_MODEL,
llm_provider=PROVIDER_NAME,
llm_provider=self.NAME,
)
@logger
def structured_response(
self, response_model: Type[T], *, llm_model: str | None = None, **kwargs
) -> T:
@@ -80,8 +171,9 @@ class Anthropic(BaseProvider):
response_model=response_model,
**{**self.DEFAULT_KWARGS, **kwargs},
)
return response
return response_model.model_validate(response)
@logger
def generate_text(self, prompt: str, *, llm_model: str, **kwargs):
messages = [
{"role": "user", "content": prompt},
@@ -94,3 +186,27 @@ class Anthropic(BaseProvider):
)
return response.content[0].text
@logger
def generate_stream_text(
self, prompt: str, *, llm_model: str, **kwargs
) -> Iterator[str]:
# Prepare the messages.
messages = [
{"role": "user", "content": prompt},
]
# Make the request.
with self.client.messages.stream(
model=llm_model or self.DEFAULT_MODEL,
messages=messages,
**{**self.DEFAULT_KWARGS, **kwargs},
) as stream:
# Yield each chunk of text from the stream.
for chunk in stream.text_stream:
yield chunk
@cached_property
def tool(self) -> Type[BaseTool]:
"""The tool implementation for Antrhopic."""
return AnthropicTool
+27
View File
@@ -0,0 +1,27 @@
import os
from functools import cached_property
from .openai import OpenAI
class Deepseek(OpenAI):
NAME = "deepseek"
DEFAULT_MODEL = "deepseek-chat"
def __init__(self, api_key: str | None = None):
api_key = api_key or os.getenv("DEEPSEEK_API_KEY")
super().__init__(api_key=api_key)
self.endpoint = "https://api.deepseek.com/v1"
@cached_property
def client(self):
"""The raw OpenAI client."""
if not self.api_key:
raise ValueError("DEEPSEEK API key is required")
try:
import openai as oa
except ImportError as exc:
raise ImportError(
"Please install the `openai` package: `pip install openai`"
) from exc
return oa.OpenAI(api_key=self.api_key, base_url=self.endpoint)
+42 -15
View File
@@ -2,35 +2,45 @@
# IT is not currently working as desired.
from functools import cached_property
from typing import Type, TypeVar
from typing import TYPE_CHECKING, Iterator, Type, TypeVar
import google.generativeai as genai
import instructor
from pydantic import BaseModel
from ..logging import logger
from ..settings import settings
from ._base import BaseProvider
PROVIDER_NAME = "gemini"
DEFAULT_MODEL = "models/gemini-1.5-flash-latest"
if TYPE_CHECKING:
from ..models import Conversation, Message
T = TypeVar("T", bound=BaseModel)
class Gemini(BaseProvider):
NAME = PROVIDER_NAME
DEFAULT_MODEL = DEFAULT_MODEL
NAME = "gemini"
DEFAULT_MODEL = "models/gemini-1.5-flash-latest"
supports_streaming = True
def __init__(self, api_key: str | None = None):
self.api_key = api_key or settings.get_api_key(PROVIDER_NAME)
self.model_name = DEFAULT_MODEL
self.api_key = api_key or settings.get_api_key(self.NAME)
self.model_name = self.DEFAULT_MODEL
def set_model(self, model_name: str):
self.model_name = model_name
@cached_property
def client(self, model_name: str = DEFAULT_MODEL):
def client(self):
"""The raw Gemini client."""
if not self.api_key:
raise ValueError("Gemini API key is required")
self.model_name = model_name
try:
import google.generativeai as genai
except ImportError as exc:
raise ImportError(
"Please install the `google-generativeai` package: `pip install google-generativeai`"
) from exc
genai.configure(api_key=self.api_key)
return genai.GenerativeModel(model_name=self.model_name)
@cached_property
@@ -38,6 +48,7 @@ class Gemini(BaseProvider):
"""A Gemini client patched with Instructor."""
return instructor.from_gemini(self.client)
@logger
def send_conversation(self, conversation: "Conversation") -> "Message":
"""Send a conversation to the Gemini API."""
from ..models import Message
@@ -61,12 +72,14 @@ class Gemini(BaseProvider):
text=response.text,
raw=response,
llm_model=self.model_name,
llm_provider=PROVIDER_NAME,
llm_provider=self.NAME,
)
@logger
def structured_response(self, prompt: str, response_model: Type[T], **kwargs) -> T:
"""Send a structured response to the Gemini API."""
llm_model = kwargs.pop("llm_model", self.model_name)
# Only try to pop if the key exists
kwargs.pop("llm_model", None) # Add default value of None
try:
response = self.structured_client.chat.completions.create(
@@ -79,15 +92,29 @@ class Gemini(BaseProvider):
raise RuntimeError(
f"Failed to send structured response to Gemini API: {e}"
) from e
return response
return response_model.model_validate(response)
@logger
def generate_text(self, prompt: str, **kwargs) -> str:
"""Generate text using the Gemini API."""
llm_model = kwargs.pop("llm_model", self.model_name)
kwargs.pop("llm_model")
try:
response = self.client.generate_content(prompt, **kwargs)
except Exception as e:
# Handle the exception appropriately, e.g., log the error or raise a custom exception
raise RuntimeError(f"Failed to generate text with Gemini API: {e}") from e
return response.text
@logger
def generate_stream_text(self, prompt: str, **kwargs) -> Iterator[str]:
"""Generate streaming text using the Gemini API."""
kwargs.pop("llm_model", None)
try:
response = self.client.generate_content(prompt, stream=True, **kwargs)
for chunk in response:
if chunk.text:
yield chunk.text
except Exception as e:
raise RuntimeError(
f"Failed to generate streaming text with Gemini API: {e}"
) from e
+55 -14
View File
@@ -1,31 +1,40 @@
from functools import cached_property
from typing import Type, TypeVar
from typing import TYPE_CHECKING, Iterator, Type, TypeVar
import groq
import instructor
from pydantic import BaseModel
from ..logging import logger
from ..settings import settings
from ._base import BaseProvider
PROVIDER_NAME = "groq"
DEFAULT_MODEL = "llama3-8b-8192"
if TYPE_CHECKING:
from ..models import Conversation, Message
T = TypeVar("T", bound=BaseModel)
class Groq(BaseProvider):
NAME = PROVIDER_NAME
DEFAULT_MODEL = DEFAULT_MODEL
NAME = "groq"
DEFAULT_MODEL = "llama3-8b-8192"
DEFAULT_MAX_TOKENS = 1_000
DEFAULT_KWARGS = {"max_tokens": DEFAULT_MAX_TOKENS}
supports_streaming = True
def __init__(self, api_key: str | None = None):
self.api_key = api_key or settings.get_api_key(PROVIDER_NAME)
self.api_key = api_key or settings.get_api_key(self.NAME)
@cached_property
def client(self):
"""The raw Groq client."""
if not self.api_key:
raise ValueError("Groq API key is required")
try:
import groq
except ImportError as exc:
raise ImportError(
"Please install the `groq` package: `pip install groq`"
) from exc
return groq.Groq(api_key=self.api_key)
@cached_property
@@ -33,6 +42,7 @@ class Groq(BaseProvider):
"""A client patched with Instructor."""
return instructor.from_groq(self.client)
@logger
def send_conversation(
self,
conversation: "Conversation",
@@ -48,7 +58,7 @@ class Groq(BaseProvider):
response = self.client.chat.completions.create(
model=conversation.llm_model or self.DEFAULT_MODEL,
messages=messages,
**kwargs,
**{**self.DEFAULT_KWARGS, **kwargs},
)
# Get the response content from the Groq response
@@ -60,9 +70,10 @@ class Groq(BaseProvider):
text=assistant_message.content or "",
raw=response,
llm_model=conversation.llm_model or self.DEFAULT_MODEL,
llm_provider=PROVIDER_NAME,
llm_provider=self.NAME,
)
@logger
def structured_response(self, prompt: str, response_model: Type[T], **kwargs) -> T:
# Ensure messages are provided in kwargs
messages = [
@@ -73,17 +84,18 @@ class Groq(BaseProvider):
messages=messages,
response_model=response_model,
model=kwargs.pop("llm_model", self.DEFAULT_MODEL),
**kwargs,
**{**self.DEFAULT_KWARGS, **kwargs},
)
return response
return response_model.model_validate(response)
@logger
def generate_text(
self,
prompt: str,
*,
llm_model: str,
**kwargs,
):
) -> str:
messages = [
{"role": "user", "content": prompt},
]
@@ -91,7 +103,36 @@ class Groq(BaseProvider):
response = self.client.chat.completions.create(
messages=messages,
model=llm_model or self.DEFAULT_MODEL,
**kwargs,
**{**self.DEFAULT_KWARGS, **kwargs},
)
return response.choices[0].message.content
return str(response.choices[0].message.content)
@logger
def generate_stream_text(
self,
prompt: str,
*,
llm_model: str | None = None,
**kwargs,
) -> Iterator[str]:
"""Generate streaming text using the Groq API."""
messages = [
{"role": "user", "content": prompt},
]
response = self.client.chat.completions.create(
messages=messages,
model=llm_model or self.DEFAULT_MODEL,
stream=True,
**{**self.DEFAULT_KWARGS, **kwargs},
)
try:
for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
except Exception as e:
raise RuntimeError(
f"Failed to generate streaming text with Groq API: {e}"
) from e
+65 -29
View File
@@ -1,26 +1,25 @@
from functools import cached_property
from typing import Type, TypeVar
from typing import TYPE_CHECKING, Iterator, Type, TypeVar
import instructor
import ollama as ol
from openai import OpenAI
from pydantic import BaseModel
from ..logging import logger
from ..settings import settings
from ._base import BaseProvider
if TYPE_CHECKING:
from ..models import Conversation, Message
T = TypeVar("T", bound=BaseModel)
PROVIDER_NAME = "ollama"
DEFAULT_MODEL = "llama3.2"
DEFAULT_TIMEOUT = 60
class Ollama(BaseProvider):
NAME = PROVIDER_NAME
DEFAULT_MODEL = DEFAULT_MODEL
TIMEOUT = DEFAULT_TIMEOUT
NAME = "ollama"
DEFAULT_MODEL = "llama3.2"
DEFAULT_TIMEOUT = 60
DEFAULT_KWARGS = {}
supports_streaming = True
def __init__(self, host_url: str | None = None):
self.host_url = host_url or settings.OLLAMA_HOST_URL
@@ -30,40 +29,51 @@ class Ollama(BaseProvider):
"""The raw Ollama client."""
if not self.host_url:
raise ValueError("No ollama host url provided")
return ol.Client(timeout=self.TIMEOUT, host=self.host_url)
try:
import openai
except ImportError as exc:
raise ImportError(
"Please install the `openai` package: `pip install openai`"
) from exc
return openai.OpenAI(base_url=f"{self.host_url}/v1", api_key="ollama")
@cached_property
def structured_client(self) -> instructor.Instructor:
"""A client patched with Instructor."""
return instructor.from_openai(
OpenAI(
base_url=f"{self.host_url}/v1",
api_key="ollama",
),
self.client,
mode=instructor.Mode.JSON,
)
def send_conversation(self, conversation: "Conversation") -> "Message":
@logger
def send_conversation(self, conversation: "Conversation", **kwargs) -> "Message":
"""Send a conversation to the Ollama API."""
from ..models import Message
messages = [
{"role": msg.role, "content": msg.text} for msg in conversation.messages
]
response = self.client.chat(
model=conversation.llm_model or DEFAULT_MODEL, messages=messages
)
assistant_message = response.get("message")
request_kwargs = {
**self.DEFAULT_KWARGS,
**kwargs,
"model": conversation.llm_model or self.DEFAULT_MODEL,
"messages": messages,
}
response = self.client.chat.completions.create(**request_kwargs)
assistant_message = response.choices[0].message
# Create and return a properly formatted Message instance
return Message(
role="assistant",
text=assistant_message.get("content"),
text=assistant_message.content or "",
raw=response,
llm_model=conversation.llm_model or self.DEFAULT_MODEL,
llm_provider=PROVIDER_NAME,
llm_provider=self.NAME,
)
@logger
def structured_response(
self,
prompt: str,
@@ -81,18 +91,44 @@ class Ollama(BaseProvider):
messages=messages,
model=llm_model or self.DEFAULT_MODEL,
response_model=response_model,
**kwargs,
**{**self.DEFAULT_KWARGS, **kwargs},
)
return response
return response_model.model_validate(response)
def generate_text(self, prompt: str, *, llm_model: str | None = None) -> str:
@logger
def generate_text(
self, prompt: str, *, llm_model: str | None = None, **kwargs
) -> str:
"""Generate text using the Ollama API."""
messages = [
{"role": "user", "content": prompt},
]
response = self.client.chat(
messages=messages, model=llm_model or self.DEFAULT_MODEL
response = self.client.chat.completions.create(
messages=messages,
model=llm_model or self.DEFAULT_MODEL,
**{**self.DEFAULT_KWARGS, **kwargs},
)
return response.get("message", {}).get("content", "")
return response.choices[0].message.content
@logger
def generate_stream_text(
self, prompt: str, *, llm_model: str, **kwargs
) -> Iterator[str]:
# Prepare the messages.
messages = [
{"role": "user", "content": prompt},
]
response = self.client.chat.completions.create(
messages=messages,
model=llm_model or self.DEFAULT_MODEL,
stream=True,
**{**self.DEFAULT_KWARGS, **kwargs},
)
# Iterate over the response and yield the content.
for chunk in response:
if chunk.choices[0].delta.content is not None:
yield chunk.choices[0].delta.content
+173 -22
View File
@@ -1,31 +1,114 @@
from functools import cached_property
from typing import Type, TypeVar
from typing import TYPE_CHECKING, Callable, Iterator, Type, TypeVar
import instructor
import openai as oa
from pydantic import BaseModel
from ..logging import logger
from ..settings import settings
from ._base import BaseProvider
from ._base_tools import BaseTool
if TYPE_CHECKING:
from ..models import Conversation, Message
T = TypeVar("T", bound=BaseModel)
PROVIDER_NAME = "openai"
DEFAULT_MODEL = "gpt-4o-mini"
class OpenAITool(BaseTool):
def get_response_schema(self):
assert self.is_executed, f"Tool {self.name} was not executed."
assert isinstance(
self.tool_id, str
), f"Expected str for `tool_id` got {self.tool_id!r}"
return {
"role": "tool",
"tool_call_id": self.tool_id,
"content": self.function_result,
}
@logger
def handle(self, response, messages) -> None:
"""Handle the tool execution result from an API response."""
tool_used = False
# Get the message from the response
assistant_message = response.choices[0].message
# Check if there's a tool call
if assistant_message.tool_calls:
tool_call = assistant_message.tool_calls[
0
] # Get the first tool call
if tool_call.function.name == self.name:
# Execute the function
import json
function_args = json.loads(tool_call.function.arguments)
self.function_result = str(self.raw_func(**function_args))
self.tool_id = tool_call.id
tool_used = True
# Add assistant's message with tool call
messages.append(
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": tool_call.id,
"type": "function",
"function": {
"name": tool_call.function.name,
"arguments": tool_call.function.arguments,
},
}
],
}
)
if tool_used:
# Add tool response message
messages.append(self.get_response_schema())
def get_input_schema(self):
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": {
"type": "object",
"properties": self.get_properties_schema(),
"required": self.required,
"additionalProperties": False,
},
},
}
class OpenAI(BaseProvider):
NAME = PROVIDER_NAME
DEFAULT_MODEL = DEFAULT_MODEL
NAME = "openai"
DEFAULT_MODEL = "gpt-4o-mini"
DEFAULT_MAX_TOKENS = None
DEFAULT_KWARGS = {}
supports_streaming = True
def __init__(self, api_key: str | None = None):
self.api_key = api_key or settings.get_api_key(PROVIDER_NAME)
self.api_key = api_key or settings.get_api_key(self.NAME)
@cached_property
def client(self):
"""The raw OpenAI client."""
if not self.api_key:
raise ValueError("OpenAI API key is required")
try:
import openai as oa
except ImportError as exc:
raise ImportError(
"Please install the `openai` package: `pip install openai`"
) from exc
return oa.OpenAI(api_key=self.api_key)
@cached_property
@@ -33,30 +116,66 @@ class OpenAI(BaseProvider):
"""A OpenAI client with Instructor."""
return instructor.from_openai(self.client)
def send_conversation(self, conversation: "Conversation", **kwargs):
@logger
def send_conversation(
self,
conversation: "Conversation",
tools: list[Callable | BaseTool] | None = None,
**kwargs,
) -> "Message":
"""Send a conversation to the OpenAI API."""
from ..models import Message
messages = [
{"role": msg.role, "content": msg.text} for msg in conversation.messages
# Format messages from conversation
formatted_messages = [
{"role": msg.role, "content": msg.text}
for msg in conversation.messages
]
response = self.client.chat.completions.create(
model=conversation.llm_model or DEFAULT_MODEL, messages=messages, **kwargs
# Set up tools if provided
converted_tools = self.make_tools(tools)
tools_config = (
[t.get_input_schema() for t in converted_tools] if tools else None
)
# Get the response content from the OpenAI response
assistant_message = response.choices[0].message
# Merge all kwargs
request_kwargs = {
**self.DEFAULT_KWARGS,
**kwargs,
"model": conversation.llm_model or self.DEFAULT_MODEL,
"messages": formatted_messages,
}
if tools_config:
request_kwargs["tools"] = tools_config
# Make initial API call
response = self.client.chat.completions.create(**request_kwargs)
# Handle tool responses if needed
while response.choices[0].message.tool_calls:
print(response)
# Handle each tool call
for tool in converted_tools:
tool.handle(response, formatted_messages)
if tool.is_executed():
# Make another API call with the updated messages
response = self.client.chat.completions.create(
**request_kwargs
)
tool.reset_result()
final_message = response.choices[0].message.content
# Create and return a properly formatted Message instance
return Message(
role="assistant",
text=assistant_message.content or "",
text=final_message or "",
raw=response,
llm_model=conversation.llm_model or DEFAULT_MODEL,
llm_provider=PROVIDER_NAME,
llm_model=conversation.llm_model or self.DEFAULT_MODEL,
llm_provider=self.NAME,
)
@logger
def structured_response(
self,
prompt: str,
@@ -74,16 +193,48 @@ class OpenAI(BaseProvider):
messages=messages,
model=llm_model or self.DEFAULT_MODEL,
response_model=response_model,
**kwargs,
**{**self.DEFAULT_KWARGS, **kwargs},
)
return response
return response_model.model_validate(response)
def generate_text(self, prompt: str, *, llm_model: str | None = None, **kwargs):
@logger
def generate_text(
self, prompt: str, *, llm_model: str | None = None, **kwargs
):
"""Generate text using the OpenAI API."""
messages = [
{"role": "user", "content": prompt},
]
response = self.client.chat.completions.create(
messages=messages, model=llm_model or self.DEFAULT_MODEL, **kwargs
messages=messages,
model=llm_model or self.DEFAULT_MODEL,
**{**self.DEFAULT_KWARGS, **kwargs},
)
return response.choices[0].message.content
@logger
def generate_stream_text(
self, prompt: str, *, llm_model: str | None = None, **kwargs
) -> Iterator[str]:
"""Generate streaming text using the OpenAI API.
Yields chunks of text as they are generated by the model.
"""
messages = [
{"role": "user", "content": prompt},
]
response = self.client.chat.completions.create(
messages=messages,
model=llm_model or self.DEFAULT_MODEL,
stream=True, # Enable streaming
**{**self.DEFAULT_KWARGS, **kwargs},
)
for chunk in response:
if chunk.choices[0].delta.content is not None:
yield chunk.choices[0].delta.content
@cached_property
def tool(self) -> Type[BaseTool]:
"""The tool implementation for OpenAI."""
return OpenAITool
+58 -16
View File
@@ -1,32 +1,45 @@
from functools import cached_property
from typing import TYPE_CHECKING, Iterator, Type, TypeVar
import instructor
import openai as oa
from pydantic import BaseModel
from ..logging import logger
from ..settings import settings
from ._base import BaseProvider
PROVIDER_NAME = "xai"
DEFAULT_MODEL = "grok-beta"
BASE_URL = "https://api.x.ai/v1"
DEFAULT_MAX_TOKENS = 1000
if TYPE_CHECKING:
from ..models import Conversation, Message
T = TypeVar("T", bound=BaseModel)
class XAI(BaseProvider):
NAME = PROVIDER_NAME
DEFAULT_MODEL = DEFAULT_MODEL
NAME = "xai"
DEFAULT_MODEL = "grok-beta"
DEFAULT_MAX_TOKENS = 1000
DEFAULT_KWARGS = {"max_tokens": DEFAULT_MAX_TOKENS}
BASE_URL = "https://api.x.ai/v1"
supports_streaming = True
supports_structured_responses = False
def __init__(self, api_key: str | None = None):
self.api_key = api_key or settings.get_api_key(PROVIDER_NAME)
self.api_key = api_key or settings.get_api_key(self.NAME)
@cached_property
def client(self):
"""The raw OpenAI client."""
if not self.api_key:
raise ValueError("XAI API key is required")
try:
import openai as oa
except ImportError as exc:
raise ImportError(
"Please install the `openai` package: `pip install openai`"
) from exc
return oa.OpenAI(
api_key=self.api_key,
base_url=BASE_URL,
base_url=self.BASE_URL,
)
@cached_property
@@ -34,7 +47,8 @@ class XAI(BaseProvider):
"""A client patched with Instructor."""
return instructor.from_openai(self.client)
def send_conversation(self, conversation: "Conversation", **kwargs):
@logger
def send_conversation(self, conversation: "Conversation", **kwargs) -> "Message":
"""Send a conversation to the OpenAI API."""
from ..models import Message
@@ -45,7 +59,7 @@ class XAI(BaseProvider):
response = self.client.chat.completions.create(
model=conversation.llm_model or self.DEFAULT_MODEL,
messages=messages,
**kwargs,
**{**self.DEFAULT_KWARGS, **kwargs},
)
# Get the response content from the OpenAI response
@@ -57,21 +71,49 @@ class XAI(BaseProvider):
text=assistant_message.content,
raw=response,
llm_model=conversation.llm_model or self.DEFAULT_MODEL,
llm_provider=PROVIDER_NAME,
llm_provider=self.NAME,
)
def structured_response(self, prompt: str, response_model, *, llm_model: str):
@logger
def structured_response(
self, prompt: str, response_model: Type[T], *, llm_model: str
) -> T:
raise NotImplementedError("XAI does not support structured responses")
def generate_text(self, prompt: str, *, llm_model: str, **kwargs):
@logger
def generate_text(self, prompt: str, *, llm_model: str, **kwargs) -> str:
# Prepare the messages.
messages = [
{"role": "user", "content": prompt},
]
# Make the request.
response = self.client.chat.completions.create(
messages=messages,
model=llm_model or self.DEFAULT_MODEL,
**kwargs,
**{**self.DEFAULT_KWARGS, **kwargs},
)
return response.choices[0].message.content
# Return the response content.
return str(response.choices[0].message.content)
@logger
def generate_stream_text(
self, prompt: str, *, llm_model: str, **kwargs
) -> Iterator[str]:
# Prepare the messages.
messages = [
{"role": "user", "content": prompt},
]
# Make the request.
response = self.client.chat.completions.create(
messages=messages,
model=llm_model or self.DEFAULT_MODEL,
stream=True,
**{**self.DEFAULT_KWARGS, **kwargs},
)
# Iterate over the response and yield the content.
for chunk in response:
yield chunk.choices[0].delta.content
+32 -6
View File
@@ -1,23 +1,49 @@
from typing import Literal, Optional, Union
from typing import Optional, Union
from pydantic import Field, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
logging_level = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
class LoggingConfig(BaseSettings):
"""The class that holds all the logging settings for the application."""
enabled: bool = Field(False, description="Enable logging")
level: logging_level = Field("INFO", description="The logging level")
is_enabled: bool = Field(False, description="Enable logging")
model_config = SettingsConfigDict(extra="forbid")
def enable_logfire(self, **kwargs) -> None:
"""Enable logging for the application."""
# adding imports here to avoid forced dependencies
try:
from logging import basicConfig
import logfire
except ImportError as e:
raise ImportError(
"To enable logging, please install logfire: `pip install logfire`"
) from e
self.is_enabled = True
logfire.configure(**kwargs)
basicConfig(handlers=[logfire.LogfireLoggingHandler()])
try:
logfire.configure(**kwargs)
basicConfig(handlers=[logfire.LogfireLoggingHandler()])
except Exception as e:
self.is_enabled = False # Reset flag on failure
raise RuntimeError("Failed to configure logging") from e
def disable_logfire(self) -> None:
"""Disable logging for the application."""
self.is_enabled = False
class Settings(BaseSettings):
"""The class that holds all the API keys for the application."""
AMAZON_PROFILE_NAME: Optional[str] = Field(
"default", description="AWS Named Profile"
)
ANTHROPIC_API_KEY: Optional[SecretStr] = Field(
None, description="API key for Anthropic"
)
+2 -2
View File
@@ -1,8 +1,8 @@
import pytest
import os
import sys
import pytest
# Add the project root to the Python path.
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+101
View File
@@ -0,0 +1,101 @@
import json
import pytest
import simplemind as sm
from simplemind.models import BasePlugin, Conversation
from simplemind.providers import Anthropic, Gemini, Groq, Ollama, OpenAI
@pytest.mark.parametrize(
"provider_cls",
[
Anthropic,
Gemini,
OpenAI,
Groq,
Ollama,
# Amazon
],
)
def test_generate_data(provider_cls):
conv = sm.create_conversation(
llm_model=provider_cls.DEFAULT_MODEL, llm_provider=provider_cls.NAME
)
conv.add_message(text="hey")
data = conv.send()
assert isinstance(data.text, str)
assert len(data.text) > 0
@pytest.fixture
def sample_conversation():
"""Create a sample conversation for testing."""
conv = Conversation(llm_provider="openai")
conv.add_message(role="user", text="Hello!")
conv.add_message(role="assistant", text="Hi there!")
conv.add_message(role="user", text="How are you?")
return conv
@pytest.fixture
def temp_json_file(tmp_path):
"""Create a temporary file path for testing."""
return tmp_path / "conversation.json"
def test_save_conversation(sample_conversation, temp_json_file):
"""Test saving a conversation to a JSON file."""
sample_conversation.save(temp_json_file)
assert temp_json_file.exists()
with open(temp_json_file) as f:
saved_data = json.load(f)
assert "id" in saved_data
assert "messages" in saved_data
assert "llm_model" in saved_data
assert "llm_provider" in saved_data
assert len(saved_data["messages"]) == 3
assert saved_data["messages"][0]["text"] == "Hello!"
assert saved_data["messages"][1]["text"] == "Hi there!"
assert saved_data["messages"][2]["text"] == "How are you?"
def test_load_conversation(sample_conversation, temp_json_file):
"""Test loading a conversation from a JSON file."""
sample_conversation.save(temp_json_file)
loaded_conv = Conversation.load(temp_json_file)
assert loaded_conv.id == sample_conversation.id
assert loaded_conv.llm_model == sample_conversation.llm_model
assert loaded_conv.llm_provider == sample_conversation.llm_provider
assert len(loaded_conv.messages) == len(sample_conversation.messages)
for original_msg, loaded_msg in zip(
sample_conversation.messages, loaded_conv.messages
):
assert loaded_msg.role == original_msg.role
assert loaded_msg.text == original_msg.text
assert loaded_msg.meta == original_msg.meta
def test_save_load_with_plugins(sample_conversation, temp_json_file):
"""Test that plugins are properly excluded from serialization."""
# Create a dummy plugin
class DummyPlugin(BasePlugin):
def initialize_hook(self, conversation):
pass
sample_conversation.add_plugin(DummyPlugin())
sample_conversation.save(temp_json_file)
loaded_conv = Conversation.load(temp_json_file)
assert len(loaded_conv.plugins) == 0
+4 -3
View File
@@ -1,8 +1,8 @@
import pytest
from simplemind.providers import Anthropic, Gemini, OpenAI, Groq, Ollama
from pydantic import BaseModel
from simplemind.providers import Amazon, Anthropic, Gemini, Groq, Ollama, OpenAI
class ResponseModel(BaseModel):
result: int
@@ -16,6 +16,7 @@ class ResponseModel(BaseModel):
OpenAI,
Groq,
Ollama,
# Amazon
],
)
def test_generate_data(provider_cls):
@@ -25,4 +26,4 @@ def test_generate_data(provider_cls):
data = provider.structured_response(prompt=prompt, response_model=ResponseModel)
assert isinstance(data, ResponseModel)
assert type(data.result) == int
assert isinstance(data.result, int)
+2 -1
View File
@@ -1,6 +1,6 @@
import pytest
from simplemind.providers import Anthropic, Gemini, OpenAI, Groq, Ollama
from simplemind.providers import Amazon, Anthropic, Gemini, Groq, Ollama, OpenAI
@pytest.mark.parametrize(
@@ -11,6 +11,7 @@ from simplemind.providers import Anthropic, Gemini, OpenAI, Groq, Ollama
OpenAI,
Groq,
Ollama,
# Amazon,
],
)
def test_generate_text(provider_cls):
+118
View File
@@ -0,0 +1,118 @@
from typing import Annotated, Literal
import pytest
from pydantic import Field
import simplemind as sm
from simplemind.providers import Anthropic, OpenAI
from simplemind.providers._base_tools import BaseTool
MODELS = [
Anthropic,
# Gemini,
OpenAI,
# Groq,
# Ollama,
# Amazon
]
def get_weather(
location: Annotated[
str, Field(description="The city and state, e.g. San Francisco, CA")
],
unit: Annotated[
Literal["celcius", "fahrenheit"],
Field(description="The unit of temperature, either 'celsius' or 'fahrenheit'"),
] = "celcius",
):
"""
Get the current weather in a given location
"""
return f"42 {unit}"
def get_location():
"""Get the current location"""
return "San Francisco,CA"
@pytest.mark.parametrize(
"provider_cls",
MODELS,
)
def test_single_tool_args(provider_cls):
conv = sm.create_conversation(
llm_model=provider_cls.DEFAULT_MODEL, llm_provider=provider_cls.NAME
)
conv.add_message(text="What is the weather in San Francisco?")
data = conv.send(tools=[get_weather])
assert "42" in data.text
@pytest.mark.parametrize(
"provider_cls",
MODELS,
)
def test_single_tool_no_args(provider_cls):
conv = sm.create_conversation(
llm_model=provider_cls.DEFAULT_MODEL, llm_provider=provider_cls.NAME
)
conv.add_message(text="What is my current location")
data = conv.send(tools=[get_location])
assert "San Francisco" in data.text
@pytest.mark.parametrize(
"provider_cls",
MODELS,
)
def test_single_tool_partial(provider_cls):
conv = sm.create_conversation(
llm_model=provider_cls.DEFAULT_MODEL, llm_provider=provider_cls.NAME
)
conv.add_message(text="Can you tell me the weather?")
conv.send(tools=[get_weather])
# Will answer something like:
"""
I can help you check the weather, but I need to know the location you're interested in.
Could you please provide a city and state (e.g., "Los Angeles, CA" or "New York, NY")
where you'd like to know the weather?
"""
conv.add_message(text="San Francisco, CA")
data = conv.send(tools=[get_weather])
assert "42" in data.text
@pytest.mark.parametrize(
"provider_cls",
MODELS,
)
def test_multiple_tools(provider_cls):
conv = sm.create_conversation(
llm_model=provider_cls.DEFAULT_MODEL, llm_provider=provider_cls.NAME
)
conv.add_message(text="What is the wheather at my current location?")
data = conv.send(tools=[get_location, get_weather])
assert "San Francisco" in data.text
assert "42" in data.text
@pytest.mark.parametrize(
"provider_cls",
MODELS,
)
def test_tool_decorator(provider_cls):
@sm.tool(llm_provider=provider_cls.NAME)
def exchange_rate(currency_pair: str) -> float:
return 7.9
assert isinstance(exchange_rate, BaseTool)
assert exchange_rate.name == "exchange_rate"
assert list(exchange_rate.properties.keys()) == ["currency_pair"]