Skip to content

Commit

Permalink
Merge pull request #38 from ios-osushi/try-use-gemini
Browse files Browse the repository at this point in the history
Use Gemini for generate articles (Beta)
  • Loading branch information
ry-itto authored Oct 6, 2024
2 parents 8356458 + d0e6fc8 commit 436a287
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 14 deletions.
9 changes: 9 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
{
"pins" : [
{
"identity" : "generative-ai-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google-gemini/generative-ai-swift.git",
"state" : {
"revision" : "44b8ce120425f9cf53ca756f3434ca2c2696f8bd",
"version" : "0.5.6"
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
Expand Down
20 changes: 19 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import PackageDescription

let package = Package(
name: "ReleaseSubscriptions",
platforms: [.macOS(.v10_15)],
platforms: [.macOS(.v11)],
products: [
.executable(name: "releaseSubscriptions", targets: ["ReleaseSubscriptions"]),
.executable(name: "releaseSummarizer", targets: ["ReleaseSummarizer"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", exact: "1.5.0"),
.package(url: "https://github.com/apple/swift-log", exact: "1.6.1"),
.package(url: "https://github.com/swiftlang/swift-markdown", exact: "0.5.0"),
.package(url: "https://github.com/behrang/YamlSwift", exact: "3.4.4"),
.package(url: "https://github.com/google-gemini/generative-ai-swift.git", exact: "0.5.6"),
],
targets: [
.executableTarget(
Expand All @@ -29,6 +31,22 @@ let package = Package(
.product(name: "Markdown", package: "swift-markdown"),
.product(name: "Yaml", package: "YamlSwift"),
]),
.executableTarget(
name: "ReleaseSummarizer",
dependencies: [
"ReleaseSummarizerCore",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Logging", package: "swift-log"),
]
),
.target(
name: "ReleaseSummarizerCore",
dependencies: [
"ReleaseSubscriptionsCore",
.product(name: "GoogleGenerativeAI", package: "generative-ai-swift"),
.product(name: "Logging", package: "swift-log"),
]
),
.testTarget(
name: "ReleaseSubscriptionsCoreTests",
dependencies: ["ReleaseSubscriptionsCore"]),
Expand Down
12 changes: 6 additions & 6 deletions Sources/ReleaseSubscriptionsCore/Entities/JSONNullable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ import Foundation

/// [iOSDC Japan 2021 | CodableでJSONのNullを出力するためのTips by hirotakan](https://fortee.jp/iosdc-japan-2021/proposal/a79f93a5-1f1b-4017-86ad-64ed6f27d2b8) をもとに作成
@propertyWrapper
struct JSONNullable<T: Codable>: Codable {
var wrappedValue: T?
init(wrappedValue: T?) {
public struct JSONNullable<T: Codable>: Codable {
public var wrappedValue: T?

public init(wrappedValue: T?) {
self.wrappedValue = wrappedValue
}

init(from decoder: Decoder) throws {
public init(from decoder: Decoder) throws {
wrappedValue = try Optional<T>(from: decoder)
}

func encode(to encoder: Encoder) throws {
public func encode(to encoder: Encoder) throws {
try wrappedValue.encode(to: encoder)
}
}
Expand Down
14 changes: 7 additions & 7 deletions Sources/ReleaseSubscriptionsCore/Entities/Release.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import Foundation

public struct Release: Identifiable {
public let id: String
let owner: String
let repository: String
let version: String
let title: String
@JSONNullable var body: String?
let url: URL
public let owner: String
public let repository: String
public let version: String
public let title: String
@JSONNullable public var body: String?
public let url: URL
@JSONNullable var createdAt: Date?
@JSONNullable var publishedAt: Date?
@JSONNullable public var publishedAt: Date?
let fetchedFromAPIAt: Date
}

Expand Down
91 changes: 91 additions & 0 deletions Sources/ReleaseSummarizer/App.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import ArgumentParser
import Foundation
import ReleaseSummarizerCore
import ReleaseSubscriptionsCore
import Logging

@main
struct App: AsyncParsableCommand {

static var configuration: CommandConfiguration {
.init(
commandName: "releaseSummarizer"
)
}

@Option(
name: .shortAndLong,
help: "Gemini APIKey for generating summary of change log"
)
var apiToken: String?

@Option(
name: .long,
help: "Generate article target date range from. yyyy/MM/dd date format string",
transform: ISO8601DateFormatter().date(from:)
)
var from: Date?

@Option(
name: .long,
help: "Generate article target date range to. yyyy/MM/dd date format string. default: today",
transform: ISO8601DateFormatter().date(from:)
)
var to: Date?

func run() async throws {
guard let apiToken = apiToken else {
Logger.app.error("An argument apiToken is needed. Please pass Gemini apiKey.")
return
}

guard let from else {
Logger.app.error("An argument from is needed or invalid. Please pass date range from.")
return
}

let repositories = try ReleaseSubscriptionsParser.parse()
let releases = try await ReleaseCollector.collect(
for: repositories,
from: from,
to: to ?? Date()
)
let generatedContent = try await ReleaseSummarizeGenerator.generate(apiToken: apiToken, prompt: prompt, releases: releases)

try ReleaseSummarizeFileHelper.writeToFile(fileContent: generatedContent)
}
}

extension App {
var prompt: String {
"""
次の文章はとあるライブラリのリリース情報です。
この内容を短くわかりやすい表現で要約してください。
要約の内容は
・改善点
・修正点
・その他
に分けて日本語で行ってください。
内容は箇条書きの Markdown 形式で出力してください。
また、各箇条書きの項目は50文字以内で収めて、ですます調で出力してください。
出力例:
##### 改善点
内容
##### 修正点
内容
##### その他
内容
"""
}
}

extension Logger {
fileprivate static let app = Logger(label: "io.github.ios-osushi.releasesummarizer")
}
25 changes: 25 additions & 0 deletions Sources/ReleaseSummarizerCore/ReleaseCollector.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// ReleaseCollector.swift
// ReleaseSubscriptions
//
// Created by 伊藤凌也 on 2024/09/28.
//

import Foundation
import ReleaseSubscriptionsCore

public struct ReleaseCollector {
public static func collect(for repositories: [GitHubRepository], from: Date, to: Date) async throws -> [(GitHubRepository, Release)] {
try OutputFileHelper.load(repositories: repositories)
.flatMap { (repository, releases) in
releases.map { (repository, $0) }
}
.filter { (repository, release) in
guard let publishedAt = release.publishedAt else {
return false
}

return (from...to).contains(publishedAt)
}
}
}
23 changes: 23 additions & 0 deletions Sources/ReleaseSummarizerCore/ReleaseSummarizeFileHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// File.swift
// ReleaseSubscriptions
//
// Created by 伊藤凌也 on 2024/09/28.
//

import Foundation
import Logging

public struct ReleaseSummarizeFileHelper {
public static func writeToFile(fileContent: String) throws {
defer {
Logger.helper.info("🎉 \(#function) finished")
}
Logger.helper.info("ℹ️ \(#function) started")
try fileContent.write(toFile: "./output.md", atomically: true, encoding: .utf8)
}
}

extension Logger {
fileprivate static let helper = Logger(label: "io.github.ios-osushi.releasesummarizer.filehelper")
}
125 changes: 125 additions & 0 deletions Sources/ReleaseSummarizerCore/ReleaseSummarizeGenerator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//
// Generator.swift
// ReleaseSubscriptions
//
// Created by 伊藤凌也 on 2024/09/28.
//

import GoogleGenerativeAI
import Logging
import ReleaseSubscriptionsCore

public struct ReleaseSummarizeGenerator {

// リリース情報に限った記事の内容を生成する
//
// フォーマット
//
//
// ## OSS のリリース情報
//
// iOS アプリ開発でよく使われている OSS のリリース情報です。
//
// ### Apple
// #### {バージョン} - ライブラリ名
//
// [{release url}]({release url})
//
// ##### 改善点
//
// 内容
//
// ##### 修正点
//
// 内容
//
// ##### その他
//
// 内容
//
// ### サードパーティ
//
// ...
//
public static func generate(
apiToken: String,
prompt: String,
releases: [(GitHubRepository, Release)]
) async throws -> String {
let grouped: [SlackWebhookDestination: [Release]] = releases.reduce(into: [:]) { partialResult, release in
let (repository, release) = release

let destination = switch repository {
case let .releases(destination, _):
destination

case let .tags(destination, _):
destination
}

if partialResult[destination] != nil {
partialResult[destination]?.append(release)
} else {
partialResult[destination] = [release]
}
}

var builder = Builder()
for (key, releases) in grouped {
builder.addSection(destination: key)

for release in releases {
Logger.generator.info("Generating release content for \(release.owner)/\(release.repository) ...")

let model = GenerativeModel(name: "gemini-1.5-flash-latest", apiKey: apiToken)
let result = try await model.generateContent(prompt, release.body ?? "")

if let content = result.text {
builder.addRelease(release: release, generatedContent: content)
}

try await Task.sleep(nanoseconds: 5000_000_000)
}
}

return builder.result
}

struct Builder {
var result = """
## OSS のリリース情報
iOS アプリ開発でよく使われている OSS のリリース情報です。
"""

mutating func addSection(destination: SlackWebhookDestination) {
let title = switch destination {
case .primary:
"Apple"

case .secondary:
"サードパーティ"
}

result.append("### \(title)\n")
}

/// #### {バージョン} - ライブラリ名
mutating func addRelease(release: Release, generatedContent: String) {
let content = """
#### \(release.version) - \(release.owner)/\(release.repository)
[\(release.url)](\(release.url)
\(generatedContent)
"""
result.append(content)
}
}
}

extension Logger {
fileprivate static let generator = Logger(label: "io.github.ios-osushi.releasesummarizer.generator")
}

0 comments on commit 436a287

Please sign in to comment.