Skip to content

Commit 7b4abe2

Browse files
committed
Create WordPressIntelligence module
1 parent 0e92c18 commit 7b4abe2

17 files changed

+192
-194
lines changed

Modules/Package.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ let package = Package(
2020
.library(name: "WordPressFlux", targets: ["WordPressFlux"]),
2121
.library(name: "WordPressShared", targets: ["WordPressShared"]),
2222
.library(name: "WordPressUI", targets: ["WordPressUI"]),
23+
.library(name: "WordPressIntelligence", targets: ["WordPressIntelligence"]),
2324
.library(name: "WordPressReader", targets: ["WordPressReader"]),
2425
.library(name: "WordPressCore", targets: ["WordPressCore"]),
2526
.library(name: "WordPressCoreProtocols", targets: ["WordPressCoreProtocols"]),
@@ -163,6 +164,10 @@ let package = Package(
163164
// This package should never have dependencies – it exists to expose protocols implemented in WordPressCore
164165
// to UI code, because `wordpress-rs` doesn't work nicely with previews.
165166
]),
167+
.target(name: "WordPressIntelligence", dependencies: [
168+
"WordPressShared",
169+
.product(name: "SwiftSoup", package: "SwiftSoup"),
170+
]),
166171
.target(name: "WordPressLegacy", dependencies: ["DesignSystem", "WordPressShared"]),
167172
.target(name: "WordPressSharedObjC", resources: [.process("Resources")], swiftSettings: [.swiftLanguageMode(.v5)]),
168173
.target(
@@ -251,6 +256,7 @@ let package = Package(
251256
.testTarget(name: "WordPressSharedObjCTests", dependencies: [.target(name: "WordPressShared"), .target(name: "WordPressTesting")], swiftSettings: [.swiftLanguageMode(.v5)]),
252257
.testTarget(name: "WordPressUIUnitTests", dependencies: [.target(name: "WordPressUI")], swiftSettings: [.swiftLanguageMode(.v5)]),
253258
.testTarget(name: "WordPressCoreTests", dependencies: [.target(name: "WordPressCore")]),
259+
.testTarget(name: "WordPressIntelligenceTests", dependencies: [.target(name: "WordPressIntelligence")])
254260
]
255261
)
256262

Modules/Sources/WordPressShared/Intelligence/IntelligenceService.swift renamed to Modules/Sources/WordPressIntelligence/IntelligenceService.swift

