camel-ai 0.2.21__py3-none-any.whl → 0.2.23a0__py3-none-any.whl

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.

Potentially problematic release.


This version of camel-ai might be problematic. Click here for more details.

Files changed (106) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/_types.py +41 -0
  3. camel/agents/_utils.py +188 -0
  4. camel/agents/chat_agent.py +556 -965
  5. camel/agents/knowledge_graph_agent.py +7 -1
  6. camel/agents/multi_hop_generator_agent.py +1 -1
  7. camel/configs/base_config.py +10 -13
  8. camel/configs/deepseek_config.py +4 -30
  9. camel/configs/gemini_config.py +5 -31
  10. camel/configs/openai_config.py +14 -32
  11. camel/configs/qwen_config.py +36 -36
  12. camel/datagen/self_improving_cot.py +79 -1
  13. camel/datagen/self_instruct/filter/instruction_filter.py +19 -3
  14. camel/datagen/self_instruct/self_instruct.py +7 -2
  15. camel/datasets/__init__.py +28 -0
  16. camel/datasets/base.py +969 -0
  17. camel/embeddings/openai_embedding.py +10 -1
  18. camel/environments/__init__.py +16 -0
  19. camel/environments/base.py +503 -0
  20. camel/extractors/__init__.py +16 -0
  21. camel/extractors/base.py +263 -0
  22. camel/interpreters/docker/Dockerfile +12 -0
  23. camel/interpreters/docker_interpreter.py +19 -1
  24. camel/interpreters/subprocess_interpreter.py +42 -17
  25. camel/loaders/__init__.py +2 -0
  26. camel/loaders/mineru_extractor.py +250 -0
  27. camel/memories/agent_memories.py +16 -1
  28. camel/memories/blocks/chat_history_block.py +10 -2
  29. camel/memories/blocks/vectordb_block.py +1 -0
  30. camel/memories/context_creators/score_based.py +20 -3
  31. camel/memories/records.py +10 -0
  32. camel/messages/base.py +8 -8
  33. camel/models/_utils.py +57 -0
  34. camel/models/aiml_model.py +48 -17
  35. camel/models/anthropic_model.py +41 -3
  36. camel/models/azure_openai_model.py +39 -3
  37. camel/models/base_model.py +132 -4
  38. camel/models/cohere_model.py +88 -11
  39. camel/models/deepseek_model.py +107 -63
  40. camel/models/gemini_model.py +133 -15
  41. camel/models/groq_model.py +72 -10
  42. camel/models/internlm_model.py +14 -3
  43. camel/models/litellm_model.py +9 -2
  44. camel/models/mistral_model.py +42 -5
  45. camel/models/model_manager.py +48 -3
  46. camel/models/moonshot_model.py +33 -4
  47. camel/models/nemotron_model.py +32 -3
  48. camel/models/nvidia_model.py +43 -3
  49. camel/models/ollama_model.py +139 -17
  50. camel/models/openai_audio_models.py +7 -1
  51. camel/models/openai_compatible_model.py +37 -3
  52. camel/models/openai_model.py +158 -46
  53. camel/models/qwen_model.py +61 -4
  54. camel/models/reka_model.py +53 -3
  55. camel/models/samba_model.py +209 -4
  56. camel/models/sglang_model.py +153 -14
  57. camel/models/siliconflow_model.py +16 -3
  58. camel/models/stub_model.py +46 -4
  59. camel/models/togetherai_model.py +38 -3
  60. camel/models/vllm_model.py +37 -3
  61. camel/models/yi_model.py +36 -3
  62. camel/models/zhipuai_model.py +38 -3
  63. camel/retrievers/__init__.py +3 -0
  64. camel/retrievers/hybrid_retrival.py +237 -0
  65. camel/toolkits/__init__.py +9 -2
  66. camel/toolkits/arxiv_toolkit.py +2 -1
  67. camel/toolkits/ask_news_toolkit.py +4 -2
  68. camel/toolkits/base.py +22 -3
  69. camel/toolkits/code_execution.py +2 -0
  70. camel/toolkits/dappier_toolkit.py +2 -1
  71. camel/toolkits/data_commons_toolkit.py +38 -12
  72. camel/toolkits/function_tool.py +13 -0
  73. camel/toolkits/github_toolkit.py +5 -1
  74. camel/toolkits/google_maps_toolkit.py +2 -1
  75. camel/toolkits/google_scholar_toolkit.py +2 -0
  76. camel/toolkits/human_toolkit.py +0 -3
  77. camel/toolkits/linkedin_toolkit.py +3 -2
  78. camel/toolkits/meshy_toolkit.py +3 -2
  79. camel/toolkits/mineru_toolkit.py +178 -0
  80. camel/toolkits/networkx_toolkit.py +240 -0
  81. camel/toolkits/notion_toolkit.py +2 -0
  82. camel/toolkits/openbb_toolkit.py +3 -2
  83. camel/toolkits/reddit_toolkit.py +11 -3
  84. camel/toolkits/retrieval_toolkit.py +6 -1
  85. camel/toolkits/semantic_scholar_toolkit.py +2 -1
  86. camel/toolkits/stripe_toolkit.py +8 -2
  87. camel/toolkits/sympy_toolkit.py +44 -1
  88. camel/toolkits/video_toolkit.py +2 -0
  89. camel/toolkits/whatsapp_toolkit.py +3 -2
  90. camel/toolkits/zapier_toolkit.py +191 -0
  91. camel/types/__init__.py +2 -2
  92. camel/types/agents/__init__.py +16 -0
  93. camel/types/agents/tool_calling_record.py +52 -0
  94. camel/types/enums.py +3 -0
  95. camel/types/openai_types.py +16 -14
  96. camel/utils/__init__.py +2 -1
  97. camel/utils/async_func.py +2 -2
  98. camel/utils/commons.py +114 -1
  99. camel/verifiers/__init__.py +23 -0
  100. camel/verifiers/base.py +340 -0
  101. camel/verifiers/models.py +82 -0
  102. camel/verifiers/python_verifier.py +202 -0
  103. {camel_ai-0.2.21.dist-info → camel_ai-0.2.23a0.dist-info}/METADATA +273 -256
  104. {camel_ai-0.2.21.dist-info → camel_ai-0.2.23a0.dist-info}/RECORD +106 -85
  105. {camel_ai-0.2.21.dist-info → camel_ai-0.2.23a0.dist-info}/WHEEL +1 -1
  106. {camel_ai-0.2.21.dist-info → camel_ai-0.2.23a0.dist-info}/LICENSE +0 -0
