Skip to content

Commit

Permalink
feat: add an option to hide <think> tag content, hidden by default (#815
Browse files Browse the repository at this point in the history
)

* feat: filter <think> tag content for stream result

* fix: only filter prefix <think> tag

* feat: add option to hide think tag content
  • Loading branch information
tisfeng authored Feb 11, 2025
1 parent 0fe7496 commit f765843
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 2 deletions.
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)
}
}

0 comments on commit f765843

Please sign in to comment.