```json
{
  "sym_variables": [
    ("x0", "chives"),
    ("x1", "basil plants")
  ],
  "objective_function": "7*x0 + 7*x1",
  "constraints": [
    "0.15*x0 + 0.99*x1 >= 33",
    "0.95*x0 + 0.06*x1 >= 50",
    "0.17*x0 + 0.6*x1 >= 27",
    "0.44*x0 + 0.75*x1 >= 25",
    "5*x0 - 9*x1 >= 0",
    "0.15*x0 + 0.99*x1 <= 112",
    "0.95*x0 + 0.06*x1 <= 77",
    "0.17*x0 + 0.6*x1 <= 123",
    "0.44*x0 + 0.75*x1 <= 105"
  ]
}
```

```python
import gurobipy as gp

try:
    # Create a new model
    m = gp.Model("plant_optimization")

    # Create variables
    chives = m.addVar(vtype=gp.GRB.INTEGER, name="chives")
    basil = m.addVar(vtype=gp.GRB.INTEGER, name="basil_plants")

    # Set objective function
    m.setObjective(7 * chives + 7 * basil, gp.GRB.MINIMIZE)

    # Add constraints
    m.addConstr(0.15 * chives + 0.99 * basil >= 33, "water_need_min")
    m.addConstr(0.95 * chives + 0.06 * basil >= 50, "beauty_rating_min")
    m.addConstr(0.17 * chives + 0.6 * basil >= 27, "planting_space_min")
    m.addConstr(0.44 * chives + 0.75 * basil >= 25, "resilience_index_min")
    m.addConstr(5 * chives - 9 * basil >= 0, "chives_basil_ratio")

    m.addConstr(0.15 * chives + 0.99 * basil <= 112, "water_need_max")  # Corrected upper bound
    m.addConstr(0.95 * chives + 0.06 * basil <= 77, "beauty_rating_max")
    m.addConstr(0.17 * chives + 0.6 * basil <= 123, "planting_space_max")
    m.addConstr(0.44 * chives + 0.75 * basil <= 105, "resilience_index_max")


    # Optimize model
    m.optimize()

    # Print results
    if m.status == gp.GRB.OPTIMAL:
        print('Obj: %g' % m.objVal)
        print('chives:', chives.x)
        print('basil_plants:', basil.x)
    elif m.status == gp.GRB.INFEASIBLE:
        print('Model is infeasible')
    else:
        print('Optimization ended with status %d' % m.status)


except gp.GurobiError as e:
    print('Error code ' + str(e.errno) + ': ' + str(e))

except AttributeError:
    print('Encountered an attribute error')
```