@@ -13,13 +13,15 @@
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
14
 
15
15
  import os
16
- from typing import Any, Dict, List, Optional, Union
16
+ from typing import Any, Dict, List, Optional, Type, Union
17
17
 
18
- from openai import OpenAI, Stream
18
+ from openai import AsyncOpenAI, AsyncStream, OpenAI, Stream
19
+ from pydantic import BaseModel
19
20
 
20
21
  from camel.configs import QWEN_API_PARAMS, QwenConfig
21
22
  from camel.messages import OpenAIMessage
22
23
  from camel.models import BaseModelBackend
24
+ from camel.models._utils import try_modify_message_with_format
23
25
  from camel.types import (
24
26
  ChatCompletion,
25
27
  ChatCompletionChunk,
@@ -81,10 +83,46 @@ class QwenModel(BaseModelBackend):
81
83
  api_key=self._api_key,
82
84
  base_url=self._url,
83
85
  )
86
+ self._async_client = AsyncOpenAI(
87
+ timeout=180,
88
+ max_retries=3,
89
+ api_key=self._api_key,
90
+ base_url=self._url,
91
+ )
92
+
93
+ async def _arun(
94
+ self,
95
+ messages: List[OpenAIMessage],
96
+ response_format: Optional[Type[BaseModel]] = None,
97
+ tools: Optional[List[Dict[str, Any]]] = None,
98
+ ) -> Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]:
99
+ r"""Runs inference of Qwen chat completion.
100
+
101
+ Args:
102
+ messages (List[OpenAIMessage]): Message list with the chat history
103
+ in OpenAI API format.
104
+
105
+ Returns:
106
+ Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]:
107
+ `ChatCompletion` in the non-stream mode, or
108
+ `AsyncStream[ChatCompletionChunk]` in the stream mode.
109
+ """
110
+ request_config = self._prepare_request(
111
+ messages, response_format, tools
112
+ )
113
+
114
+ response = await self._async_client.chat.completions.create(
115
+ messages=messages,
116
+ model=self.model_type,
117
+ **request_config,
118
+ )
119
+ return response
84
120
 
85
- def run(
121
+ def _run(
86
122
  self,
87
123
  messages: List[OpenAIMessage],
124
+ response_format: Optional[Type[BaseModel]] = None,
125
+ tools: Optional[List[Dict[str, Any]]] = None,
88
126
  ) -> Union[ChatCompletion, Stream[ChatCompletionChunk]]:
89
127
  r"""Runs inference of Qwen chat completion.
90
128
 
@@ -97,13 +135,32 @@ class QwenModel(BaseModelBackend):
97
135
  `ChatCompletion` in the non-stream mode, or
98
136
  `Stream[ChatCompletionChunk]` in the stream mode.
99
137
  """
138
+ request_config = self._prepare_request(
139
+ messages, response_format, tools
140
+ )
141
+
100
142
  response = self._client.chat.completions.create(
101
143
  messages=messages,
102
144
  model=self.model_type,
103
- **self.model_config_dict,
145
+ **request_config,
104
146
  )
105
147
  return response
106
148
 
149
+ def _prepare_request(
150
+ self,
151
+ messages: List[OpenAIMessage],
152
+ response_format: Optional[Type[BaseModel]] = None,
153
+ tools: Optional[List[Dict[str, Any]]] = None,
154
+ ) -> Dict[str, Any]:
155
+ request_config = self.model_config_dict.copy()
156
+ if tools:
157
+ request_config["tools"] = tools
158
+ elif response_format:
159
+ try_modify_message_with_format(messages[-1], response_format)
160
+ request_config["response_format"] = {"type": "json_object"}
161
+
162
+ return request_config
163
+
107
164
  @property
108
165
  def token_counter(self) -> BaseTokenCounter:
109
166
  r"""Initialize the token counter for the model backend.
@@ -11,7 +11,9 @@
11
11
  # See the License for the specific language governing permissions and
12
12
  # limitations under the License.
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
14
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union
15
+
16
+ from pydantic import BaseModel
15
17
 
16
18
  from camel.configs import REKA_API_PARAMS, RekaConfig
17
19
  from camel.messages import OpenAIMessage
@@ -70,7 +72,7 @@ class RekaModel(BaseModelBackend):
70
72
  url: Optional[str] = None,
71
73
  token_counter: Optional[BaseTokenCounter] = None,
72
74
  ) -> None:
73
- from reka.client import Reka
75
+ from reka.client import AsyncReka, Reka
74
76
 
75
77
  if model_config_dict is None:
76
78
  model_config_dict = RekaConfig().as_dict()
@@ -80,6 +82,9 @@ class RekaModel(BaseModelBackend):
80
82
  model_type, model_config_dict, api_key, url, token_counter
81
83
  )
82
84
  self._client = Reka(api_key=self._api_key, base_url=self._url)
85
+ self._async_client = AsyncReka(
86
+ api_key=self._api_key, base_url=self._url
87
+ )
83
88
 
84
89
  def _convert_reka_to_openai_response(
85
90
  self, response: 'ChatResponse'
@@ -117,6 +122,8 @@ class RekaModel(BaseModelBackend):
117
122
  def _convert_openai_to_reka_messages(
118
123
  self,
119
124
  messages: List[OpenAIMessage],
125
+ response_format: Optional[Type[BaseModel]] = None,
126
+ tools: Optional[List[str]] = None,
120
127
  ) -> List["ChatMessage"]:
121
128
  r"""Converts OpenAI API messages to Reka API messages.
122
129
 
@@ -173,9 +180,52 @@ class RekaModel(BaseModelBackend):
173
180
  )
174
181
  return self._token_counter
175
182
 
176
- def run(
183
+ async def _arun(
184
+ self,
185
+ messages: List[OpenAIMessage],
186
+ response_format: Optional[Type[BaseModel]] = None,
187
+ tools: Optional[List[Dict[str, Any]]] = None,
188
+ ) -> ChatCompletion:
189
+ r"""Runs inference of Mistral chat completion.
190
+
191
+ Args:
192
+ messages (List[OpenAIMessage]): Message list with the chat history
193
+ in OpenAI API format.
194
+
195
+ Returns:
196
+ ChatCompletion.
197
+ """
198
+ reka_messages = self._convert_openai_to_reka_messages(messages)
199
+
200
+ response = await self._async_client.chat.create(
201
+ messages=reka_messages,
202
+ model=self.model_type,
203
+ **self.model_config_dict,
204
+ )
205
+
206
+ openai_response = self._convert_reka_to_openai_response(response)
207
+
208
+ # Add AgentOps LLM Event tracking
209
+ if LLMEvent:
210
+ llm_event = LLMEvent(
211
+ thread_id=openai_response.id,
212
+ prompt=" ".join(
213
+ [message.get("content") for message in messages] # type: ignore[misc]
214
+ ),
215
+ prompt_tokens=openai_response.usage.input_tokens, # type: ignore[union-attr]
216
+ completion=openai_response.choices[0].message.content,
217
+ completion_tokens=openai_response.usage.output_tokens, # type: ignore[union-attr]
218
+ model=self.model_type,
219
+ )
220
+ record(llm_event)
221
+
222
+ return openai_response
223
+
224
+ def _run(
177
225
  self,
178
226
  messages: List[OpenAIMessage],
227
+ response_format: Optional[Type[BaseModel]] = None,
228
+ tools: Optional[List[Dict[str, Any]]] = None,
179
229
  ) -> ChatCompletion:
180
230
  r"""Runs inference of Mistral chat completion.
181
231
 
@@ -15,10 +15,11 @@ import json
15
15
  import os
16
16
  import time
17
17
  import uuid
18
- from typing import Any, Dict, List, Optional, Union
18
+ from typing import Any, Dict, List, Optional, Type, Union
19
19
 
20
20
  import httpx
21
- from openai import OpenAI, Stream
21
+ from openai import AsyncOpenAI, AsyncStream, OpenAI, Stream
22
+ from pydantic import BaseModel
22
23
 
23
24
  from camel.configs import (
24
25
  SAMBA_CLOUD_API_PARAMS,
@@ -105,6 +106,12 @@ class SambaModel(BaseModelBackend):
105
106
  base_url=self._url,
106
107
  api_key=self._api_key,
107
108
  )
109
+ self._async_client = AsyncOpenAI(
110
+ timeout=180,
111
+ max_retries=3,
112
+ base_url=self._url,
113
+ api_key=self._api_key,
114
+ )
108
115
 
109
116
  @property
110
117
  def token_counter(self) -> BaseTokenCounter:
@@ -148,8 +155,35 @@ class SambaModel(BaseModelBackend):
148
155
  " SambaNova service"
149
156
  )
150
157
 
151
- def run( # type: ignore[misc]
152
- self, messages: List[OpenAIMessage]
158
+ async def _arun( # type: ignore[misc]
159
+ self,
160
+ messages: List[OpenAIMessage],
161
+ response_format: Optional[Type[BaseModel]] = None,
162
+ tools: Optional[List[Dict[str, Any]]] = None,
163
+ ) -> Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]:
164
+ r"""Runs SambaNova's service.
165
+
166
+ Args:
167
+ messages (List[OpenAIMessage]): Message list with the chat history
168
+ in OpenAI API format.
169
+
170
+ Returns:
171
+ Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]:
172
+ `ChatCompletion` in the non-stream mode, or
173
+ `AsyncStream[ChatCompletionChunk]` in the stream mode.
174
+ """
175
+ if "tools" in self.model_config_dict:
176
+ del self.model_config_dict["tools"]
177
+ if self.model_config_dict.get("stream") is True:
178
+ return await self._arun_streaming(messages)
179
+ else:
180
+ return await self._arun_non_streaming(messages)
181
+
182
+ def _run( # type: ignore[misc]
183
+ self,
184
+ messages: List[OpenAIMessage],
185
+ response_format: Optional[Type[BaseModel]] = None,
186
+ tools: Optional[List[Dict[str, Any]]] = None,
153
187
  ) -> Union[ChatCompletion, Stream[ChatCompletionChunk]]:
154
188
  r"""Runs SambaNova's service.
