Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Modules/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ let package = Package(
.library(name: "WordPressFlux", targets: ["WordPressFlux"]),
.library(name: "WordPressShared", targets: ["WordPressShared"]),
.library(name: "WordPressUI", targets: ["WordPressUI"]),
.library(name: "WordPressIntelligence", targets: ["WordPressIntelligence"]),
.library(name: "WordPressReader", targets: ["WordPressReader"]),
.library(name: "WordPressCore", targets: ["WordPressCore"]),
.library(name: "WordPressCoreProtocols", targets: ["WordPressCoreProtocols"]),
Expand Down Expand Up @@ -163,6 +164,10 @@ let package = Package(
// This package should never have dependencies – it exists to expose protocols implemented in WordPressCore
// to UI code, because `wordpress-rs` doesn't work nicely with previews.
]),
.target(name: "WordPressIntelligence", dependencies: [
"WordPressShared",
.product(name: "SwiftSoup", package: "SwiftSoup"),
]),
.target(name: "WordPressLegacy", dependencies: ["DesignSystem", "WordPressShared"]),
.target(name: "WordPressSharedObjC", resources: [.process("Resources")], swiftSettings: [.swiftLanguageMode(.v5)]),
.target(
Expand Down Expand Up @@ -251,6 +256,7 @@ let package = Package(
.testTarget(name: "WordPressSharedObjCTests", dependencies: [.target(name: "WordPressShared"), .target(name: "WordPressTesting")], swiftSettings: [.swiftLanguageMode(.v5)]),
.testTarget(name: "WordPressUIUnitTests", dependencies: [.target(name: "WordPressUI")], swiftSettings: [.swiftLanguageMode(.v5)]),
.testTarget(name: "WordPressCoreTests", dependencies: [.target(name: "WordPressCore")]),
.testTarget(name: "WordPressIntelligenceTests", dependencies: [.target(name: "WordPressIntelligence")])
]
)

Expand Down
74 changes: 74 additions & 0 deletions Modules/Sources/WordPressIntelligence/IntelligenceService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import Foundation
import FoundationModels

/// Service for AI-powered content generation and analysis features.
///
/// This service provides tag suggestions, post summaries, excerpt generation,
/// and other intelligence features using Foundation Models (iOS 26+).
public actor IntelligenceService {
/// Maximum context size for language model sessions (in tokens).
///
/// A single token corresponds to three or four characters in languages like
/// English, Spanish, or German, and one token per character in languages like
/// Japanese, Chinese, or Korean. In a single session, the sum of all tokens
/// in the instructions, all prompts, and all outputs count toward the context window size.
///
/// https://developer.apple.com/documentation/foundationmodels/generating-content-and-performing-tasks-with-foundation-models#Consider-context-size-limits-per-session
public static let contextSizeLimit = 4096

/// Checks if intelligence features are supported on the current device.
public nonisolated static var isSupported: Bool {
guard #available(iOS 26, *) else {
return false
}
switch SystemLanguageModel.default.availability {
case .available:
return true
case .unavailable(let reason):
switch reason {
case .appleIntelligenceNotEnabled, .modelNotReady:
return true
case .deviceNotEligible:
return false
@unknown default:
return false
}
}
}

public init() {}

Check failure on line 39 in Modules/Sources/WordPressIntelligence/IntelligenceService.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this function is empty, or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=wordpress-mobile_WordPress-iOS&issues=AZrrp1LItfqK3Lt3jD9C&open=AZrrp1LItfqK3Lt3jD9C&pullRequest=25034

// MARK: - Public API

/// Suggests tags for a WordPress post.
@available(iOS 26, *)
public func suggestTags(post: String, siteTags: [String] = [], postTags: [String] = []) async throws -> [String] {
try await TagSuggestion.execute(post: post, siteTags: siteTags, postTags: postTags)
}

/// Summarizes a WordPress post.
@available(iOS 26, *)
public func summarizePost(content: String) async throws -> String {
try await PostSummary.execute(content: content)
}

