Skip to content

Commit 46251f9

Browse files
committed
Add initial 'ocd' command set
1 parent eb00c12 commit 46251f9

18 files changed

+1027
-1
lines changed

ocd/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# vim: set fileencoding=utf-8 ts=4 sw=4 tw=0 et :

ocd/__main__.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
OCD: The OpenAI CLI by davedittrich.
5+
6+
This entry point script uses the ``cliff`` library to provide a
7+
feature-rich CLI for accessing the OpenAI API. It has some overlap
8+
with the OpenAI CLI ``openai``, but has additional capabilities
9+
that don't require post-processing of output from API calls
10+
using other programs, though also not precluding their use if
11+
that is desired.
12+
13+
"""
14+
15+
# Standard imports
16+
import os
17+
import sys
18+
import textwrap
19+
20+
# External imports
21+
from cliff.app import App
22+
from cliff.commandmanager import CommandManager
23+
from psec.secrets_environment import SecretsEnvironment
24+
from psec.utils import (
25+
bell,
26+
get_default_environment,
27+
show_current_value,
28+
Timer,
29+
)
30+
31+
# Internal imports
32+
import openai
33+
from openai.version import VERSION
34+
35+
DEFAULT_ENVIRONMENT = get_default_environment()
36+
37+
38+
class OCDApp(App):
39+
"""Main CLI application class."""
40+
41+
def __init__(self):
42+
super().__init__(
43+
description=__doc__.strip(),
44+
version=VERSION,
45+
command_manager=CommandManager(
46+
namespace='ocd'
47+
),
48+
deferred_help=True,
49+
)
50+
self.timer = Timer()
51+
self.secrets = SecretsEnvironment(
52+
environment=DEFAULT_ENVIRONMENT,
53+
export_env_vars=True,
54+
)
55+
self.secrets.read_secrets_and_descriptions()
56+
openai.organization = os.environ.get(
57+
"OPENAI_ORGANIZATION_ID",
58+
self.secrets.get_secret("openai_organization_id"),
59+
)
60+
openai.api_key = os.environ.get(
61+
"OPENAI_API_KEY",
62+
self.secrets.get_secret("openai_api_key"),
63+
)
64+
self.openai_docs_base = 'https://beta.openai.com/docs'
65+
66+
def build_option_parser(self, description, version):
67+
parser = super().build_option_parser(
68+
description,
69+
version
70+
)
71+
# TODO(dittrich): Migrate these formatter hacks somewhere else.
72+
#
73+
# OCD hack: Make ``help`` output report main program name,
74+
# even if run as ``python -m psec.main`` or such.
75+
if parser.prog.endswith('.py'):
76+
parser.prog = self.command_manager.namespace
77+
# Replace the cliff SmartHelpFormatter class before first use
78+
# by subcommand `--help`.
79+
# pylint: disable=wrong-import-order
80+
from psec.utils import CustomFormatter
81+
from cliff import _argparse
82+
_argparse.SmartHelpFormatter = CustomFormatter
83+
# pylint: enable=wrong-import-order
84+
# We also need to change app parser, which is separate.
85+
parser.formatter_class = CustomFormatter
86+
# Global options
87+
parser.add_argument(
88+
'--api-base',
89+
default=openai.api_base,
90+
help='API base URL'
91+
)
92+
parser.add_argument(
93+
'--browser',
94+
action='store',
95+
default=None,
96+
help='Browser to use'
97+
)
98+
parser.add_argument(
99+
'--force-browser',
100+
action='store_true',
101+
default=False,
102+
help='Open the browser even if process has no TTY'
103+
)
104+
parser.add_argument(
105+
'--elapsed',
106+
action='store_true',
107+
dest='elapsed',
108+
default=False,
109+
help='Print elapsed time on exit'
110+
)
111+
parser.add_argument(
112+
'-e', '--environment',
113+
metavar='<environment>',
114+
dest='environment',
115+
default=DEFAULT_ENVIRONMENT,
116+
help='Deployment environment selector (Env: D2_ENVIRONMENT)'
117+
)
118+
parser.epilog = textwrap.dedent(f"""
119+
Current working dir: {os.getcwd()}
120+
Python interpreter: {sys.executable} (v{sys.version.split()[0]})
121+
122+
To control the browser that is used when opening web pages, set the
123+
BROWSER environment variable (e.g., ``BROWSER=lynx``). See:
124+
https://github.com/python/cpython/blob/3.8/Lib/webbrowser.py
125+
126+
Environment variables consumed:
127+
BROWSER Default browser for use by webbrowser.open().{show_current_value('BROWSER')}
128+
D2_ENVIRONMENT Default environment identifier.{show_current_value('D2_ENVIRONMENT')}
129+
D2_SECRETS_BASEDIR Default base directory for storing secrets.{show_current_value('D2_SECRETS_BASEDIR')}
130+
""") # noqa
131+
return parser
132+
133+
def initialize_app(self, argv):
134+
self.LOG.debug('[*] initialize_app(%s)', self.__class__.NAME)
135+
if sys.version_info <= (3, 7):
136+
raise RuntimeError(
137+
"[-] this program requires Python 3.7 or higher"
138+
)
139+
self.timer.start()
140+
141+
def prepare_to_run_command(self, cmd):
142+
self.LOG.debug("[*] prepare_to_run_command('%s')", cmd.cmd_name)
143+
#
144+
self.LOG.debug(
145+
"[+] using environment '%s'", self.options.environment
146+
)
147+
if self.options.api_base is not None:
148+
openai.api_base = self.options.api_base
149+
self.LOG.debug("[*] running command '%s'", cmd.cmd_name)
150+
151+
def clean_up(self, cmd, result, err):
152+
self.LOG.debug("[-] clean_up command '%s'", cmd.cmd_name)
153+
if err:
154+
self.LOG.debug("[-] got an error: %s", str(err))
155+
sys.exit(result)
156+
if (
157+
self.options.elapsed
158+
or (
159+
self.options.verbose_level > 1
160+
and cmd.cmd_name != "complete"
161+
)
162+
):
163+
self.timer.stop()
164+
elapsed = self.timer.elapsed()
165+
self.stderr.write('[+] elapsed time {}\n'.format(elapsed))
166+
bell()
167+
168+
169+
def main(argv=sys.argv[1:]):
170+
"""
171+
Command line interface entry point for the ``ocd`` program.
172+
"""
173+
myapp = OCDApp()
174+
return myapp.run(argv)
175+
176+
177+
if __name__ == '__main__':
178+
sys.exit(main(sys.argv[1:]))
179+
180+
# vim: set fileencoding=utf-8 ts=4 sw=4 tw=0 et :

ocd/completions/__init__.py

Whitespace-only changes.

ocd/completions/create.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
Create a completion using a model.
5+
"""
6+
7+
# Standard imports
8+
import argparse
9+
import logging
10+
import sys
11+
12+
# External imports
13+
from cliff.show import ShowOne
14+
15+
# Local imports
16+
import openai
17+
from ocd.utils import (
18+
DEFAULT_MAX_TOKENS,
19+
DEFAULT_MODEL_ID,
20+
DEFAULT_TEMPERATURE,
21+
get_text_from_completion,
22+
ranged_type,
23+
print_completion,
24+
)
25+
26+
class CompletionsCreate(ShowOne):
27+
"""
28+
Create a completion from a prompt using a model.
29+
30+
The prompt is the string used to generate the completion.
31+
Trailing whitespace is removed from the prompt as it impacts
32+
tokenization of the prompt string. While the OpenAI API allows for
33+
the prompt to be a list of strings or tokens in addition to just
34+
a simple string, passing those is not supported at this time.
35+
36+
You can optionally specify a suffix, which is a string that
37+
comes after a completion of inserted text.
38+
39+
For information on sampling temperature, see:
40+
"How to sample from language models", by Ben Mann, Medium, May 24, 2019,
41+
https://towardsdatascience.com/how-to-sample-from-language-models-682bceb97277
42+
43+
"""
44+
45+
logger = logging.getLogger(__name__)
46+
47+
def get_parser(self, prog_name): # noqa
48+
parser = super().get_parser(prog_name)
49+
parser.add_argument(
50+
"-m", "--model",
51+
dest="model_id",
52+
default=DEFAULT_MODEL_ID,
53+
help="ID of the model to use",
54+
)
55+
parser.add_argument(
56+
"--echo",
57+
action="store_true",
58+
default=False,
59+
help="echo back the prompt in addition to the completion",
60+
)
61+
parser.add_argument(
62+
"--max-tokens",
63+
default=DEFAULT_MAX_TOKENS,
64+
help="maximum tokens",
65+
)
66+
parser.add_argument(
67+
"--temperature",
68+
default=DEFAULT_TEMPERATURE,
69+
type=ranged_type(float, 0.0, 1.0),
70+
help="sampling temperature to use",
71+
)
72+
parser.add_argument(
73+
"--prompt",
74+
default=None,
75+
required=True,
76+
help="prompt for completion",
77+
)
78+
parser.add_argument(
79+
"--suffix",
80+
default=None,
81+
help="suffix that comes after a completion of inserted text",
82+
)
83+
output_group = parser.add_mutually_exclusive_group()
84+
output_group.add_argument(
85+
"-a", "--all",
86+
action="store_true",
87+
default=False,
88+
help="return all results in the completion",
89+
)
90+
output_group.add_argument(
91+
"-u", "--usage",
92+
action="store_true",
93+
default=False,
94+
help="output usage information",
95+
)
96+
return parser
97+
98+
def take_action(self, parsed_args): # noqa
99+
prompt = parsed_args.prompt.strip()
100+
completion = openai.Completion.create(
101+
model=parsed_args.model_id,
102+
prompt=prompt,
103+
suffix=parsed_args.suffix,
104+
temperature=parsed_args.temperature,
105+
echo=parsed_args.echo,
106+
)
107+
if not completion:
108+
raise RuntimeError("[-] no completion produced")
109+
if parsed_args.echo:
110+
print_completion(get_text_from_completion(completion)) # noqa
111+
sys.exit(0)
112+
columns = ['prompt', 'completion']
113+
data = [
114+
prompt,
115+
(
116+
completion if parsed_args.all
117+
else get_text_from_completion(completion)
118+
)
119+
]
120+
if parsed_args.usage:
121+
for key, value in completion.usage.items(): # type: ignore
122+
columns.append(key)
123+
data.append(value)
124+
return columns, data
125+
126+
127+
# vim: set fileencoding=utf-8 ts=4 sw=4 tw=0 et :

ocd/edits/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)