Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add an option to hide <think> tag content, hidden by default #815

Merged
merged 3 commits into from
Feb 11, 2025
Merged
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
28 changes: 28 additions & 0 deletions Easydict/App/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -4672,6 +4672,34 @@
}
}
},
"service.configuration.openai.hide_think_tag_content.title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hide <think> Tag Content"
}
},
"sk" : {
"stringUnit" : {
"state" : "translated",
"value" : "Skryť obsah <think> Tag"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "隐藏 <think> 标签内容"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "隱藏 <think> 標籤內容"
}
}
}
},
"service.configuration.openai.system_prompt.placeholder" : {
"localizations" : {
"en" : {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,5 @@ enum ServiceConfigurationKey: String {
case enableCustomPrompt
case systemPrompt
case userPrompt
case thinkTag
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Foundation
import RegexBuilder

extension StreamService {
/// Throttle update result text, avoid update UI too frequently.
Expand Down Expand Up @@ -52,7 +53,11 @@ extension StreamService {
// If error is not nil, means stream is finished.
result.isStreamFinished = error != nil

let finalText = resultText?.trim() ?? ""
var finalText = resultText?.trim() ?? ""

if hideThinkTagContent {
finalText = finalText.filterThinkTagContent().trim()
}

let updateCompletion = { [weak result] in
guard let result else { return }
Expand Down
8 changes: 8 additions & 0 deletions Easydict/Swift/Service/OpenAI/StreamService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ public class StreamService: QueryService {
serviceDefaultsKey(.serviceUsageStatus, defaultValue: .default)
}

var thinkTagKey: Defaults.Key<Bool> {
boolDefaultsKey(.thinkTag, defaultValue: true)
}

var hideThinkTagContent: Bool {
Defaults[thinkTagKey]
}

// In general, LLM services need to observe these keys to enable validation button.
var observeKeys: [Defaults.Key<String>] {
[
Expand Down
35 changes: 35 additions & 0 deletions Easydict/Swift/Utility/Extensions/String/String+Regex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Foundation
import RegexBuilder

extension String {
func extract(withPattern pattern: String) -> String? {
Expand All @@ -24,3 +25,37 @@ extension String {
return nil
}
}

extension String {
/// Filter ^<think>...</think> tag content.
/// Example:
/// - "<think>hello" -> ""
/// - "<think></think>hello" -> "hello"
/// - "<think>hello</think>world" -> "world"
/// - "hello<think>world</think>" -> "hello<think>world</think>"
/// - "no tags here" -> "no tags here"
func filterThinkTagContent() -> String {
filterTagContent("think")
}

func filterTagContent(_ tag: String) -> String {
let startTag = "<\(tag)>"
let endTag = "</\(tag)>"

// Tag pattern
let tagPattern = Regex {
Anchor.startOfSubject
startTag
ZeroOrMore {
// Match any character (non-greedy) until </tag> is found
NegativeLookahead(endTag)
CharacterClass.any
}
// Match the closing tag if it exists
Optionally(endTag)
}

// Replace all matches with an empty string
return replacing(tagPattern, with: "")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ struct StreamConfigurationView: View {
showTranslationToggle: Bool = true,
showSentenceToggle: Bool = true,
showDictionaryToggle: Bool = true,
showUsageStatusPicker: Bool = true
showUsageStatusPicker: Bool = true,
showThinkTagContent: Bool = true
) {
self.service = service

Expand All @@ -41,6 +42,7 @@ struct StreamConfigurationView: View {
self.showSentenceToggle = showSentenceToggle
self.showDictionaryToggle = showDictionaryToggle
self.showUsageStatusPicker = showUsageStatusPicker
self.showThinkTagSection = showThinkTagContent

// Disable user to edit built-in supported models.
self.isEditable = service.serviceType() != .builtInAI
Expand All @@ -66,6 +68,7 @@ struct StreamConfigurationView: View {
let showSentenceToggle: Bool
let showDictionaryToggle: Bool
let showUsageStatusPicker: Bool
let showThinkTagSection: Bool

var isEditable = true

Expand Down Expand Up @@ -176,6 +179,13 @@ struct StreamConfigurationView: View {
values: ServiceUsageStatus.allCases
)
}

if showThinkTagSection {
ToggleCell(
titleKey: "service.configuration.openai.hide_think_tag_content.title",
key: service.thinkTagKey
)
}
}
}
}
17 changes: 17 additions & 0 deletions EasydictSwiftTests/Test.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,20 @@ import Translation
}
#expect(true, "Concurrent test getSelectedText completed without crash")
}

@Test func testFilterThinkTags() {
let testCases: [(input: String, expected: String)] = [
("<think>hello", ""),
("<think></think>hello", "hello"),
("<think>hello</think>world", "world"),
("hello<think>world</think>", "hello<think>world</think>"),
("no tags here", "no tags here"),
("", ""),
]

for (index, testCase) in testCases.enumerated() {
let result = testCase.input.filterThinkTagContent()
print("Test Case \(index + 1): \(result)")
#expect(result == testCase.expected)
}
}