Skip to content

Commit

Permalink
fix: Fixed the bug that incorrectly parsed the response from Claude. #…
Browse files Browse the repository at this point in the history
  • Loading branch information
bookfere committed Mar 18, 2024
1 parent bc36961 commit 548db7f
Show file tree
Hide file tree
Showing 14 changed files with 390 additions and 220 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ assignees: ''
---

**Basic information**
Some required information from your use case.

* Operating System: **Windows/macOS/Linux distro**
* Calibre Version: **x.x.x**
* Plugin Version: **x.x.x**
* Plugin Installation: **From Calibre/Rolling Release**

**Describe the bug**
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ assignees: ''
---

**基础信息**
一些有关你使用情况的必要信息。

* 操作系统: **Windows/macOS/Linux 发行版**
* Calibre 版本: **x.x.x**
* 插件版本:**x.x.x**
* 插件来源: **安装自 Calibre/Rolling Release**

**描述错误**
Expand Down
2 changes: 1 addition & 1 deletion engines/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,5 @@ def _parse_stream(self, data):
chunk = json.loads(line.split('data: ')[1])
if chunk.get('type') == 'message_stop':
break
if chunk.get('type') == 'content_block_start':
if chunk.get('type') == 'content_block_delta':
yield str(chunk.get('delta').get('text'))
44 changes: 26 additions & 18 deletions setting.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from .lib.translation import get_engine_class

