vectorvein 0.1.0__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {vectorvein-0.1.0 → vectorvein-0.1.2}/PKG-INFO +2 -2
  2. {vectorvein-0.1.0 → vectorvein-0.1.2}/pyproject.toml +5 -5
  3. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/chat_clients/__init__.py +5 -3
  4. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/chat_clients/openai_compatible_client.py +13 -8
  5. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/chat_clients/utils.py +44 -218
  6. {vectorvein-0.1.0 → vectorvein-0.1.2}/tests/test_create_chat_client.py +7 -5
  7. {vectorvein-0.1.0 → vectorvein-0.1.2}/README.md +0 -0
  8. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/__init__.py +0 -0
  9. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/chat_clients/anthropic_client.py +0 -0
  10. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/chat_clients/base_client.py +0 -0
  11. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/chat_clients/deepseek_client.py +0 -0
  12. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/chat_clients/gemini_client.py +0 -0
  13. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/chat_clients/groq_client.py +0 -0
  14. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/chat_clients/local_client.py +0 -0
  15. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/chat_clients/minimax_client.py +0 -0
  16. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/chat_clients/mistral_client.py +0 -0
  17. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/chat_clients/moonshot_client.py +0 -0
  18. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/chat_clients/openai_client.py +0 -0
  19. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/chat_clients/qwen_client.py +0 -0
  20. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/chat_clients/yi_client.py +0 -0
  21. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/chat_clients/zhipuai_client.py +0 -0
  22. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/settings/__init__.py +0 -0
  23. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/types/defaults.py +0 -0
  24. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/types/enums.py +0 -0
  25. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/types/llm_parameters.py +0 -0
  26. {vectorvein-0.1.0 → vectorvein-0.1.2}/src/vectorvein/utilities/media_processing.py +0 -0
  27. {vectorvein-0.1.0 → vectorvein-0.1.2}/tests/__init__.py +0 -0
  28. {vectorvein-0.1.0 → vectorvein-0.1.2}/tests/cat.png +0 -0
  29. {vectorvein-0.1.0 → vectorvein-0.1.2}/tests/sample_settings.py +0 -0
  30. {vectorvein-0.1.0 → vectorvein-0.1.2}/tests/test_format_messages.py +0 -0
  31. {vectorvein-0.1.0 → vectorvein-0.1.2}/tests/test_image_input_chat_client.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vectorvein
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Default template for PDM package
5
5
  Author-Email: Anderson <andersonby@163.com>
6
6
  License: MIT
7
- Requires-Python: >=3.8
7
+ Requires-Python: >=3.10
8
8
  Requires-Dist: openai>=1.37.1
9
9
  Requires-Dist: tiktoken>=0.7.0
10
10
  Requires-Dist: httpx>=0.27.0
@@ -1,7 +1,4 @@
1
1
  [project]
2
- name = "vectorvein"
3
- version = "0.1.0"
4
- description = "Default template for PDM package"
5
2
  authors = [
6
3
  { name = "Anderson", email = "andersonby@163.com" },
7
4
  ]
@@ -13,17 +10,20 @@ dependencies = [
13
10
  "pydantic>=2.8.2",
14
11
  "Pillow>=10.4.0",
15
12
  ]
16
- requires-python = ">=3.8"
13
+ description = "Default template for PDM package"
14
+ name = "vectorvein"
17
15
  readme = "README.md"
16
+ requires-python = ">=3.10"
17
+ version = "0.1.2"
18
18
 
19
19
  [project.license]
20
20
  text = "MIT"
21
21
 
22
22
  [build-system]
23
+ build-backend = "pdm.backend"
23
24
  requires = [
24
25
  "pdm-backend",
25
26
  ]
26
- build-backend = "pdm.backend"
27
27
 
28
28
  [tool.pdm]
29
29
  distribution = true
@@ -17,7 +17,7 @@ from .deepseek_client import DeepSeekChatClient, AsyncDeepSeekChatClient
17
17
  from ..types import defaults as defs
18
18
  from ..types.enums import BackendType, ContextLengthControlType
19
19
  from .anthropic_client import AnthropicChatClient, AsyncAnthropicChatClient
20
- from .utils import format_messages
20
+ from .utils import format_messages, get_token_counts, ToolCallContentProcessor
21
21
 