155
189
 
@@ -398,3 +432,174 @@ class SambaModel(BaseModelBackend):
398
432
  bool: Whether the model is in stream mode.
399
433
  """
400
434
  return self.model_config_dict.get('stream', False)
435
+
436
+ async def _arun_streaming(
437
+ self, messages: List[OpenAIMessage]
438
+ ) -> AsyncStream[ChatCompletionChunk]:
439
+ r"""Handles streaming inference with SambaNova's API.
440
+
441
+ Args:
442
+ messages (List[OpenAIMessage]): A list of messages representing the
443
+ chat history in OpenAI API format.
444
+
445
+ Returns:
446
+ AsyncStream[ChatCompletionChunk]: A generator yielding
447
+ `ChatCompletionChunk` objects as they are received from the
448
+ API.
449
+
450
+ Raises:
451
+ RuntimeError: If the HTTP request fails.
452
+ ValueError: If the API doesn't support stream mode.
453
+ """
454
+ # Handle SambaNova's Cloud API
455
+ if self._url == "https://api.sambanova.ai/v1":
456
+ response = await self._async_client.chat.completions.create(
457
+ messages=messages,
458
+ model=self.model_type,
459
+ **self.model_config_dict,
460
+ )
461
+
462
+ # Add AgentOps LLM Event tracking
463
+ if LLMEvent:
464
+ llm_event = LLMEvent(
465
+ thread_id=response.id,
466
+ prompt=" ".join(
467
+ [message.get("content") for message in messages] # type: ignore[misc]
468
+ ),
469
+ prompt_tokens=response.usage.prompt_tokens, # type: ignore[union-attr]
470
+ completion=response.choices[0].message.content,
471
+ completion_tokens=response.usage.completion_tokens, # type: ignore[union-attr]
472
+ model=self.model_type,
473
+ )
474
+ record(llm_event)
475
+
476
+ return response
477
+
478
+ elif self._url == "https://sambaverse.sambanova.ai/api/predict":
479
+ raise ValueError(
480
+ "https://sambaverse.sambanova.ai/api/predict doesn't support"
481
+ " stream mode"
482
+ )
483
+ raise RuntimeError(f"Unknown URL: {self._url}")
484
+
485
+ async def _arun_non_streaming(
486
+ self, messages: List[OpenAIMessage]
487
+ ) -> ChatCompletion:
488
+ r"""Handles non-streaming inference with SambaNova's API.
489
+
490
+ Args:
491
+ messages (List[OpenAIMessage]): A list of messages representing the
492
+ message in OpenAI API format.
493
+
494
+ Returns:
495
+ ChatCompletion: A `ChatCompletion` object containing the complete
496
+ response from the API.
497
+
498
+ Raises:
499
+ RuntimeError: If the HTTP request fails.
500
+ ValueError: If the JSON response cannot be decoded or is missing
501
+ expected data.
502
+ """
503
+ # Handle SambaNova's Cloud API
504
+ if self._url == "https://api.sambanova.ai/v1":
505
+ response = await self._async_client.chat.completions.create(
506
+ messages=messages,
507
+ model=self.model_type,
508
+ **self.model_config_dict,
509
+ )
510
+
511
+ # Add AgentOps LLM Event tracking
512
+ if LLMEvent:
513
+ llm_event = LLMEvent(
514
+ thread_id=response.id,
515
+ prompt=" ".join(
516
+ [message.get("content") for message in messages] # type: ignore[misc]
517
+ ),
518
+ prompt_tokens=response.usage.prompt_tokens, # type: ignore[union-attr]
519
+ completion=response.choices[0].message.content,
520
+ completion_tokens=response.usage.completion_tokens, # type: ignore[union-attr]
521
+ model=self.model_type,
522
+ )
523
+ record(llm_event)
524
+
525
+ return response
526
+
527
+ # Handle SambaNova's Sambaverse API
528
+ else:
529
+ headers = {
530
+ "Content-Type": "application/json",
531
+ "key": str(self._api_key),
532
+ "modelName": self.model_type,
533
+ }
534
+
535
+ data = {
536
+ "instance": json.dumps(
537
+ {
538
+ "conversation_id": str(uuid.uuid4()),
539
+ "messages": messages,
540
+ }
541
+ ),
542
+ "params": {
543
+ "do_sample": {"type": "bool", "value": "true"},
544
+ "max_tokens_to_generate": {
545
+ "type": "int",
546
+ "value": str(self.model_config_dict.get("max_tokens")),
547
+ },
548
+ "process_prompt": {"type": "bool", "value": "true"},
549
+ "repetition_penalty": {
550
+ "type": "float",
551
+ "value": str(
552
+ self.model_config_dict.get("repetition_penalty")
553
+ ),
554
+ },
555
+ "return_token_count_only": {
556
+ "type": "bool",
557
+ "value": "false",
558
+ },
559
+ "select_expert": {
560
+ "type": "str",
561
+ "value": self.model_type.split("/")[1],
562
+ },
563
+ "stop_sequences": {
564
+ "type": "str",
565
+ "value": self.model_config_dict.get("stop_sequences"),
566
+ },
567
+ "temperature": {
568
+ "type": "float",
569
+ "value": str(
570
+ self.model_config_dict.get("temperature")
571
+ ),
572
+ },
573
+ "top_k": {
574
+ "type": "int",
575
+ "value": str(self.model_config_dict.get("top_k")),
576
+ },
577
+ "top_p": {
578
+ "type": "float",
579
+ "value": str(self.model_config_dict.get("top_p")),
580
+ },
581
+ },
582
+ }
583
+
584
+ try:
585
+ # Send the request and handle the response
586
+ with httpx.Client() as client:
587
+ response = client.post(
588
+ self._url, # type: ignore[arg-type]
589
+ headers=headers,
590
+ json=data,
591
+ )
592
+
593
+ raw_text = response.text
594
+ # Split the string into two dictionaries
595
+ dicts = raw_text.split("}\n{")
596
+
597
+ # Keep only the last dictionary
598
+ last_dict = "{" + dicts[-1]
599
+
600
+ # Parse the dictionary
601
+ last_dict = json.loads(last_dict)
602
+ return self._sambaverse_to_openai_response(last_dict) # type: ignore[arg-type]
603
+
604
+ except httpx.HTTPStatusError:
605
+ raise RuntimeError(f"HTTP request failed: {raw_text}")
@@ -12,11 +12,13 @@
12
12
  # limitations under the License.
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
14
  import logging
15
+ import subprocess
15
16
  import threading
16
17
  import time
17
- from typing import Any, Dict, List, Optional, Union
18
+ from typing import Any, Dict, List, Optional, Type, Union
18
19
 
19
- from openai import OpenAI, Stream
20
+ from openai import AsyncOpenAI, AsyncStream, OpenAI, Stream
21
+ from pydantic import BaseModel
20
22
 
21
23
  from camel.configs import SGLANG_API_PARAMS, SGLangConfig
22
24
  from camel.messages import OpenAIMessage
@@ -85,13 +87,14 @@ class SGLangModel(BaseModelBackend):
85
87
  api_key="Set-but-ignored", # required but ignored
86
88
  base_url=self._url,
87
89
  )
90
+ self._async_client = AsyncOpenAI(
91
+ timeout=180,
92
+ max_retries=3,
93
+ api_key="Set-but-ignored", # required but ignored
94
+ base_url=self._url,
95
+ )
88
96
 
89
97
  def _start_server(self) -> None:
90
- from sglang.utils import ( # type: ignore[import-untyped]
91
- execute_shell_command,
92
- wait_for_server,
93
- )
94
-
95
98
  try:
96
99
  if not self._url:
97
100
  cmd = (
@@ -101,10 +104,10 @@ class SGLangModel(BaseModelBackend):
101
104
  f"--host 0.0.0.0"
102
105
  )
103
106
 
104
- server_process = execute_shell_command(cmd)
105
- wait_for_server("http://localhost:30000")
107
+ server_process = _execute_shell_command(cmd)
108
+ _wait_for_server("http://localhost:30000")
106
109
  self._url = "http://127.0.0.1:30000/v1"
107
- self.server_process = server_process
110
+ self.server_process = server_process # type: ignore[assignment]
108
111
  # Start the inactivity monitor in a background thread
109
112
  self._inactivity_thread = threading.Thread(
110
113
  target=self._monitor_inactivity, daemon=True
@@ -131,8 +134,6 @@ class SGLangModel(BaseModelBackend):
131
134
  r"""Monitor whether the server process has been inactive for over 10
