Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
7584be2
correct context windows for suggest_next
Jun 22, 2023
d5e35d7
wip on workflow detectiopn & saving using suffix trees
Jun 27, 2023
fcfabea
workflows: slightly modify ux flow for saving recipes
Jun 28, 2023
4d7b091
fix prediction logic in suggest_next and add simple test
jj-ctrlstack Jun 22, 2023
9aa682a
fix adding to history, remove the extra argument
jj-ctrlstack Jun 22, 2023
00b7baa
implement shell config option to install function which reloads history
jj-ctrlstack Jun 22, 2023
210d092
add installation check and update shell history handling
jj-ctrlstack Jun 23, 2023
49ce906
move feedback event to lookup_entry_point
jj-ctrlstack Jun 23, 2023
c3dfeaf
bump version to 0.1.8
jj-ctrlstack Jun 23, 2023
610f9cf
update dtt choice text with emojis
jj-ctrlstack Jun 23, 2023
ce9edbe
remove duplicate exception handling
jj-ctrlstack Jun 23, 2023
a59753c
add git commands to shell history
jj-ctrlstack Jun 23, 2023
928d7d5
ignore KeyboardInterrupt in query_mode and fall through
jj-ctrlstack Jun 23, 2023
b98feac
update get_latest_commits to account for multi-line commit messages
jj-ctrlstack Jun 23, 2023
ca6b477
add setting for generating commit messages
jj-ctrlstack Jun 23, 2023
d7f153a
if I ctrl-c in dtt mode be done
joshualwhite Jun 23, 2023
c283fc0
revert last commit
joshualwhite Jun 23, 2023
a2405b0
appropriate color formatting for comments
Jun 23, 2023
01ce9d7
bump versions in requirements
Jun 23, 2023
47e83dd
early return of curated results
Jun 23, 2023
53823a1
sync dependencies between requirements.txt and setup.py
Jun 23, 2023
8fb7d3e
fix typo
Jun 23, 2023
0cb3cc8
replace Levenshtein with thefuzz for command similarity
jj-ctrlstack Jun 23, 2023
b31cfa6
0.1.9 release
Jun 23, 2023
164b29e
suppress unnecessary warning message
Jun 23, 2023
3be6107
0.1.10 release
Jun 23, 2023
b4992a2
use the same version of setuptools everywhere
Jun 23, 2023
5199b5b
curated results in ctrl-e lookups
Jun 26, 2023
58b3cd7
intercept error codes in zsh
Jun 28, 2023
d82bc00
fix color output
Jun 28, 2023
b35f719
implement role-level runner
jj-ctrlstack Jun 26, 2023
1d22bd6
allow skipping initial configuration with skip-config
jj-ctrlstack Jun 27, 2023
f0e9a98
handle KeyboardInterrupt in config_flow
jj-ctrlstack Jun 28, 2023
3c6cb23
fix EOF error when attempting to run registration flow with redirecte…
Jun 28, 2023
3e7ba9d
update version to 0.11.0+snapshot
Jun 28, 2023
f8d0df3
local runner should retry failed calls to token issuer
Jun 28, 2023
cb1dc1e
fix json decoding in index_content
jj-ctrlstack Jun 29, 2023
ea934ff
simplify ProgressSpinner by removing queue and thread
jj-ctrlstack Jun 29, 2023
90fb69b
handle KeyboardInterrupt in update_history
jj-ctrlstack Jun 29, 2023
d71f9ff
allow the user to ctrl-c the config flow
joshualwhite Jun 29, 2023
bfa1f8a
fallback to shutil for terminal size, if that fails use default
joshualwhite Jun 29, 2023
9028da7
guard against NotImplementedError
Jun 29, 2023
f161c5a
raise exception when correction request fails
Jun 29, 2023
cc300c3
wip on workflow detectiopn & saving using suffix trees
Jun 27, 2023
e748e91
workflows: slightly modify ux flow for saving recipes
Jun 28, 2023
c9a4c50
adding debugging & notes to workflow detection
Jun 29, 2023
be0661a
add um --scan on optional zsh hook
Jun 29, 2023
6b0d72b
continuing to iterate of workflows ux
Jun 30, 2023
c5f58b7
remove repeated & equal commands in a workflow
Jun 30, 2023
6728f77
shorten results list (taking highest score values) with option to vie…
joshualwhite Jul 3, 2023
ffa760d
fix prediction logic in suggest_next and add simple test
jj-ctrlstack Jun 22, 2023
9d999a6
implement shell config option to install function which reloads history
jj-ctrlstack Jun 22, 2023
cef007b
add installation check and update shell history handling
jj-ctrlstack Jun 23, 2023
be679cb
sync dependencies between requirements.txt and setup.py
Jun 23, 2023
358f137
replace Levenshtein with thefuzz for command similarity
jj-ctrlstack Jun 23, 2023
5a0607d
0.1.9 release
Jun 23, 2023
3ad9345
show version changes periodically if the running version is not latest
jj-ctrlstack Jun 29, 2023
9b29e47
curated function to handle missing message and dedupe results
jj-ctrlstack Jun 30, 2023
b4f9284
update ProgressSpinner with support for clear and optional header
jj-ctrlstack Jun 30, 2023
16eddf5
show history progress only if needed and make it temporary
jj-ctrlstack Jun 30, 2023
8f8040c
update index list with terminal size awareness and ellipsis for long …
jj-ctrlstack Jul 1, 2023
a1bf1ae
improve error handling in query and dtt to not mess the UI
jj-ctrlstack Jul 1, 2023
f3fc669
wip on workflow detectiopn & saving using suffix trees
Jun 27, 2023
dc1d951
workflows: slightly modify ux flow for saving recipes
Jun 28, 2023
ab3a3a7
fix prediction logic in suggest_next and add simple test
jj-ctrlstack Jun 22, 2023
80903d6
implement shell config option to install function which reloads history
jj-ctrlstack Jun 22, 2023
39c1bea
add installation check and update shell history handling
jj-ctrlstack Jun 23, 2023
b60b271
intercept error codes in zsh
Jun 28, 2023
b0d9f63
continuing to iterate of workflows ux
Jun 30, 2023
2a37152
shorten results list (taking highest score values) with option to vie…
joshualwhite Jul 3, 2023
a395cc5
point at production api
joshualwhite Jul 3, 2023
0797498
fix workflow scanning after rebase
joshualwhite Jul 3, 2023
6bff190
Merge remote-tracking branch 'origin' into workflow-detection
joshualwhite Jul 3, 2023
6874063
filter None results before sorting
joshualwhite Jul 3, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions \
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
replace Levenshtein with thefuzz for command similarity

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Author: JJ <[email protected]>
#
# interactive rebase in progress; onto 6728f77
# Last commands done (6 commands done):
# pick 086129c sync dependencies between requirements.txt and setup.py
# pick e04cb78 replace Levenshtein with thefuzz for command similarity
# Next commands to do (17 remaining commands):
# pick db006cc 0.1.9 release
# pick 40ca2cb show version changes periodically if the running version is not latest
# You are currently rebasing branch 'workflow-detection' on '6728f77'.
#
# Changes to be committed:
# modified: promptops/query/suggest_next.py
# modified: tests/promptops/test_suggest_next.py
#
2 changes: 1 addition & 1 deletion promptops/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def get_history_db() -> VectorDB:

def check_history(embedding):
history_db = get_history_db()
results = history_db.search(embedding, k=3, min_similarity=0.8)
results = history_db.search(embedding, k=2, min_similarity=0.85)
results = [(obj, score) for obj, score in results if not isinstance(obj, dict) or not obj.get("ignore", False)]
return results

Expand Down
29 changes: 26 additions & 3 deletions promptops/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,26 @@ def query_mode(args):
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.INFO, format="%(message)s")

if args.shell_config:
from promptops.shells import get_shell
print(get_shell().get_config())
sys.exit(0)

settings.model = args.mode
settings.history_context = args.history_context
settings.request_explanation = args.explain

last_command_result = os.environ.get("PROMPTOPS_LAST_COMMAND_RESULT")
last_command = os.environ.get("PROMPTOPS_LAST_COMMAND")

if args.scan:
from promptops.query.detect import detect_workflows
detect_workflows()

question = " ".join(args.question)
if not question.strip() and last_command_result != "0" and last_command:
question = last_command
try:
if question.strip():
feedback({"event": "query_mode"})
Expand Down Expand Up @@ -89,7 +104,7 @@ def lookup_mode(args):


def recipe_mode(args):
from promptops.recipes.creation import recipe_entrypoint
from promptops.recipes.entrypoint import recipe_entrypoint
recipe_entrypoint(args)