22
22
 
23
23
  BackendMap = {
@@ -103,8 +103,10 @@ def create_async_chat_client(
103
103
 
104
104
 
105
105
  __all__ = [
106
+ "BackendType",
107
+ "format_messages",
108
+ "get_token_counts",
106
109
  "create_chat_client",
107
110
  "create_async_chat_client",
108
- "format_messages",
109
- "BackendType",
111
+ "ToolCallContentProcessor",
110
112
  ]
@@ -11,9 +11,8 @@ from openai import OpenAI, AsyncOpenAI, AzureOpenAI, AsyncAzureOpenAI
11
11
 
12
12
  from .base_client import BaseChatClient, BaseAsyncChatClient
13
13
  from .utils import (
14
- tool_use_re,
15
14
  cutoff_messages,
16
- extract_tool_calls,
15
+ ToolCallContentProcessor,
17
16
  generate_tool_use_system_prompt,
18
17
  )
19
18
  from ..settings import settings
@@ -120,13 +119,15 @@ class OpenAICompatibleChatClient(BaseChatClient):
120
119
  for chunk in response:
121
120
  if len(chunk.choices) == 0:
122
121
  continue
122
+ if not chunk.choices[0].delta:
123
+ continue
123
124
  if self.model_setting.function_call_available:
124
125
  yield chunk.choices[0].delta.model_dump()
125
126
  else:
126
127
  message = chunk.choices[0].delta.model_dump()
127
128
  full_content += message["content"] if message["content"] else ""
128
129
  if tools:
129
- tool_call_data = extract_tool_calls(full_content)
130
+ tool_call_data = ToolCallContentProcessor(result["content"]).tool_calls
130
131
  if tool_call_data:
131
132
  message["tool_calls"] = tool_call_data["tool_calls"]
132
133
  if full_content in ("<", "<|", "<|▶", "<|▶|") or full_content.startswith("<|▶|>"):
@@ -149,10 +150,11 @@ class OpenAICompatibleChatClient(BaseChatClient):
149
150
  tool_call.model_dump() for tool_call in response.choices[0].message.tool_calls
150
151
  ]
151
152
  else:
152
- tool_call_data = extract_tool_calls(result["content"])
153
+ tool_call_content_processor = ToolCallContentProcessor(result["content"])
154
+ tool_call_data = tool_call_content_processor.tool_calls
153
155
  if tool_call_data:
154
156
  result["tool_calls"] = tool_call_data["tool_calls"]
155
- result["content"] = tool_use_re.sub("", result["content"])
157
+ result["content"] = tool_call_content_processor.non_tool_content
156
158
  return result
157
159
 
158
160
 
@@ -255,13 +257,15 @@ class AsyncOpenAICompatibleChatClient(BaseAsyncChatClient):
255
257
  async for chunk in response:
256
258
  if len(chunk.choices) == 0:
257
259
  continue
260
+ if not chunk.choices[0].delta:
261
+ continue
258
262
  if self.model_setting.function_call_available:
259
263
  yield chunk.choices[0].delta.model_dump()
260
264
  else:
261
265
  message = chunk.choices[0].delta.model_dump()
262
266
  full_content += message["content"] if message["content"] else ""
263
267
  if tools:
264
- tool_call_data = extract_tool_calls(full_content)
268
+ tool_call_data = ToolCallContentProcessor(result["content"]).tool_calls
265
269
  if tool_call_data:
266
270
  message["tool_calls"] = tool_call_data["tool_calls"]
267
271
  if full_content in ("<", "<|", "<|▶", "<|▶|") or full_content.startswith("<|▶|>"):
@@ -284,8 +288,9 @@ class AsyncOpenAICompatibleChatClient(BaseAsyncChatClient):
284
288
  tool_call.model_dump() for tool_call in response.choices[0].message.tool_calls
285
289
  ]
286
290
  else:
287
- tool_call_data = extract_tool_calls(result["content"])
291
+ tool_call_content_processor = ToolCallContentProcessor(result["content"])
292
+ tool_call_data = tool_call_content_processor.tool_calls
288
293
  if tool_call_data:
289
294
  result["tool_calls"] = tool_call_data["tool_calls"]
290
- result["content"] = tool_use_re.sub("", result["content"])
295
+ result["content"] = tool_call_content_processor.non_tool_content
291
296
  return result
