jarvis-ai-assistant 0.1.5__py3-none-any.whl → 0.1.6__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 jarvis-ai-assistant might be problematic. Click here for more details.

jarvis/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Jarvis AI Assistant"""
2
2
 
3
- __version__ = "0.1.5"
3
+ __version__ = "0.1.6"
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
jarvis/agent.py CHANGED
@@ -2,7 +2,7 @@ import json
2
2
  import subprocess
3
3
  from typing import Dict, Any, List, Optional
4
4
  from .tools import ToolRegistry
5
- from .utils import Spinner, PrettyOutput, OutputType, get_multiline_input
5
+ from .utils import PrettyOutput, OutputType, get_multiline_input
6
6
  from .models import BaseModel
7
7
  import re
8
8
  import os
@@ -14,28 +14,77 @@ class Agent:
14
14
  self.model = model
15
15
  self.tool_registry = tool_registry or ToolRegistry(model)
16
16
  self.name = name
17
- # 编译正则表达式
18
- self.tool_call_pattern = re.compile(r'<tool_call>\s*({[^}]+})\s*</tool_call>')
17
+
18
+ # 构建工具说明
19
+ tools_prompt = "Available Tools:\n"
20
+ for tool in self.tool_registry.get_all_tools():
21
+ tools_prompt += f"- Tool: {tool['function']['name']}\n"
22
+ tools_prompt += f" Description: {tool['function']['description']}\n"
23
+ tools_prompt += f" Arguments: {tool['function']['parameters']}\n"
24
+
19
25
  self.messages = [
20
26
  {
21
27
  "role": "system",
22
- "content": f"""You are {name}, a rigorous AI assistant that executes tasks step by step.
28
+ "content": f"""You are {name}, an AI assistant that follows the ReAct (Reasoning + Acting) framework to solve tasks step by step.
29
+
30
+ FRAMEWORK:
31
+ 1. Thought: Analyze the current situation and plan the next step
32
+ 2. Action: Execute ONE specific tool call
33
+ 3. Observation: Review the result
34
+ 4. Next: Plan the next step or conclude
35
+
36
+ FORMAT:
37
+ Thought: I need to [reasoning about the current situation]...
38
+ Action: I will use [tool] to [purpose]...
39
+ <START_TOOL_CALL>
40
+ name: tool_name
41
+ arguments:
42
+ param1: value1
43
+ <END_TOOL_CALL>
44
+
45
+ After receiving result:
46
+ Observation: The tool returned [analyze result]...
47
+ Next: Based on this, I will [next step]...
48
+
49
+ CORE RULES:
50
+ 1. ONE Action Per Response
51
+ - Only ONE tool call per response
52
+ - Additional tool calls will be ignored
53
+ - Complete current step before next
54
+
55
+ 2. Clear Reasoning
56
+ - Explain your thought process
57
+ - Justify tool selection
58
+ - Analyze results thoroughly
59
+
60
+ Examples:
61
+ ✓ Good Response:
62
+ Thought: I need to check the content of utils.py first to understand its structure.
63
+ Action: I will read the file content.
64
+ <START_TOOL_CALL>
65
+ name: file_operation
66
+ arguments:
67
+ operation: read
68
+ filepath: src/utils.py
69
+ <END_TOOL_CALL>
23
70
 
24
- Key Principles:
25
- 1. Execute ONE step at a time
26
- 2. Wait for each step's result before planning the next
27
- 3. Use tools to obtain all data, no fabrication
28
- 4. Create sub-agents for independent subtasks
29
- 5. Think carefully before each action
71
+ Bad Response:
72
+ Thought: Let's analyze the code.
73
+ Action: I'll read and check everything.
74
+ [Multiple or vague tool calls...]
30
75
 
31
- """ + self.tool_registry.tool_help_text()
76
+ Remember:
77
+ - Always start with "Thought:"
78
+ - Use exactly ONE tool per response
79
+ - Wait for results before next step
80
+ - Clearly explain your reasoning
81
+
82
+ {tools_prompt}"""
32
83
  }
33
84
  ]
34
- self.spinner = Spinner()
35
85
 
36
86
  def _call_model(self, messages: List[Dict], use_tools: bool = True) -> Dict:
37
87
  """调用模型获取响应"""
38
- self.spinner.start()
39
88
  try:
40
89
  return self.model.chat(
41
90
  messages=messages,
@@ -43,12 +92,14 @@ Key Principles:
43
92
  )
44
93
  except Exception as e:
45
94
  raise Exception(f"{self.name}: 模型调用失败: {str(e)}")
46
- finally:
47
- self.spinner.stop()
48
95
 
49
96
  def run(self, user_input: str) -> str:
50
97
  """处理用户输入并返回响应,返回任务总结报告"""
51
98
  self.clear_history()
99
+
100
+ # 显示任务开始
101
+ PrettyOutput.section(f"开始新任务: {self.name}", OutputType.PLANNING)
102
+
52
103
  self.messages.append({
53
104
  "role": "user",
54
105
  "content": user_input
@@ -56,9 +107,14 @@ Key Principles:
56
107
 
57
108
  while True:
58
109
  try:
110
+ # 显示思考状态
111
+ PrettyOutput.print("分析任务...", OutputType.PROGRESS)
59
112
  response = self._call_model(self.messages)
60
113
  current_response = response
61
114
 
115
+ # 流式输出已经在model中处理,这里添加换行
116
+ PrettyOutput.print_stream_end()
117
+
62
118
  self.messages.append({
63
119
  "role": "assistant",
64
120
  "content": response["message"].get("content", ""),
@@ -67,10 +123,16 @@ Key Principles:
67
123
 
68
124
  if len(current_response["message"]["tool_calls"]) > 0:
69
125
  if current_response["message"].get("content"):
70
- PrettyOutput.print(f"{self.name}: {current_response['message']['content']}", OutputType.SYSTEM)
126
+ PrettyOutput.print(current_response["message"]["content"], OutputType.SYSTEM)
71
127
 
72
- tool_result = self.tool_registry.handle_tool_calls(current_response["message"]["tool_calls"])
73
- PrettyOutput.print(f"{self.name} Tool Result: {tool_result}", OutputType.RESULT)
128
+ try:
129
+ # 显示工具调用
130
+ PrettyOutput.print("执行工具调用...", OutputType.PROGRESS)
131
+ tool_result = self.tool_registry.handle_tool_calls(current_response["message"]["tool_calls"])
132
+ PrettyOutput.print(tool_result, OutputType.RESULT)
133
+ except Exception as e:
134
+ PrettyOutput.print(str(e), OutputType.ERROR)
135
+ tool_result = f"Tool call failed: {str(e)}"
74
136
 
75
137
  self.messages.append({
76
138
  "role": "tool",
@@ -78,15 +140,12 @@ Key Principles:
78
140
  })
79
141
  continue
80
142
 
81
- final_content = current_response["message"].get("content", "")
82
- if final_content:
83
- PrettyOutput.print(f"{self.name}: {final_content}", OutputType.SYSTEM)
84
-
143
+ # 获取用户输入
85
144
  user_input = get_multiline_input(f"{self.name}: 您可以继续输入,或输入空行结束当前任务")
86
145
  if not user_input:
87
- PrettyOutput.print(f"{self.name}: 正在生成任务总结...", OutputType.INFO)
146
+ PrettyOutput.print("生成任务总结...", OutputType.PROGRESS)
88
147
 
89
- # 优化后的任务总结提示语
148
+ # 生成任务总结
90
149
  summary_prompt = {
91
150
  "role": "user",
92
151
  "content": """The task has been completed. Based on the previous analysis and execution results, provide a task summary including:
@@ -103,21 +162,25 @@ Key Principles:
103
162
 
104
163
  Focus only on facts and actual results. Be direct and concise."""
105
164
  }
165
+
106
166
  while True:
107
167
  try:
108
168
  summary_response = self._call_model(self.messages + [summary_prompt], use_tools=False)
109
169
  summary = summary_response["message"].get("content", "")
110
170
 
111
- PrettyOutput.print(f"==============={self.name} 任务总结===============", OutputType.INFO)
171
+ # 显示任务总结
172
+ PrettyOutput.section("任务总结", OutputType.SUCCESS)
112
173
  PrettyOutput.print(summary, OutputType.SYSTEM)
113
- PrettyOutput.print("=" * (len(self.name) + 16), OutputType.INFO)
174
+ PrettyOutput.section("任务完成", OutputType.SUCCESS)
114
175
 
115
176
  return summary
116
177
 
117
178
  except Exception as e:
118
- error_msg = f"{self.name}: 生成任务总结时出错: {str(e)}"
119
- PrettyOutput.print(error_msg, OutputType.ERROR)
120
-
179
+ PrettyOutput.print(str(e), OutputType.ERROR)
180
+
181
+ if user_input == "__interrupt__":
182
+ PrettyOutput.print("任务已取消", OutputType.WARNING)
183
+ return "Task cancelled by user"
121
184
 
122
185
  self.messages.append({
123
186
  "role": "user",
@@ -125,8 +188,7 @@ Focus only on facts and actual results. Be direct and concise."""
125
188
  })
