diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000..bf4b530 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,34 @@ +name: Python CI + +on: + push: + branches: [main, "**"] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: [3.11] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest + + - name: Run tests + run: | + pytest -v diff --git a/README.md b/README.md index 47c8e44..56bd7c4 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,32 @@ Example: coaia tash my_key --f path/to/value/file.txt ``` +#### Fetch Value from Redis + +To fetch a value from Redis by key: + +```bash +coaia fetch +``` + +Example: + +```bash +coaia fetch my_key +``` + +To fetch a value from Redis and save it to a file: + +```bash +coaia fetch --output +``` + +Example: + +```bash +coaia fetch my_key --output path/to/output/file.txt +``` + #### Process Custom Tags Enable custom quick addons for assistants or bots using process tags. To add a new process tag to `coaia.json`, include entries like: @@ -129,4 +155,3 @@ Contributions are welcome! Please open an issue or submit a pull request for any ## License This project is licensed under the MIT License. See the LICENSE file for more details. - diff --git a/coaiapy/coaiacli.py b/coaiapy/coaiacli.py index 1fe7b15..4da491f 100644 --- a/coaiapy/coaiacli.py +++ b/coaiapy/coaiacli.py @@ -8,7 +8,7 @@ sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) -from coaiamodule import read_config, transcribe_audio, summarizer, tash, abstract_process_send, initial_setup +from coaiamodule import read_config, transcribe_audio, summarizer, tash, abstract_process_send, initial_setup, fetch_key_val from cofuse import ( get_comments, post_comment, create_session_and_save, add_trace_node_and_save, @@ -157,6 +157,11 @@ def main(): parser_ds_items_create.add_argument('-e','--expected', help="Expected output") parser_ds_items_create.add_argument('-m','--metadata', help="Optional metadata as JSON string") + # Subparser for 'fetch' command + parser_fetch = subparsers.add_parser('fetch', help='Fetch a value from Redis by key.') + parser_fetch.add_argument('key', type=str, help="The key to fetch.") + parser_fetch.add_argument('-O', '--output', type=str, help="Filename to save the fetched value.") + args = parser.parse_args() if args.command == 'init': @@ -210,6 +215,8 @@ def main(): f.write(summary) else: print(f"{summary}") + elif args.command == 'fetch': + fetch_key_val(args.key, args.output) elif args.command == 'fuse': if args.fuse_command == 'comments': if args.action == 'list': diff --git a/coaiapy/coaiamodule.py b/coaiapy/coaiamodule.py index 291b60c..9934dba 100644 --- a/coaiapy/coaiamodule.py +++ b/coaiapy/coaiamodule.py @@ -91,7 +91,7 @@ def remove_placeholder_lines(text): # Split the text into lines lines = text.split('\n') # Iterate over the lines and remove lines starting with "Placeholder" - cleaned_lines = [line for line in lines if not line.startswith("Placeholder")] + cleaned_lines = [line for line for line in lines if not line.startswith("Placeholder")] # Join the cleaned lines back into a string cleaned_text = '\n'.join(cleaned_lines) @@ -261,7 +261,7 @@ def d2s_send(input_message, temperature=None,pre=''): temperature = config['d2s_default_temperature'] if temperature is None else temperature - system_instruction = config.get('d2s_instruction', 'You do : Receive a text that requires to put details into shapes. you keep the essence of what is discussed foreach elements you observe and able yo group in the inout text.') + system_instruction = config.get('d2s_instruction', 'You do : Receive a text that requires to put details into shapes. you group elements of different nature and summarize them. REMEMBER: Dont introduce nor conclude, just output results. No comments.') # Concatenate preprompt_instruction with input_message @@ -604,6 +604,31 @@ def tash(k:str,v:str,ttl=None,quiet=False): print('Stashing failed') return result +def fetch_key_val(key, output_file=None): + try: + jtalecnf = read_config()['jtaleconf'] + _r = _newjtaler(jtalecnf) + if _r is None: + print("Error: Redis connection failed.") + sys.exit(2) + value = _r.get(key) + if value is None: + print(f"Error: Key '{key}' not found in Redis memory.") + sys.exit(1) + value = value.decode('utf-8') + if output_file: + with open(output_file, 'w') as file: + file.write(value) + print(f"Key: {key} fetched and saved to {output_file}") + else: + print(value) + except redis.ConnectionError: + print("Error: Redis connection failed.") + sys.exit(2) + except Exception as e: + print(f"Error: {str(e)}") + sys.exit(1) + def initial_setup(): sample_config = { "username": "SSH_USER", diff --git a/coaiapy/test_cli_fetch.py b/coaiapy/test_cli_fetch.py new file mode 100644 index 0000000..453403b --- /dev/null +++ b/coaiapy/test_cli_fetch.py @@ -0,0 +1,51 @@ +import pytest +from unittest.mock import patch, MagicMock +import sys +import os +import io +import redis +from coaiapy.coaiacli import fetch_key_val + +# Mock Redis fixture +@pytest.fixture +def mock_redis(monkeypatch): + mock_redis_instance = MagicMock() + monkeypatch.setattr('redis.Redis', lambda *args, **kwargs: mock_redis_instance) + return mock_redis_instance + +# Mock broken Redis fixture +@pytest.fixture +def mock_broken_redis(monkeypatch): + mock_redis_instance = MagicMock() + mock_redis_instance.get.side_effect = redis.ConnectionError + monkeypatch.setattr('redis.Redis', lambda *args, **kwargs: mock_redis_instance) + return mock_redis_instance + +def test_fetch_stdout_key_found(mock_redis): + mock_redis.get.return_value = b'This is a memory snippet.' + with patch('sys.stdout', new_callable=io.StringIO) as mock_stdout, \ + patch('coaiapy.coaiamodule._newjtaler') as mock_newjtaler: + mock_newjtaler.return_value = mock_redis + fetch_key_val('mykey') + assert mock_stdout.getvalue().strip() == 'This is a memory snippet.' + +def test_fetch_output_file_key_found(mock_redis): + mock_redis.get.return_value = b'This is a memory snippet.' + output_file = 'memory.txt' + fetch_key_val('mykey', output_file) + with open(output_file, 'r') as file: + assert file.read().strip() == 'This is a memory snippet.' + os.remove(output_file) + +def test_fetch_key_not_found_exits_1(mock_redis): + mock_redis.get.return_value = None + with pytest.raises(SystemExit) as pytest_wrapped_e: + fetch_key_val('mykey') + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 1 + +def test_fetch_redis_down_exits_2(mock_broken_redis): + with pytest.raises(SystemExit) as pytest_wrapped_e: + fetch_key_val('mykey') + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 2