132
135
  minutes.
133
136
  """
134
- from sglang.utils import terminate_process
135
-
136
137
  while True:
137
138
  # Check every 10 seconds
138
139
  time.sleep(10)
@@ -143,7 +144,7 @@ class SGLangModel(BaseModelBackend):
143
144
  time.time() - self.last_run_time > 600
144
145
  ):
145
146
  if self.server_process:
146
- terminate_process(self.server_process)
147
+ _terminate_process(self.server_process)
147
148
  self.server_process = None
148
149
  self._client = None # Invalidate the client
149
150
  logging.info(
@@ -178,9 +179,49 @@ class SGLangModel(BaseModelBackend):
178
179
  "input into SGLang model backend."
179
180
  )
180
181
 
181
- def run(
182
+ async def _arun(
182
183
  self,
183
184
  messages: List[OpenAIMessage],
185
+ response_format: Optional[Type[BaseModel]] = None,
186
+ tools: Optional[List[Dict[str, Any]]] = None,
187
+ ) -> Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]:
188
+ r"""Runs inference of OpenAI chat completion.
189
+
190
+ Args:
191
+ messages (List[OpenAIMessage]): Message list with the chat history
192
+ in OpenAI API format.
193
+
194
+ Returns:
195
+ Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]:
196
+ `ChatCompletion` in the non-stream mode, or
197
+ `AsyncStream[ChatCompletionChunk]` in the stream mode.
198
+ """
199
+
200
+ # Ensure server is running
201
+ self._ensure_server_running()
202
+
203
+ with self._lock:
204
+ # Update last run time
205
+ self.last_run_time = time.time()
206
+
207
+ if self._client is None:
208
+ raise RuntimeError(
209
+ "Client is not initialized. Ensure the server is running."
210
+ )
211
+
212
+ response = await self._async_client.chat.completions.create(
213
+ messages=messages,
214
+ model=self.model_type,
215
+ **self.model_config_dict,
216
+ )
217
+
218
+ return response
219
+
220
+ def _run(
221
+ self,
222
+ messages: List[OpenAIMessage],
223
+ response_format: Optional[Type[BaseModel]] = None,
224
+ tools: Optional[List[Dict[str, Any]]] = None,
184
225
  ) -> Union[ChatCompletion, Stream[ChatCompletionChunk]]:
185
226
  r"""Runs inference of OpenAI chat completion.
186
227
 
@@ -223,3 +264,101 @@ class SGLangModel(BaseModelBackend):
223
264
  bool: Whether the model is in stream mode.
224
265
  """
225
266
  return self.model_config_dict.get('stream', False)
267
+
268
+
269
+ # Below are helper functions from sglang.utils
270
+ def _terminate_process(process):
271
+ _kill_process_tree(process.pid)
272
+
273
+
274
+ def _kill_process_tree(
275
+ parent_pid, include_parent: bool = True, skip_pid: Optional[int] = None
276
+ ):
277
+ r"""Kill the process and all its child processes."""
278
+ import os
279
+ import signal
280
+
281
+ import psutil
282
+
283
+ if parent_pid is None:
284
+ parent_pid = os.getpid()
285
+ include_parent = False
286
+
287
+ try:
288
+ itself = psutil.Process(parent_pid)
289
+ except psutil.NoSuchProcess:
290
+ return
291
+
292
+ children = itself.children(recursive=True)
293
+ for child in children:
294
+ if child.pid == skip_pid:
295
+ continue
296
+ try:
297
+ child.kill()
298
+ except psutil.NoSuchProcess:
299
+ pass
300
+
301
+ if include_parent:
302
+ try:
303
+ itself.kill()
304
+
305
+ # Sometime processes cannot be killed with SIGKILL
306
+ # so we send an additional signal to kill them.
307
+ itself.send_signal(signal.SIGQUIT)
308
+ except psutil.NoSuchProcess:
309
+ pass
310
+
311
+
312
+ def _execute_shell_command(command: str) -> subprocess.Popen:
313
+ r"""Execute a shell command and return the process handle
314
+
315
+ Args:
316
+ command: Shell command as a string (can include \\ line continuations)
317
+ Returns:
318
+ subprocess.Popen: Process handle
319
+ """
320
+ import subprocess
321
+
322
+ # Replace \ newline with space and split
323
+ command = command.replace("\\\n", " ").replace("\\", " ")
324
+ parts = command.split()
325
+
326
+ return subprocess.Popen(parts, text=True, stderr=subprocess.STDOUT)
327
+
328
+
329
+ def _wait_for_server(base_url: str, timeout: Optional[int] = None) -> None:
330
+ r"""Wait for the server to be ready by polling the /v1/models endpoint.
331
+
332
+ Args:
333
+ base_url: The base URL of the server
334
+ timeout: Maximum time to wait in seconds. None means wait forever.
335
+ """
336
+ import requests
337
+
338
+ start_time = time.time()
339
+ while True:
340
+ try:
341
+ response = requests.get(
342
+ f"{base_url}/v1/models",
343
+ headers={"Authorization": "Bearer None"},
344
+ )
345
+ if response.status_code == 200:
346
+ time.sleep(5)
347
+ print(
348
+ """\n
349
+ NOTE: Typically, the server runs in a separate terminal.
350
+ In this notebook, we run the server and notebook code
351
+ together, so their outputs are combined.
352
+ To improve clarity, the server logs are displayed in the
353
+ original black color, while the notebook outputs are
354
+ highlighted in blue.
355
+ """
356
+ )
357
+ break
358
+
359
+ if timeout and time.time() - start_time > timeout:
360
+ raise TimeoutError(
361
+ "Server did not become ready within timeout period"
362
+ )
363
+ except requests.exceptions.RequestException:
364
+ time.sleep(1)
@@ -12,9 +12,10 @@
12
12
  # limitations under the License.
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
14
  import os
15
- from typing import Any, Dict, List, Optional, Union
15
+ from typing import Any, Dict, List, Optional, Type, Union
16
16
 
17
- from openai import OpenAI, Stream
17
+ from openai import AsyncStream, OpenAI, Stream
18
+ from pydantic import BaseModel
18
19
 
19
20
  from camel.configs import SILICONFLOW_API_PARAMS, SiliconFlowConfig
20
21
  from camel.messages import OpenAIMessage
@@ -82,9 +83,11 @@ class SiliconFlowModel(BaseModelBackend):
82
83
  base_url=self._url,
83
84
  )
84
85
 
85
- def run(
86
+ def _run(
86
87
  self,
87
88
  messages: List[OpenAIMessage],
89
+ response_format: Optional[Type[BaseModel]] = None,
90
+ tools: Optional[List[Dict[str, Any]]] = None,
88
91
  ) -> Union[ChatCompletion, Stream[ChatCompletionChunk]]:
89
92
  r"""Runs inference of SiliconFlow chat completion.
90
93
 
@@ -104,6 +107,16 @@ class SiliconFlowModel(BaseModelBackend):
104
107
  )
105
108
  return response
106
109
 
110
+ async def _arun(
111
+ self,
112
+ messages: List[OpenAIMessage],
113
+ response_format: Optional[Type[BaseModel]] = None,
114
+ tools: Optional[List[Dict[str, Any]]] = None,
115
+ ) -> Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]:
116
+ raise NotImplementedError(
117
+ "SiliconFlow does not support async inference."
118
+ )
119
+
107
120
  @property
108
121
  def token_counter(self) -> BaseTokenCounter:
109
122
  r"""Initialize the token counter for the model backend.