Expand Down Expand Up @@ -165,6 +180,7 @@ def entry_alias():
parser.add_argument(
"--mode", default=settings.model, choices=["fast", "accurate"], help="fast or accurate (default: %(default)s)"
)
parser.add_argument("--scan", help="scan for existing workflows", action="store_true", default=False)
parser.add_argument("question", nargs=REMAINDER, help="the question to ask")
registered = user.has_registered()
args = parser.parse_args()
Expand Down Expand Up @@ -205,7 +221,14 @@ def entry_alias():

if args.question and len(args.question) > 0:
if args.question[0] == 'workflow' or args.question[0] == 'recipe':
return recipe_mode(args)
subparser = ArgumentParser(
prog=f"{alias} recipe",
usage=f"{alias} recipe [prompt]",
description=f"{alias} create and manage recipes",
)
subparser.add_argument("question", nargs=REMAINDER, help="the question to ask")
sub_args = subparser.parse_args(args.question[1:])
return recipe_mode(sub_args)
elif args.question[0] == 'index':
# index subcommand
subparser = ArgumentParser(
Expand All @@ -220,7 +243,6 @@ def entry_alias():
return index_mode(sub_args)
query_mode(args)


def entry_main():
# Set the global exception handler
sys.excepthook = handle_exception(sys.excepthook)
Expand Down Expand Up @@ -268,6 +290,7 @@ def entry_main():
parser_question.add_argument("--shell-config", action="store_true", help="print configuration for your shell")
parser_question.add_argument("--install", action="store_true", help="print install script")
parser_question.add_argument("question", nargs=REMAINDER, help="the question to ask")
parser_question.add_argument("--scan", help="scan for existing workflows", action="store_true", default=False)
parser_question.set_defaults(func=query_mode)

parser_runner = subparsers.add_parser("runner", help="run commands from slack")
Expand Down
1 change: 1 addition & 0 deletions promptops/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Result:
explanation: str = None
lang: str = "shell"
origin: str = "promptops"
score: float = 1

@staticmethod
def from_dict(data: dict) -> 'Result':
Expand Down
153 changes: 153 additions & 0 deletions promptops/query/detect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import hashlib

import requests

from promptops import user, settings
from promptops import trace

from promptops.ui.vim import edit_with_vim

from promptops.query.suggest_next import SuffixTree
from promptops.ui import selections
import shlex
from thefuzz import fuzz


def hash_it(item):
string = " && ".join([i.strip() for i in item])
return hashlib.sha256(string.encode('utf-8')).hexdigest()


def similarity(item1, item2):
string1 = " && ".join(sorted(item1)).strip()
string2 = " && ".join(sorted(item2)).strip()
tokens1 = shlex.split(string1)
tokens2 = shlex.split(string2)
if tokens1[0] != tokens2[0]:
return 0
return fuzz.ratio(sorted(tokens1[1:]), sorted(tokens2[1:])) / 100.0


def filter_similar(items):
response = requests.post(
settings.endpoint + "/workflows/hashes",
json={'trace_id': trace.trace_id},
headers={"user-agent": f"promptops-cli; user_id={user.user_id()}"}
)

if response.status_code != 200:
print("error", response.json())
raise Exception(f"there was problem with the response, status: {response.status_code}")

existing_hashes = response.json().get("hashes", [])
unique_items = []

for i, item in enumerate(items):
if hash_it(item) in existing_hashes:
continue

unique = True
for item2 in unique_items:
if similarity(item, item2) > 0.75:
unique = False
break
if unique:
unique_items.append(item)

unique_items = filter(lambda x: not all([i == x[0] for i in x]), unique_items)

return list(unique_items)


def print_steps(item):
for i, txt in enumerate(item):
print(f'{i + 1}. {txt}')
print()


def edit(steps):
print()
options = ["edit in vim", "clarify", "continue"]
selection = None
while selection != 2:
ui = selections.UI(options, is_loading=False)
selection = ui.input()
print()
if selection == 0:
content = edit_with_vim("\n".join(steps))
steps = [line for line in content.split("\n") if line.strip() != ""]
print_steps(steps)
else:
print("TODO")
pass


def handle_detected_workflow(item):
print("\nDetected Workflow:")
item = list(set(item))
print_steps(item)
hashed = hash_it(item)

options = ["save", "skip", "edit"]
ui = selections.UI(options, is_loading=False)
selection = ui.input()
while selection != 0:
if selection == 2:
edit(item)
else:
return None
ui = selections.UI(options, is_loading=False)
selection = ui.input()

print()
return {
'commands': item,
'hash': hashed
}


def save_workflows(workflows):
req = {
'items': workflows,
'trace_id': trace.trace_id,
}

response = requests.post(
settings.endpoint + "/workflows",
json=req,
headers={
"user-agent": f"promptops-cli; user_id={user.user_id()}",
}
)

if response.status_code != 200:
print("error", response.json())
raise Exception(f"there was problem with the response, status: {response.status_code}")


def detect_workflows():
suffix_tree = SuffixTree(5, 10000)
detected = suffix_tree.find_repeated_sequences()
detected = filter_similar(detected)
print(f"We detected {len(detected)} possible workflows")

workflows = []
for item in detected:
try:
maybe_recipe = handle_detected_workflow(item)
if maybe_recipe:
workflows.append(maybe_recipe)
except KeyboardInterrupt:
if len(workflows) == 0:
return

print("Do you want to save the selections you have made so far?\n")
options = ["save", "exit"]
ui = selections.UI(options, is_loading=False)
selection = ui.input()
if selection == 0:
save_workflows(workflows)
return

save_workflows(workflows)

5 changes: 4 additions & 1 deletion promptops/query/dtos.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from dataclasses import dataclass
from typing import List


@dataclass
class Result:
script: str
script: str = None
scripts: List[str] = None
explanation: str = None
lang: str = "shell"
origin: str = "promptops"
score: float = 1

@staticmethod
def from_dict(data: dict) -> "Result":
Expand Down
47 changes: 47 additions & 0 deletions promptops/query/lookup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import json
import logging
import os
import sys
import threading
import queue

import requests

from promptops import user, settings, trace
from promptops.feedback import feedback

from promptops.similarity import embedding
Expand Down Expand Up @@ -114,6 +120,12 @@ def loader(self):
processor=lambda x: x["question"] if isinstance(x, dict) else x,
)
matches.extend([(r["corrected"], score) for r, score in corrected_matches])

