devops-bot-sdk 1.4.149__tar.gz → 1.4.150__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 (95) hide show
  1. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/__init__.py +2 -2
  4. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/prompt_studio.py +51 -0
  5. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/api.py +61 -5
  6. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/README.md +0 -0
  7. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  8. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  9. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  10. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/devops_bot_sdk.egg-info/requires.txt +0 -0
  11. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  12. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/pyproject.toml +0 -0
  13. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agentd.py +0 -0
  14. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/__init__.py +0 -0
  15. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/builder.py +0 -0
  16. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/chat.py +0 -0
  17. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/connectors.py +0 -0
  18. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/engines/__init__.py +0 -0
  19. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/engines/claude_code.py +0 -0
  20. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/engines/http.py +0 -0
  21. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/engines/mcp.py +0 -0
  22. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/engines/n8n.py +0 -0
  23. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/engines/shell.py +0 -0
  24. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/integrations.py +0 -0
  25. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/inventory.py +0 -0
  26. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/library/__init__.py +0 -0
  27. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/manifest.py +0 -0
  28. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/prompt_template.py +0 -0
  29. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/prompt_vars.py +0 -0
  30. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/run_log.py +0 -0
  31. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/secrets.py +0 -0
  32. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/usage.py +0 -0
  33. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/agents/varstore.py +0 -0
  34. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/cli.py +0 -0
  35. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/client.py +0 -0
  36. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/collectors/__init__.py +0 -0
  37. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/collectors/files.py +0 -0
  38. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/collectors/process.py +0 -0
  39. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/collectors/screenshot.py +0 -0
  40. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/config.py +0 -0
  41. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/crucial.py +0 -0
  42. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/design_verify/__init__.py +0 -0
  43. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/design_verify/assets.py +0 -0
  44. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/design_verify/bootstrap.py +0 -0
  45. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/design_verify/browser.py +0 -0
  46. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/design_verify/compare.py +0 -0
  47. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/design_verify/loop.py +0 -0
  48. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/exceptions.py +0 -0
  49. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/git_ops.py +0 -0
  50. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/graphify.py +0 -0
  51. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/hooks/__init__.py +0 -0
  52. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/hooks/crucial_guard.py +0 -0
  53. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ipc/__init__.py +0 -0
  54. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ipc/electron_bridge.py +0 -0
  55. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ipc/handlers.py +0 -0
  56. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/local_exec.py +0 -0
  57. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/models/__init__.py +0 -0
  58. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/models/envelope.py +0 -0
  59. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/models/requests.py +0 -0
  60. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/models/responses.py +0 -0
  61. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/models/snapshots.py +0 -0
  62. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/platform_compat.py +0 -0
  63. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/py.typed +0 -0
  64. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/run_auto.py +0 -0
  65. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/sse.py +0 -0
  66. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/test.py +0 -0
  67. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/test_pipeline.py +0 -0
  68. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/__init__.py +0 -0
  69. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/dashboard.py +0 -0
  70. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/health.py +0 -0
  71. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/ip_allowlist.py +0 -0
  72. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/jobs.py +0 -0
  73. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/models.py +0 -0
  74. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/runner.py +0 -0
  75. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/security.py +0 -0
  76. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/server.py +0 -0
  77. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/service.py +0 -0
  78. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/setup_manager.py +0 -0
  79. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/static/agents.html +0 -0
  80. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/static/app.js +0 -0
  81. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/static/custom-agents.html +0 -0
  82. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/static/default-agents.html +0 -0
  83. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/static/index.html +0 -0
  84. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/static/runs.html +0 -0
  85. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/static/setup.html +0 -0
  86. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/static/styles.css +0 -0
  87. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/ui/static/tickets.html +0 -0
  88. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/sdk/updater.py +0 -0
  89. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/setup.cfg +0 -0
  90. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/tests/test_agent_chat_features.py +0 -0
  91. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/tests/test_agent_prompt_models.py +0 -0
  92. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/tests/test_agents_connectors_builder.py +0 -0
  93. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/tests/test_git_action.py +0 -0
  94. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/tests/test_health.py +0 -0
  95. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.150}/tests/test_updater_extras.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.149
3
+ Version: 1.4.150
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.149
3
+ Version: 1.4.150
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.4.149
3
+ Version: 1.4.150
4
4
 
5
5
  Public surface:
6
6
  BackendClient.from_config() — create client from ~/.agentos/config.toml
@@ -30,7 +30,7 @@ Rules:
30
30
  - All data egress through submit_webhook only