@@ -12,7 +12,49 @@ from ..utilities.media_processing import ImageProcessor
12
12
  chatgpt_encoding = tiktoken.encoding_for_model("gpt-3.5-turbo")
13
13
  gpt_4o_encoding = tiktoken.encoding_for_model("gpt-4o")
14
14
 
15
- tool_use_re = re.compile(r"<\|▶\|>(.*?)<\|◀\|>", re.DOTALL)
15
+
16
+ class ToolCallContentProcessor:
17
+ tool_use_re = re.compile(r"<\|▶\|>(.*?)<\|◀\|>", re.DOTALL)
18
+
19
+ def __init__(self, content: str):
20
+ self.content = content
21
+
22
+ @property
23
+ def non_tool_content(self):
24
+ return re.sub(self.tool_use_re, "", self.content).strip()
25
+
26
+ @property
27
+ def tool_calls(self):
28
+ if "<|▶|>" not in self.content or "<|◀|>" not in self.content:
29
+ return {}
30
+ tool_calls_matches = re.findall(self.tool_use_re, self.content)
31
+ if tool_calls_matches:
32
+ tool_call_data = {}
33
+ for match in tool_calls_matches:
34
+ try:
35
+ tool_call_data = json.loads(match)
36
+ except json.JSONDecodeError:
37
+ print(f"Failed to parse tool call data:\nContent: {self.content}\nMatch: {match}")
38
+
39
+ if not tool_call_data:
40
+ return {}
41
+
42
+ arguments = json.dumps(tool_call_data["arguments"], ensure_ascii=False)
43
+ return {
44
+ "tool_calls": [
45
+ {
46
+ "index": 0,
47
+ "id": "fc1",
48
+ "function": {
49
+ "arguments": arguments,
50
+ "name": tool_call_data["name"],
51
+ },
52
+ "type": "function",
53
+ }
54
+ ]
55
+ }
56
+ else:
57
+ return {}
16
58
 
17
59
 
18
60
  def get_assistant_role_key(backend: BackendType) -> str:
@@ -48,7 +90,7 @@ def convert_type(value, value_type):
48
90
  return value # 如果类型未知,返回原始值
49
91
 
50
92
 
51
- def get_token_counts(text: str, model: str = "") -> int:
93
+ def get_token_counts(text: str | dict, model: str = "") -> int:
52
94
  if not isinstance(text, str):
53
95
  text = str(text)
54
96
  if model == "gpt-3.5-turbo":
@@ -396,189 +438,6 @@ def format_text_message(content, role, attachments, backend, native_multimodal):
396
438
  return {"role": role, "content": content}
397
439
 
398
440
 