curated_matches = lookup_curated(item)
for i, (match, score) in enumerate(curated_matches):
content = "\n".join([line for line in match.split("\n") if line and not line.startswith("#")])
matches.append(content, score)

matches = [(match, score / 100) for match, score in matches]
matches = sorted(matches, key=lambda x: x[1], reverse=True)
# dedupe
Expand Down Expand Up @@ -263,3 +275,38 @@ def entry_point(args):
sys.exit(1)
sys.stdout.write(text)
sys.stdout.flush()


def lookup_curated(q: str) -> list[tuple[str, int]]:
req = {
"query": q,
"trace_id": trace.trace_id,
"platform": sys.platform,
"shell": os.environ.get("SHELL"),
}
logging.debug("curated query with request: %s", req)
response = requests.post(
settings.endpoint + "/curated",
json=req,
headers={
"user-agent": f"promptops-cli; user_id={user.user_id()}",
},
)
if response.status_code != 200:
# this exception completely destroys the ui
return []
# raise Exception(f"there was problem with the response, status: {response.status_code}, text: {response.text}")

data = response.json()
try:
return [(entry["content"]["content"], entry["score"] * 100) for entry in data["items"] if entry["score"] >= 0.75]
except KeyError:
logging.debug("no suggestions in response: %s", json.dumps(data, indent=2))

try:
message = data.get("message")
except KeyError:
message = "-"
logging.debug("no message in response: %s", json.dumps(data, indent=2))

return [(message, 0)]
Loading