/// Summarizes a support ticket to a short title.
@available(iOS 26, *)
public func summarizeSupportTicket(content: String) async throws -> String {
try await SupportTicketSummary.execute(content: content)
}

/// Extracts relevant text from post content (removes HTML, limits size).
public nonisolated func extractRelevantText(from post: String, ratio: CGFloat = 0.6) -> String {
Self.extractRelevantText(from: post, ratio: ratio)
}

// MARK: - Shared Utilities

/// Extracts relevant text from post content, removing HTML and limiting size.
public nonisolated static func extractRelevantText(from post: String, ratio: CGFloat = 0.6) -> String {
let extract = try? ContentExtractor.extractRelevantText(from: post)
let postSizeLimit = Double(IntelligenceService.contextSizeLimit) * ratio
return String((extract ?? post).prefix(Int(postSizeLimit)))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Foundation
import WordPressShared

/// Target length for generated text.
public enum ContentLength: Int, CaseIterable, Sendable {
case short
case medium
case long

public var displayName: String {
switch self {
case .short:
AppLocalizedString("generation.length.short", value: "Short", comment: "Generated content length (needs to be short)")

Check failure on line 13 in Modules/Sources/WordPressIntelligence/Parameters/ContentLength.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal 3 times.

See more on https://sonarcloud.io/project/issues?id=wordpress-mobile_WordPress-iOS&issues=AZrrp1MDtfqK3Lt3jD9D&open=AZrrp1MDtfqK3Lt3jD9D&pullRequest=25034
case .medium:
AppLocalizedString("generation.length.medium", value: "Medium", comment: "Generated content length (needs to be short)")
case .long:
AppLocalizedString("generation.length.long", value: "Long", comment: "Generated content length (needs to be short)")
}
}

public var trackingName: String {
switch self {
case .short: "short"
case .medium: "medium"
case .long: "long"
}
}

public var promptModifier: String {
"\(wordRange) words"
}

private var wordRange: String {
switch self {
case .short: "20-40"
case .medium: "50-70"
case .long: "120-180"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Foundation
import WordPressShared

/// Writing style for generated text.
public enum WritingStyle: String, CaseIterable, Sendable {
case engaging
case conversational
case witty
case formal
case professional

public var displayName: String {
switch self {
case .engaging:
AppLocalizedString("generation.style.engaging", value: "Engaging", comment: "AI generation style")

Check failure on line 15 in Modules/Sources/WordPressIntelligence/Parameters/WritingStyle.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal 5 times.

See more on https://sonarcloud.io/project/issues?id=wordpress-mobile_WordPress-iOS&issues=AZrrp1MMtfqK3Lt3jD9E&open=AZrrp1MMtfqK3Lt3jD9E&pullRequest=25034
case .conversational:
AppLocalizedString("generation.style.conversational", value: "Conversational", comment: "AI generation style")
case .witty:
AppLocalizedString("generation.style.witty", value: "Witty", comment: "AI generation style")
case .formal:
AppLocalizedString("generation.style.formal", value: "Formal", comment: "AI generation style")
case .professional:
AppLocalizedString("generation.style.professional", value: "Professional", comment: "AI generation style")
}
}

var promptModifier: String {
"\(rawValue) (\(promptModifierDetails))"
}

var promptModifierDetails: String {
switch self {
case .engaging: "engaging and compelling tone"
case .witty: "witty, creative, entertaining"
case .conversational: "friendly and conversational tone"
case .formal: "formal and academic tone"
case .professional: "professional and polished tone"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import Foundation
import FoundationModels

/// Excerpt generation for WordPress posts.
///
/// Generates multiple excerpt variations for blog posts with customizable
/// length and writing style. Supports session-based usage (for UI with continuity)
/// and one-shot generation (for tests and background tasks).
@available(iOS 26, *)
public struct ExcerptGeneration {
public var length: ContentLength
public var style: WritingStyle
public var options: GenerationOptions

public init(
length: ContentLength,
style: WritingStyle,
options: GenerationOptions = GenerationOptions(temperature: 0.7)
) {
self.length = length
self.style = style
self.options = options
}

/// Generates excerpts with this configuration.
public func generate(for content: String) async throws -> [String] {
let content = IntelligenceService.extractRelevantText(from: content)
let response = try await makeSession().respond(
to: makePrompt(content: content),
generating: Result.self,
options: options
)
return response.content.excerpts
}

/// Creates a language model session configured for excerpt generation.
public func makeSession() -> LanguageModelSession {
LanguageModelSession(
model: .init(guardrails: .permissiveContentTransformations),
instructions: Self.instructions
)
}

/// Instructions for the language model session.
public static var instructions: String {
"""
Generate exactly 3 excerpts for the blog post and follow the instructions from the prompt regarding the length and the style.

Generate excerpts in the same language as the POST_CONTENT.

**Paramters**
- POST_CONTENT: contents of the post (HTML or plain text)
- GENERATED_CONTENT_LENGTH: the length of the generated content
- GENERATION_STYLE: the writing style to follow

**Requirements**
- Each excerpt must follow the provided GENERATED_CONTENT_LENGTH and use GENERATION_STYLE

**Excerpt best practices**
- Follow the best practices for post excerpts esteblished in the WordPress ecosystem
- Include the post's main value proposition
- Use active voice (avoid "is", "are", "was", "were" when possible)
- End with implicit promise of more information
- Do not use ellipsis (...) at the end
- Focus on value, not summary
- Include strategic keywords naturally
- 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.
"""
}


/// Creates a prompt for this excerpt configuration.
public func makePrompt(content: String) -> String {
"""
Generate three different excerpts for the given post and parameters

GENERATED_CONTENT_LENGTH: \(length.promptModifier)

GENERATION_STYLE: \(style.promptModifier)

POST_CONTENT: '''
\(content)
"""
}

/// Prompt for generating additional excerpt options.
public static var loadMorePrompt: String {
"Generate additional three options"
}

// MARK: - Result Type

@Generable
public struct Result {
@Guide(description: "Three different excerpt variations")
public var excerpts: [String]
}
}
37 changes: 37 additions & 0 deletions Modules/Sources/WordPressIntelligence/UseCases/PostSummary.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation
import FoundationModels

@available(iOS 26, *)
extension IntelligenceService {
/// Post summarization for WordPress content.
///
/// Generates concise summaries that capture the main points and key information
/// from WordPress post content in the same language as the source.
public enum PostSummary {
static func execute(content: String) async throws -> String {
let content = IntelligenceService.extractRelevantText(from: content, ratio: 0.8)

let instructions = """
You are helping a WordPress user understand the content of a post.
Generate a concise summary that captures the main points and key information.
The summary should be clear, informative, and written in a neutral tone.
You MUST generate the summary in the same language as the post content.

Do not include anything other than the summary in the response.
"""

let session = LanguageModelSession(
model: .init(guardrails: .permissiveContentTransformations),
instructions: instructions
)

let prompt = """
Summarize the following post:

\(content)
"""

return try await session.respond(to: prompt).content
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Foundation
import FoundationModels

@available(iOS 26, *)
extension IntelligenceService {
/// Support ticket summarization.
///
/// Generates short, concise titles (fewer than 10 words) for support
/// conversations based on the opening message.
public enum SupportTicketSummary {
static func execute(content: String) async throws -> String {
let instructions = """
You are helping a user by summarizing their support request down to a single sentence
with fewer than 10 words.

The summary should be clear, informative, and written in a neutral tone.
You MUST generate the summary in the same language as the support request.

Do not include anything other than the summary in the response.
"""

let session = LanguageModelSession(
model: .init(guardrails: .permissiveContentTransformations),
instructions: instructions
)

let prompt = """
Give me an appropriate conversation title for the following opening message of the conversation:

\(content)
"""

return try await session.respond(
to: prompt,
generating: Result.self,
options: GenerationOptions(temperature: 1.0)
).content.title
}

@Generable
struct Result {
@Guide(description: "The conversation title")
var title: String
}
}
}
Loading