399
- def format_messages_v1(
400
- messages: list, backend: BackendType = BackendType.OpenAI, native_multimodal: bool = False
401
- ) -> list:
402
- """将 VectorVein 的 Message 序列化后的格式转换为不同模型支持的格式
403
-
404
- Args:
405
- messages (list): VectorVein messages list.
406
- backend (str, optional): Messages format target backend. Defaults to BackendType.OpenAI.
407
- native_multimodal (bool, optional): Use native multimodal ability. Defaults to False.
408
-
409
- Returns:
410
- list: _description_
411
- """
412
-
413
- backend = backend.lower()
414
- formatted_messages = []
415
- for message in messages:
416
- content = message["content"]["text"]
417
- if message["content_type"] == "TXT":
418
- role = "user" if message["author_type"] == "U" else get_assistant_role_key(backend)
419
- if not message.get("attachments"):
420
- if backend == BackendType.Gemini:
421
- formatted_message = {"role": role, "parts": [{"text": content}]}
422
- else:
423
- formatted_message = {"role": role, "content": content}
424
- formatted_messages.append(formatted_message)
425
- continue
426
-
427
- images_extensions = ("jpg", "jpeg", "png", "bmp")
428
- has_images = any(attachment.lower().endswith(images_extensions) for attachment in message["attachments"])
429
-
430
- content += "\n# Attachments:\n"
431
- content += "\n".join([f"- {attachment}" for attachment in message["attachments"]])
432
-
433
- if native_multimodal and has_images:
434
- if backend == BackendType.Gemini:
435
- parts = [{"text": content}]
436
- for attachment in message["attachments"]:
437
- if attachment.lower().endswith(images_extensions):
438
- parts.append(format_image_message(image=attachment, backend=backend))
439
- formatted_message = {"role": role, "parts": parts}
440
- else:
441
- formatted_message = {
442
- "role": role,
443
- "content": [
444
- {"type": "text", "text": content},
445
- *[
446
- format_image_message(image=attachment, backend=backend)
447
- for attachment in message["attachments"]
448
- if attachment.lower().endswith(images_extensions)
449
- ],
450
- ],
451
- }
452
- formatted_messages.append(formatted_message)
453
- else:
454
- if backend == BackendType.Gemini:
455
- formatted_message = {"role": role, "parts": [{"text": content}]}
456
- else:
457
- formatted_message = {"role": role, "content": content}
458
- formatted_messages.append(formatted_message)
459
- elif message["content_type"] == "WKF" and message["status"] in ("S", "R"):
460
- # TODO: 目前只考虑单个 tool_call 的情况
461
- if backend in (BackendType.OpenAI, BackendType.ZhiPuAI, BackendType.Mistral):
462
- tool_call_message = {
463
- "content": None,
464
- "role": "assistant",
465
- "tool_calls": [
466
- {
467
- "id": message["metadata"]["selected_workflow"]["tool_call_id"],
468
- "type": "function",
469
- "function": {
470
- "name": message["metadata"]["selected_workflow"]["function_name"],
471
- "arguments": json.dumps(message["metadata"]["selected_workflow"]["params"]),
472
- },
473
- }
474
- ],
475
- }
476
- elif backend == BackendType.Anthropic:
477
- tool_call_message = {
478
- "role": "assistant",
479
- "content": [
480
- {
481
- "type": "tool_use",
482
- "id": message["metadata"]["selected_workflow"]["tool_call_id"],
483
- "name": message["metadata"]["selected_workflow"]["function_name"],
484
- "input": message["metadata"]["selected_workflow"]["params"],
485
- },
486
- ],
487
- }
488
- if content:
489
- tool_call_message["content"].insert(
490
- 0,
491
- {
492
- "type": "text",
493
- "text": content,
494
- },
495
- )
496
- elif backend == BackendType.Gemini:
497
- tool_call_message = {
498
- "role": "model",
499
- "parts": [
500
- {
501
- "functionCall": {
502
- "name": message["metadata"]["selected_workflow"]["function_name"],
503
- "args": message["metadata"]["selected_workflow"]["params"],
504
- }
505
- },
506
- ],
507
- }
508
- if content:
509
- tool_call_message["parts"].insert(
510
- 0,
511
- {
512
- "text": content,
513
- },
514
- )
515
- else:
516
- tool_call_message = {
517
- "content": json.dumps(
518
- {
519
- "name": message["metadata"]["selected_workflow"]["function_name"],
520
- "arguments": json.dumps(message["metadata"]["selected_workflow"]["params"]),
521
- },
522
- ensure_ascii=False,
523
- ),
524
- "role": "assistant",
525
- }
526
- formatted_messages.append(tool_call_message)
527
-
528
- if backend in (BackendType.OpenAI, BackendType.ZhiPuAI, BackendType.Mistral):
529
- tool_call_result_message = {
530
- "role": "tool",
531
- "tool_call_id": message["metadata"]["selected_workflow"]["tool_call_id"],
532
- "name": message["metadata"]["selected_workflow"]["function_name"],
533
- "content": message["metadata"].get("workflow_result", ""),
534
- }
535
- elif backend == BackendType.Anthropic:
536
- tool_call_result_message = {
537
- "role": "user",
538
- "content": [
539
- {
540
- "type": "tool_result",
541
- "tool_use_id": message["metadata"]["selected_workflow"]["tool_call_id"],
542
- "content": message["metadata"].get("workflow_result", ""),
543
- }
544
- ],
545
- }
546
- elif backend == BackendType.Gemini:
547
- tool_call_result_message = {
548
- "role": "function",
549
- "parts": [
550
- {
551
- "functionResponse": {
552
- "name": message["metadata"]["selected_workflow"]["function_name"],
553
- "response": {
554
- "name": message["metadata"]["selected_workflow"]["function_name"],
555
- "content": message["metadata"].get("workflow_result", ""),
556
- },
557
- }
558
- }
559
- ],
560
- }
561
- else:
562
- tool_call_result_message = {
563
- "role": "user",
564
- "content": json.dumps(
565
- {
566
- "function": message["metadata"]["selected_workflow"]["function_name"],
567
- "result": message["metadata"].get("workflow_result", ""),
568
- },
569
- ensure_ascii=False,
570
- ),
571
- }
572
- formatted_messages.append(tool_call_result_message)
573
-
574
- if content and backend not in (BackendType.Mistral, BackendType.Anthropic, BackendType.Gemini):
575
- formatted_messages.append({"role": "assistant", "content": content})
576
- else:
577
- continue
578
-
579
- return formatted_messages
580
-
581
-
582
441
  def generate_tool_use_system_prompt(tools: list, format_type: str = "json") -> str:
583
442
  if format_type == "json":
584
443
  return (
@@ -600,36 +459,3 @@ def generate_tool_use_system_prompt(tools: list, format_type: str = "json") -> s
600
459
  "## Output format\n<|▶|><invoke><tool_name>[function name:str]</tool_name><parameters><parameter_1_name>[parameter_1_value]</parameter_1_name><parameter_2_name>[parameter_2_value]</parameter_2_name>...</parameters></invoke><|◀|>\n\n"
601
460
  "## Example output\n<|▶|><invoke><tool_name>calculator</tool_name><parameters><first_operand>1984135</first_operand><second_operand>9343116</second_operand><operator>*</operator></parameters></invoke><|◀|>"
602
461
  )
603
-
604
-
605
- def extract_tool_calls(content: str) -> dict:
606
- if "<|▶|>" not in content or "<|◀|>" not in content:
607
- return {}
608
- tool_calls_matches = tool_use_re.findall(content)
609
- if tool_calls_matches:
610
- tool_call_data = {}
611
- for match in tool_calls_matches:
612
- try:
613
- tool_call_data = json.loads(match)
614
- except json.JSONDecodeError:
615
- print(f"Failed to parse tool call data:\nContent: {content}\nMatch: {match}")
616
-
617
- if not tool_call_data:
618
- return {}
619
-
620
- arguments = json.dumps(tool_call_data["arguments"], ensure_ascii=False)
621
- return {
622
- "tool_calls": [
623
- {
624
- "index": 0,
625
- "id": "fc1",
626
- "function": {
627
- "arguments": arguments,
628
- "name": tool_call_data["name"],
629
- },
630
- "type": "function",
631
- }
632
- ]
633
- }
634
- else:
635
- return {}
@@ -181,14 +181,16 @@ backend = BackendType.MiniMax
181
181
  model = "abab6.5s-chat"
182
182
  backend = BackendType.Gemini
183
183
  model = "gemini-1.5-flash"
184
+ backend = BackendType.OpenAI
185
+ model = "gpt-35-turbo"
184
186
  start_time = time.perf_counter()
185
187
  # test_sync(backend=backend, model=model, stream=False, use_tool=False)
186
- test_sync(backend=backend, model=model, stream=False, use_tool=True)
188
+ # test_sync(backend=backend, model=model, stream=False, use_tool=True)
187
189
  # test_sync(backend=backend, model=model, stream=True, use_tool=False)
188
- test_sync(backend=backend, model=model, stream=True, use_tool=True)
190
+ # test_sync(backend=backend, model=model, stream=True, use_tool=True)
189
191
  # asyncio.run(test_async(backend=backend, model=model, stream=False, use_tool=False))
190
- asyncio.run(test_async(backend=backend, model=model, stream=False, use_tool=True))
191
- # asyncio.run(test_async(backend=backend, model=model, stream=True, use_tool=False))
192
- asyncio.run(test_async(backend=backend, model=model, stream=True, use_tool=True))
192
+ # asyncio.run(test_async(backend=backend, model=model, stream=False, use_tool=True))
193
+ asyncio.run(test_async(backend=backend, model=model, stream=True, use_tool=False))
194
+ # asyncio.run(test_async(backend=backend, model=model, stream=True, use_tool=True))
193
195
  end_time = time.perf_counter()
194
196
  print(f"Stream time elapsed: {end_time - start_time} seconds")
File without changes
File without changes
File without changes