Как создать локальное средство генерации кода с open source моделями и библиотекой Guidance от Microsoft

Когда-то, использовав ныне устаревший Codex API от OpenAI, я написал плагин VSCode. Им неплохо выполнялись простые инструкции в выбранном коде в VSCode, например добавление к выделенной функции строки документации.

Я плотно занимаюсь локальными большими языковыми моделями, поэтому естественным продолжением было создание чего-то подобного независимо от OpenAI.

С этой целью я запустил проект Oasis. Идея очень простая: генерировать строки документации для кода, напрямую вызывая API text-generation-webui.

Вскоре получил результаты, аналогичные достигнутым старым плагином  —  теперь с WizardLM вместо OpenAI. Но лицензия здесь ограничена: модель предназначена только для исследований.

Мне же она нужна для коммерческих целей или повседневной работы, и без проблем с лицензией.

Когда-то я написал о небольшом экспериментальнном проекте для генерации кода, созданном с применением модели Salesforce Codegen на процессоре. Тогда я не подключил ее к VSCode главным образом потому, что это модель автозаполнения, и она не для следования инструкциям.

Сейчас же, когда больше пишу подсказки и познакомился с библиотекой Guidance от Microsoft, я решил создать жизнеспособный продукт, объединив:

  1. Salesforce Codegen для генерации кода.
  2. Библиотеку Guidance для генерирования моделью того, что действительно нужно, а не случайного ввода.

За несколько дней я сделал из них новую версию плагина и вполне доволен первыми результатами. Надо признать: решение получилось намного сложнее ожидаемого, но вполне рабочее. А главное  —  доступна даже версия модели с 350 млн параметров, которой при этом корректно генерируются строки документации.

Конечно, она не всегда применима, и модели покрупнее обычно все-таки намного лучше в работе, зато у нее отмечается огромный рост производительности вывода и доступности  —  версия модели с 350 млн параметров развертывается практически у всех.

Текущая версия этого плагина доступна на GitHub.

Рассмотрим, как она создавалась:

  1. Написание расширения VSCode.
  2. Настройка HTTP-сервера с помощью библиотеки Guidance.
  3. Настройка сервера подсказок промежуточного ПО для синтаксического анализа кода и динамического выбора подсказок.
  4. Подробно о том, как сделать эффективной работу команды add_docstring с менее мощными моделями, написать подсказку-руководство и убедиться, что во вводе/выводе нет неожиданностей. Этот пункт подразделяется дальше на:

a) синтаксический анализ входных данных;

б) подсказку-руководство Add Docstring по добавлению строки документации;

в) создание выходных данных.

Примечание: формат строки документации довольно прост, никаких препятствий для доработки сгенерированного формата нет.

Вот высокоуровневая схема того, что мы создаем:

Основной поток

Написание расширения VSCode. Основы

Сначала берем из официальной документации hello world образец расширения extension.ts, добавляем основную логику: определяем, как инициализировать расширение; после инициализации регистрируем имеющиеся команды. Вот код:

export function activate(context: vscode.ExtensionContext) {
const commands = [
["addDocstring", "add_docstring"],
["addTypeHints", "add_type_hints"],
["addUnitTest", "add_unit_test"],
["fixSyntaxError", "fix_syntax_error"],
["customPrompt", "custom_prompt"],
];
commands.forEach(tuple_ => {
const [commandName, oasisCommand] = tuple_;
const command = vscode.commands.registerCommand(`oasis.${commandName}`, () => {
useoasis(oasisCommand);
});
context.subscriptions.push(command);
});
}

Здесь каждая ожидаемая команда привязывается к вызову функции основной логики useoasis. Перейдем к этой функции позже.

Эти команды должны соответствовать тому, что определено в package.json в проекте. Определим блоки для объявления команд из расширения, начнем с contributes.commands:

[
{
"command": "oasis.addDocstring",
"title": "Add Docstring to Selection"
},
{
"command": "oasis.addTypeHints",
"title": "Add type hints to selection"
},
{
"command": "oasis.fixSyntaxError",
"title": "Fix syntax error"
},
{
"command": "oasis.customPrompt",
"title": "Custom Prompt"
}
]

Чтобы пользователь выбирал блок кода мышью, находил эту команду правой кнопкой, применяем конфигурацию contributes.menus.editor/context:

"editor/context": [
{
"command": "oasis.addDocstring",
"when": "editorHasSelection",
"group": "7_modification"
},
{
"command": "oasis.addTypeHints",
"when": "editorHasSelection",
"group": "7_modification"
},
{
"command": "oasis.fixSyntaxError",
"when": "editorHasSelection",
"group": "7_modification"
},
{
"command": "oasis.customPrompt",
"when": "editorHasSelection",
"group": "7_modification"
}
]

Для работы с командами привязки этого достаточно. Вернемся к основной функции расширения:

async function useoasis(command: string) {
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
return;
}
console.log("Reading config")
const oasisUrl = vscode.workspace.getConfiguration('oasis').get("prompt_server_url")
console.log("Oasis URL:", oasisUrl);
const document = activeEditor.document;
const selection = activeEditor.selection;

const text = document.getText(selection);

const requestBody = JSON.stringify({
data: text
});
const url = `${oasisUrl}/${command}`;

console.log("Calling API", url, "with body: ", requestBody);

let response: oasisResponse | undefined = undefined;
try {
response = await got(url, {
method: "POST",
headers: {
// eslint-disable-next-line @typescript-eslint/naming-convention
"Content-Type": "application/json",
// eslint-disable-next-line @typescript-eslint/naming-convention
},
body: requestBody,
timeout: {
request: 300000 // макс. пять минут
}
}).json();
} catch (e: any) {
vscode.window.showErrorMessage("Oasis Plugin: error calling the API")
try {
const apiStatusCode = `Error calling API: ${e.response.statusCode}`;
vscode.window.showErrorMessage(apiStatusCode);
} catch (error) {
console.error("Error parsing error response code", error);
}
}

if (response) {
console.log("From got", response.text);
const editedText = response.text;

activeEditor.edit(editBuilder => {
console.log("Edit builder", editBuilder);
editBuilder.replace(selection, editedText);
});
}
};

Чуть длиннее, но тоже просто. Сначала, чтобы избежать ссылок на строки со значением null, проверяем наличие активного редактора. Вот простой, слегка переформатированный прием:

const oasisUrl = vscode.workspace
.getConfiguration('oasis')
.get("prompt_server_url")

Здесь для определения, где обслуживается сервер запросов, считывается конфигурация рабочей области. Поэтому добавляем в конфигурацию расширения package.json следующее:

"contributes": {
"configuration": {
"title": "Oasis",
"properties": {
"oasis.prompt_server_url": {
"type": "string",
"default": "http://0.0.0.0:9000",
"description": "The URL where the extension can find the prompt server. Defaults to: 'http://0.0.0.0:9000'"
}
}
},

Этим не только определяется значение по умолчанию, но и предоставляется конфигурация для переопределения его пользователем через пользовательский интерфейс или редактор JSON. Вот пример скриншота пользовательского интерфейса:

Вернемся к коду расширения:

const text = document.getText(selection);

const requestBody = JSON.stringify({
data: text
});
const url = `${oasisUrl}/${command}`;

console.log("Calling API", url, "with body: ", requestBody);

Довольно просто: считываем выделенный текст в редакторе, затем готовим запрос. command  —  это параметр встроенного в функцию activate замыкания.

Используя библиотеку got, делаем запрос:

let response: oasisResponse | undefined = undefined;
try {
response = await got(url, {
method: "POST",
headers: {
// eslint-disable-next-line @typescript-eslint/naming-convention
"Content-Type": "application/json",
// eslint-disable-next-line @typescript-eslint/naming-convention
},
body: requestBody,
timeout: {
request: 300000 // макс. пять минут
}
}).json();
} catch (e: any) {
vscode.window.showErrorMessage("Oasis Plugin: error calling the API")
try {
const apiStatusCode = `Error calling API: ${e.response.statusCode}`;
vscode.window.showErrorMessage(apiStatusCode);
} catch (error) {
console.error("Error parsing error response code", error);
}
}

Если запрос не выполняется, отображаем в пользовательском интерфейсе ошибку:

Не очень информативно, но пользователь хотя бы знает: больше ждать не стоит. Затем, если все хорошо, выполняем синтаксический анализ и с помощью активного редактора заменяем выделенное на ответ из API:

if (response) {
console.log("From got", response.text);
const editedText = response.text;

activeEditor.edit(editBuilder => {
console.log("Edit builder", editBuilder);
editBuilder.replace(selection, editedText);
});
}

Библиотека Guidance для настройки HTTP-сервера

Guidance  —  очень интересная библиотека от Microsoft для работы с большими языковыми моделями. Ознакомьтесь с ее Readme.

Главное преимущество Guidance  —  возможность выполнения конкретных задач с моделями, гораздо менее эффективными в следовании инструкциям.

Недостаток  —  значительное увеличение затрат на разработку, ведь в модели на стандартном языке программирования применяется много логических этапов.

При разработке с Guidance перезагрузка сервера затруднительна, особенно если модель тяжелая. Поэтому загрузку модели и библиотеку перенесем на сервер, который не нужно часто менять.

Исходный код этого сервера прост, умещается в одном файле:

class Request(BaseModel):
input_vars: Dict[str, str]
output_vars: List[str]
guidance_kwargs: Dict[str, str]
prompt_template: str


app = FastAPI()

print("Loading model, this may take a while...")
# model = "TheBloke/wizardLM-7B-HF"
model = "Salesforce/codegen-350m-mono"
# model = "Salesforce/codegen-2b-mono"
# model = "Salesforce/codegen-6b-mono"
# model = "Salesforce/codegen-16B-mono"

llama = guidance.llms.Transformers(model, quantization_config=nf4_config, revision="main")
print("Server loaded!")


@app.post("/")
def call_llama(request: Request):
input_vars = request.input_vars
kwargs = request.guidance_kwargs
output_vars = request.output_vars

guidance_program: Program = guidance(request.prompt_template)
program_result = guidance_program(
**kwargs,
stream=False,
async_mode=False,
caching=False,
**input_vars,
llm=llama,
)
output = {}
for output_var in output_vars:
output[output_var] = program_result[output_var]
return output

Вот и все. Принимается шаблон подсказки с входными и ожидаемыми выходными переменными из HTTP-запроса и направляется через библиотеку Guidance, затем извлекаются ожидаемые выходные переменные и возвращаются в HTTP-ответе.

Что касается quantization_config, здесь применяется новейший 4-битный метод квантования Hugging Face.

Для задействования этого сервера также понадобится клиент:

import requests

guidance_url = "http://0.0.0.0:9090"
def call_guidance(prompt_template, output_vars, input_vars=None, guidance_kwargs=None):
if input_vars is None:
input_vars = {}
if guidance_kwargs is None:
guidance_kwargs = {}

data = {
"prompt_template": prompt_template,
"output_vars": output_vars,
"guidance_kwargs": guidance_kwargs,
"input_vars": input_vars,
}

response = requests.post(
guidance_url,
json=data
)

response.raise_for_status()

return response.json()

Здесь ничего интересного, сервер не взаимодействует напрямую с расширением VSCode.

Настройка сервера подсказок промежуточного ПО для синтаксического анализа кода и динамического выбора подсказок

Переходим к промежуточному ПО, которым получается запрос от VSCode, основному серверному коду:

from fastapi import FastAPI, HTTPException
import logging
import codegen_guidance_prompts
import wizard_lm_guidance_prompts

from pydantic import BaseModel
from guidance_client import call_guidance
from commands.commands import build_command_mapping


logger = logging.getLogger("uvicorn")
logger.setLevel(logging.DEBUG)

class Request(BaseModel):
data: str


app = FastAPI()


prompts_module = codegen_guidance_prompts
commands_mapping = build_command_mapping(prompts_module)


@app.post("/{command}/")
def read_root(command, request: Request):
logger.info("Received command: '%s'", command)
logger.debug("Received data: '%s'", request)
received_code = request.data
try:
command_to_apply = commands_mapping[command]
logger.info("Loaded command: '%s'", command_to_apply)

except KeyError:
raise HTTPException(status_code=404, detail=f"Command not supported: {command}")

prompt_key, prompt_to_apply, extracted_input = command_to_apply.prompt_picker(received_code)
logger.info("Extracted input: %s", extracted_input)

keys_difference = set(prompt_to_apply.input_vars) - set(extracted_input.keys())

if keys_difference:
error_msg = f"Missing input keys for the prompt: {keys_difference}"
logger.error(error_msg)
raise HTTPException(status_code=500, detail=error_msg)


logger.info("Loaded command: '%s'", command_to_apply.__class__.__name__)
logger.info("Parsed input :")
for key, item in extracted_input.items():
logger.info("(%s): '%s'", key, item)
logger.info("Calling LLM...")

result = call_guidance(
prompt_template=prompt_to_apply.prompt_template,
input_vars=extracted_input,
output_vars=prompt_to_apply.output_vars,
guidance_kwargs={}
)
logger.info("LLM output: '%s'", result)

result = command_to_apply.output_extractor(prompt_key, extracted_input, result)

logger.info("parsed output: '%s'", result)

return {"text": result}

Несмотря на внешнюю простоту, здесь скрывается много неприятных деталей. Но общая идея контроллера действительно проста:

  1. Принимается пара входных параметров: command, входной код.
  2. Находится подходящая подсказка.
  3. Выполняется синтаксический анализ входных данных в конкретный формат.
  4. Запускается через сервер Guidance.
  5. Выполняется синтаксический анализ выходных данных в правильный формат.
  6. Отправляется ответ.

Подробнее о команде «add_docstring»

Синтаксический анализ входных данных

С простыми частями разобрались, теперь посложнее: расширим каждый этап.

Сначала выясняем, применение какой команды запрашивается:

logger.info("Received command: '%s'", command)
logger.debug("Received data: '%s'", request)
received_code = request.data
try:
command_to_apply = commands_mapping[command]
logger.info("Loaded command: '%s'", command_to_apply)

Это словарь, созданный с помощью такого вызова:

commands_mapping = build_command_mapping(prompts_module)

Здесь мы определяем доступные команды. Доступна пока только одна:

def build_command_mapping(prompt_module: PromptModuleInterface):
add_docstring_command = DocStringCommand(
prompt={
"generic_prompt": prompt_module.doc_string_guidance_prompt,
"function_prompt": prompt_module.function_doc_string_guidance_prompt
}
)
commands_mapping = {
"add_docstring": add_docstring_command,
# "add_type_hints": ADD_TYPE_HINTS,
# "fix_syntax_error": FIX_SYNTAX_ERROR,
# "custom_prompt": CUSTOM_PROMPT
}

return commands_mapping

Классом DocStringCommand реализуется абстрактный класс Command:

@dataclass
class Command:
prompt: Dict[str, GuidancePrompt]

@abc.abstractclassmethod
def prompt_picker(self, input_: str) -> Tuple[str, GuidancePrompt, Dict[str, str]]:
raise NotImplementedError()

@abc.abstractclassmethod
def output_extractor(self, prompt_key: str, extracted_input: Dict[str, str], result: Dict[str, str]) -> str:
raise NotImplementedError()


class DocStringCommand(Command):
def __init__(self, *args, **kwargs):
super().__init__(*args,**kwargs)

В этом классе содержится словарь подсказок-руководств и реализовывается две функции: для выбора корректной подсказки и извлечения выходных данных. GuidancePrompt похож на сервер Guidance:

@dataclass
class GuidancePrompt:
prompt_template: str
input_vars: Dict[str, str]
output_vars: Dict[str, str]
guidance_kwargs: Dict[str, str]

Вот как выбирается подсказка:

def prompt_picker(self, input_: str) -> Tuple[str, GuidancePrompt, Dict[str, str]]:
prompt_key = "None"
try:
function_header, function_body, leading_indentation, indentation_type = function_parser(input_)
prompt_key = "function_prompt"
return_value = prompt_key, self.prompt[prompt_key], {
"function_header": function_header,
"function_body": function_body,
"leading_indentation": leading_indentation,
"indentation_type": indentation_type
}

except (FailedToParseFunctionException):
logger.warn("Failed to identify specific type of code block, falling back to generic prompt")
prompt_key = "generic_prompt"
return_value = prompt_key, self.prompt[prompt_key], {"input": input_}

logger.info("Chosen prompt: %s", prompt_key)
return return_value

Здесь имеется две подсказки: generic_prompt и function_prompt. Если идентифицировать функцию, применяется вторая, специализированная, в противном случае  —  обобщенная.

Возвращаемое значение  —  кортеж в формате (prompt_key, prompt, input_dict). Применим эти значения позже при вызове с этой подсказкой большой языковой модели и синтаксическом анализе ее выходных данных.

Возвращаясь чуть назад, при идентификации функции вызываем function_parser. Обследуем эту кроличью нору:

def function_parser(input_code_str: str) -> Tuple[str, str, str]:
leading_indentation = _get_leading_indentation(input_code_str)
simple_indented_code = _remove_extra_indentation(input_code_str, leading_indentation)
indentation_type = _get_indentation_type(input_code_str)

parsed = ast.parse(simple_indented_code, filename="<string>")
parsed
try:
first_node = parsed.body[0]
except IndexError:
raise FailedToParseFunctionException from IndexError

if not isinstance(first_node, ast.FunctionDef):
raise FailedToParseFunctionException(f"Parsed type is not a function: '{type(first_node)}'")

function_body = _extract_function_body(first_node, leading_indentation, indentation_type)
function_header = _extract_function_header(first_node)
return function_header, function_body, leading_indentation, indentation_type

Основная идея здесь  —  используя документацию ast.parse, найти на абстрактном синтаксическом дереве тело/заголовок функции. Но зачем эти странности с отступом?

Если передать в ast.parse следующую строку:

"""  
def hello():
print('hello!')
"""

Возникнет IndentationError. Похоже, здесь предполагается, что этим синтаксическим анализатором всегда считывается файл. То есть эта строка интерпретируется как исходный код на уровне модуля, поэтому ее отступ неправильный.

Найденные мною решения  —  в полном исходном коде.

Вернемся чуть назад и, получив от синтаксического анализатора заголовок и тело функции, перейдем к приготовленной подсказке-руководству.

Подсказка-руководство по добавлению строки документации

Сначала продемонстрируем, как выполняется это генерирование, приняв во входных данных одну из функций Oasis:

def _extract_function_header(fun_code: ast.FunctionDef) -> str:
full = ast.unparse(fun_code)
body = ast.unparse(fun_code.body)
return full.split(body[0:USED_BODY_CHARS_TO_SPLIT])[0].strip()

Вот строка документации, сгенерированная Oasis:

def _extract_function_header(fun_code: ast.FunctionDef) -> str:
"""This function extracts the function header from the given function definition

Parameters:
fun_code (ast.FunctionDef): The function definition to extract the function header from.

Returns:
str: The function header.

"""

full = ast.unparse(fun_code)
body = ast.unparse(fun_code.body)
return full.split(body[0:USED_BODY_CHARS_TO_SPLIT])[0].strip()

Как же она генерируется? Подсказка длинновата, разберем ее по блокам. Возьмем примеры «подсказок с несколькими выстрелами»:

function_doc_string_guidance_prompt = GuidancePrompt(
prompt_template="""
def print_hello_world():
print("hello_world")

def print_hello_world():
# Строка документации ниже
\"\"\"This functions print the string 'hello_world' to the standard output.\"\"\"

def sum_2(x, y):
return x + y

def sum_2(x, y):
# Строка документации ниже
\"\"\"This functions receives two parameters and returns the sum.

Parameters:
int: x - first number to sum
int: y - second number to sum

Returns:
int: sum of the two given integers
\"\"\"
return x + y

Отличие от WizardLM: здесь инструкция не подается в модель явно, просто предоставляются шаблоны для автозаполнения. Несколькими примерами определяем, как выполнять генерацию:

"""
(few shot examples above...)

{{leading_indentation}}{{function_header}}
{{function_body}}

{{leading_indentation}}{{function_header}}
# Строка документации ниже
{{leading_indentation}}\"\"\"{{gen 'description' temperature=0.1 max_tokens=128 stop='.'}}

Parameters: {{gen 'parameters' temperature=0.1 max_tokens=128 stop='Returns:'}}
Returns: {{gen 'returns' temperature=0.1 max_tokens=128 stop='\"\"\"'}}
""",
guidance_kwargs={},
input_vars=["function_header", "function_body", "leading_indentation"],
output_vars=["description", "parameters", "returns"],
)

В самом конце определяем ожидаемые входные и выходные переменные. Это, строго говоря, не часть шаблона  —  она полезна при подключении к этой подсказке из высокоуровневого вызова в кодовой базе.

В конце подсказки, в первом блоке добавляется входной исходный код, как в примерах «подсказок с несколькими выстрелами»:

"""
{{leading_indentation}}{{function_header}}
{{function_body}}
"""

Синтаксис, как у шаблонизатора Jinja2. Это переменные, заменяемые перед отправкой входных данных в большую языковую модель.

Чтобы запустить ожидаемое от модели автозаполнение, вставляем сюда только заголовок функции и комментарий # Строка документации ниже, как в примерах «подсказок с несколькими выстрелами»:

{{leading_indentation}}{{function_header}}
# Строка документации ниже

Начинаем первую генерацию с этой команды, отступ пока игнорируем:

\"\"\"{{gen 'description' temperature=0.1 max_tokens=128 stop='.'}}

Здесь запускается строка документации, генерация выполняется моделью до символа «.». Вот пример генерации без тройных кавычек во выходных данных:

This function extracts the function header from the given function definition

Вот следующая сгенерированная переменная:

Parameters: {{gen 'parameters' temperature=0.1 max_tokens=128 stop='Returns:'}}

которая разворачивается в:

fun_code (ast.FunctionDef): The function definition to extract the function header from.

и, наконец:

{{gen 'returns' temperature=0.1 max_tokens=128 stop='\"\"\"'}}

превращается в:

str: The function header.

Но это лишь разрозненные фрагменты, реальной строки документации пока нет. Создадим ее из них.

Создание выходных данных

Переходим в конец серверного контроллера промежуточного ПО:

result = call_guidance(
prompt_template=prompt_to_apply.prompt_template,
input_vars=extracted_input,
output_vars=prompt_to_apply.output_vars,
guidance_kwargs={}
)
logger.info("LLM output: '%s'", result)

result = command_to_apply.output_extractor(prompt_key, extracted_input, result)

Чтобы сгенерировать словарь с предыдущими генерациями, используем подсказку выше с клиентом guidance. Формат такой:

{
"description": "Func description",
"parameters": "a (int): a number to use in the sum",
"returns": "(int): returns the sum"
}

Передаем его в средство извлечения выходных данных DocStringCommand. Это длинная функция, рассмотрим только ее части.

Вначале определяем, как анализировать выходные данные в зависимости от применяемой подсказки:

def output_extractor(self, prompt_key, extracted_input, result: Dict[str, str]) -> str:
if prompt_key == "generic_prompt":
return result["output"]
elif prompt_key == "function_prompt":
ind = extracted_input["leading_indentation"]

Затем при выполнении синтаксического анализа выходных данных function_prompt имеется логика преобразования сгенерированных параметров в часть строки документации:

# (скучное решение проблемы отступа выше...)
try:
parameters = result["parameters"].strip().split("\n")
except (KeyError, ValueError):
parameters = []

# (то же для части с «returns»...)
parameters_string = ""
if parameters:
parameters_string = body_indentation + "Parameters: \n"
for param in parameters:
parameters_string += f"{body_indentation}{indentation_type}{param.lstrip().strip()}\n"

parameters_string += "\n"

Дальше используем заголовок функции, добавляем созданные ранее описание и строку параметров в той же функции:

(...)
code_with_docstring = (header_indentation + extracted_input["function_header"] + "\n")
code_with_docstring += (body_indentation + '"""')
code_with_docstring += (result["description"].lstrip().strip() + "\n\n")

logger.info("Header with description: %s", code_with_docstring)
if parameters_string:
code_with_docstring += (parameters_string)
if returns_string:
code_with_docstring += (returns_string)


code_with_docstring += (body_indentation + '"""\n\n')

code_with_docstring += extracted_input["function_body"]
logger.info("Generated code with docstring: %s", code_with_docstring)
return code_with_docstring

Так мы «вручную» воссоздаем строку документации с генерациями из модели. Это не единственный способ: прямо из руководства можно взять строку целиком, избежав части всех этих телодвижений, хотя идентифицировать начало строки документации и чуть поменять подсказку все же придется.

Заключение

Далековато мы забрались в программную инженерию, использовав библиотеку Guidance: чтобы создавать с ней решения, простой подсказки для большой языковой модели мало, нужно много дополнительных усилий.

Однако в некоторых случаях они окупаются: когда требуется точность, повышение производительности от применения подсказок будет совсем не лишним.

Настолько сложным оказалось применение ast.parse, что возникли две проблемы:

  1. Из-за IndentationError пришлось написать еще много кода.
  2. Удаление дублей новых строк из проанализированного кода.

Вторая проблема сейчас  —  это источник багов в Oasis. Возможно, придется полностью удалить ast.parse и написать собственный синтаксический анализатор.

В целом, это хорошая база для написания направляемых подсказок, полезных при генерации кода.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Paolo Rechia: Building Oasis: a Local Code Generator Tool Using Open Source Models and Microsoft’s Guidance Library

Предыдущая статья5 востребованных методов программирования на Bash
Следующая статьяКак развернуть 2-уровневую архитектуру с AWS и Terraform Cloud