126
189
 
127
190
  except Exception as e:
128
- error_msg = f"{self.name}: 处理响应时出错: {str(e)}"
129
- PrettyOutput.print(error_msg, OutputType.ERROR)
191
+ PrettyOutput.print(str(e), OutputType.ERROR)
130
192
 
131
193
  def clear_history(self):
132
194
  """清除对话历史,只保留系统提示"""
jarvis/main.py CHANGED
@@ -19,7 +19,7 @@ from jarvis.zte_llm import create_zte_llm
19
19
  # 定义支持的平台和模型
20
20
  SUPPORTED_PLATFORMS = {
21
21
  "ollama": {
22
- "models": ["llama3.2", "qwen2.5:14b"],
22
+ "models": ["qwen2.5:14b", "qwq"],
23
23
  "default": "qwen2.5:14b"
24
24
  },
25
25
  "ddgs": {
@@ -88,13 +88,16 @@ def select_task(tasks: dict) -> str:
88
88
 
89
89
  def main():
90
90
  """Main entry point for Jarvis."""
91
+
92
+ load_env_from_file()
93
+
91
94
  parser = argparse.ArgumentParser(description="Jarvis AI Assistant")
92
95
 
93
96
  # 添加平台选择参数
94
97
  parser.add_argument(
95
98
  "--platform",
96
99
  choices=list(SUPPORTED_PLATFORMS.keys()),
97
- default="ddgs",
100
+ default=os.getenv("JARVIS_PLATFORM") or "ddgs",
98
101
  help="选择运行平台 (默认: ollama)"
99
102
  )
100
103
 
@@ -107,13 +110,13 @@ def main():
107
110
  # 添加API基础URL参数
108
111
  parser.add_argument(
109
112
  "--api-base",
110
- default="http://localhost:11434",
113
+ default=os.getenv("JARVIS_OLLAMA_API_BASE") or "http://localhost:11434",
111
114
  help="Ollama API基础URL (仅用于Ollama平台, 默认: http://localhost:11434)"
112
115
  )
113
116
 
114
117
  args = parser.parse_args()
115
118
 
116
- load_env_from_file()
119
+ args.model = args.model or os.getenv("JARVIS_MODEL")
117
120
 
118
121
  # 验证并设置默认模型
119
122
  if args.model:
@@ -162,12 +165,9 @@ def main():
162
165
  while True:
163
166
  try:
164
167
  user_input = get_multiline_input("请输入您的任务(输入空行退出):")
165
- if not user_input:
168
+ if not user_input or user_input == "__interrupt__":
166
169
  break
167
170
  agent.run(user_input)
168
- except KeyboardInterrupt:
169
- print("\n正在退出...")
170
- break
171
171
  except Exception as e:
172
172
  PrettyOutput.print(f"错误: {str(e)}", OutputType.ERROR)
173
173
 
jarvis/models.py CHANGED
@@ -1,10 +1,10 @@
1
- import json
2
1
  import re
3
2
  import time
4
3
  from typing import Dict, List, Optional
5
4
  from duckduckgo_search import DDGS
6
5
  import ollama
7
6
  from abc import ABC, abstractmethod
7
+ import yaml
8
8
 
9
9
  from .utils import OutputType, PrettyOutput
10
10
 
@@ -18,32 +18,46 @@ class BaseModel(ABC):
18
18
 
19
19
  @staticmethod
20
20
  def extract_tool_calls(content: str) -> List[Dict]:
21
- """从内容中提取工具调用"""
22
- tool_calls = []
23
- # 使用非贪婪匹配来获取标签之间的所有内容
24
- pattern = re.compile(r'<tool_call>(.*?)</tool_call>', re.DOTALL)
21
+ """从内容中提取工具调用,只返回第一个有效的工具调用"""
22
+ # 分割内容为行
23
+ lines = content.split('\n')
24
+ tool_call_lines = []
25
+ in_tool_call = False
25
26
 
26
- matches = pattern.finditer(content)
27
- for match in matches:
28
- try:
29
- # 提取并解析 JSON
30
- tool_call_text = match.group(1).strip()
31
- tool_call_data = json.loads(tool_call_text)
27
+ # 逐行处理
28
+ for line in lines:
29
+ if not line:
30
+ continue
32
31
 
33
- # 验证必要的字段
34
- if "name" in tool_call_data and "arguments" in tool_call_data:
35
- tool_calls.append({
36
- "function": {
37
- "name": tool_call_data["name"],
38
- "arguments": tool_call_data["arguments"]
39
- }
40
- })
41
- except json.JSONDecodeError:
42
- continue # 跳过无效的 JSON
43
- except Exception:
44
- continue # 跳过其他错误
45
-
46
- return tool_calls
32
+ if line == '<START_TOOL_CALL>':
33
+ tool_call_lines = []
34
+ in_tool_call = True
35
+ continue
36
+ elif line == '<END_TOOL_CALL>':
37
+ if in_tool_call and tool_call_lines:
38
+ try:
39
+ # 解析工具调用内容
40
+ tool_call_text = '\n'.join(tool_call_lines)
41
+ tool_call_data = yaml.safe_load(tool_call_text)
42
+
43
+ # 验证必要的字段
44
+ if "name" in tool_call_data and "arguments" in tool_call_data:
45
+ # 只返回第一个有效的工具调用
46
+ return [{
47
+ "function": {
48
+ "name": tool_call_data["name"],
49
+ "arguments": tool_call_data["arguments"]
50
+ }
51
+ }]
52
+ except yaml.YAMLError:
53
+ pass # 跳过无效的YAML
54
+ except Exception:
55
+ pass # 跳过其他错误
56
+ in_tool_call = False
57
+ elif in_tool_call:
58
+ tool_call_lines.append(line)
59
+
60
+ return [] # 如果没有找到有效的工具调用,返回空列表
47
61
 
48
62
 
49
63
  class DDGSModel(BaseModel):
@@ -57,11 +71,7 @@ class DDGSModel(BaseModel):
57
71
  self.model_name = model_name
58
72
 
59
73
  def __make_prompt(self, messages: List[Dict], tools: Optional[List[Dict]] = None) -> str:
60
- prompt = "You are an AI Agent skilled in utilizing tools and planning tasks. Based on the task input by the user and the list of available tools, you output the tool invocation methods in a specified format. The user will provide feedback on the results of the tool execution, allowing you to continue analyzing and ultimately complete the user's designated task. Below is the list of tools and their usage methods. Let's use them step by step to accomplish the user's task.\n"
61
- for tool in tools:
62
- prompt += f"- Tool: {tool['function']['name']}\n"
63
- prompt += f" Description: {tool['function']['description']}\n"
64
- prompt += f" Arguments: {tool['function']['parameters']}\n"
74
+ prompt = ""
65
75
  for message in messages:
66
76
  prompt += f"[{message['role']}]: {message['content']}\n"
67
77
  return prompt
@@ -70,6 +80,7 @@ class DDGSModel(BaseModel):
70
80
  ddgs = DDGS()
71
81
  prompt = self.__make_prompt(messages, tools)
72
82
  content = ddgs.chat(prompt)
83
+ PrettyOutput.print_stream(content, OutputType.SYSTEM)
73
84
  tool_calls = BaseModel.extract_tool_calls(content)
74
85
  return {
75
86
  "message": {
@@ -90,16 +101,25 @@ class OllamaModel(BaseModel):
90
101
  def chat(self, messages: List[Dict], tools: Optional[List[Dict]] = None) -> Dict:
91
102
  """调用Ollama API获取响应"""
92
103
  try:
93
- response = self.client.chat(
104
+ # 使用流式调用
105
+ stream = self.client.chat(
94
106
  model=self.model_name,
95
107
  messages=messages,
96
- tools=tools
108
+ stream=True
97
109
  )
98
110
 
99
- content = response.message.content
100
- tool_calls = response.message.tool_calls or BaseModel.extract_tool_calls(content)
111
+ # 收集完整响应
112
+ content_parts = []
113
+ for chunk in stream:
114
+ if chunk.message.content:
115
+ content_parts.append(chunk.message.content)
116
+ # 实时打印内容
117
+ PrettyOutput.print_stream(chunk.message.content, OutputType.SYSTEM)
118
+
119
+ # 合并完整内容
120
+ content = "".join(content_parts)
121
+ tool_calls = BaseModel.extract_tool_calls(content)
101
122
 
102
- # 转换响应格式
103
123
  return {
104
124
  "message": {
105
125
  "content": content,
jarvis/tools/__init__.py CHANGED
@@ -1,16 +1,16 @@
1
1
  from .base import Tool, ToolRegistry
2
- from .python_script import PythonScript
3
2
  from .file_ops import FileOperationTool
4
3
  from .search import SearchTool
5
4
  from .shell import ShellTool
6
5
  from .webpage import WebpageTool
6
+ from .user_input import UserInputTool
7
7
 
8
8
  __all__ = [
9
9
  'Tool',
10
10
  'ToolRegistry',
11
- 'PythonScript',
12
11
  'FileOperationTool',
13
12
  'SearchTool',
14
13
  'ShellTool',
15
14
  'WebpageTool',
16
- ]
15
+ 'UserInputTool',
16
+ ]
jarvis/tools/base.py CHANGED
@@ -35,17 +35,17 @@ class ToolRegistry:
35
35
  """注册所有默认工具"""
36
36
  from .search import SearchTool
37
37
  from .shell import ShellTool
38
- from .python_script import PythonScriptTool
39
38
  from .file_ops import FileOperationTool
40
39
  from .webpage import WebpageTool
41
40
  from .sub_agent import SubAgentTool
41
+ from .user_input import UserInputTool
42
42
 
43
43
  tools = [
44
44
  SearchTool(),
45
45
  ShellTool(),
46
- PythonScriptTool(),
47
46
  FileOperationTool(),
48
47
  WebpageTool(),
48
+ UserInputTool(),
49
49
  ]
50
50
 
51
51
  for tool in tools:
@@ -84,115 +84,47 @@ class ToolRegistry:
84
84
  return tool.execute(arguments)
85
85
 
86
86
  def handle_tool_calls(self, tool_calls: List[Dict]) -> str:
87
- """处理工具调用"""
88
- results = []
89
- for tool_call in tool_calls:
90
- name = tool_call["function"]["name"]
91
- args = tool_call["function"]["arguments"]
92
- if isinstance(args, str):
93
- try:
94
- args = json.loads(args)
95
- except json.JSONDecodeError:
96
- return f"Invalid JSON in arguments for tool {name}"
97
-
98
- PrettyOutput.print(f"Calling tool: {name}", OutputType.INFO)
99
- if isinstance(args, dict):
100
- for key, value in args.items():
101
- PrettyOutput.print(f" - {key}: {value}", OutputType.INFO)
102
- else:
103
- PrettyOutput.print(f" Arguments: {args}", OutputType.INFO)
104
- PrettyOutput.print("", OutputType.INFO)
87
+ """处理工具调用,只处理第一个工具"""
88
+ if not tool_calls:
89
+ return ""
105
90
 
106
- result = self.execute_tool(name, args)
107
- if result["success"]:
108
- stdout = result["stdout"]
109
- stderr = result.get("stderr", "")
110
- output_parts = []
111
- output_parts.append(f"Result:\n{stdout}")
112
- if stderr:
113
- output_parts.append(f"Errors:\n{stderr}")
114
- output = "\n\n".join(output_parts)
115
- else:
116
- error_msg = result["error"]
117
- output = f"Execution failed: {error_msg}"
118
-
119
- results.append(output)
120
- return "\n".join(results)
121
-
122
- def tool_help_text(self) -> str:
123
- """返回所有工具的帮助文本"""
124
- return """Available Tools:
125
-
126
- 1. search: Search for information using DuckDuckGo
127
- 2. read_webpage: Extract content from webpages
128
- 3. execute_python: Run Python code with dependency management
129
- 4. execute_shell: Execute shell commands
130
- 5. file_operation: Read/write files in workspace directory
131
- 6. create_sub_agent: Create a sub-agent to handle subtasks (ONLY for independent subtasks)
132
-
133
- Core Rules:
134
- 1. ONE Step at a Time
135
- - Execute only one action per response
136
- - Explain what you're going to do before doing it
137
- - Wait for the result before planning next step
138
- - No multiple actions or parallel execution
139
-
140
- 2. Sub-Agent Usage (ONLY for Subtasks)
141
- - Create sub-agents ONLY to handle independent subtasks
142
- - Main task should be broken down into clear subtasks first
143
- - Examples of good subtask delegation:
144
- * Analyzing each Python file in a directory
145
- * Processing each data file in a batch
146
- * Researching different aspects of a topic
147
- * Reviewing multiple code components
148
- - Do NOT use for:
149
- * Main task execution
150
- * Single file operations
151
- * Simple sequential steps
152
- * Tasks requiring continuous context
153
-
154
- 3. Output Guidelines
155
- - Focus on essential information
156
- - Clear step-by-step explanations
157
- - Summarize results concisely
158
-
159
- Tool Call Format:
160
- <tool_call>
161
- {
162
- "name": "tool_name",
163
- "arguments": {
164
- "param1": "value1"
165
- }
166
- }
167
- </tool_call>
168
-
169
- Example (Correct - Subtask Delegation):
170
- Assistant: I'll create a sub-agent to handle the code analysis subtask.
171
- <tool_call>
172
- {
173
- "name": "create_sub_agent",
174
- "arguments": {
175
- "name": "CodeAnalyzer",
176
- "task": "Analyze the utils.py file structure and generate documentation",
177
- "context": "This is part of the larger documentation task"
178
- }
179
- }
180
- </tool_call>
181
-
182
- Example (Incorrect - Main Task):
183
- ❌ Assistant: I'll create a sub-agent to handle the entire project analysis.
184
- <tool_call>
185
- {
186
- "name": "create_sub_agent",
187
- "arguments": {
188
- "name": "ProjectAnalyzer",
189
- "task": "Analyze and document the entire project"
190
- }
191
- }
192
- </tool_call>
193
-
194
- Remember:
195
- 1. ONE step at a time
196
- 2. Create sub-agents ONLY for subtasks
197
- 3. Explain before acting
198
- 4. Wait for results"""
91
+ # 只处理第一个工具调用
92
+ tool_call = tool_calls[0]
93
+ name = tool_call["function"]["name"]
94
+ args = tool_call["function"]["arguments"]
95
+
96
+ if isinstance(args, str):
97
+ try:
98
+ args = json.loads(args)
99
+ except json.JSONDecodeError:
100
+ PrettyOutput.print(f"工具参数格式无效: {name}", OutputType.ERROR)
101
+ return ""
102
+
103
+ # 显示工具调用信息
104
+ PrettyOutput.section(f"执行工具: {name}", OutputType.TOOL)
105
+ if isinstance(args, dict):
106
+ for key, value in args.items():
107
+ PrettyOutput.print(f"参数: {key} = {value}", OutputType.DEBUG)
108
+ else:
109
+ PrettyOutput.print(f"参数: {args}", OutputType.DEBUG)
110
+
111
+ # 执行工具调用
112
+ result = self.execute_tool(name, args)
113
+
114
+ # 处理结果
115
+ if result["success"]:
116
+ stdout = result["stdout"]
117
+ stderr = result.get("stderr", "")
118
+ output_parts = []
119
+ if stdout:
120
+ output_parts.append(f"输出:\n{stdout}")
121
+ if stderr:
122
+ output_parts.append(f"错误:\n{stderr}")
123
+ output = "\n\n".join(output_parts)
124
+ PrettyOutput.section("执行成功", OutputType.SUCCESS)
125
+ else:
126
+ error_msg = result["error"]
127
+ output = f"执行失败: {error_msg}"
128
+ PrettyOutput.section("执行失败", OutputType.ERROR)
129
+
130
+ return output