Lines changed: 0 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -71,82 +71,4 @@ public actor IntelligenceService {
7171
let postSizeLimit = Double(IntelligenceService.contextSizeLimit) * ratio
7272
return String((extract ?? post).prefix(Int(postSizeLimit)))
7373
}
74-
75-
// MARK: - Shared Parameters
76-
77-
/// Writing style for generated text.
78-
public enum WritingStyle: String, CaseIterable, Sendable {
79-
case engaging
80-
case conversational
81-
case witty
82-
case formal
83-
case professional
84-
85-
public var displayName: String {
86-
switch self {
87-
case .engaging:
88-
NSLocalizedString("generation.style.engaging", value: "Engaging", comment: "AI generation style")
89-
case .conversational:
90-
NSLocalizedString("generation.style.conversational", value: "Conversational", comment: "AI generation style")
91-
case .witty:
92-
NSLocalizedString("generation.style.witty", value: "Witty", comment: "AI generation style")
93-
case .formal:
94-
NSLocalizedString("generation.style.formal", value: "Formal", comment: "AI generation style")
95-
case .professional:
96-
NSLocalizedString("generation.style.professional", value: "Professional", comment: "AI generation style")
97-
}
98-
}
99-
100-
var promptModifier: String {
101-
"\(rawValue) (\(promptModifierDetails))"
102-
}
103-
104-
var promptModifierDetails: String {
105-
switch self {
106-
case .engaging: "engaging and compelling tone"
107-
case .witty: "witty, creative, entertaining"
108-
case .conversational: "friendly and conversational tone"
109-
case .formal: "formal and academic tone"
110-
case .professional: "professional and polished tone"
111-
}
112-
}
113-
}
114-
115-
/// Target length for generated text.
116-
public enum WritingLength: Int, CaseIterable, Sendable {
117-
case short
118-
case medium
119-
case long
120-
121-
public var displayName: String {
122-
switch self {
123-
case .short:
124-
NSLocalizedString("generation.length.short", value: "Short", comment: "Generated content length (needs to be short)")
125-
case .medium:
126-
NSLocalizedString("generation.length.medium", value: "Medium", comment: "Generated content length (needs to be short)")
127-
case .long:
128-
NSLocalizedString("generation.length.long", value: "Long", comment: "Generated content length (needs to be short)")
129-
}
130-
}
131-
132-
public var trackingName: String {
133-
switch self {
134-
case .short: "short"
135-
case .medium: "medium"
136-
case .long: "long"
137-
}
138-
}
139-
140-
public var promptModifier: String {
141-
"\(wordRange) words"
142-
}
143-
144-
private var wordRange: String {
145-
switch self {
146-
case .short: "20-40"
147-
case .medium: "50-70"
148-
case .long: "120-180"
149-
}
150-
}
151-
}
15274
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Foundation
2+
import WordPressShared
3+
4+
/// Target length for generated text.
5+
public enum ContentLength: Int, CaseIterable, Sendable {
6+
case short
7+
case medium
8+
case long
9+
10+
public var displayName: String {
11+
switch self {
12+
case .short:
13+
AppLocalizedString("generation.length.short", value: "Short", comment: "Generated content length (needs to be short)")
14+
case .medium:
15+
AppLocalizedString("generation.length.medium", value: "Medium", comment: "Generated content length (needs to be short)")
16+
case .long:
17+
AppLocalizedString("generation.length.long", value: "Long", comment: "Generated content length (needs to be short)")
18+
}
19+
}
20+
21+
public var trackingName: String {
22+
switch self {
23+
case .short: "short"
24+
case .medium: "medium"
25+
case .long: "long"
26+
}
27+
}
28+
29+
public var promptModifier: String {
30+
"\(wordRange) words"
31+
}
32+
33+
private var wordRange: String {
34+
switch self {
35+
case .short: "20-40"
36+
case .medium: "50-70"
37+
case .long: "120-180"
38+
}
39+
}
40+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Foundation
2+
import WordPressShared
3+
4+
/// Writing style for generated text.
5+
public enum WritingStyle: String, CaseIterable, Sendable {
6+
case engaging
7+
case conversational
8+
case witty
9+
case formal
10+
case professional
11+
12+
public var displayName: String {
13+
switch self {
14+
case .engaging:
15+
AppLocalizedString("generation.style.engaging", value: "Engaging", comment: "AI generation style")
16+
case .conversational:
17+
AppLocalizedString("generation.style.conversational", value: "Conversational", comment: "AI generation style")
18+
case .witty:
19+
AppLocalizedString("generation.style.witty", value: "Witty", comment: "AI generation style")
20+
case .formal:
21+
AppLocalizedString("generation.style.formal", value: "Formal", comment: "AI generation style")
22+
case .professional:
23+
AppLocalizedString("generation.style.professional", value: "Professional", comment: "AI generation style")
24+
}
25+
}
26+
27+
var promptModifier: String {
28+
"\(rawValue) (\(promptModifierDetails))"
29+
}
30+
31+
var promptModifierDetails: String {
32+
switch self {
33+
case .engaging: "engaging and compelling tone"
34+
case .witty: "witty, creative, entertaining"
35+
case .conversational: "friendly and conversational tone"
36+
case .formal: "formal and academic tone"
37+
case .professional: "professional and polished tone"
38+
}
39+
}
40+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import Foundation
2+
import FoundationModels
3+
4+
/// Excerpt generation for WordPress posts.
5+
///
6+
/// Generates multiple excerpt variations for blog posts with customizable
7+
/// length and writing style. Supports session-based usage (for UI with continuity)
8+
/// and one-shot generation (for tests and background tasks).
9+
@available(iOS 26, *)
10+
public struct ExcerptGeneration {
11+
public let length: ContentLength
12+
public let style: WritingStyle
13+
public var options: GenerationOptions
14+
15+
public init(length: ContentLength, style: WritingStyle, options: GenerationOptions = GenerationOptions(temperature: 0.7)) {
16+
self.length = length
17+
self.style = style
18+
self.options = options
19+
}
20+
21+
// MARK: - Instance Methods
22+
23+
/// Creates a language model session configured for excerpt generation.
24+
public func makeSession() -> LanguageModelSession {
25+
LanguageModelSession(
26+
model: .init(guardrails: .permissiveContentTransformations),
27+
instructions: Self.instructions
28+
)
29+
}
30+
31+
/// Creates a prompt for this excerpt configuration.
32+
public func makePrompt(content: String) -> String {
33+
"""
34+
Generate three different excerpts for the given post and parameters
35+
36+
GENERATED_CONTENT_LENGTH: \(length.promptModifier)
37+
38+
GENERATION_STYLE: \(style.promptModifier)
39+
40+
POST_CONTENT: '''
41+
\(content)
42+
"""
43+
}
44+
45+
/// Generates excerpts with this configuration.
46+
public func generate(content: String) async throws -> [String] {
47+
let extractedContent = IntelligenceService.extractRelevantText(from: content)
48+
let session = makeSession()
49+
50+
let response = try await session.respond(
51+
to: makePrompt(content: extractedContent),
52+
generating: Result.self,
53+
options: options
54+
)
55+
56+
return response.content.excerpts
57+
}
58+
59+
// MARK: - Building Blocks (for UI with session continuity)
60+
61+
/// Instructions for the language model session.
62+
public static var instructions: String {
63+
"""
64+
Generate exactly 3 excerpts for the blog post and follow the instructions from the prompt regarding the length and the style.
65+
66+
Generate excerpts in the same language as the POST_CONTENT.
67+
68+
**Paramters**
69+
- POST_CONTENT: contents of the post (HTML or plain text)
70+
- GENERATED_CONTENT_LENGTH: the length of the generated content
71+
- GENERATION_STYLE: the writing style to follow
72+
73+
**Requirements**
74+
- Each excerpt must follow the provided GENERATED_CONTENT_LENGTH and use GENERATION_STYLE
75+
76+
**Excerpt best practices**
77+
- Follow the best practices for post excerpts esteblished in the WordPress ecosystem
78+
- Include the post's main value proposition
79+
- Use active voice (avoid "is", "are", "was", "were" when possible)
80+
- End with implicit promise of more information
81+
- Do not use ellipsis (...) at the end
82+
- Focus on value, not summary
83+
- Include strategic keywords naturally
84+
- Write independently from the introduction – excerpt shouldn't just duplicate your opening paragraph. While your introduction eases readers into the topic, your excerpt needs to work as standalone copy that makes sense out of context—whether it appears in search results, social media cards, or email newsletters.
85+
"""
86+
}
87+
88+
/// Prompt for generating additional excerpt options.
89+
public static var loadMorePrompt: String {
90+
"Generate additional three options"
91+
}
92+
93+
// MARK: - Result Type
94+
95+
@Generable
96+
public struct Result {
97+
@Guide(description: "Three different excerpt variations")
98+
public var excerpts: [String]
99+
}
100+
}

Modules/Sources/WordPressShared/Intelligence/UseCases/TagSuggestion.swift renamed to Modules/Sources/WordPressIntelligence/UseCases/TagSuggestion.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import FoundationModels
3+
import WordPressShared
34

45
@available(iOS 26, *)
56
extension IntelligenceService {

0 commit comments

Comments
 (0)