31
31
  """
32
32
 
33
- __version__ = "1.4.149" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
33
+ __version__ = "1.4.150" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
34
34
  __author__ = "AgentOS" # sdk.client.SDK_VERSION and the UI server version all read this.
35
35
 
36
36
  from sdk.client import BackendClient
@@ -164,6 +164,57 @@ def build_regenerate_prompt(system_prompt: str, feedback: str = "",
164
164
  )
165
165
 
166
166
 
167
+ _VALIDATE_INPUTS_INSTRUCTION = """You are screening the inputs a user typed to CREATE an AI work agent, BEFORE a system prompt is generated. Decide whether each field is appropriate and meaningful.
168
+
169
+ Mark a field invalid when it is:
170
+ - offensive, abusive, hateful, harassing, or sexual;
171
+ - gibberish, random characters, or meaningless filler;
172
+ - clearly not what the field is for — in particular a JOB that does not describe an actual task/objective (e.g. a personal message like "shabana I love u", a person's name, or random words), an AGENT NAME that is not a plausible name for an agent, or an AGENT TYPE that is not a sensible category (e.g. developer, documentation, qa, support, data).
173
+
174
+ Ignore any field whose value is empty. Be reasonable: a short-but-genuine job (e.g. "summarise a repo") is VALID. Only flag inputs that are clearly wrong.
175
+
176
+ Return ONLY a JSON object, no prose:
177
+ {"valid": true|false, "issues": {"agent_name": "<short polite fix instruction>", "agent_type": "<...>", "job": "<...>"}}
178
+ Include a key in "issues" ONLY for a field you marked invalid; each message is a short, polite instruction to the user (e.g. "Kindly update the Job — it should describe the task the agent will perform."). If everything is fine, return {"valid": true, "issues": {}}.
179
+
180
+ INPUTS TO SCREEN:
181
+ AGENT NAME: __NAME__
182
+ AGENT TYPE: __TYPE__
183
+ MAIN JOB: __JOB__
184
+ """
185
+
186
+
187
+ def build_validate_inputs_prompt(agent_name: str, agent_type: str, job: str) -> str:
188
+ """Prompt that asks the engine to screen agent_name/agent_type/job for
189
+ offensive or meaningless input before generation/regeneration."""
190
+ return (
191
+ _VALIDATE_INPUTS_INSTRUCTION
192
+ .replace("__NAME__", (agent_name or "").strip() or "(empty)")
193
+ .replace("__TYPE__", (agent_type or "").strip() or "(empty)")
194
+ .replace("__JOB__", (job or "").strip() or "(empty)")
195
+ )
196
+
197
+
198
+ def parse_validation(model_output: str) -> dict:
199
+ """Parse the input-screening reply into ``{"valid": bool, "issues": {field: msg}}``.
200
+
201
+ Tolerant of fences/prose. Only the three known fields are kept. Defaults to
202
+ VALID on an unparseable reply so the caller can fail-open (proceed to generate).
203
+ """
204
+ data = _extract_json(model_output)
205
+ if not isinstance(data, dict):
206
+ return {"valid": True, "issues": {}}
207
+ raw_issues = data.get("issues") if isinstance(data.get("issues"), dict) else {}
208
+ issues = {
209
+ k: str(v).strip()
210
+ for k, v in raw_issues.items()
211
+ if k in ("agent_name", "agent_type", "job") and str(v or "").strip()
212
+ }
213
+ # Trust an explicit False, but also treat "issues present" as invalid.
214
+ valid = bool(data.get("valid", True)) and not issues
215
+ return {"valid": valid, "issues": issues}
216
+
217
+
167
218
  def parse_generation(model_output: str) -> dict:
168
219
  """Parse a generate/regenerate model reply into ``{system_prompt, variables}``.
169
220
 
@@ -929,6 +929,41 @@ async def _run_prompt_engine(prompt: str) -> str:
929
929
  return result.get("result", "") or ""
930
930
 
931
931
 
932
+ # ── Unified revision format ───────────────────────────────────────────────────
933
+ # Missing / empty required fields → HTTP 422 (Pydantic on the request model,
934
+ # unchanged). When the inputs are PRESENT but need human revision (offensive /
935
+ # meaningless input, or a prompt missing required sections) EVERY endpoint returns
936
+ # the SAME shape — all messages collected into one ``ai_comments`` list:
937
+ # {"status": "needs_revision", "engine": "claude_code",
938
+ # "ai_comments": ["<message>", "<message>", ...]}
939
+ # Success responses carry status="ok" and ai_comments=[] so the client always reads
940
+ # the same two fields.
941
+
942
+ def _needs_revision(ai_comments: list[str]) -> dict:
943
+ """The single revision envelope used by every agent-prompt endpoint."""
944
+ return {"status": "needs_revision", "engine": "claude_code", "ai_comments": ai_comments}
945
+
946
+
947
+ async def _screen_agent_inputs(agent_name: str, agent_type: str, job: str) -> list[str]:
948
+ """LLM screen for offensive / meaningless inputs → list of comment messages
949
+ (empty when the inputs are fine).
950
+
951
+ FAIL-OPEN: any engine/parse error → ``[]`` (proceed), so a validator hiccup
952
+ never blocks a legitimate request.
953
+ """
954
+ from sdk.agents.prompt_studio import build_validate_inputs_prompt, parse_validation
955
+
956
+ try:
957
+ verdict = parse_validation(
958
+ await _run_prompt_engine(build_validate_inputs_prompt(agent_name, agent_type, job))
959
+ )
960
+ except Exception: # noqa: BLE001 — fail-open
961
+ return []
962
+ if verdict.get("valid", True):
963
+ return []
964
+ return [str(msg).strip() for msg in (verdict.get("issues") or {}).values() if str(msg or "").strip()]
965
+
966
+
932
967
  @router.post("/agent-prompt/generate", dependencies=[_auth])
933
968
  async def agent_prompt_generate(body: AgentPromptGenerateRequest) -> dict:
934
969
  """name + type + job → a structured system prompt + {{variables}} (claude_code).
@@ -938,17 +973,24 @@ async def agent_prompt_generate(body: AgentPromptGenerateRequest) -> dict:
938
973
  """
939
974
  from sdk.agents.prompt_studio import build_generate_prompt, parse_generation
940
975
 
976
+ # Empty agent_name/agent_type/job already 422'd by the request model.
941
977
  agent_name = body.agent_name
942
978
  agent_type = body.agent_type
943
979
  job = body.job
944
980
 
981
+ # Semantic screen (offensive / meaningless) → unified needs_revision. Fail-open.
982
+ comments = await _screen_agent_inputs(agent_name, agent_type, job)
983
+ if comments:
984
+ return _needs_revision(comments)
985
+
945
986
  text = await _run_prompt_engine(
946
987
  build_generate_prompt(agent_name, agent_type, job, body.context)
947
988
  )
948
989
  parsed = parse_generation(text)
949
990
  if not parsed["system_prompt"]:
950
991
  raise HTTPException(status_code=502, detail="generation returned no system_prompt")
951
- return {"agent_name": agent_name, "agent_type": agent_type, "engine": "claude_code", **parsed}
992
+ return {"status": "ok", "engine": "claude_code", "ai_comments": [],
993
+ "agent_name": agent_name, "agent_type": agent_type, **parsed}
952
994
 
953
995
 
954
996
  @router.post("/agent-prompt/regenerate", dependencies=[_auth])
@@ -960,6 +1002,12 @@ async def agent_prompt_regenerate(body: PromptRegenerateRequest) -> dict:
960
1002
  """
961
1003
  from sdk.agents.prompt_studio import build_regenerate_prompt, parse_generation
962
1004
 
1005
+ # Empty system_prompt already 422'd by the request model. Screen the agent
1006
+ # metadata (blank fields ignored by the screener; fail-open).
1007
+ comments = await _screen_agent_inputs(body.agent_name, body.agent_type, body.job)
1008
+ if comments:
1009
+ return _needs_revision(comments)
1010
+
963
1011
  text = await _run_prompt_engine(build_regenerate_prompt(
964
1012
  body.system_prompt, body.feedback,
965
1013
  body.agent_name, body.agent_type, body.job,
@@ -967,9 +1015,8 @@ async def agent_prompt_regenerate(body: PromptRegenerateRequest) -> dict:
967
1015
  parsed = parse_generation(text)
968
1016
  if not parsed["system_prompt"]:
969
1017
  raise HTTPException(status_code=502, detail="regeneration returned no system_prompt")
970
- return {"agent_name": body.agent_name,
971
- "agent_type": body.agent_type,
972
- "engine": "claude_code", **parsed}
1018
+ return {"status": "ok", "engine": "claude_code", "ai_comments": [],
1019
+ "agent_name": body.agent_name, "agent_type": body.agent_type, **parsed}
973
1020
 
974
1021
 
975
1022
  @router.post("/agent-prompt/verify", dependencies=[_auth])
@@ -985,6 +1032,7 @@ async def agent_prompt_verify(body: PromptVerifyRequest) -> dict:
985
1032
  from sdk.agents.prompt_template import check_system_prompt
986
1033
  from sdk.agents.prompt_vars import extract_variables
987
1034
 
1035
+ # Too-short system_prompt already 422'd by the request model.
988
1036
  system_prompt = body.system_prompt
989
1037
 
990
1038
  standards = check_system_prompt(system_prompt)
@@ -1008,13 +1056,21 @@ async def agent_prompt_verify(body: PromptVerifyRequest) -> dict:
1008
1056
  "type": str(m.get("type", "string")).strip() or "string",
1009
1057
  })
1010
1058
 
1059
+ # Structural gaps → the SAME unified ai_comments list (all messages together).
1060
+ ai_comments = (
1061
+ [f"Missing required section: {s}." for s in standards["missing_required"]]
1062
+ + [f"Recommended section missing: {s}." for s in standards["missing_recommended"]]
1063
+ )
1064
+
1011
1065
  return {
1066
+ "status": "needs_revision" if standards["missing_required"] else "ok",
1067
+ "engine": "claude_code",
1068
+ "ai_comments": ai_comments,
1012
1069
  "valid": not standards["missing_required"],
1013
1070
  "missing_required": standards["missing_required"],
1014
1071
  "missing_recommended": standards["missing_recommended"],
1015
1072
  "variables": variables,
1016
1073
  "secrets": analysis.get("secrets", []),
1017
- "engine": "claude_code",
1018
1074
  }
1019
1075
 
1020
1076