Skip to content

Instantly share code, notes, and snippets.

@jenya239
Created February 13, 2025 12:25
Show Gist options
  • Save jenya239/e2817c5c392947aa3cb924bca1bd345f to your computer and use it in GitHub Desktop.
Save jenya239/e2817c5c392947aa3cb924bca1bd345f to your computer and use it in GitHub Desktop.
скрипт генерации краткого описания проекта
#!/usr/bin/env ruby
require 'net/http'
require 'json'
require 'uri'
require 'digest'
require 'fileutils'
# Константы
OPENAI_API_KEY = ENV['OPENAI_API_KEY']
OPENAI_API_URL = "https://api.openai.com/v1/chat/completions"
MODEL = "gpt-3.5-turbo"
MAX_FILE_SIZE = 20 * 1024 # 20 КБ
ALLOWED_EXTENSIONS = %w[cpp hpp txt rb js py java cs go rs] # расширения файлов
# Пути к служебной директории и файлам базы данных
SERVICE_DIR = ".service_data"
DB_FILE = File.join(SERVICE_DIR, "db.json")
DIR_SUMMARY_FILE = File.join(SERVICE_DIR, "dir_summaries.json")
FINAL_SUMMARY_FILE = "project_summary.txt"
FINAL_SUMMARY_DATA_FILE = File.join(SERVICE_DIR, "final_summary.json")
# Rate limiting: 3 запроса в минуту -> минимум 20 секунд между запросами
RATE_LIMIT_DELAY = 20.0
$last_api_call_time ||= Time.now - RATE_LIMIT_DELAY
# Функции для работы с базой данных файлов
def load_db
if File.exist?(DB_FILE)
JSON.parse(File.read(DB_FILE))
else
{}
end
end
def save_db(db)
File.write(DB_FILE, JSON.pretty_generate(db))
end
# Функции для работы с описаниями директорий
def load_dir_summaries
if File.exist?(DIR_SUMMARY_FILE)
JSON.parse(File.read(DIR_SUMMARY_FILE))
else
{}
end
end
def save_dir_summaries(summaries)
File.write(DIR_SUMMARY_FILE, JSON.pretty_generate(summaries))
end
# Функции для работы с итоговым описанием проекта
def load_final_summary
if File.exist?(FINAL_SUMMARY_DATA_FILE)
JSON.parse(File.read(FINAL_SUMMARY_DATA_FILE))
else
nil
end
end
def save_final_summary(data)
File.write(FINAL_SUMMARY_DATA_FILE, JSON.pretty_generate(data))
end
# Вычисление SHA256-хэша файла
def compute_file_hash(file_path)
Digest::SHA256.file(file_path).hexdigest
end
# Отправка запроса к API OpenAI через Net::HTTP с учетом rate limiting
def send_openai_request(prompt)
# Rate limiting: не более 3 запросов в минуту
elapsed = Time.now - $last_api_call_time
if elapsed < RATE_LIMIT_DELAY
sleep_time = RATE_LIMIT_DELAY - elapsed
puts "Ожидание #{sleep_time.round(2)} секунд из-за ограничения запросов..."
sleep(sleep_time)
end
$last_api_call_time = Time.now
uri = URI(OPENAI_API_URL)
header = {
"Content-Type" => "application/json",
"Authorization" => "Bearer #{OPENAI_API_KEY}"
}
body = {
"model" => MODEL,
"messages" => [
{"role" => "system", "content" => "Ты — ассистент, который генерирует краткие технические описания кода."},
{"role" => "user", "content" => prompt}
],
"temperature" => 0.2
}
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.request_uri, header)
request.body = body.to_json
response = http.request(request)
if response.code.to_i == 200
result = JSON.parse(response.body)
result["choices"][0]["message"]["content"]
else
puts "Ошибка запроса: #{response.code} #{response.body}"
nil
end
end
# Генерация описания для файла с помощью промпта
def generate_file_description(file_path, file_content)
prompt = <<~PROMPT
Дай краткое техническое описание файла с исходным кодом. Выдели основные имена, классы, функции и опиши базовую логику работы. Сделай описание существенно короче, чем исходный файл.
Файл: #{file_path}
--------------------------------------------------
#{file_content}
PROMPT
send_openai_request(prompt)
end
# Генерация описания для директории на основе описаний файлов
def generate_directory_description(dir_path, file_descriptions)
combined_text = file_descriptions.join("\n")
prompt = <<~PROMPT
На основе следующих описаний файлов, дай краткое описание директории "#{dir_path}", выделив основные имена и ключевые элементы кода. Сделай описание кратким:
#{combined_text}
PROMPT
send_openai_request(prompt)
end
# Генерация итогового описания проекта на основе описаний директорий
def generate_project_summary(dir_summaries)
# Используем только поле "summary" из сохранённых описаний директорий
combined_text = dir_summaries.map { |_, data| data["summary"] }.join("\n")
prompt = <<~PROMPT
На основе следующих описаний директорий, дай краткое техническое описание проекта, выделив ключевые компоненты и базовую логику. Сделай итоговое описание кратким:
#{combined_text}
PROMPT
send_openai_request(prompt)
end
# --- Основная логика скрипта ---
# Создаём служебную директорию, если её нет
FileUtils.mkdir_p(SERVICE_DIR)
# Загружаем базу данных для файлов и директорий
db = load_db
dir_summaries = load_dir_summaries
# Хеш для группировки описаний файлов по директориям
directory_files = {}
# Обходим все файлы в директории src (рекурсивно)
Dir.glob("src/**/*") do |file|
next unless File.file?(file)
ext = File.extname(file).downcase.delete_prefix('.')
next unless ALLOWED_EXTENSIONS.include?(ext)
next if File.size(file) > MAX_FILE_SIZE
mod_time = File.mtime(file).to_s
file_hash = compute_file_hash(file)
db_entry = db[file]
needs_update = db_entry.nil? || (db_entry["modified_time"] != mod_time) || (db_entry["hash"] != file_hash)
if needs_update
file_content = File.read(file)
puts "Генерирую описание для файла: #{file}"
description = generate_file_description(file, file_content)
if description
db[file] = {
"modified_time" => mod_time,
"hash" => file_hash,
"description" => description
}
else
puts "Не удалось получить описание для файла #{file}"
next
end
else
description = db_entry["description"]
end
# Группируем описания по директориям
dir = File.dirname(file)
directory_files[dir] ||= []
directory_files[dir] << description
end
# Сохраняем обновлённую базу данных файлов
save_db(db)
# Обрабатываем каждую директорию: генерируем описание на основе описаний файлов
directory_files.each do |dir, descriptions|
combined_text = descriptions.join("\n")
current_hash = Digest::SHA256.hexdigest(combined_text)
if dir_summaries.key?(dir) && dir_summaries[dir]["hash"] == current_hash
puts "Директория #{dir} не изменилась, используем ранее сохранённое описание."
else
puts "Генерирую описание для директории: #{dir}"
dir_summary_text = generate_directory_description(dir, descriptions)
if dir_summary_text
dir_summaries[dir] = {"hash" => current_hash, "summary" => dir_summary_text}
else
puts "Не удалось получить описание для директории #{dir}"
end
end
end
# Сохраняем описания директорий
save_dir_summaries(dir_summaries)
# Генерируем итоговое описание проекта на основе описаний директорий
puts "Генерирую итоговое описание проекта..."
# Для итогового описания используем объединённый текст описаний директорий в отсортированном порядке
final_input_text = dir_summaries.sort_by { |dir, _| dir }.map { |_, data| data["summary"] }.join("\n")
final_input_hash = Digest::SHA256.hexdigest(final_input_text)
final_summary_data = load_final_summary
if final_summary_data && final_summary_data["hash"] == final_input_hash
puts "Итоговое описание проекта не изменилось, используем ранее сохранённое."
project_summary = final_summary_data["summary"]
else
project_summary = generate_project_summary(dir_summaries)
if project_summary
final_summary_data = {"hash" => final_input_hash, "summary" => project_summary}
save_final_summary(final_summary_data)
else
puts "Не удалось сгенерировать итоговое описание проекта."
end
end
if project_summary
File.write(FINAL_SUMMARY_FILE, project_summary)
puts "Итоговое описание проекта сохранено в файл #{FINAL_SUMMARY_FILE}"
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment