2024-08-12 04:42:37 +00:00
#!/bin/python3
# Chat with an intelligent assistant in your terminal
from ollama import Client
import re
import pyperclip
import sys
import argparse
2024-09-25 18:56:48 +00:00
import pygments
from pygments . lexers import get_lexer_by_name
from pygments . formatters import TerminalFormatter
2024-10-01 19:50:55 +00:00
import os
2024-08-12 04:42:37 +00:00
2024-09-25 19:54:27 +00:00
server = ' localhost:11434 '
2024-09-03 09:38:36 +00:00
model = ' llama3.1:8b-instruct-q8_0 '
temp = 0.2
2024-08-12 04:42:37 +00:00
pattern = r ' ```[a-z]* \ n[ \ s \ S]*? \ n``` '
line_pattern = r ' `[a-z]*[ \ s \ S]*?` '
2024-10-01 19:50:55 +00:00
def save_conversation ( filename = ' conversation.md ' ) :
# check if filename already exists and increment filename if so
if not filename . endswith ( ' .md ' ) :
filename + = ' .md '
base , extension = os . path . splitext ( filename )
i = 1
while os . path . exists ( filename ) :
filename = f " { base } _ { i } { extension } "
i + = 1
# save conversation to filename
global conversation
with open ( filename , ' w ' ) as f :
f . write ( conversation )
2024-10-01 18:50:35 +00:00
def parse_commands ( text ) :
# See if user wrote any commands here
2024-10-01 19:50:55 +00:00
# returns bool: True if command was executed, False if not
# importantly, the command doesn't need to execute succesfully for it to return True
2024-10-01 18:50:35 +00:00
tokens = text . split ( ' ' )
match tokens [ 0 ] :
case ' /save ' :
2024-10-01 19:50:55 +00:00
if len ( tokens ) > 1 :
save_conversation ( tokens [ 1 ] )
else :
save_conversation ( )
return True
2024-10-01 18:50:35 +00:00
case ' /clear ' :
2024-10-01 19:50:55 +00:00
global history
history = [ system_prompt ]
return True
2024-10-01 20:25:30 +00:00
case ' /clipboard ' :
context_query = ' \n \n The following is context provided by the user: \n '
context_query + = get_string_from_clipboard ( ) + ' \n '
return text . split ( ' /clipboard ' ) [ 1 ] + context_query
2024-10-01 18:50:35 +00:00
case ' /exit ' :
2024-10-01 19:50:55 +00:00
exit ( )
return False
2024-10-01 18:50:35 +00:00
2024-09-25 18:56:48 +00:00
def highlight_code ( language_name , code ) :
# Check if the language is specified in the first line
lexer_name = language_name
if lexer_name == None :
lines = code . split ( ' \n ' )
for line in lines :
if line . strip ( ) . startswith ( ' ``` ' ) :
lexer_name = line . strip ( ) . split ( ' ``` ' ) [ 1 ] . strip ( )
break
elif line . strip ( ) . startswith ( ' lang: ' ) :
lexer_name = line . strip ( ) . split ( ' : ' ) [ 1 ] . strip ( )
break
if lexer_name :
try :
# Try to get the lexer by name
lexer = get_lexer_by_name ( lexer_name )
except ValueError :
# If the lexer is not found, guess it
lexer = guess_lexer ( code . split ( ' \n ' ) [ 1 : - 1 ] )
if not lexer :
# If no lexer is guessed, default to bash
lexer = get_lexer_by_name ( ' bash ' )
else :
# If no language is specified, guess the lexer
2024-09-25 19:44:28 +00:00
print ( " LEXER NAME " + lexer_name )
2024-09-25 18:56:48 +00:00
lexer = guess_lexer ( code . split ( ' \n ' ) [ 1 : - 1 ] )
if not lexer :
# If no lexer is guessed, default to bash
lexer = get_lexer_by_name ( ' bash ' )
formatter = TerminalFormatter ( )
2024-09-25 19:44:28 +00:00
just_code = code . split ( ' \n ' ) [ 0 ]
newlines = ' \n ' . join ( code . split ( ' \n ' ) [ 1 : ] )
# if code is a code block, strip surrounding block markers
2024-09-25 18:56:48 +00:00
lines = code . split ( ' \n ' )
2024-09-25 19:44:28 +00:00
if ( len ( lines ) > 2 ) and ( ' ``` ' in lines [ 0 ] ) and ( ' ``` ' in lines [ - 1 ] ) :
2024-09-25 18:56:48 +00:00
just_code = ' \n ' . join ( code . split ( ' \n ' ) [ 1 : - 1 ] )
highlighted_code = pygments . highlight ( just_code , lexer , formatter )
2024-09-25 19:44:28 +00:00
return highlighted_code + newlines
2024-09-25 18:56:48 +00:00
2024-08-12 04:42:37 +00:00
def extract_code_block ( markdown_text ) :
# Use the regular expression pattern to find all matches in the markdown text
matches = re . finditer ( pattern , markdown_text )
# Iterate over the matches and extract the code blocks
code_blocks = [ ]
for match in matches :
code_block = match . group ( 0 )
2024-09-25 18:56:48 +00:00
highlighted_code = highlight_code ( None , code_block )
# Add the highlighted code block to the list of code blocks
code_blocks . append ( highlighted_code )
2024-08-12 04:42:37 +00:00
if len ( code_blocks ) == 0 :
line_matches = re . finditer ( line_pattern , markdown_text )
for match in line_matches :
code_block = match . group ( 0 )
code_blocks . append ( code_block [ 1 : - 1 ] )
return code_blocks
def copy_string_to_clipboard ( string ) :
try :
pyperclip . copy ( string )
except :
return
2024-10-01 20:25:30 +00:00
def get_string_from_clipboard ( ) :
try :
result = pyperclip . paste ( )
except :
result = ' '
return result
2024-08-12 04:42:37 +00:00
code_history = [ ]
2024-10-01 19:50:55 +00:00
system_prompt = { " role " : " system " , " content " : " You are a helpful, smart, kind, and efficient AI assistant. You always fulfill the user ' s requests accurately and concisely. " }
history = [ system_prompt ]
2024-08-12 04:42:37 +00:00
2024-10-01 18:50:35 +00:00
conversation = " "
2024-08-12 04:42:37 +00:00
def chat ( message , stream = True ) :
history . append ( { " role " : " user " , " content " : message } )
completion = client . chat (
model = model ,
2024-09-03 09:38:36 +00:00
options = { " temperature " : temp } ,
2024-08-12 04:42:37 +00:00
messages = history ,
stream = stream
)
result = ' '
2024-09-25 18:56:48 +00:00
language = ' '
large_chunk = [ ]
2024-08-12 04:42:37 +00:00
for chunk in completion :
if stream :
2024-09-25 18:56:48 +00:00
text = chunk [ ' message ' ] [ ' content ' ]
large_chunk . append ( text )
large_text = ' ' . join ( large_chunk )
2024-09-25 19:44:28 +00:00
# update language if entering or leaving code block
2024-09-25 18:56:48 +00:00
if ( ' \n ' in large_text ) and ( ' ``` ' in large_text ) :
language = large_text . split ( ' ``` ' ) [ 1 ] . split ( ' \n ' ) [ 0 ]
2024-09-25 19:44:28 +00:00
if language == ' ' :
language = None
2024-09-25 18:56:48 +00:00
print ( large_text , end = ' ' , flush = True )
large_chunk = [ ]
large_text = ' '
2024-09-25 19:44:28 +00:00
# Only print full lines
2024-09-25 18:56:48 +00:00
if ' \n ' in large_text :
output = large_text
2024-09-25 19:44:28 +00:00
if language :
2024-09-25 18:56:48 +00:00
output = highlight_code ( language , output )
print ( output , end = ' ' , flush = True )
large_chunk = [ ]
result + = text
2024-09-03 09:38:36 +00:00
if not stream :
result = completion [ ' message ' ] [ ' content ' ]
2024-08-12 04:42:37 +00:00
if stream :
2024-09-25 20:19:03 +00:00
print ( large_text , flush = True )
2024-08-12 04:42:37 +00:00
history . append ( { " role " : ' assistant ' , ' content ' : result } )
return result
2024-09-25 21:44:54 +00:00
def chat2 ( args , user_input , stream = True ) :
2024-10-01 19:50:55 +00:00
global conversation
2024-10-01 20:25:30 +00:00
command_result = parse_commands ( user_input )
if command_result :
if type ( command_result ) == bool :
return ' '
elif type ( command_result ) == str : # sometimes I want to change the user prompt with a command
user_input = command_result
if args . reflect :
2024-10-01 20:00:32 +00:00
print ( ' assistant: ' , end = ' ' )
2024-09-25 21:33:14 +00:00
result = reflection_mode ( user_input , stream )
else :
2024-10-01 20:00:32 +00:00
print ( ' assistant: ' , end = ' ' )
2024-09-25 21:33:14 +00:00
result = chat ( user_input , stream )
2024-10-01 19:50:55 +00:00
2024-10-01 20:25:30 +00:00
conversation + = ' user: ' + user_input + ' \n '
conversation + = ' assistant: ' + result + ' \n '
2024-09-25 21:33:14 +00:00
return result
def highlightify_text ( full_text ) :
lines = full_text . split ( ' \n ' )
result = ' '
language = None
for line in lines :
text = line + ' \n '
# update language if entering or leaving code block
if ' ``` ' in text :
language = text . split ( ' ``` ' ) [ 1 ] . split ( ' \n ' ) [ 0 ]
if language == ' ' :
language = None
result + = text
text = ' '
# Only print full lines
if ' \n ' in text :
output = text
if language :
output = highlight_code ( language , output )
result + = output
return result
2024-08-12 04:42:37 +00:00
def parse_args ( ) :
# Create the parser
parser = argparse . ArgumentParser ( description = ' Copy and open a source file in TextEdit ' )
# Add the --follow-up (-f) argument
parser . add_argument ( ' --follow-up ' , ' -f ' , nargs = ' ? ' , const = True , default = False , help = ' Ask a follow up question when piping in context ' )
# Add the --copy (-c) argument
parser . add_argument ( ' --copy ' , ' -c ' , action = ' store_true ' , help = ' copy a codeblock if it appears ' )
# Add the --shell (-s) argument
parser . add_argument ( ' --shell ' , ' -s ' , nargs = ' ? ' , const = True , default = False , help = ' output a shell command that does as described ' )
# Add the --model (-m) argument
parser . add_argument ( ' --model ' , ' -m ' , nargs = ' ? ' , const = True , default = False , help = ' Specify model ' )
2024-09-03 09:38:36 +00:00
# Add the --temp (-t) argument
parser . add_argument ( ' --temp ' , ' -t ' , nargs = ' ? ' , const = True , default = False , help = ' Specify temperature ' )
2024-09-25 21:33:14 +00:00
# Add the --host argument
2024-09-25 20:02:02 +00:00
parser . add_argument ( ' --host ' , nargs = ' ? ' , const = True , default = False , help = ' Specify host of ollama server ' )
2024-09-25 21:33:14 +00:00
# Add the --reflect argument
parser . add_argument ( ' --reflect ' , action = ' store_true ' , help = ' Use reflection prompting style to improve output. May be slower and not work with all models. ' )
2024-08-12 04:42:37 +00:00
# Parse the arguments
return parser . parse_args ( )
2024-09-25 21:33:14 +00:00
def reflection_mode ( query , should_print = False ) :
reflection_prompt = """
You are a helpful ai assistant that answers every question thoroughly and accurately . You always begin your response with a < planning > < / planning > section where you lay out your plan for answering the question . It is important that you don ' t make any assumptions while planning. Then you <reflect></reflect> on your plan to make sure it correctly answers the user ' s question . Then , if you are confident your plan in correct , you give your < draft answer > , followed by < final reflection > to make sure the answer correctly addresses the user ' s question. Finally, give a <final answer> with your answer to the user. If there are any ambiguous or unknown requirements, ask the user for more information as your final answer. You must always have a <final answer> no matter what, even if you are asking for clarifying questions. If you do not have the <final answer> tags, the user will not see your response. Additionally, the user can not see your planning or reflecting, they can only see what goes in the <final answer></final answer> tags, so make sure you provide any information you want to tell the user in there.
"""
result = chat ( reflection_prompt + query , stream = False )
highlighted_result = highlightify_text ( result )
# print('==DEBUG==')
# print(highlighted_result)
# print('==DEBUG==')
final_answer = highlighted_result . split ( ' <final answer> ' )
while len ( final_answer ) < 2 :
final_answer = chat ( ' Please put your final answer in <final answer></final answer> tags. ' , stream = False )
final_answer = highlighted_result . split ( ' <final answer> ' )
final_answer = final_answer [ 1 ] . split ( ' </final answer> ' ) [ 0 ]
if should_print :
print ( final_answer )
return final_answer
2024-09-25 19:54:27 +00:00
def set_host ( host ) :
global server
server = host
2024-08-12 04:42:37 +00:00
def arg_follow_up ( args ) :
sys . stdin = open ( ' /dev/tty ' )
if args . follow_up != True :
second_input = args . follow_up
else :
second_input = input ( ' > ' )
return second_input
def arg_shell ( args ) :
2024-09-03 09:38:36 +00:00
query = ' Form a shell command based on the following description. Only output a working shell command . \n Description: '
2024-08-12 04:42:37 +00:00
if args . shell != True :
query + = args . shell
else :
query + = input ( ' > ' )
2024-09-25 21:33:14 +00:00
result = chat2 ( args , query , False )
2024-08-12 04:42:37 +00:00
result = blocks [ 0 ] if len ( blocks := extract_code_block ( result ) ) else result
2024-09-25 21:33:14 +00:00
print ( result )
2024-08-12 04:42:37 +00:00
copy_string_to_clipboard ( result )
def handle_piped_input ( args ) :
all_input = sys . stdin . read ( )
2024-09-25 17:26:05 +00:00
query = ' Use the following context to answer the question. There will be no follow up questions from the user so make sure your answer is complete: \n START CONTEXT \n ' + all_input + ' \n END CONTEXT \n After you answer the question, reflect on your answer and determine if it answers the question correctly. '
2024-08-12 04:42:37 +00:00
if args . copy :
query + = ' Answer the question using a codeblock for any code or shell scripts \n '
if args . follow_up :
query + = arg_follow_up ( args )
query + = ' \n '
2024-09-25 21:33:14 +00:00
result = chat2 ( args , query )
2024-08-12 04:42:37 +00:00
blocks = extract_code_block ( result )
if args . copy and len ( blocks ) :
copy_string_to_clipboard ( blocks [ 0 ] )
def handle_non_piped_input ( args ) :
if args . shell :
arg_shell ( args )
exit ( )
if args . follow_up :
user_input = arg_follow_up ( args )
2024-09-25 21:33:14 +00:00
result = chat2 ( args , user_input )
2024-08-12 04:42:37 +00:00
exit ( )
while True :
try :
user_input = input ( ' > ' )
except ( EOFError , KeyboardInterrupt ) :
print ( )
exit ( )
else :
2024-09-25 21:33:14 +00:00
result = chat2 ( args , user_input )
2024-08-12 04:42:37 +00:00
2024-09-25 20:02:02 +00:00
client = None
2024-08-12 04:42:37 +00:00
def main ( ) :
args = parse_args ( )
2024-09-25 19:54:27 +00:00
if args . host :
set_host ( args . host )
2024-09-25 20:02:02 +00:00
# Point to the local server
global client
client = Client ( host = server )
2024-08-12 04:42:37 +00:00
if args . model :
global model
model = args . model
2024-09-03 09:38:36 +00:00
if args . temp :
global temp
temp = float ( args . temp )
2024-08-12 04:42:37 +00:00
if not sys . stdin . isatty ( ) :
handle_piped_input ( args )
else :
handle_non_piped_input ( args )
if __name__ == ' __main__ ' :
main ( )