from .engines import (
builtin_engines, GeminiPro, ChatgptTranslate, ClaudeTranslate)
builtin_engines, GeminiPro, ChatgptTranslate, AzureChatgptTranslate,
ClaudeTranslate)
from .engines.custom import CustomTranslate
from .components import (
layout_info, AlertMessage, TargetLang, SourceLang, EngineList,
Expand Down Expand Up @@ -470,10 +471,10 @@ def layout_engine(self):
chatgpt_model = QWidget()
chatgpt_model_layout = QHBoxLayout(chatgpt_model)
chatgpt_model_layout.setContentsMargins(0, 0, 0, 0)
chatgpt_select = QComboBox()
chatgpt_custom = QLineEdit()
chatgpt_model_layout.addWidget(chatgpt_select)
chatgpt_model_layout.addWidget(chatgpt_custom)
chatgpt_model_select = QComboBox()
chatgpt_model_custom = QLineEdit()
chatgpt_model_layout.addWidget(chatgpt_model_select)
chatgpt_model_layout.addWidget(chatgpt_model_custom)
chatgpt_layout.addRow(_('Model'), chatgpt_model)

self.disable_wheel_event(chatgpt_model)
Expand Down Expand Up @@ -549,6 +550,7 @@ def show_chatgpt_preferences():
if not is_chatgpt and not is_claude:
chatgpt_group.setVisible(False)
return
chatgpt_group.setVisible(True)
if is_chatgpt:
temperature_value.setRange(0, 2)
chatgpt_group.setTitle(_('Tune ChatGPT'))
Expand All @@ -568,22 +570,28 @@ def show_chatgpt_preferences():
self.chatgpt_endpoint.setCursorPosition(0)
# Model
if self.current_engine.model is not None:
chatgpt_layout.setRowVisible(chatgpt_model, True)
chatgpt_select.clear()
chatgpt_select.addItems(self.current_engine.models)
chatgpt_select.addItem(_('Custom'))
chatgpt_model_select.clear()
if issubclass(self.current_engine, AzureChatgptTranslate):
chatgpt_model_select.addItem(
_('The model depends on your Azure project.'))
chatgpt_model_select.setDisabled(True)
chatgpt_model_custom.setVisible(False)
return
chatgpt_model_select.setDisabled(False)
chatgpt_model_select.addItems(self.current_engine.models)
chatgpt_model_select.addItem(_('Custom'))
model = config.get('model', self.current_engine.model)
chatgpt_select.setCurrentText(
chatgpt_model_select.setCurrentText(
model if model in self.current_engine.models
else _('Custom'))

def setup_chatgpt_model(model):
if model in self.current_engine.models:
chatgpt_custom.setVisible(False)
chatgpt_model_custom.setVisible(False)
else:
chatgpt_custom.setVisible(True)
chatgpt_model_custom.setVisible(True)
if model != _('Custom'):
chatgpt_custom.setText(model)
chatgpt_model_custom.setText(model)
setup_chatgpt_model(model)

def update_chatgpt_model(model):
Expand All @@ -595,13 +603,13 @@ def change_chatgpt_model(model):
setup_chatgpt_model(model)
update_chatgpt_model(model)

chatgpt_custom.textChanged.connect(
chatgpt_model_custom.textChanged.connect(
lambda model: update_chatgpt_model(model=model.strip()))
chatgpt_select.currentTextChanged.connect(change_chatgpt_model)
chatgpt_model_select.currentTextChanged.connect(
change_chatgpt_model)
self.save_config.connect(
lambda: chatgpt_select.setCurrentText(config.get('model')))
else:
chatgpt_layout.setRowVisible(chatgpt_model, False)
lambda: chatgpt_model_select.setCurrentText(
config.get('model')))

# Sampling
sampling = config.get('sampling', self.current_engine.sampling)
Expand Down
14 changes: 7 additions & 7 deletions tests/test_convertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def test_translate_done_other_to_library(
self, mock_open, mock_os_rename, mock_open_path):
self.job.failed = False
self.job.description = 'test description'
self.job.log_path = '/path/to/log'
self.job.log_path = 'C:\\path\\to\\log'
metadata_config = {'lang_mark': True}
self.worker.config = {
'ebook_metadata': metadata_config,
Expand All @@ -210,18 +210,19 @@ def test_translate_done_other_to_library(
self.ebook.custom_title = 'test custom title'
self.ebook.target_lang = 'German'
self.worker.working_jobs = {
self.job: (self.ebook, '/path/to/test.srt')}
self.job: (self.ebook, 'C:\\path\\to\\test.srt')}
metadata = Mock()
self.worker.api.get_metadata.return_value = metadata
self.worker.api.format_abspath.return_value = '/path/to/test[m].srt'
self.worker.api.format_abspath.return_value = \
'C:\\path\\to\\test[m].srt'
self.worker.db.create_book_entry.return_value = 90

self.worker.translate_done(self.job)

self.worker.api.get_metadata.assert_called_once_with(89)
self.worker.db.create_book_entry.assert_called_once_with(metadata)
self.worker.api.add_format.assert_called_once_with(
90, 'srt', '/path/to/test.srt', run_hooks=False)
90, 'srt', 'C:\\path\\to\\test.srt', run_hooks=False)
self.worker.gui.library_view.model.assert_called_once()
self.worker.gui.library_view.model().books_added \
.assert_called_once_with(1)
Expand All @@ -233,7 +234,7 @@ def test_translate_done_other_to_library(
arguments = self.worker.gui.proceed_question.mock_calls[0].args
self.assertIsInstance(arguments[0], Callable)
self.assertIs(self.worker.gui.job_manager.launch_gui_app, arguments[1])
self.assertEqual('/path/to/log', arguments[2])
self.assertEqual('C:\\path\\to\\log', arguments[2])
self.assertEqual(_('Ebook Translation Log'), arguments[3])
self.assertEqual(_('Translation Completed'), arguments[4])
self.assertEqual(_(
Expand All @@ -244,13 +245,12 @@ def test_translate_done_other_to_library(

mock_payload = Mock()
arguments[0](mock_payload)
mock_open_path.assert_called_once_with('/path/to/test[m].srt')
mock_open_path.assert_called_once_with('C:\\path\\to\\test[m].srt')

arguments = self.worker.gui.proceed_question.mock_calls[0].kwargs
self.assertEqual(True, arguments.get('log_is_file'))
self.assertIs(self.icon, arguments.get('icon'))


@patch(module_name + '.open_path')
@patch(module_name + '.os.rename')
@patch(module_name + '.open')
Expand Down
158 changes: 150 additions & 8 deletions tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
from ..engines.deepl import DeeplTranslate
from ..engines.openai import ChatgptTranslate
from ..engines.microsoft import AzureChatgptTranslate
from ..engines.anthropic import ClaudeTranslate
from ..engines.custom import (
create_engine_template, load_engine_data, CustomTranslate)


load_translations()

moudle_name = 'calibre_plugins.ebook_translator.engines'


class TestBase(unittest.TestCase):
def setUp(self):
Expand All @@ -29,7 +32,7 @@ def test_placeholder(self):
Base.placeholder[1].format(1),
'xxx %s xxx' % mark))

@patch('calibre_plugins.ebook_translator.engines.base.os.path.isfile')
@patch(moudle_name + '.base.os.path.isfile')
def test_get_external_program(self, mock_os_path_isfile):
mock_os_path_isfile.side_effect = lambda p: p in [
'/path/to/real', '/path/to/folder/real', '/path/to/specify/real']
Expand All @@ -51,7 +54,7 @@ def test_get_external_program(self, mock_os_path_isfile):
self.translator.get_external_program('/path/to/fake'))


@patch('calibre_plugins.ebook_translator.engines.base.Browser')
@patch(moudle_name + '.base.Browser')
class TestDeepl(unittest.TestCase):
def setUp(self):
DeeplTranslate.set_config({'api_keys': ['a', 'b', 'c']})
Expand Down Expand Up @@ -104,9 +107,9 @@ def setUp(self):
self.translator.set_source_lang('English')
self.translator.set_target_lang('Chinese')

@patch('calibre_plugins.ebook_translator.engines.openai.EbookTranslator')
@patch('calibre_plugins.ebook_translator.engines.base.Request')
@patch('calibre_plugins.ebook_translator.engines.base.Browser')
@patch(moudle_name + '.openai.EbookTranslator')
@patch(moudle_name + '.base.Request')
@patch(moudle_name + '.base.Browser')
def test_translate_stream(self, mock_browser, mock_request, mock_et):
url = 'https://api.openai.com/v1/chat/completions'
prompt = ('You are a meticulous translator who translates any given '
Expand Down Expand Up @@ -165,9 +168,9 @@ def setUp(self):
self.translator.set_source_lang('English')
self.translator.set_target_lang('Chinese')

@patch('calibre_plugins.ebook_translator.engines.base.Request')
@patch('calibre_plugins.ebook_translator.engines.base.Browser')
def test_translate(self, mock_browser, mock_request):
@patch(moudle_name + '.base.Request')
@patch(moudle_name + '.base.Browser')
def test_translate_stream(self, mock_browser, mock_request):
prompt = ('You are a meticulous translator who translates any given '
'content. Translate the given content from English to '
'Chinese only. Do not explain any term or answer any '
Expand Down Expand Up @@ -201,6 +204,145 @@ def test_translate(self, mock_browser, mock_request):
self.assertEqual('你好世界!', ''.join(result))


class TestClaudeTranslate(unittest.TestCase):
def setUp(self):
ClaudeTranslate.set_config({'api_keys': ['a', 'b', 'c']})
ClaudeTranslate.lang_codes = {
'source': {'English': 'EN'},
'target': {'Chinese': 'ZH'},
}

self.translator = ClaudeTranslate()
self.translator.set_source_lang('English')
self.translator.set_target_lang('Chinese')

@patch(moudle_name + '.anthropic.EbookTranslator')
@patch(moudle_name + '.base.Request')
@patch(moudle_name + '.base.Browser')
def test_translate(self, mock_browser, mock_request, mock_et):
prompt = ('You are a meticulous translator who translates any given '
'content. Translate the given content from English to '
'Chinese only. Do not explain any term or answer any '
'question-like content.')
data = json.dumps({
'stream': False,
'max_tokens': 4096,
'model': 'claude-2.1',
'top_k': 1,
'system': prompt,
'messages': [{'role': 'user', 'content': 'Hello World!'}],
'temperature': 1.0,
})
mock_et.__version__ = '1.0.0'
headers = {
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01',
'x-api-key': 'a',
'User-Agent': 'Ebook-Translator/1.0.0',
}

data_sample = """
{
"content": [
{
"text": "你好世界!",
"type": "text"
}
],
"id": "msg_013Zva2CMHLNnXjNJJKqJ2EF",
"model": "claude-3-opus-20240229",
"role": "assistant",
"stop_reason": "end_turn",
"stop_sequence": null,
"type": "message",
"usage": {
"input_tokens": 10,
"output_tokens": 25
}
}
"""
mock_response = Mock()
mock_response.read.return_value = data_sample.encode()
mock_browser.return_value.response.return_value = mock_response
url = 'https://api.anthropic.com/v1/messages'
self.translator.endpoint = url
self.translator.stream = False
result = self.translator.translate('Hello World!')
mock_request.assert_called_with(
url, data, headers=headers, timeout=30.0, method='POST')
self.assertEqual('你好世界!', result)

@patch(moudle_name + '.anthropic.EbookTranslator')
@patch(moudle_name + '.base.Request')
@patch(moudle_name + '.base.Browser')
def test_translate_stream(self, mock_browser, mock_request, mock_et):
prompt = ('You are a meticulous translator who translates any given '
'content. Translate the given content from English to '
'Chinese only. Do not explain any term or answer any '
'question-like content.')
data = json.dumps({
'stream': True,
'max_tokens': 4096,
'model': 'claude-2.1',
'top_k': 1,
'system': prompt,
'messages': [{'role': 'user', 'content': 'Hello World!'}],
'temperature': 1.0,
})
mock_et.__version__ = '1.0.0'
headers = {
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01',
'x-api-key': 'a',
'User-Agent': 'Ebook-Translator/1.0.0',
}

data_sample = """
event: message_start
data: {"type":"message_start","message":{}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{}}
event: ping
data: {"type": "ping"}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"text":"你"}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"text":"好"}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"text":"世"}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"text":"界"}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"text":"!"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_delta
data: {"type":"message_delta","delta":{}}
event: message_stop
data: {"type":"message_stop"}
"""
mock_response = Mock()
mock_response.readline.side_effect = data_sample.encode().splitlines()
mock_browser.return_value.response.return_value = mock_response
url = 'https://api.anthropic.com/v1/messages'
self.translator.endpoint = url
result = self.translator.translate('Hello World!')
mock_request.assert_called_with(
url, data, headers=headers, timeout=30.0, method='POST')
self.assertIsInstance(result, GeneratorType)
self.assertEqual('你好世界!', ''.join(result))


class TestFunction(unittest.TestCase):
def test_create_engine_template(self):
expect = """{
Expand Down
Loading

0 comments on commit 548db7f

Please sign in to comment.